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
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""Narrow injectable boundaries and configured adapters for deployment."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
from collections.abc import Callable, Mapping
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Protocol
|
|
10
|
+
|
|
11
|
+
from dploydb.candidate import (
|
|
12
|
+
CandidateStageObserver,
|
|
13
|
+
CandidateValidationResult,
|
|
14
|
+
HealthChecker,
|
|
15
|
+
run_candidate_stage,
|
|
16
|
+
)
|
|
17
|
+
from dploydb.config import LoadedConfiguration, ProductionTopology
|
|
18
|
+
from dploydb.cutover import (
|
|
19
|
+
create_final_backup,
|
|
20
|
+
migrate_production_database,
|
|
21
|
+
restore_final_backup,
|
|
22
|
+
)
|
|
23
|
+
from dploydb.health import ApplicationHealthChecker, CandidateHealthResult
|
|
24
|
+
from dploydb.locking import DeploymentLock
|
|
25
|
+
from dploydb.models import (
|
|
26
|
+
BackupArtifact,
|
|
27
|
+
MigrationCommandEvidence,
|
|
28
|
+
ProductionMigrationResult,
|
|
29
|
+
RemoteBackupArtifact,
|
|
30
|
+
VerifiedDatabaseRestoreResult,
|
|
31
|
+
)
|
|
32
|
+
from dploydb.releases import ReleaseHistorySnapshot
|
|
33
|
+
from dploydb.retention import RetentionResult, apply_retention
|
|
34
|
+
from dploydb.runners.base import (
|
|
35
|
+
ApplicationRunner,
|
|
36
|
+
ProductionApplicationRunner,
|
|
37
|
+
ProductionStop,
|
|
38
|
+
)
|
|
39
|
+
from dploydb.runners.docker_compose_production import DockerComposeProductionRunner
|
|
40
|
+
from dploydb.state import StateStore
|
|
41
|
+
from dploydb.storage.base import RemoteBackupStorage
|
|
42
|
+
from dploydb.storage.local import LocalBackupStorage
|
|
43
|
+
from dploydb.storage.s3 import configured_s3_storage
|
|
44
|
+
from dploydb.subprocesses import SubprocessRunner
|
|
45
|
+
from dploydb.traffic import CommandTrafficController, TrafficController
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class PreCutoverStage(Protocol):
|
|
49
|
+
"""Caller-owned rehearsal and candidate boundary."""
|
|
50
|
+
|
|
51
|
+
def run(
|
|
52
|
+
self,
|
|
53
|
+
loaded: LoadedConfiguration,
|
|
54
|
+
*,
|
|
55
|
+
version: str,
|
|
56
|
+
config_path: Path,
|
|
57
|
+
operation_id: str,
|
|
58
|
+
store: StateStore,
|
|
59
|
+
lock: DeploymentLock,
|
|
60
|
+
cancellation_event: threading.Event | None,
|
|
61
|
+
stage_observer: CandidateStageObserver,
|
|
62
|
+
) -> CandidateValidationResult: ...
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ProductionHealthBoundary(Protocol):
|
|
66
|
+
"""Final and rollback application health boundary."""
|
|
67
|
+
|
|
68
|
+
def check_application(
|
|
69
|
+
self,
|
|
70
|
+
*,
|
|
71
|
+
version: str,
|
|
72
|
+
database_path: Path,
|
|
73
|
+
cancellation_event: threading.Event | None = None,
|
|
74
|
+
) -> CandidateHealthResult: ...
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class CutoverDatabase(Protocol):
|
|
78
|
+
"""Database transaction boundary owned by the deployment operation."""
|
|
79
|
+
|
|
80
|
+
def create_final(
|
|
81
|
+
self,
|
|
82
|
+
*,
|
|
83
|
+
operation_id: str,
|
|
84
|
+
stopped: ProductionStop,
|
|
85
|
+
) -> BackupArtifact: ...
|
|
86
|
+
|
|
87
|
+
def migrate(
|
|
88
|
+
self,
|
|
89
|
+
*,
|
|
90
|
+
operation_id: str,
|
|
91
|
+
stopped: ProductionStop,
|
|
92
|
+
final_backup: BackupArtifact,
|
|
93
|
+
traffic_activated: bool,
|
|
94
|
+
evidence_sink: Callable[[MigrationCommandEvidence], None],
|
|
95
|
+
cancellation_event: threading.Event | None,
|
|
96
|
+
log_path: Path,
|
|
97
|
+
) -> ProductionMigrationResult: ...
|
|
98
|
+
|
|
99
|
+
def restore(
|
|
100
|
+
self,
|
|
101
|
+
*,
|
|
102
|
+
operation_id: str,
|
|
103
|
+
stopped: ProductionStop,
|
|
104
|
+
final_backup: BackupArtifact,
|
|
105
|
+
traffic_activated: bool,
|
|
106
|
+
) -> VerifiedDatabaseRestoreResult: ...
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class FinalBackupReplicator(Protocol):
|
|
110
|
+
"""Required off-server commit boundary before production migration."""
|
|
111
|
+
|
|
112
|
+
def replicate(
|
|
113
|
+
self,
|
|
114
|
+
artifact: BackupArtifact,
|
|
115
|
+
*,
|
|
116
|
+
release_id: str,
|
|
117
|
+
) -> RemoteBackupArtifact: ...
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class PostActivationRetention(Protocol):
|
|
121
|
+
"""Cleanup boundary invoked only after active pointers are durable."""
|
|
122
|
+
|
|
123
|
+
def apply(self, history: ReleaseHistorySnapshot) -> RetentionResult: ...
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(frozen=True, slots=True)
|
|
127
|
+
class DeploymentDependencies:
|
|
128
|
+
"""Injectable operational boundaries used by the coordinator."""
|
|
129
|
+
|
|
130
|
+
pre_cutover: PreCutoverStage
|
|
131
|
+
production: ProductionApplicationRunner
|
|
132
|
+
traffic: TrafficController
|
|
133
|
+
database: CutoverDatabase
|
|
134
|
+
health: ProductionHealthBoundary
|
|
135
|
+
remote_backup: FinalBackupReplicator | None = None
|
|
136
|
+
retention: PostActivationRetention | None = None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class ConfiguredPreCutover:
|
|
140
|
+
def __init__(
|
|
141
|
+
self,
|
|
142
|
+
*,
|
|
143
|
+
command_environment: Mapping[str, str],
|
|
144
|
+
command_runner: SubprocessRunner | None,
|
|
145
|
+
application_runner: ApplicationRunner | None,
|
|
146
|
+
health_checker: HealthChecker | None,
|
|
147
|
+
) -> None:
|
|
148
|
+
self.command_environment = command_environment
|
|
149
|
+
self.command_runner = command_runner
|
|
150
|
+
self.application_runner = application_runner
|
|
151
|
+
self.health_checker = health_checker
|
|
152
|
+
|
|
153
|
+
def run(
|
|
154
|
+
self,
|
|
155
|
+
loaded: LoadedConfiguration,
|
|
156
|
+
*,
|
|
157
|
+
version: str,
|
|
158
|
+
config_path: Path,
|
|
159
|
+
operation_id: str,
|
|
160
|
+
store: StateStore,
|
|
161
|
+
lock: DeploymentLock,
|
|
162
|
+
cancellation_event: threading.Event | None,
|
|
163
|
+
stage_observer: CandidateStageObserver,
|
|
164
|
+
) -> CandidateValidationResult:
|
|
165
|
+
return run_candidate_stage(
|
|
166
|
+
loaded,
|
|
167
|
+
version=version,
|
|
168
|
+
config_path=config_path,
|
|
169
|
+
operation_id=operation_id,
|
|
170
|
+
store=store,
|
|
171
|
+
lock=lock,
|
|
172
|
+
command_environment=self.command_environment,
|
|
173
|
+
command_runner=self.command_runner,
|
|
174
|
+
application_runner=self.application_runner,
|
|
175
|
+
health_checker=self.health_checker,
|
|
176
|
+
cancellation_event=cancellation_event,
|
|
177
|
+
complete_operation=False,
|
|
178
|
+
stage_observer=stage_observer,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class ConfiguredCutoverDatabase:
|
|
183
|
+
def __init__(
|
|
184
|
+
self,
|
|
185
|
+
loaded: LoadedConfiguration,
|
|
186
|
+
*,
|
|
187
|
+
config_path: Path,
|
|
188
|
+
command_environment: Mapping[str, str],
|
|
189
|
+
command_runner: SubprocessRunner | None,
|
|
190
|
+
) -> None:
|
|
191
|
+
self.loaded = loaded
|
|
192
|
+
self.config_path = config_path
|
|
193
|
+
self.command_environment = command_environment
|
|
194
|
+
self.command_runner = command_runner
|
|
195
|
+
|
|
196
|
+
def create_final(
|
|
197
|
+
self,
|
|
198
|
+
*,
|
|
199
|
+
operation_id: str,
|
|
200
|
+
stopped: ProductionStop,
|
|
201
|
+
) -> BackupArtifact:
|
|
202
|
+
return create_final_backup(
|
|
203
|
+
self.loaded,
|
|
204
|
+
operation_id=operation_id,
|
|
205
|
+
stopped=stopped,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def migrate(
|
|
209
|
+
self,
|
|
210
|
+
*,
|
|
211
|
+
operation_id: str,
|
|
212
|
+
stopped: ProductionStop,
|
|
213
|
+
final_backup: BackupArtifact,
|
|
214
|
+
traffic_activated: bool,
|
|
215
|
+
evidence_sink: Callable[[MigrationCommandEvidence], None],
|
|
216
|
+
cancellation_event: threading.Event | None,
|
|
217
|
+
log_path: Path,
|
|
218
|
+
) -> ProductionMigrationResult:
|
|
219
|
+
return migrate_production_database(
|
|
220
|
+
self.loaded,
|
|
221
|
+
operation_id=operation_id,
|
|
222
|
+
stopped=stopped,
|
|
223
|
+
final_backup=final_backup,
|
|
224
|
+
config_path=self.config_path,
|
|
225
|
+
traffic_activated=traffic_activated,
|
|
226
|
+
evidence_sink=evidence_sink,
|
|
227
|
+
command_environment=self.command_environment,
|
|
228
|
+
command_runner=self.command_runner,
|
|
229
|
+
cancellation_event=cancellation_event,
|
|
230
|
+
log_path=log_path,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def restore(
|
|
234
|
+
self,
|
|
235
|
+
*,
|
|
236
|
+
operation_id: str,
|
|
237
|
+
stopped: ProductionStop,
|
|
238
|
+
final_backup: BackupArtifact,
|
|
239
|
+
traffic_activated: bool,
|
|
240
|
+
) -> VerifiedDatabaseRestoreResult:
|
|
241
|
+
return restore_final_backup(
|
|
242
|
+
self.loaded,
|
|
243
|
+
operation_id=operation_id,
|
|
244
|
+
stopped=stopped,
|
|
245
|
+
final_backup=final_backup,
|
|
246
|
+
traffic_activated=traffic_activated,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class ConfiguredFinalBackupReplicator:
|
|
251
|
+
"""Upload a final local backup through the configured S3-compatible target."""
|
|
252
|
+
|
|
253
|
+
def __init__(self, storage: RemoteBackupStorage) -> None:
|
|
254
|
+
self.storage = storage
|
|
255
|
+
|
|
256
|
+
def replicate(
|
|
257
|
+
self,
|
|
258
|
+
artifact: BackupArtifact,
|
|
259
|
+
*,
|
|
260
|
+
release_id: str,
|
|
261
|
+
) -> RemoteBackupArtifact:
|
|
262
|
+
return self.storage.put(artifact, release_id=release_id)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class ConfiguredPostActivationRetention:
|
|
266
|
+
"""Apply the configured keep policy to local and optional remote storage."""
|
|
267
|
+
|
|
268
|
+
def __init__(
|
|
269
|
+
self,
|
|
270
|
+
*,
|
|
271
|
+
project: str,
|
|
272
|
+
keep_last: int,
|
|
273
|
+
local_storage: LocalBackupStorage,
|
|
274
|
+
remote_storage: RemoteBackupStorage | None,
|
|
275
|
+
) -> None:
|
|
276
|
+
self.project = project
|
|
277
|
+
self.keep_last = keep_last
|
|
278
|
+
self.local_storage = local_storage
|
|
279
|
+
self.remote_storage = remote_storage
|
|
280
|
+
|
|
281
|
+
def apply(self, history: ReleaseHistorySnapshot) -> RetentionResult:
|
|
282
|
+
self.local_storage.ensure_layout()
|
|
283
|
+
return apply_retention(
|
|
284
|
+
project=self.project,
|
|
285
|
+
keep_last=self.keep_last,
|
|
286
|
+
history=history,
|
|
287
|
+
local_storage=self.local_storage,
|
|
288
|
+
remote_storage=self.remote_storage,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def default_dependencies(
|
|
293
|
+
loaded: LoadedConfiguration,
|
|
294
|
+
*,
|
|
295
|
+
topology: ProductionTopology,
|
|
296
|
+
config_path: Path,
|
|
297
|
+
working_directory: Path,
|
|
298
|
+
command_environment: Mapping[str, str],
|
|
299
|
+
candidate_command_runner: SubprocessRunner | None,
|
|
300
|
+
candidate_application_runner: ApplicationRunner | None,
|
|
301
|
+
candidate_health_checker: HealthChecker | None,
|
|
302
|
+
) -> tuple[DeploymentDependencies, ApplicationHealthChecker]:
|
|
303
|
+
production = DockerComposeProductionRunner(
|
|
304
|
+
project=loaded.config.project,
|
|
305
|
+
application=loaded.config.application,
|
|
306
|
+
topology=topology,
|
|
307
|
+
database_environment_name=loaded.config.database.path_env,
|
|
308
|
+
production_database_path=loaded.config.database.path,
|
|
309
|
+
secrets=loaded.secrets,
|
|
310
|
+
working_directory=working_directory,
|
|
311
|
+
command_environment=command_environment,
|
|
312
|
+
)
|
|
313
|
+
traffic = CommandTrafficController(
|
|
314
|
+
traffic=loaded.config.traffic,
|
|
315
|
+
secrets=loaded.secrets,
|
|
316
|
+
working_directory=working_directory,
|
|
317
|
+
command_environment=command_environment,
|
|
318
|
+
)
|
|
319
|
+
health = ApplicationHealthChecker(
|
|
320
|
+
application=loaded.config.application,
|
|
321
|
+
health_url=topology.health_url,
|
|
322
|
+
database_environment_name=loaded.config.database.path_env,
|
|
323
|
+
secrets=loaded.secrets,
|
|
324
|
+
working_directory=working_directory,
|
|
325
|
+
command_environment=command_environment,
|
|
326
|
+
)
|
|
327
|
+
remote_storage: RemoteBackupStorage | None = None
|
|
328
|
+
remote_backup: FinalBackupReplicator | None = None
|
|
329
|
+
remote_config = loaded.config.backup.remote
|
|
330
|
+
if remote_config is not None and remote_config.enabled:
|
|
331
|
+
remote_storage = configured_s3_storage(
|
|
332
|
+
remote_config,
|
|
333
|
+
secrets=loaded.secrets,
|
|
334
|
+
environment=command_environment,
|
|
335
|
+
)
|
|
336
|
+
if remote_config is not None and remote_config.required:
|
|
337
|
+
assert remote_storage is not None
|
|
338
|
+
remote_backup = ConfiguredFinalBackupReplicator(remote_storage)
|
|
339
|
+
retention = ConfiguredPostActivationRetention(
|
|
340
|
+
project=loaded.config.project,
|
|
341
|
+
keep_last=loaded.config.backup.keep_last,
|
|
342
|
+
local_storage=LocalBackupStorage(loaded.config.backup.local_directory),
|
|
343
|
+
remote_storage=remote_storage,
|
|
344
|
+
)
|
|
345
|
+
return (
|
|
346
|
+
DeploymentDependencies(
|
|
347
|
+
pre_cutover=ConfiguredPreCutover(
|
|
348
|
+
command_environment=command_environment,
|
|
349
|
+
command_runner=candidate_command_runner,
|
|
350
|
+
application_runner=candidate_application_runner,
|
|
351
|
+
health_checker=candidate_health_checker,
|
|
352
|
+
),
|
|
353
|
+
production=production,
|
|
354
|
+
traffic=traffic,
|
|
355
|
+
database=ConfiguredCutoverDatabase(
|
|
356
|
+
loaded,
|
|
357
|
+
config_path=config_path,
|
|
358
|
+
command_environment=command_environment,
|
|
359
|
+
command_runner=None,
|
|
360
|
+
),
|
|
361
|
+
health=health,
|
|
362
|
+
remote_backup=remote_backup,
|
|
363
|
+
retention=retention,
|
|
364
|
+
),
|
|
365
|
+
health,
|
|
366
|
+
)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Typed compact release summaries and full operation evidence helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal, cast
|
|
6
|
+
|
|
7
|
+
from dploydb.health import CandidateHealthResult
|
|
8
|
+
from dploydb.models import BackupArtifact, ReleaseHealthEvidence, ReleaseHookEvidence
|
|
9
|
+
from dploydb.redaction import JsonValue
|
|
10
|
+
from dploydb.runners.base import (
|
|
11
|
+
ProductionCleanup,
|
|
12
|
+
ProductionDiscovery,
|
|
13
|
+
ProductionInspection,
|
|
14
|
+
ProductionRestart,
|
|
15
|
+
ProductionStart,
|
|
16
|
+
ProductionStop,
|
|
17
|
+
)
|
|
18
|
+
from dploydb.traffic import TrafficHookResult
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def hook_summary(result: TrafficHookResult) -> ReleaseHookEvidence:
|
|
22
|
+
command = result.command
|
|
23
|
+
return ReleaseHookEvidence(
|
|
24
|
+
action=result.action.value,
|
|
25
|
+
passed=result.passed,
|
|
26
|
+
outcome=command.outcome.value,
|
|
27
|
+
exit_code=command.exit_code,
|
|
28
|
+
output_complete=not command.stdout.truncated and not command.stderr.truncated,
|
|
29
|
+
duration_seconds=command.duration_seconds,
|
|
30
|
+
termination_reason=(
|
|
31
|
+
None if command.termination_reason is None else command.termination_reason.value
|
|
32
|
+
),
|
|
33
|
+
forced_kill=command.forced_kill,
|
|
34
|
+
cleanup_error=command.cleanup_error,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def health_summary(
|
|
39
|
+
result: CandidateHealthResult,
|
|
40
|
+
*,
|
|
41
|
+
role: Literal["new", "previous"],
|
|
42
|
+
version: str,
|
|
43
|
+
) -> ReleaseHealthEvidence:
|
|
44
|
+
return ReleaseHealthEvidence(
|
|
45
|
+
role=role,
|
|
46
|
+
version=version,
|
|
47
|
+
url=result.readiness.url,
|
|
48
|
+
readiness_attempts=result.readiness.attempt_count,
|
|
49
|
+
smoke_outcome=None if result.smoke is None else "succeeded",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def backup_evidence(artifact: BackupArtifact) -> dict[str, JsonValue]:
|
|
54
|
+
return {
|
|
55
|
+
"backup_id": artifact.metadata.backup_id,
|
|
56
|
+
"sha256": artifact.metadata.sha256,
|
|
57
|
+
"size_bytes": artifact.metadata.size_bytes,
|
|
58
|
+
"database_path": str(artifact.database_path),
|
|
59
|
+
"metadata_path": str(artifact.metadata_path),
|
|
60
|
+
"sqlite": cast(JsonValue, artifact.metadata.sqlite.model_dump(mode="json")),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def inspection_evidence(inspection: ProductionInspection) -> dict[str, JsonValue]:
|
|
65
|
+
return {
|
|
66
|
+
"handle": cast(JsonValue, inspection.handle.model_dump(mode="json")),
|
|
67
|
+
"running": inspection.running,
|
|
68
|
+
"mounts": [
|
|
69
|
+
{
|
|
70
|
+
"mount_type": mount.mount_type,
|
|
71
|
+
"source": mount.source,
|
|
72
|
+
"destination": mount.destination,
|
|
73
|
+
"read_write": mount.read_write,
|
|
74
|
+
}
|
|
75
|
+
for mount in inspection.mounts
|
|
76
|
+
],
|
|
77
|
+
"command": inspection.command.as_evidence(),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def discovery_evidence(discovery: ProductionDiscovery) -> dict[str, JsonValue]:
|
|
82
|
+
return {
|
|
83
|
+
"query": discovery.query.as_evidence(),
|
|
84
|
+
"inspection": inspection_evidence(discovery.inspection),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def stop_evidence(stopped: ProductionStop) -> dict[str, JsonValue]:
|
|
89
|
+
return {
|
|
90
|
+
"handle": cast(JsonValue, stopped.handle.model_dump(mode="json")),
|
|
91
|
+
"command": stopped.command.as_evidence(),
|
|
92
|
+
"inspection": inspection_evidence(stopped.inspection),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def start_evidence(started: ProductionStart) -> dict[str, JsonValue]:
|
|
97
|
+
return {
|
|
98
|
+
"handle": cast(JsonValue, started.handle.model_dump(mode="json")),
|
|
99
|
+
"container_reference": started.container_reference,
|
|
100
|
+
"command": started.command.as_evidence(),
|
|
101
|
+
"inspection": inspection_evidence(started.inspection),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def cleanup_evidence(cleanup: ProductionCleanup) -> dict[str, JsonValue]:
|
|
106
|
+
return {
|
|
107
|
+
"presence_query": cleanup.presence_query.as_evidence(),
|
|
108
|
+
"remove_command": (
|
|
109
|
+
None if cleanup.remove_command is None else cleanup.remove_command.as_evidence()
|
|
110
|
+
),
|
|
111
|
+
"compose_down": cleanup.compose_down.as_evidence(),
|
|
112
|
+
"proof": {
|
|
113
|
+
"container_absent": cleanup.proof.container_absent,
|
|
114
|
+
"networks_absent": cleanup.proof.networks_absent,
|
|
115
|
+
"proven": cleanup.proof.proven,
|
|
116
|
+
"container_query": cleanup.proof.container_query.as_evidence(),
|
|
117
|
+
"network_query": cleanup.proof.network_query.as_evidence(),
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def restart_evidence(restarted: ProductionRestart) -> dict[str, JsonValue]:
|
|
123
|
+
return {
|
|
124
|
+
"handle": cast(JsonValue, restarted.handle.model_dump(mode="json")),
|
|
125
|
+
"network_refreshes": [
|
|
126
|
+
{
|
|
127
|
+
"network_name": refresh.network_name,
|
|
128
|
+
"aliases": list(refresh.aliases),
|
|
129
|
+
"disconnect": refresh.disconnect.as_evidence(),
|
|
130
|
+
"connect": refresh.connect.as_evidence(),
|
|
131
|
+
}
|
|
132
|
+
for refresh in restarted.network_refreshes
|
|
133
|
+
],
|
|
134
|
+
"command": restarted.command.as_evidence(),
|
|
135
|
+
"inspection": inspection_evidence(restarted.inspection),
|
|
136
|
+
}
|