fdai-deployment-cli 0.1.2__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.
- fdai_deployment_cli/__about__.py +3 -0
- fdai_deployment_cli/__init__.py +5 -0
- fdai_deployment_cli/__main__.py +5 -0
- fdai_deployment_cli/acr_publish.py +585 -0
- fdai_deployment_cli/aks_catalog_review.py +137 -0
- fdai_deployment_cli/aks_historical_reconciliation.py +732 -0
- fdai_deployment_cli/aks_job_environment.py +59 -0
- fdai_deployment_cli/aks_job_execution.py +794 -0
- fdai_deployment_cli/aks_kubernetes_defaults.py +66 -0
- fdai_deployment_cli/aks_preflight.py +422 -0
- fdai_deployment_cli/aks_readiness.py +220 -0
- fdai_deployment_cli/aks_service_update.py +180 -0
- fdai_deployment_cli/aks_workload_jobs.py +281 -0
- fdai_deployment_cli/application_state_adoption.py +347 -0
- fdai_deployment_cli/azure_actor.py +61 -0
- fdai_deployment_cli/azure_naming.py +252 -0
- fdai_deployment_cli/azure_retail_prices.py +173 -0
- fdai_deployment_cli/bootstrap_approval.py +99 -0
- fdai_deployment_cli/bootstrap_channel.py +321 -0
- fdai_deployment_cli/bootstrap_executor.py +311 -0
- fdai_deployment_cli/bootstrap_plan.py +237 -0
- fdai_deployment_cli/bootstrap_reconcile.py +402 -0
- fdai_deployment_cli/bootstrap_support.py +138 -0
- fdai_deployment_cli/bootstrap_worker.py +448 -0
- fdai_deployment_cli/bundle.py +352 -0
- fdai_deployment_cli/capacity.py +143 -0
- fdai_deployment_cli/catalog_review_profile.py +306 -0
- fdai_deployment_cli/cli.py +798 -0
- fdai_deployment_cli/cli_help.py +214 -0
- fdai_deployment_cli/cli_parser.py +791 -0
- fdai_deployment_cli/cli_plan.py +450 -0
- fdai_deployment_cli/compiler.py +99 -0
- fdai_deployment_cli/console_artifact.py +218 -0
- fdai_deployment_cli/console_config.py +214 -0
- fdai_deployment_cli/console_update.py +832 -0
- fdai_deployment_cli/contracts.py +307 -0
- fdai_deployment_cli/control_package.py +140 -0
- fdai_deployment_cli/deadline_transport.py +70 -0
- fdai_deployment_cli/deployment_cost.py +131 -0
- fdai_deployment_cli/deployment_deadline.py +28 -0
- fdai_deployment_cli/deployment_kit.py +790 -0
- fdai_deployment_cli/deployment_kit_cache.py +191 -0
- fdai_deployment_cli/deployment_progress.py +648 -0
- fdai_deployment_cli/development_authority_cli.py +56 -0
- fdai_deployment_cli/development_authority_profile.py +319 -0
- fdai_deployment_cli/doctor.py +136 -0
- fdai_deployment_cli/entitlement_preflight.py +68 -0
- fdai_deployment_cli/entitlement_transport.py +49 -0
- fdai_deployment_cli/entra_bootstrap.py +579 -0
- fdai_deployment_cli/entra_control_profile.py +214 -0
- fdai_deployment_cli/entra_graph.py +349 -0
- fdai_deployment_cli/entra_profiles.py +215 -0
- fdai_deployment_cli/entra_source.py +244 -0
- fdai_deployment_cli/execution_bundle.py +478 -0
- fdai_deployment_cli/execution_copies.py +63 -0
- fdai_deployment_cli/foundation_adoption.py +438 -0
- fdai_deployment_cli/foundation_adoption_evidence.py +245 -0
- fdai_deployment_cli/foundation_adoption_host.py +157 -0
- fdai_deployment_cli/foundation_adoption_transport.py +51 -0
- fdai_deployment_cli/foundation_failure.py +72 -0
- fdai_deployment_cli/foundation_image.py +168 -0
- fdai_deployment_cli/foundation_input.py +235 -0
- fdai_deployment_cli/foundation_output.py +154 -0
- fdai_deployment_cli/foundation_plan.py +381 -0
- fdai_deployment_cli/foundation_process.py +64 -0
- fdai_deployment_cli/foundation_progress.py +231 -0
- fdai_deployment_cli/github_actions.py +171 -0
- fdai_deployment_cli/github_apply_artifact.py +259 -0
- fdai_deployment_cli/github_artifact_io.py +48 -0
- fdai_deployment_cli/github_dispatch.py +248 -0
- fdai_deployment_cli/github_plan_artifact.py +181 -0
- fdai_deployment_cli/github_provider_artifact.py +170 -0
- fdai_deployment_cli/github_transport.py +46 -0
- fdai_deployment_cli/github_workflow_values.py +312 -0
- fdai_deployment_cli/hub_image_deploy.py +354 -0
- fdai_deployment_cli/hub_image_stage.py +162 -0
- fdai_deployment_cli/hub_image_trust.py +281 -0
- fdai_deployment_cli/hub_ownership.py +161 -0
- fdai_deployment_cli/hub_workload_handoff.py +264 -0
- fdai_deployment_cli/hub_workload_handoff_inputs.py +227 -0
- fdai_deployment_cli/identity_profile.py +227 -0
- fdai_deployment_cli/image_targets.py +197 -0
- fdai_deployment_cli/installation_scope.py +280 -0
- fdai_deployment_cli/license.py +311 -0
- fdai_deployment_cli/license_issue.py +239 -0
- fdai_deployment_cli/lifecycle_authorization.py +252 -0
- fdai_deployment_cli/lifecycle_commands.py +173 -0
- fdai_deployment_cli/lifecycle_configuration.py +722 -0
- fdai_deployment_cli/lifecycle_configuration_artifact.py +45 -0
- fdai_deployment_cli/lifecycle_configuration_cli.py +324 -0
- fdai_deployment_cli/lifecycle_configuration_document.py +74 -0
- fdai_deployment_cli/lifecycle_configuration_entities.py +146 -0
- fdai_deployment_cli/lifecycle_configuration_package.py +492 -0
- fdai_deployment_cli/lifecycle_configuration_publication.py +277 -0
- fdai_deployment_cli/lifecycle_configuration_publication_cli.py +191 -0
- fdai_deployment_cli/lifecycle_configuration_sealing.py +127 -0
- fdai_deployment_cli/lifecycle_enrollment.py +45 -0
- fdai_deployment_cli/lifecycle_infrastructure_agent.py +174 -0
- fdai_deployment_cli/lifecycle_installation_binding.py +65 -0
- fdai_deployment_cli/lifecycle_native.py +247 -0
- fdai_deployment_cli/lifecycle_native_reconciliation.py +568 -0
- fdai_deployment_cli/lifecycle_native_transition.py +339 -0
- fdai_deployment_cli/lifecycle_ownership_exclusion.py +78 -0
- fdai_deployment_cli/lifecycle_plan.py +800 -0
- fdai_deployment_cli/lifecycle_promotion.py +185 -0
- fdai_deployment_cli/lifecycle_release.py +308 -0
- fdai_deployment_cli/lifecycle_self_upgrade.py +202 -0
- fdai_deployment_cli/lifecycle_upgrade_bundle.py +176 -0
- fdai_deployment_cli/notification_recipients_input.py +222 -0
- fdai_deployment_cli/observer_artifact.py +106 -0
- fdai_deployment_cli/oci_archive.py +353 -0
- fdai_deployment_cli/offline_kit.py +797 -0
- fdai_deployment_cli/offline_prepare.py +135 -0
- fdai_deployment_cli/operational_evidence_verifier_input.py +575 -0
- fdai_deployment_cli/plan_input.py +101 -0
- fdai_deployment_cli/private_output.py +192 -0
- fdai_deployment_cli/profile.py +70 -0
- fdai_deployment_cli/progress.py +183 -0
- fdai_deployment_cli/readiness.py +286 -0
- fdai_deployment_cli/run_command_receiver.py +418 -0
- fdai_deployment_cli/runtime_build.py +386 -0
- fdai_deployment_cli/runtime_profile.py +287 -0
- fdai_deployment_cli/runtime_release.py +745 -0
- fdai_deployment_cli/runtime_stage.py +82 -0
- fdai_deployment_cli/runtime_support_installation.py +215 -0
- fdai_deployment_cli/simulation.py +80 -0
- fdai_deployment_cli/source_application_inputs.py +397 -0
- fdai_deployment_cli/source_azure.py +586 -0
- fdai_deployment_cli/source_change_cli.py +107 -0
- fdai_deployment_cli/source_change_parser.py +75 -0
- fdai_deployment_cli/source_change_plan.py +181 -0
- fdai_deployment_cli/source_changed_update.py +196 -0
- fdai_deployment_cli/source_deploy.py +170 -0
- fdai_deployment_cli/source_foundation.py +174 -0
- fdai_deployment_cli/source_host_tools.py +163 -0
- fdai_deployment_cli/source_image_stage.py +400 -0
- fdai_deployment_cli/source_input.py +173 -0
- fdai_deployment_cli/source_projection.py +164 -0
- fdai_deployment_cli/source_receiver.py +76 -0
- fdai_deployment_cli/source_recovery.py +264 -0
- fdai_deployment_cli/source_runtime.py +81 -0
- fdai_deployment_cli/source_runtime_support.py +368 -0
- fdai_deployment_cli/source_service_update.py +726 -0
- fdai_deployment_cli/source_service_update_cli.py +47 -0
- fdai_deployment_cli/source_snapshot.py +178 -0
- fdai_deployment_cli/source_teardown.py +574 -0
- fdai_deployment_cli/source_terraform.py +85 -0
- fdai_deployment_cli/source_transport.py +399 -0
- fdai_deployment_cli/standalone_aks_inventory.py +558 -0
- fdai_deployment_cli/standalone_aks_job_execution.py +769 -0
- fdai_deployment_cli/standalone_aks_nodepool_guard.py +141 -0
- fdai_deployment_cli/standalone_application.py +977 -0
- fdai_deployment_cli/standalone_application_completion.py +151 -0
- fdai_deployment_cli/standalone_application_input_recovery.py +107 -0
- fdai_deployment_cli/standalone_application_recovery.py +116 -0
- fdai_deployment_cli/standalone_approval_input.py +24 -0
- fdai_deployment_cli/standalone_catalog_checkpoint.py +122 -0
- fdai_deployment_cli/standalone_catalog_review.py +251 -0
- fdai_deployment_cli/standalone_checkpoint_failure.py +127 -0
- fdai_deployment_cli/standalone_console_publish.py +174 -0
- fdai_deployment_cli/standalone_database_principals.py +41 -0
- fdai_deployment_cli/standalone_deploy.py +638 -0
- fdai_deployment_cli/standalone_development_authority.py +166 -0
- fdai_deployment_cli/standalone_foundation_adoption.py +87 -0
- fdai_deployment_cli/standalone_foundation_transition.py +767 -0
- fdai_deployment_cli/standalone_host.py +4608 -0
- fdai_deployment_cli/standalone_host_rebind.py +226 -0
- fdai_deployment_cli/standalone_host_residual.py +104 -0
- fdai_deployment_cli/standalone_host_state.py +145 -0
- fdai_deployment_cli/standalone_host_values.py +563 -0
- fdai_deployment_cli/standalone_kubernetes_tools.py +100 -0
- fdai_deployment_cli/standalone_license_installation.py +177 -0
- fdai_deployment_cli/standalone_management_egress.py +83 -0
- fdai_deployment_cli/standalone_migration_evidence.py +38 -0
- fdai_deployment_cli/standalone_operational_evidence.py +356 -0
- fdai_deployment_cli/standalone_planned_outputs.py +147 -0
- fdai_deployment_cli/standalone_private_access_probe.py +100 -0
- fdai_deployment_cli/standalone_product_profile.py +110 -0
- fdai_deployment_cli/standalone_remote_prepare.py +442 -0
- fdai_deployment_cli/standalone_residual_apply.py +540 -0
- fdai_deployment_cli/standalone_residual_successor.py +431 -0
- fdai_deployment_cli/standalone_review.py +266 -0
- fdai_deployment_cli/standalone_service_update_finalization.py +116 -0
- fdai_deployment_cli/standalone_service_update_recovery.py +277 -0
- fdai_deployment_cli/standalone_stage_targets.py +325 -0
- fdai_deployment_cli/standalone_status.py +67 -0
- fdai_deployment_cli/standalone_terraform_environment.py +237 -0
- fdai_deployment_cli/standalone_transfer_cleanup.py +237 -0
- fdai_deployment_cli/standalone_trial_activation.py +193 -0
- fdai_deployment_cli/state.py +434 -0
- fdai_deployment_cli/state_handoff.py +253 -0
- fdai_deployment_cli/status_projection.py +97 -0
- fdai_deployment_cli/support_install.py +257 -0
- fdai_deployment_cli/target.py +46 -0
- fdai_deployment_cli/trust/capabilities.json +62 -0
- fdai_deployment_cli/trust/deployment-bundle-root.pub +3 -0
- fdai_deployment_cli/trust/deployment-release-root.pub +3 -0
- fdai_deployment_cli/trust/upstream-signing-key.pub +3 -0
- fdai_deployment_cli/trust_roots.py +50 -0
- fdai_deployment_cli-0.1.2.dist-info/METADATA +112 -0
- fdai_deployment_cli-0.1.2.dist-info/RECORD +203 -0
- fdai_deployment_cli-0.1.2.dist-info/WHEEL +4 -0
- fdai_deployment_cli-0.1.2.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Stage local runtime release bytes before the enclosing kit is signed."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from tempfile import TemporaryDirectory
|
|
8
|
+
|
|
9
|
+
from fdai_deployment_cli.offline_kit import (
|
|
10
|
+
_MAX_FILE_BYTES,
|
|
11
|
+
MANIFEST_NAME,
|
|
12
|
+
SIGNATURE_NAME,
|
|
13
|
+
_copy_verified_file,
|
|
14
|
+
_scan_tree,
|
|
15
|
+
_sha256_nofollow,
|
|
16
|
+
)
|
|
17
|
+
from fdai_deployment_cli.private_output import _open_private_parent
|
|
18
|
+
from fdai_deployment_cli.runtime_release import load_runtime_release, validate_runtime_images
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def stage_runtime_release(
|
|
22
|
+
source: Path,
|
|
23
|
+
kit: Path,
|
|
24
|
+
*,
|
|
25
|
+
deployment_bundle: Path,
|
|
26
|
+
source_commit: str,
|
|
27
|
+
platform_tag: str,
|
|
28
|
+
) -> str:
|
|
29
|
+
"""Copy a complete local inventory without registry access or image execution.
|
|
30
|
+
|
|
31
|
+
V2 inventories also require valid service and ClamAV OCI content. Legacy v1
|
|
32
|
+
remains stageable for inspection, not complete preparation. This is release
|
|
33
|
+
assembly, not a signature or production-eligibility check. The caller supplies
|
|
34
|
+
a private kit directory and signs it after staging.
|
|
35
|
+
"""
|
|
36
|
+
directory = _open_private_parent(kit / "runtime")
|
|
37
|
+
os.close(directory)
|
|
38
|
+
if any(
|
|
39
|
+
(kit / name).exists() or (kit / name).is_symlink()
|
|
40
|
+
for name in (MANIFEST_NAME, SIGNATURE_NAME)
|
|
41
|
+
):
|
|
42
|
+
raise ValueError("runtime staging requires an unsigned kit")
|
|
43
|
+
release = load_runtime_release(
|
|
44
|
+
source,
|
|
45
|
+
expected_source_commit=source_commit,
|
|
46
|
+
expected_platform_tag=platform_tag,
|
|
47
|
+
)
|
|
48
|
+
bundle_details = deployment_bundle.lstat()
|
|
49
|
+
if bundle_details.st_size > _MAX_FILE_BYTES:
|
|
50
|
+
raise ValueError("deployment bundle exceeds the offline kit file size limit")
|
|
51
|
+
if release.deployment_bundle_sha256 != _sha256_nofollow(
|
|
52
|
+
deployment_bundle, expected=bundle_details
|
|
53
|
+
):
|
|
54
|
+
raise ValueError("runtime inventory does not match the deployment bundle")
|
|
55
|
+
destination = kit / "runtime"
|
|
56
|
+
if destination.exists() or destination.is_symlink():
|
|
57
|
+
raise ValueError("runtime release destination already exists")
|
|
58
|
+
digests, sizes, _total = _scan_tree(source / "runtime")
|
|
59
|
+
with TemporaryDirectory(prefix=".runtime-", dir=kit) as temporary:
|
|
60
|
+
staging = Path(temporary)
|
|
61
|
+
payload = staging / "runtime"
|
|
62
|
+
payload.mkdir(mode=0o700)
|
|
63
|
+
for relative, digest in digests.items():
|
|
64
|
+
target = payload / relative
|
|
65
|
+
target.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
66
|
+
_copy_verified_file(
|
|
67
|
+
source / "runtime" / relative,
|
|
68
|
+
target,
|
|
69
|
+
expected_digest=digest,
|
|
70
|
+
expected_size=sizes[relative],
|
|
71
|
+
)
|
|
72
|
+
copied = load_runtime_release(
|
|
73
|
+
staging,
|
|
74
|
+
expected_source_commit=source_commit,
|
|
75
|
+
expected_platform_tag=platform_tag,
|
|
76
|
+
)
|
|
77
|
+
if copied.digest != release.digest:
|
|
78
|
+
raise ValueError("runtime inventory changed during staging")
|
|
79
|
+
if copied.schema_version in {"fdai.runtime-release.v2", "fdai.runtime-release.v3"}:
|
|
80
|
+
validate_runtime_images(staging, copied)
|
|
81
|
+
payload.rename(destination)
|
|
82
|
+
return copied.digest
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Install verified runtime support from one signed aggregate lock."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from fdai_deployment_cli.contracts import canonical_digest, load_json_object
|
|
14
|
+
from fdai_deployment_cli.offline_kit import _read_regular as _read_kit_regular
|
|
15
|
+
from fdai_deployment_cli.private_output import write_private_output
|
|
16
|
+
from fdai_deployment_cli.standalone_host_state import private_json as _private_json
|
|
17
|
+
from fdai_deployment_cli.support_install import (
|
|
18
|
+
_validate_requirements as _validate_support_requirements,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
_DIGEST = re.compile(r"[0-9a-f]{64}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def install_runtime_support(
|
|
25
|
+
work_dir: Path, *, artifact_root: Path, kit_manifest_digest: str
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Install the admitted hashed support lock and verify resumable completion."""
|
|
28
|
+
|
|
29
|
+
if _DIGEST.fullmatch(kit_manifest_digest) is None:
|
|
30
|
+
raise ValueError("runtime migration support kit binding is invalid")
|
|
31
|
+
support = artifact_root / "support/python"
|
|
32
|
+
inventory_path = support / "inventory.json"
|
|
33
|
+
requirements = support / "requirements/support.txt"
|
|
34
|
+
try:
|
|
35
|
+
inventory_bytes = _read_kit_regular(inventory_path, 4 * 1024 * 1024)
|
|
36
|
+
requirement_bytes = _read_kit_regular(requirements, 8 * 1024 * 1024)
|
|
37
|
+
except (OSError, UnicodeError, ValueError) as exc:
|
|
38
|
+
raise ValueError("runtime migration support contract is unavailable") from exc
|
|
39
|
+
inventory = load_json_object(
|
|
40
|
+
inventory_bytes,
|
|
41
|
+
label="runtime support inventory",
|
|
42
|
+
max_bytes=4 * 1024 * 1024,
|
|
43
|
+
)
|
|
44
|
+
_validate_support_requirements(requirement_bytes)
|
|
45
|
+
if (
|
|
46
|
+
inventory.get("schema_version") != "fdai.runtime-wheelhouse.v1"
|
|
47
|
+
or inventory.get("artifact_kind") != "local-runtime-wheelhouse"
|
|
48
|
+
or inventory.get("status") != "complete"
|
|
49
|
+
or inventory.get("support_requirements") != "requirements/support.txt"
|
|
50
|
+
):
|
|
51
|
+
raise ValueError("runtime migration support inventory is incomplete")
|
|
52
|
+
files = _mapping(inventory.get("files"), "runtime support file inventory")
|
|
53
|
+
expected_versions: dict[str, str] = {}
|
|
54
|
+
requirement_lines = set(requirement_bytes.decode("utf-8").splitlines())
|
|
55
|
+
for field in ("packages", "support_packages"):
|
|
56
|
+
records = _mapping(inventory.get(field), f"runtime support {field}")
|
|
57
|
+
for name, item in records.items():
|
|
58
|
+
record = _mapping(item, f"runtime support package {name}")
|
|
59
|
+
version, wheel = record.get("version"), record.get("wheel")
|
|
60
|
+
digest = files.get(wheel) if isinstance(wheel, str) else None
|
|
61
|
+
normalized = re.sub(r"[-_.]+", "-", name.casefold())
|
|
62
|
+
if (
|
|
63
|
+
not normalized
|
|
64
|
+
or not isinstance(version, str)
|
|
65
|
+
or not isinstance(wheel, str)
|
|
66
|
+
or not isinstance(digest, str)
|
|
67
|
+
or _DIGEST.fullmatch(digest) is None
|
|
68
|
+
or f"{name}=={version} --hash=sha256:{digest}" not in requirement_lines
|
|
69
|
+
or normalized in expected_versions
|
|
70
|
+
):
|
|
71
|
+
raise ValueError("runtime migration support package contract is invalid")
|
|
72
|
+
expected_versions[normalized] = version
|
|
73
|
+
wheels = sorted(support.rglob("*.whl"))
|
|
74
|
+
if not wheels or any(not wheel.is_file() or wheel.is_symlink() for wheel in wheels):
|
|
75
|
+
raise ValueError("runtime migration support wheelhouse is invalid")
|
|
76
|
+
locations = sorted({wheel.parent for wheel in wheels}, key=str)
|
|
77
|
+
if not 1 <= len(locations) <= 64:
|
|
78
|
+
raise ValueError("runtime migration support wheel locations are invalid")
|
|
79
|
+
|
|
80
|
+
environment = work_dir / "runtime-venv"
|
|
81
|
+
receipt_path = work_dir / "runtime-support-installation.json"
|
|
82
|
+
retained_environment = environment.exists() or environment.is_symlink()
|
|
83
|
+
retained_receipt = receipt_path.exists() or receipt_path.is_symlink()
|
|
84
|
+
if retained_environment or retained_receipt:
|
|
85
|
+
if not retained_environment or not retained_receipt:
|
|
86
|
+
raise ValueError("runtime migration support installation is incomplete")
|
|
87
|
+
receipt = _private_json(receipt_path, "runtime support installation receipt")
|
|
88
|
+
if (
|
|
89
|
+
receipt.get("schema_version") != "fdai.runtime-support-installation.v1"
|
|
90
|
+
or receipt.get("kit_manifest_digest") != kit_manifest_digest
|
|
91
|
+
or receipt.get("inventory_digest") != hashlib.sha256(inventory_bytes).hexdigest()
|
|
92
|
+
or receipt.get("requirements_digest") != hashlib.sha256(requirement_bytes).hexdigest()
|
|
93
|
+
or receipt.get("dependencies_verified") is not True
|
|
94
|
+
):
|
|
95
|
+
raise ValueError("runtime migration support receipt differs")
|
|
96
|
+
readback_digest = _runtime_support_readback(
|
|
97
|
+
environment, expected_versions=expected_versions, work_dir=work_dir
|
|
98
|
+
)
|
|
99
|
+
if receipt.get("package_readback_digest") != readback_digest:
|
|
100
|
+
raise ValueError("runtime migration support package readback differs")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
_run(
|
|
104
|
+
(sys.executable, "-m", "venv", str(environment)),
|
|
105
|
+
cwd=work_dir,
|
|
106
|
+
timeout=120,
|
|
107
|
+
reason="runtime migration environment creation failed",
|
|
108
|
+
)
|
|
109
|
+
links = tuple(
|
|
110
|
+
argument for location in locations for argument in ("--find-links", str(location))
|
|
111
|
+
)
|
|
112
|
+
_run(
|
|
113
|
+
(
|
|
114
|
+
str(environment / "bin/pip"),
|
|
115
|
+
"install",
|
|
116
|
+
"--no-index",
|
|
117
|
+
"--no-cache-dir",
|
|
118
|
+
"--only-binary",
|
|
119
|
+
":all:",
|
|
120
|
+
"--require-hashes",
|
|
121
|
+
*links,
|
|
122
|
+
"--requirement",
|
|
123
|
+
str(requirements),
|
|
124
|
+
),
|
|
125
|
+
cwd=work_dir,
|
|
126
|
+
timeout=900,
|
|
127
|
+
reason="runtime migration support installation failed",
|
|
128
|
+
)
|
|
129
|
+
readback_digest = _runtime_support_readback(
|
|
130
|
+
environment, expected_versions=expected_versions, work_dir=work_dir
|
|
131
|
+
)
|
|
132
|
+
write_private_output(
|
|
133
|
+
receipt_path,
|
|
134
|
+
json.dumps(
|
|
135
|
+
{
|
|
136
|
+
"schema_version": "fdai.runtime-support-installation.v1",
|
|
137
|
+
"kit_manifest_digest": kit_manifest_digest,
|
|
138
|
+
"inventory_digest": hashlib.sha256(inventory_bytes).hexdigest(),
|
|
139
|
+
"requirements_digest": hashlib.sha256(requirement_bytes).hexdigest(),
|
|
140
|
+
"package_readback_digest": readback_digest,
|
|
141
|
+
"dependencies_verified": True,
|
|
142
|
+
},
|
|
143
|
+
sort_keys=True,
|
|
144
|
+
separators=(",", ":"),
|
|
145
|
+
)
|
|
146
|
+
+ "\n",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _runtime_support_readback(
|
|
151
|
+
environment: Path, *, expected_versions: dict[str, str], work_dir: Path
|
|
152
|
+
) -> str:
|
|
153
|
+
pip = str(environment / "bin/pip")
|
|
154
|
+
_run(
|
|
155
|
+
(pip, "check"),
|
|
156
|
+
cwd=work_dir,
|
|
157
|
+
timeout=120,
|
|
158
|
+
reason="runtime migration support dependency check failed",
|
|
159
|
+
)
|
|
160
|
+
try:
|
|
161
|
+
entries = json.loads(
|
|
162
|
+
_capture(
|
|
163
|
+
(pip, "list", "--format=json"),
|
|
164
|
+
cwd=work_dir,
|
|
165
|
+
timeout=120,
|
|
166
|
+
reason="runtime migration support package readback failed",
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
except json.JSONDecodeError as exc:
|
|
170
|
+
raise ValueError("runtime migration support package readback is invalid") from exc
|
|
171
|
+
if not isinstance(entries, list):
|
|
172
|
+
raise ValueError("runtime migration support package readback is invalid")
|
|
173
|
+
observed: dict[str, str] = {}
|
|
174
|
+
for entry in entries:
|
|
175
|
+
if not isinstance(entry, dict):
|
|
176
|
+
raise ValueError("runtime migration support package readback is invalid")
|
|
177
|
+
name, version = entry.get("name"), entry.get("version")
|
|
178
|
+
if not isinstance(name, str) or not isinstance(version, str):
|
|
179
|
+
raise ValueError("runtime migration support package readback is invalid")
|
|
180
|
+
normalized = re.sub(r"[-_.]+", "-", name.casefold())
|
|
181
|
+
if normalized in observed:
|
|
182
|
+
raise ValueError("runtime migration support package readback contains duplicates")
|
|
183
|
+
observed[normalized] = version
|
|
184
|
+
if any(observed.get(name) != version for name, version in expected_versions.items()):
|
|
185
|
+
raise ValueError("runtime migration support packages differ from the signed inventory")
|
|
186
|
+
return canonical_digest(observed)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _run(command: tuple[str, ...], *, cwd: Path, timeout: int, reason: str) -> None:
|
|
190
|
+
result = subprocess.run(command, cwd=cwd, check=False, capture_output=True, timeout=timeout)
|
|
191
|
+
if result.returncode != 0:
|
|
192
|
+
raise ValueError(reason)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _capture(command: tuple[str, ...], *, cwd: Path, timeout: int, reason: str) -> str:
|
|
196
|
+
result = subprocess.run(
|
|
197
|
+
command,
|
|
198
|
+
cwd=cwd,
|
|
199
|
+
check=False,
|
|
200
|
+
capture_output=True,
|
|
201
|
+
text=True,
|
|
202
|
+
timeout=timeout,
|
|
203
|
+
)
|
|
204
|
+
if result.returncode != 0:
|
|
205
|
+
raise ValueError(reason)
|
|
206
|
+
return result.stdout
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _mapping(value: object, label: str) -> dict[str, Any]:
|
|
210
|
+
if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
|
|
211
|
+
raise ValueError(f"{label} is invalid")
|
|
212
|
+
return {str(key): item for key, item in value.items()}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
__all__ = ["install_runtime_support"]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Deterministic no-mutation rehearsal of the genesis state machine."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime, timedelta
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from fdai_deployment_cli.contracts import SubscriptionProvisioningManifest, canonical_digest
|
|
9
|
+
from fdai_deployment_cli.state import (
|
|
10
|
+
GENESIS_HASH,
|
|
11
|
+
ProvisionEvent,
|
|
12
|
+
RunState,
|
|
13
|
+
append_event,
|
|
14
|
+
read_journal,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def rehearse(
|
|
19
|
+
manifest: SubscriptionProvisioningManifest,
|
|
20
|
+
*,
|
|
21
|
+
run_id: str,
|
|
22
|
+
journal: Path,
|
|
23
|
+
interrupt_after: str | None = None,
|
|
24
|
+
started_at: datetime | None = None,
|
|
25
|
+
) -> tuple[ProvisionEvent, ...]:
|
|
26
|
+
"""Rehearse remaining stages and optionally stop after one completed stage."""
|
|
27
|
+
|
|
28
|
+
events = read_journal(journal) if journal.exists() else ()
|
|
29
|
+
if events and events[-1].run_id != run_id:
|
|
30
|
+
raise ValueError("simulation run_id does not match the existing journal")
|
|
31
|
+
if events and events[-1].context_digest != manifest.digest:
|
|
32
|
+
raise ValueError("simulation manifest does not match the existing journal")
|
|
33
|
+
if events and events[-1].state is RunState.READY:
|
|
34
|
+
return events
|
|
35
|
+
known_stages = {entry.entry_id for entry in manifest.entries}
|
|
36
|
+
if interrupt_after is not None and interrupt_after not in known_stages:
|
|
37
|
+
raise ValueError("simulation interrupt stage is not in the manifest")
|
|
38
|
+
completed = {event.stage for event in events if event.state is RunState.COMPLETED}
|
|
39
|
+
previous_digest = events[-1].digest if events else GENESIS_HASH
|
|
40
|
+
sequence = len(events)
|
|
41
|
+
base = started_at or datetime.now(UTC)
|
|
42
|
+
for entry in manifest.entries:
|
|
43
|
+
if entry.entry_id in completed:
|
|
44
|
+
continue
|
|
45
|
+
sequence += 1
|
|
46
|
+
event = ProvisionEvent(
|
|
47
|
+
run_id=run_id,
|
|
48
|
+
context_digest=manifest.digest,
|
|
49
|
+
sequence=sequence,
|
|
50
|
+
stage=entry.entry_id,
|
|
51
|
+
attempt=1,
|
|
52
|
+
state=RunState.COMPLETED,
|
|
53
|
+
occurred_at=(base + timedelta(microseconds=sequence)).isoformat(),
|
|
54
|
+
previous_digest=previous_digest,
|
|
55
|
+
reason_code="simulation-only",
|
|
56
|
+
evidence_digest=canonical_digest(
|
|
57
|
+
{"mode": "simulation", "stage": entry.entry_id, "status": "completed"}
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
append_event(journal, event)
|
|
61
|
+
previous_digest = event.digest
|
|
62
|
+
if entry.entry_id == interrupt_after:
|
|
63
|
+
return read_journal(journal)
|
|
64
|
+
sequence += 1
|
|
65
|
+
ready = ProvisionEvent(
|
|
66
|
+
run_id=run_id,
|
|
67
|
+
context_digest=manifest.digest,
|
|
68
|
+
sequence=sequence,
|
|
69
|
+
stage="system-readiness",
|
|
70
|
+
attempt=1,
|
|
71
|
+
state=RunState.READY,
|
|
72
|
+
occurred_at=(base + timedelta(microseconds=sequence)).isoformat(),
|
|
73
|
+
previous_digest=previous_digest,
|
|
74
|
+
reason_code="simulation-only",
|
|
75
|
+
evidence_digest=canonical_digest(
|
|
76
|
+
{"mode": "simulation", "stage": "system-readiness", "status": "ready"}
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
append_event(journal, ready)
|
|
80
|
+
return read_journal(journal)
|