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/runners/base.py
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""Typed candidate-application lifecycle contracts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import threading
|
|
7
|
+
from collections.abc import Mapping, Sequence
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Protocol
|
|
11
|
+
|
|
12
|
+
from dploydb.models import ProductionApplicationHandle
|
|
13
|
+
from dploydb.subprocesses import CommandResult
|
|
14
|
+
|
|
15
|
+
_OPERATION_ID = re.compile(r"op_[0-9a-f]{32}\Z")
|
|
16
|
+
_RELEASE_IDENTIFIER = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,63}\Z")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def validate_operation_id(value: str) -> str:
|
|
20
|
+
"""Validate an opaque operation identifier before deriving resource names."""
|
|
21
|
+
if not isinstance(value, str):
|
|
22
|
+
raise TypeError("operation_id must be a string")
|
|
23
|
+
if _OPERATION_ID.fullmatch(value) is None:
|
|
24
|
+
raise ValueError("operation_id must be an opaque DployDB operation ID")
|
|
25
|
+
return value
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def validate_release_identifier(value: str) -> str:
|
|
29
|
+
"""Validate the bounded value made available to Compose interpolation."""
|
|
30
|
+
if not isinstance(value, str):
|
|
31
|
+
raise TypeError("version must be a string")
|
|
32
|
+
if _RELEASE_IDENTIFIER.fullmatch(value) is None or ".." in value:
|
|
33
|
+
raise ValueError(
|
|
34
|
+
"version must be 1-64 letters, digits, dots, underscores, or hyphens, "
|
|
35
|
+
"start with a letter or digit, and contain no traversal sequence"
|
|
36
|
+
)
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CommandExecutor(Protocol):
|
|
41
|
+
"""Subset of the bounded subprocess runner used by application runners."""
|
|
42
|
+
|
|
43
|
+
def run(
|
|
44
|
+
self,
|
|
45
|
+
command: Sequence[str],
|
|
46
|
+
*,
|
|
47
|
+
timeout_seconds: float,
|
|
48
|
+
environment: Mapping[str, str],
|
|
49
|
+
working_directory: Path | None = None,
|
|
50
|
+
cancellation_event: threading.Event | None = None,
|
|
51
|
+
) -> CommandResult: ...
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True, slots=True)
|
|
55
|
+
class CandidateHandle:
|
|
56
|
+
"""Opaque identity and expected isolation boundary for one candidate."""
|
|
57
|
+
|
|
58
|
+
operation_id: str
|
|
59
|
+
version: str
|
|
60
|
+
compose_project: str
|
|
61
|
+
container_name: str
|
|
62
|
+
rehearsal_database_path: Path
|
|
63
|
+
candidate_database_path: str
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True, slots=True)
|
|
67
|
+
class CandidateMount:
|
|
68
|
+
"""One selected mount from live Docker inspection evidence."""
|
|
69
|
+
|
|
70
|
+
mount_type: str
|
|
71
|
+
source: str
|
|
72
|
+
destination: str
|
|
73
|
+
read_write: bool
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True, slots=True)
|
|
77
|
+
class CandidateInspection:
|
|
78
|
+
"""Validated live evidence for an isolated running candidate."""
|
|
79
|
+
|
|
80
|
+
container_id: str
|
|
81
|
+
container_name: str
|
|
82
|
+
running: bool
|
|
83
|
+
compose_project: str
|
|
84
|
+
compose_service: str
|
|
85
|
+
operation_id: str
|
|
86
|
+
host_ip: str
|
|
87
|
+
host_port: int
|
|
88
|
+
container_port: int
|
|
89
|
+
mounts: tuple[CandidateMount, ...]
|
|
90
|
+
command: CommandResult
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True, slots=True)
|
|
94
|
+
class CandidateStart:
|
|
95
|
+
"""Evidence returned after Compose creates the isolated candidate."""
|
|
96
|
+
|
|
97
|
+
handle: CandidateHandle
|
|
98
|
+
container_reference: str
|
|
99
|
+
command: CommandResult
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True, slots=True)
|
|
103
|
+
class CandidateLogs:
|
|
104
|
+
"""Bounded redacted application-log capture."""
|
|
105
|
+
|
|
106
|
+
handle: CandidateHandle
|
|
107
|
+
command: CommandResult
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True, slots=True)
|
|
111
|
+
class CandidateCleanupProof:
|
|
112
|
+
"""Read-only proof that no candidate container or project network remains."""
|
|
113
|
+
|
|
114
|
+
container_absent: bool
|
|
115
|
+
networks_absent: bool
|
|
116
|
+
container_query: CommandResult
|
|
117
|
+
network_query: CommandResult
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def proven(self) -> bool:
|
|
121
|
+
return self.container_absent and self.networks_absent
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True, slots=True)
|
|
125
|
+
class CandidateCleanup:
|
|
126
|
+
"""Commands and terminal proof from idempotent isolated cleanup."""
|
|
127
|
+
|
|
128
|
+
presence_query: CommandResult
|
|
129
|
+
remove_command: CommandResult | None
|
|
130
|
+
compose_down: CommandResult
|
|
131
|
+
proof: CandidateCleanupProof
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class CandidateRunnerError(RuntimeError):
|
|
135
|
+
"""Typed low-level failure containing only bounded redacted evidence."""
|
|
136
|
+
|
|
137
|
+
def __init__(
|
|
138
|
+
self,
|
|
139
|
+
message: str,
|
|
140
|
+
*,
|
|
141
|
+
command: CommandResult | None = None,
|
|
142
|
+
cleanup: CandidateCleanup | None = None,
|
|
143
|
+
) -> None:
|
|
144
|
+
super().__init__(message)
|
|
145
|
+
self.command = command
|
|
146
|
+
self.cleanup = cleanup
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def cleanup_proven(self) -> bool | None:
|
|
150
|
+
return None if self.cleanup is None else self.cleanup.proof.proven
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class CandidateStartError(CandidateRunnerError):
|
|
154
|
+
"""Candidate creation failed; cleanup evidence states whether retry is safe."""
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class CandidateInspectionError(CandidateRunnerError):
|
|
158
|
+
"""Live Docker state contradicted the required candidate isolation boundary."""
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class CandidateCleanupError(CandidateRunnerError):
|
|
162
|
+
"""Candidate cleanup could not be proven."""
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class ApplicationRunner(Protocol):
|
|
166
|
+
"""Small Milestone 4A candidate lifecycle; production controls come later."""
|
|
167
|
+
|
|
168
|
+
def start(
|
|
169
|
+
self,
|
|
170
|
+
*,
|
|
171
|
+
operation_id: str,
|
|
172
|
+
version: str,
|
|
173
|
+
rehearsal_database_path: Path,
|
|
174
|
+
cancellation_event: threading.Event | None = None,
|
|
175
|
+
) -> CandidateStart: ...
|
|
176
|
+
|
|
177
|
+
def inspect(
|
|
178
|
+
self,
|
|
179
|
+
handle: CandidateHandle,
|
|
180
|
+
*,
|
|
181
|
+
cancellation_event: threading.Event | None = None,
|
|
182
|
+
) -> CandidateInspection: ...
|
|
183
|
+
|
|
184
|
+
def collect_logs(
|
|
185
|
+
self,
|
|
186
|
+
handle: CandidateHandle,
|
|
187
|
+
*,
|
|
188
|
+
cancellation_event: threading.Event | None = None,
|
|
189
|
+
) -> CandidateLogs: ...
|
|
190
|
+
|
|
191
|
+
def stop(self, handle: CandidateHandle) -> CandidateCleanup: ...
|
|
192
|
+
|
|
193
|
+
def prove_cleanup(self, handle: CandidateHandle) -> CandidateCleanupProof: ...
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@dataclass(frozen=True, slots=True)
|
|
197
|
+
class ProductionInspection:
|
|
198
|
+
"""Validated live Docker evidence for one exact production application."""
|
|
199
|
+
|
|
200
|
+
handle: ProductionApplicationHandle
|
|
201
|
+
running: bool
|
|
202
|
+
mounts: tuple[CandidateMount, ...]
|
|
203
|
+
command: CommandResult
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@dataclass(frozen=True, slots=True)
|
|
207
|
+
class ProductionDiscovery:
|
|
208
|
+
"""Configured Compose lookup plus validated current-application inspection."""
|
|
209
|
+
|
|
210
|
+
query: CommandResult
|
|
211
|
+
inspection: ProductionInspection
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@dataclass(frozen=True, slots=True)
|
|
215
|
+
class ProductionStop:
|
|
216
|
+
"""Exact-container stop command and proof that the previous app is stopped."""
|
|
217
|
+
|
|
218
|
+
handle: ProductionApplicationHandle
|
|
219
|
+
command: CommandResult
|
|
220
|
+
inspection: ProductionInspection
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@dataclass(frozen=True, slots=True)
|
|
224
|
+
class ProductionStart:
|
|
225
|
+
"""Evidence returned after a new release container is created."""
|
|
226
|
+
|
|
227
|
+
handle: ProductionApplicationHandle
|
|
228
|
+
container_reference: str
|
|
229
|
+
command: CommandResult
|
|
230
|
+
inspection: ProductionInspection
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@dataclass(frozen=True, slots=True)
|
|
234
|
+
class ProductionNetworkRefresh:
|
|
235
|
+
"""Exact stopped-container network endpoint refresh evidence."""
|
|
236
|
+
|
|
237
|
+
network_name: str
|
|
238
|
+
aliases: tuple[str, ...]
|
|
239
|
+
disconnect: CommandResult
|
|
240
|
+
connect: CommandResult
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@dataclass(frozen=True, slots=True)
|
|
244
|
+
class ProductionRestart:
|
|
245
|
+
"""Exact previous-container restart plus running-state proof."""
|
|
246
|
+
|
|
247
|
+
handle: ProductionApplicationHandle
|
|
248
|
+
command: CommandResult
|
|
249
|
+
inspection: ProductionInspection
|
|
250
|
+
network_refreshes: tuple[ProductionNetworkRefresh, ...] = ()
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@dataclass(frozen=True, slots=True)
|
|
254
|
+
class ProductionLogs:
|
|
255
|
+
"""Bounded logs collected from one exact production container."""
|
|
256
|
+
|
|
257
|
+
handle: ProductionApplicationHandle
|
|
258
|
+
command: CommandResult
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@dataclass(frozen=True, slots=True)
|
|
262
|
+
class ProductionCleanupProof:
|
|
263
|
+
"""Proof that a failed new release container and network are absent."""
|
|
264
|
+
|
|
265
|
+
container_absent: bool
|
|
266
|
+
networks_absent: bool
|
|
267
|
+
container_query: CommandResult
|
|
268
|
+
network_query: CommandResult
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def proven(self) -> bool:
|
|
272
|
+
return self.container_absent and self.networks_absent
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@dataclass(frozen=True, slots=True)
|
|
276
|
+
class ProductionCleanup:
|
|
277
|
+
"""Idempotent exact-target cleanup evidence for a failed new release."""
|
|
278
|
+
|
|
279
|
+
presence_query: CommandResult
|
|
280
|
+
remove_command: CommandResult | None
|
|
281
|
+
compose_down: CommandResult
|
|
282
|
+
proof: ProductionCleanupProof
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class ProductionRunnerError(RuntimeError):
|
|
286
|
+
"""Base production lifecycle failure with bounded redacted command evidence."""
|
|
287
|
+
|
|
288
|
+
def __init__(
|
|
289
|
+
self,
|
|
290
|
+
message: str,
|
|
291
|
+
*,
|
|
292
|
+
command: CommandResult | None = None,
|
|
293
|
+
cleanup: ProductionCleanup | None = None,
|
|
294
|
+
) -> None:
|
|
295
|
+
super().__init__(message)
|
|
296
|
+
self.command = command
|
|
297
|
+
self.cleanup = cleanup
|
|
298
|
+
|
|
299
|
+
@property
|
|
300
|
+
def cleanup_proven(self) -> bool | None:
|
|
301
|
+
return None if self.cleanup is None else self.cleanup.proof.proven
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class ProductionDiscoveryError(ProductionRunnerError):
|
|
305
|
+
"""Configured current application could not be uniquely and safely identified."""
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class ProductionInspectionError(ProductionRunnerError):
|
|
309
|
+
"""Live state contradicted the recorded production application identity."""
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class ProductionStopError(ProductionRunnerError):
|
|
313
|
+
"""The current application could not be proven stopped."""
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class ProductionStartError(ProductionRunnerError):
|
|
317
|
+
"""The new release could not be started with proven cleanup evidence."""
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class ProductionRestartError(ProductionRunnerError):
|
|
321
|
+
"""The exact previous application could not be proven restarted."""
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class ProductionCleanupError(ProductionRunnerError):
|
|
325
|
+
"""Failed new-release resources could not be proven absent."""
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
class ProductionApplicationRunner(Protocol):
|
|
329
|
+
"""Milestone 5 production application lifecycle boundary."""
|
|
330
|
+
|
|
331
|
+
def discover_current(
|
|
332
|
+
self,
|
|
333
|
+
*,
|
|
334
|
+
cancellation_event: threading.Event | None = None,
|
|
335
|
+
) -> ProductionDiscovery: ...
|
|
336
|
+
|
|
337
|
+
def inspect(
|
|
338
|
+
self,
|
|
339
|
+
handle: ProductionApplicationHandle,
|
|
340
|
+
*,
|
|
341
|
+
expected_running: bool,
|
|
342
|
+
cancellation_event: threading.Event | None = None,
|
|
343
|
+
) -> ProductionInspection: ...
|
|
344
|
+
|
|
345
|
+
def inspect_live(
|
|
346
|
+
self,
|
|
347
|
+
handle: ProductionApplicationHandle,
|
|
348
|
+
*,
|
|
349
|
+
cancellation_event: threading.Event | None = None,
|
|
350
|
+
) -> ProductionInspection: ...
|
|
351
|
+
|
|
352
|
+
def stop_current(
|
|
353
|
+
self,
|
|
354
|
+
handle: ProductionApplicationHandle,
|
|
355
|
+
*,
|
|
356
|
+
cancellation_event: threading.Event | None = None,
|
|
357
|
+
) -> ProductionStop: ...
|
|
358
|
+
|
|
359
|
+
def start_new(
|
|
360
|
+
self,
|
|
361
|
+
*,
|
|
362
|
+
operation_id: str,
|
|
363
|
+
release_id: str,
|
|
364
|
+
version: str,
|
|
365
|
+
cancellation_event: threading.Event | None = None,
|
|
366
|
+
) -> ProductionStart: ...
|
|
367
|
+
|
|
368
|
+
def collect_logs(
|
|
369
|
+
self,
|
|
370
|
+
handle: ProductionApplicationHandle,
|
|
371
|
+
*,
|
|
372
|
+
cancellation_event: threading.Event | None = None,
|
|
373
|
+
) -> ProductionLogs: ...
|
|
374
|
+
|
|
375
|
+
def remove_new(self, handle: ProductionApplicationHandle) -> ProductionCleanup: ...
|
|
376
|
+
|
|
377
|
+
def prove_release_absent(
|
|
378
|
+
self,
|
|
379
|
+
*,
|
|
380
|
+
release_id: str,
|
|
381
|
+
version: str,
|
|
382
|
+
) -> ProductionCleanupProof: ...
|
|
383
|
+
|
|
384
|
+
def restart_previous(
|
|
385
|
+
self,
|
|
386
|
+
handle: ProductionApplicationHandle,
|
|
387
|
+
*,
|
|
388
|
+
cancellation_event: threading.Event | None = None,
|
|
389
|
+
) -> ProductionRestart: ...
|