dploydb 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dploydb/__init__.py +1 -0
- dploydb/__main__.py +6 -0
- dploydb/backup.py +455 -0
- dploydb/candidate.py +868 -0
- dploydb/cli.py +1111 -0
- dploydb/config.py +784 -0
- dploydb/cutover.py +326 -0
- dploydb/deploy.py +1163 -0
- dploydb/deployment_dependencies.py +366 -0
- dploydb/deployment_evidence.py +136 -0
- dploydb/diagnostics.py +1020 -0
- dploydb/errors.py +140 -0
- dploydb/health.py +625 -0
- dploydb/locking.py +609 -0
- dploydb/manual_restore.py +825 -0
- dploydb/migration.py +575 -0
- dploydb/models.py +927 -0
- dploydb/recovery.py +1210 -0
- dploydb/redaction.py +229 -0
- dploydb/releases.py +611 -0
- dploydb/restore.py +461 -0
- dploydb/retention.py +165 -0
- dploydb/runners/__init__.py +33 -0
- dploydb/runners/base.py +389 -0
- dploydb/runners/docker_compose.py +590 -0
- dploydb/runners/docker_compose_production.py +966 -0
- dploydb/sqlite_checks.py +136 -0
- dploydb/state.py +604 -0
- dploydb/storage/__init__.py +13 -0
- dploydb/storage/base.py +52 -0
- dploydb/storage/local.py +301 -0
- dploydb/storage/s3.py +737 -0
- dploydb/subprocesses.py +641 -0
- dploydb/traffic.py +165 -0
- dploydb-0.1.0.dist-info/METADATA +583 -0
- dploydb-0.1.0.dist-info/RECORD +40 -0
- dploydb-0.1.0.dist-info/WHEEL +4 -0
- dploydb-0.1.0.dist-info/entry_points.txt +2 -0
- dploydb-0.1.0.dist-info/licenses/LICENSE +201 -0
- dploydb-0.1.0.dist-info/licenses/NOTICE +4 -0
dploydb/traffic.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Bounded command-based maintenance and traffic hook execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import threading
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from enum import StrEnum
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Final, Protocol
|
|
12
|
+
|
|
13
|
+
from dploydb.config import TrafficConfig
|
|
14
|
+
from dploydb.redaction import JsonValue, SecretRegistry
|
|
15
|
+
from dploydb.runners.base import CommandExecutor
|
|
16
|
+
from dploydb.subprocesses import CommandOutcome, CommandResult, SubprocessRunner
|
|
17
|
+
|
|
18
|
+
TRAFFIC_MAX_OUTPUT_BYTES: Final = 256 * 1024
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TrafficAction(StrEnum):
|
|
22
|
+
"""The four developer-supplied cutover hook actions."""
|
|
23
|
+
|
|
24
|
+
ENABLE_MAINTENANCE = "enable_maintenance"
|
|
25
|
+
DISABLE_MAINTENANCE = "disable_maintenance"
|
|
26
|
+
ACTIVATE_NEW = "activate_new"
|
|
27
|
+
ACTIVATE_OLD = "activate_old"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class TrafficHookResult:
|
|
32
|
+
"""Complete bounded evidence for one attempted traffic action."""
|
|
33
|
+
|
|
34
|
+
action: TrafficAction
|
|
35
|
+
command: CommandResult
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def passed(self) -> bool:
|
|
39
|
+
"""Require exit zero and complete stdout/stderr evidence."""
|
|
40
|
+
return (
|
|
41
|
+
self.command.outcome is CommandOutcome.SUCCEEDED
|
|
42
|
+
and not self.command.stdout.truncated
|
|
43
|
+
and not self.command.stderr.truncated
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def as_evidence(self) -> dict[str, JsonValue]:
|
|
47
|
+
return {
|
|
48
|
+
"action": self.action.value,
|
|
49
|
+
"passed": self.passed,
|
|
50
|
+
"command": self.command.as_evidence(),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TrafficController(Protocol):
|
|
55
|
+
"""Narrow hook boundary consumed by the deployment coordinator."""
|
|
56
|
+
|
|
57
|
+
def enable_maintenance(
|
|
58
|
+
self,
|
|
59
|
+
*,
|
|
60
|
+
cancellation_event: threading.Event | None = None,
|
|
61
|
+
) -> TrafficHookResult: ...
|
|
62
|
+
|
|
63
|
+
def disable_maintenance(
|
|
64
|
+
self,
|
|
65
|
+
*,
|
|
66
|
+
cancellation_event: threading.Event | None = None,
|
|
67
|
+
) -> TrafficHookResult: ...
|
|
68
|
+
|
|
69
|
+
def activate_new(
|
|
70
|
+
self,
|
|
71
|
+
*,
|
|
72
|
+
cancellation_event: threading.Event | None = None,
|
|
73
|
+
) -> TrafficHookResult: ...
|
|
74
|
+
|
|
75
|
+
def activate_old(
|
|
76
|
+
self,
|
|
77
|
+
*,
|
|
78
|
+
cancellation_event: threading.Event | None = None,
|
|
79
|
+
) -> TrafficHookResult: ...
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class CommandTrafficController:
|
|
83
|
+
"""Execute configured traffic hooks without a shell and preserve every outcome."""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
*,
|
|
88
|
+
traffic: TrafficConfig,
|
|
89
|
+
secrets: SecretRegistry,
|
|
90
|
+
working_directory: Path,
|
|
91
|
+
command_environment: Mapping[str, str] | None = None,
|
|
92
|
+
command_runner: CommandExecutor | None = None,
|
|
93
|
+
) -> None:
|
|
94
|
+
if not working_directory.is_absolute():
|
|
95
|
+
raise ValueError("traffic hook working_directory must be absolute")
|
|
96
|
+
self.traffic = traffic
|
|
97
|
+
self.secrets = secrets
|
|
98
|
+
self.working_directory = working_directory
|
|
99
|
+
self.command_environment = dict(
|
|
100
|
+
os.environ if command_environment is None else command_environment
|
|
101
|
+
)
|
|
102
|
+
self.command_runner = command_runner or SubprocessRunner(
|
|
103
|
+
secrets=secrets,
|
|
104
|
+
max_output_bytes=TRAFFIC_MAX_OUTPUT_BYTES,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def enable_maintenance(
|
|
108
|
+
self,
|
|
109
|
+
*,
|
|
110
|
+
cancellation_event: threading.Event | None = None,
|
|
111
|
+
) -> TrafficHookResult:
|
|
112
|
+
return self._run(
|
|
113
|
+
TrafficAction.ENABLE_MAINTENANCE,
|
|
114
|
+
self.traffic.maintenance_on_command,
|
|
115
|
+
cancellation_event=cancellation_event,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def disable_maintenance(
|
|
119
|
+
self,
|
|
120
|
+
*,
|
|
121
|
+
cancellation_event: threading.Event | None = None,
|
|
122
|
+
) -> TrafficHookResult:
|
|
123
|
+
return self._run(
|
|
124
|
+
TrafficAction.DISABLE_MAINTENANCE,
|
|
125
|
+
self.traffic.maintenance_off_command,
|
|
126
|
+
cancellation_event=cancellation_event,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def activate_new(
|
|
130
|
+
self,
|
|
131
|
+
*,
|
|
132
|
+
cancellation_event: threading.Event | None = None,
|
|
133
|
+
) -> TrafficHookResult:
|
|
134
|
+
return self._run(
|
|
135
|
+
TrafficAction.ACTIVATE_NEW,
|
|
136
|
+
self.traffic.activate_new_command,
|
|
137
|
+
cancellation_event=cancellation_event,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def activate_old(
|
|
141
|
+
self,
|
|
142
|
+
*,
|
|
143
|
+
cancellation_event: threading.Event | None = None,
|
|
144
|
+
) -> TrafficHookResult:
|
|
145
|
+
return self._run(
|
|
146
|
+
TrafficAction.ACTIVATE_OLD,
|
|
147
|
+
self.traffic.activate_old_command,
|
|
148
|
+
cancellation_event=cancellation_event,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def _run(
|
|
152
|
+
self,
|
|
153
|
+
action: TrafficAction,
|
|
154
|
+
command: tuple[str, ...],
|
|
155
|
+
*,
|
|
156
|
+
cancellation_event: threading.Event | None,
|
|
157
|
+
) -> TrafficHookResult:
|
|
158
|
+
result = self.command_runner.run(
|
|
159
|
+
command,
|
|
160
|
+
timeout_seconds=self.traffic.timeout_seconds,
|
|
161
|
+
environment=self.command_environment,
|
|
162
|
+
working_directory=self.working_directory,
|
|
163
|
+
cancellation_event=cancellation_event,
|
|
164
|
+
)
|
|
165
|
+
return TrafficHookResult(action=action, command=result)
|