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,164 @@
|
|
|
1
|
+
"""Derive bounded source snapshots for one service image build."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shlex
|
|
7
|
+
from pathlib import PurePosixPath
|
|
8
|
+
|
|
9
|
+
from fdai_deployment_cli.runtime_release import RUNTIME_SERVICES
|
|
10
|
+
from fdai_deployment_cli.source_input import SourceDeploymentInput, _git, _read_tracked
|
|
11
|
+
|
|
12
|
+
_SUPPORT_FILES = frozenset(
|
|
13
|
+
{
|
|
14
|
+
".dockerignore",
|
|
15
|
+
"scripts/automation/run-bounded-command.py",
|
|
16
|
+
"scripts/deployment/azure/genesis_checks.py",
|
|
17
|
+
"scripts/deployment/azure/genesis_subprocess.py",
|
|
18
|
+
"scripts/deployment/azure/source_image_build.py",
|
|
19
|
+
}
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def service_source_projection(
|
|
24
|
+
source: SourceDeploymentInput,
|
|
25
|
+
service: str,
|
|
26
|
+
) -> tuple[str, ...]:
|
|
27
|
+
"""Return the exact tracked files needed to build one baseline service image.
|
|
28
|
+
|
|
29
|
+
The Dockerfile remains the source of its copy inputs. Unsupported Dockerfile
|
|
30
|
+
syntax fails closed instead of broadening the projection to the repository.
|
|
31
|
+
"""
|
|
32
|
+
if service not in RUNTIME_SERVICES:
|
|
33
|
+
raise ValueError("source projection service is outside the baseline inventory")
|
|
34
|
+
entries = _tracked_entries(source)
|
|
35
|
+
dockerfile = f"services/{service}/docker/Dockerfile"
|
|
36
|
+
if dockerfile not in entries:
|
|
37
|
+
raise ValueError("source projection Dockerfile is unavailable")
|
|
38
|
+
content = _read_tracked(source.root, dockerfile, mode=entries[dockerfile]).decode("utf-8")
|
|
39
|
+
selected = set(_SUPPORT_FILES)
|
|
40
|
+
explicit_filtered: set[str] = set()
|
|
41
|
+
selected.add(dockerfile)
|
|
42
|
+
for item in _docker_copy_sources(content):
|
|
43
|
+
expanded = _expand_tracked_source(item, entries)
|
|
44
|
+
selected.update(expanded)
|
|
45
|
+
if _explicit_filtered_source(item):
|
|
46
|
+
explicit_filtered.update(expanded)
|
|
47
|
+
missing = selected - entries.keys()
|
|
48
|
+
if missing:
|
|
49
|
+
raise ValueError("source projection requires unavailable tracked inputs")
|
|
50
|
+
selected = {
|
|
51
|
+
path for path in selected if path in explicit_filtered or _included_build_input(path)
|
|
52
|
+
}
|
|
53
|
+
_include_symlink_targets(source, selected, entries)
|
|
54
|
+
return tuple(sorted(selected))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _tracked_entries(source: SourceDeploymentInput) -> dict[str, str]:
|
|
58
|
+
entries: dict[str, str] = {}
|
|
59
|
+
for entry in _git(source.root, "ls-tree", "-rz", "--full-tree", source.commit).split(b"\0"):
|
|
60
|
+
if not entry:
|
|
61
|
+
continue
|
|
62
|
+
metadata, raw_path = entry.split(b"\t", 1)
|
|
63
|
+
mode, kind, _blob = metadata.decode("ascii").split()
|
|
64
|
+
if kind != "blob" or mode not in {"100644", "100755", "120000"}:
|
|
65
|
+
raise ValueError("source projection contains an unsupported tracked input")
|
|
66
|
+
entries[raw_path.decode("utf-8")] = mode
|
|
67
|
+
return entries
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _docker_copy_sources(content: str) -> tuple[str, ...]:
|
|
71
|
+
logical: list[str] = []
|
|
72
|
+
pending = ""
|
|
73
|
+
for raw_line in content.splitlines():
|
|
74
|
+
line = raw_line.strip()
|
|
75
|
+
if not line or line.startswith("#"):
|
|
76
|
+
continue
|
|
77
|
+
pending = f"{pending} {line}".strip()
|
|
78
|
+
if pending.endswith("\\"):
|
|
79
|
+
pending = pending[:-1].rstrip()
|
|
80
|
+
continue
|
|
81
|
+
logical.append(pending)
|
|
82
|
+
pending = ""
|
|
83
|
+
if pending:
|
|
84
|
+
raise ValueError("source projection Dockerfile has an incomplete instruction")
|
|
85
|
+
|
|
86
|
+
sources: list[str] = []
|
|
87
|
+
for instruction in logical:
|
|
88
|
+
parts = shlex.split(instruction)
|
|
89
|
+
if not parts:
|
|
90
|
+
continue
|
|
91
|
+
if parts[0].upper() == "ADD":
|
|
92
|
+
raise ValueError("source projection does not accept Docker ADD inputs")
|
|
93
|
+
if parts[0].upper() != "COPY":
|
|
94
|
+
continue
|
|
95
|
+
if any(part.startswith("--from=") for part in parts[1:]):
|
|
96
|
+
continue
|
|
97
|
+
operands = [part for part in parts[1:] if not part.startswith("--")]
|
|
98
|
+
if len(operands) < 2:
|
|
99
|
+
raise ValueError("source projection COPY instruction is invalid")
|
|
100
|
+
for value in operands[:-1]:
|
|
101
|
+
if any(character in value for character in "*?[") or value.startswith("$"):
|
|
102
|
+
raise ValueError("source projection does not accept dynamic COPY inputs")
|
|
103
|
+
path = PurePosixPath(value)
|
|
104
|
+
if path.is_absolute() or any(part in {"", ".", ".."} for part in path.parts):
|
|
105
|
+
raise ValueError("source projection COPY input is invalid")
|
|
106
|
+
sources.append(path.as_posix())
|
|
107
|
+
if not sources:
|
|
108
|
+
raise ValueError("source projection Dockerfile has no local COPY inputs")
|
|
109
|
+
return tuple(sources)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _expand_tracked_source(value: str, entries: dict[str, str]) -> set[str]:
|
|
113
|
+
if value in entries:
|
|
114
|
+
return {value}
|
|
115
|
+
prefix = value.rstrip("/") + "/"
|
|
116
|
+
matches = {path for path in entries if path.startswith(prefix)}
|
|
117
|
+
if not matches:
|
|
118
|
+
raise ValueError("source projection COPY input is unavailable")
|
|
119
|
+
return matches
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _included_build_input(path: str) -> bool:
|
|
123
|
+
if "/tests/" in path and not path.startswith("services/core-control-plane/tests/scenarios/"):
|
|
124
|
+
return False
|
|
125
|
+
if path.endswith(".md") and path not in {
|
|
126
|
+
"README.md",
|
|
127
|
+
"rule-catalog/README.md",
|
|
128
|
+
"extensions/cost-governance/README.md",
|
|
129
|
+
"extensions/cost-governance/README-ko.md",
|
|
130
|
+
}:
|
|
131
|
+
return False
|
|
132
|
+
parts = PurePosixPath(path).parts
|
|
133
|
+
return not (
|
|
134
|
+
"__pycache__" in parts
|
|
135
|
+
or "node_modules" in parts
|
|
136
|
+
or "dist" in parts
|
|
137
|
+
or path.endswith(".pyc")
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _explicit_filtered_source(path: str) -> bool:
|
|
142
|
+
parts = PurePosixPath(path).parts
|
|
143
|
+
return "tests" in parts or "docs" in parts or path.endswith(".md")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _include_symlink_targets(
|
|
147
|
+
source: SourceDeploymentInput,
|
|
148
|
+
selected: set[str],
|
|
149
|
+
entries: dict[str, str],
|
|
150
|
+
) -> None:
|
|
151
|
+
pending = [path for path in selected if entries[path] == "120000"]
|
|
152
|
+
while pending:
|
|
153
|
+
relative = pending.pop()
|
|
154
|
+
target = os.fsdecode(_read_tracked(source.root, relative, mode="120000"))
|
|
155
|
+
resolved = PurePosixPath(relative).parent / target
|
|
156
|
+
if resolved.is_absolute() or ".." in resolved.parts:
|
|
157
|
+
raise ValueError("source projection link leaves the tracked file set")
|
|
158
|
+
normalized = resolved.as_posix()
|
|
159
|
+
if normalized not in entries:
|
|
160
|
+
raise ValueError("source projection link target is unavailable")
|
|
161
|
+
if normalized not in selected:
|
|
162
|
+
selected.add(normalized)
|
|
163
|
+
if entries[normalized] == "120000":
|
|
164
|
+
pending.append(normalized)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Build a dependency-free receiver from the exact selected source, never a release kit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import io
|
|
7
|
+
import zipfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from fdai_deployment_cli.private_output import read_private_bytes, write_private_bytes
|
|
11
|
+
from fdai_deployment_cli.source_input import _read_tracked
|
|
12
|
+
from fdai_deployment_cli.source_snapshot import verify_source_snapshot
|
|
13
|
+
from fdai_deployment_cli.source_transport import _records
|
|
14
|
+
|
|
15
|
+
_MODULES = (
|
|
16
|
+
"__init__",
|
|
17
|
+
"__about__",
|
|
18
|
+
"contracts",
|
|
19
|
+
"private_output",
|
|
20
|
+
"source_input",
|
|
21
|
+
"source_snapshot",
|
|
22
|
+
"source_transport",
|
|
23
|
+
)
|
|
24
|
+
_ENTRY = b"from fdai_deployment_cli.source_transport import main\nraise SystemExit(main())\n"
|
|
25
|
+
_MAX_BOOTSTRAP = 4 * 1024 * 1024
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def prepare_source_receiver(snapshot: Path, work_dir: Path, *, snapshot_digest: str) -> str:
|
|
29
|
+
"""Retain a deterministic private zipapp and return its independently transferable digest.
|
|
30
|
+
|
|
31
|
+
Only verified snapshot modules and a fixed launcher are packaged. This does not install
|
|
32
|
+
dependencies, execute source, authorize effects, or grant signed-release provenance.
|
|
33
|
+
A retained bootstrap must match exactly; ambiguous partial outputs are never overwritten.
|
|
34
|
+
"""
|
|
35
|
+
verify_source_snapshot(snapshot, expected_digest=snapshot_digest)
|
|
36
|
+
records = {
|
|
37
|
+
record["path"]: record
|
|
38
|
+
for record in _records(
|
|
39
|
+
read_private_bytes(snapshot / "source-input.json", max_bytes=16 * 1024 * 1024),
|
|
40
|
+
snapshot_digest,
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
payload = io.BytesIO()
|
|
44
|
+
with zipfile.ZipFile(payload, "w", compression=zipfile.ZIP_STORED) as archive:
|
|
45
|
+
_add(archive, "__main__.py", _ENTRY)
|
|
46
|
+
for module in _MODULES:
|
|
47
|
+
relative = f"packages/deployment-cli/src/fdai_deployment_cli/{module}.py"
|
|
48
|
+
content = _read_tracked(snapshot / "tree", relative, mode="100644")
|
|
49
|
+
record = records.get(relative)
|
|
50
|
+
if (
|
|
51
|
+
record is None
|
|
52
|
+
or record["mode"] != "100644"
|
|
53
|
+
or hashlib.sha256(content).hexdigest() != record["sha256"]
|
|
54
|
+
):
|
|
55
|
+
raise ValueError("source receiver module differs from the selected snapshot")
|
|
56
|
+
if len(content) > _MAX_BOOTSTRAP or payload.tell() + len(content) > _MAX_BOOTSTRAP:
|
|
57
|
+
raise ValueError("source receiver bootstrap exceeds its bound")
|
|
58
|
+
_add(archive, f"fdai_deployment_cli/{module}.py", content)
|
|
59
|
+
verify_source_snapshot(snapshot, expected_digest=snapshot_digest)
|
|
60
|
+
content = payload.getvalue()
|
|
61
|
+
if len(content) > _MAX_BOOTSTRAP:
|
|
62
|
+
raise ValueError("source receiver bootstrap exceeds its bound")
|
|
63
|
+
destination = work_dir / "source-receiver.pyz"
|
|
64
|
+
if destination.exists() or destination.is_symlink():
|
|
65
|
+
if read_private_bytes(destination, max_bytes=_MAX_BOOTSTRAP) != content:
|
|
66
|
+
raise ValueError("source receiver bootstrap differs; preserve the run")
|
|
67
|
+
else:
|
|
68
|
+
write_private_bytes(destination, content)
|
|
69
|
+
return hashlib.sha256(content).hexdigest()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _add(archive: zipfile.ZipFile, name: str, content: bytes) -> None:
|
|
73
|
+
member = zipfile.ZipInfo(name)
|
|
74
|
+
member.create_system = 3
|
|
75
|
+
member.external_attr = 0o100600 << 16
|
|
76
|
+
archive.writestr(member, content)
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""Resume an immutable source installation with explicitly selected recovery execution code."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from fdai_deployment_cli.contracts import canonical_digest, load_json_object
|
|
11
|
+
from fdai_deployment_cli.deployment_deadline import DeploymentDeadline
|
|
12
|
+
from fdai_deployment_cli.private_output import read_private_bytes
|
|
13
|
+
from fdai_deployment_cli.runtime_profile import RuntimeDeploymentProfile
|
|
14
|
+
from fdai_deployment_cli.source_input import inspect_source
|
|
15
|
+
from fdai_deployment_cli.source_snapshot import verify_source_snapshot
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class RecoveredApplicationEvidence:
|
|
20
|
+
"""Verified recovered Foundation inputs for source application continuation."""
|
|
21
|
+
|
|
22
|
+
status: dict[str, object]
|
|
23
|
+
run_binding: str
|
|
24
|
+
state_handoff_digest: str
|
|
25
|
+
source_snapshot_digest: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def retained_source_run(
|
|
29
|
+
work_dir: Path,
|
|
30
|
+
*,
|
|
31
|
+
runtime_profile_digest: str,
|
|
32
|
+
region: str,
|
|
33
|
+
monthly_cost_ceiling: int,
|
|
34
|
+
) -> dict[str, object]:
|
|
35
|
+
"""Verify original application source and selected settings without rewriting the run."""
|
|
36
|
+
intent = load_json_object(
|
|
37
|
+
read_private_bytes(work_dir / "source-intent.json", max_bytes=65536),
|
|
38
|
+
label="original source intent",
|
|
39
|
+
)
|
|
40
|
+
preparation = load_json_object(
|
|
41
|
+
read_private_bytes(work_dir / "source-preparation.json", max_bytes=65536),
|
|
42
|
+
label="original source preparation",
|
|
43
|
+
)
|
|
44
|
+
digest = preparation.pop("receipt_digest", None)
|
|
45
|
+
if (
|
|
46
|
+
canonical_digest(preparation) != digest
|
|
47
|
+
or preparation.get("schema_version") != "fdai.source-deployment-preparation.v1"
|
|
48
|
+
or preparation.get("state") != "prepared"
|
|
49
|
+
or preparation.get("provenance") != "operator-selected-source"
|
|
50
|
+
or preparation.get("release_signature_verified") is not False
|
|
51
|
+
or any(
|
|
52
|
+
preparation.get(key) is not False
|
|
53
|
+
for key in (
|
|
54
|
+
"apply_authorized",
|
|
55
|
+
"mutation_performed",
|
|
56
|
+
"deployment_ready",
|
|
57
|
+
"subscription_ready",
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
or preparation.get("intent_digest") != canonical_digest(intent)
|
|
61
|
+
or intent.get("schema_version") != "fdai.source-deployment-intent.v1"
|
|
62
|
+
or intent.get("environment") != "dev"
|
|
63
|
+
or intent.get("region") != region
|
|
64
|
+
or intent.get("monthly_cost_ceiling") != monthly_cost_ceiling
|
|
65
|
+
or preparation.get("runtime_profile_digest") != runtime_profile_digest
|
|
66
|
+
or not isinstance(intent.get("runtime_profile"), dict)
|
|
67
|
+
or canonical_digest(intent["runtime_profile"]) != runtime_profile_digest
|
|
68
|
+
):
|
|
69
|
+
raise ValueError("source recovery must preserve the original installation settings")
|
|
70
|
+
snapshot = verify_source_snapshot(
|
|
71
|
+
work_dir / "source-snapshot", expected_digest=str(preparation.get("source_snapshot_digest"))
|
|
72
|
+
)
|
|
73
|
+
if (
|
|
74
|
+
snapshot != intent.get("source")
|
|
75
|
+
or snapshot.get("source_commit") != preparation.get("source_commit")
|
|
76
|
+
or canonical_digest(snapshot) != preparation.get("source_input_digest")
|
|
77
|
+
or canonical_digest(snapshot) != intent.get("source_input_digest")
|
|
78
|
+
):
|
|
79
|
+
raise ValueError("source recovery application snapshot differs from the original run")
|
|
80
|
+
preparation["receipt_digest"] = digest
|
|
81
|
+
return preparation
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def recovered_application_evidence(
|
|
85
|
+
*,
|
|
86
|
+
work_dir: Path,
|
|
87
|
+
recovery_directory: Path,
|
|
88
|
+
progress: dict[str, object],
|
|
89
|
+
runtime_profile_digest: str,
|
|
90
|
+
region: str,
|
|
91
|
+
monthly_cost_ceiling: int,
|
|
92
|
+
) -> RecoveredApplicationEvidence:
|
|
93
|
+
"""Reverify the complete recovered handoff before source application effects."""
|
|
94
|
+
|
|
95
|
+
prepared = retained_source_run(
|
|
96
|
+
work_dir,
|
|
97
|
+
runtime_profile_digest=runtime_profile_digest,
|
|
98
|
+
region=region,
|
|
99
|
+
monthly_cost_ceiling=monthly_cost_ceiling,
|
|
100
|
+
)
|
|
101
|
+
if (
|
|
102
|
+
progress.get("stage") != "application-plan"
|
|
103
|
+
or progress.get("reason_code") != "source_application_continuation_ready"
|
|
104
|
+
or progress.get("next_action") != "continue_with_original_source_snapshot"
|
|
105
|
+
):
|
|
106
|
+
raise ValueError("source recovery is not ready for application continuation")
|
|
107
|
+
|
|
108
|
+
foundation_root = work_dir / "foundation"
|
|
109
|
+
marker = _private_record(foundation_root / "source-genesis.json", "source Genesis marker")
|
|
110
|
+
marker_digest = marker.pop("receipt_digest", None)
|
|
111
|
+
recovery = _private_record(
|
|
112
|
+
recovery_directory / "recovery-apply-receipt.json",
|
|
113
|
+
"Foundation recovery receipt",
|
|
114
|
+
)
|
|
115
|
+
recovery_digest = recovery.pop("receipt_digest", None)
|
|
116
|
+
enrollment = _private_record(
|
|
117
|
+
recovery_directory / "runner-enrollment-receipt.json",
|
|
118
|
+
"recovered runner enrollment receipt",
|
|
119
|
+
)
|
|
120
|
+
enrollment_digest = enrollment.pop("receipt_digest", None)
|
|
121
|
+
state = _private_record(
|
|
122
|
+
recovery_directory / "foundation-state-handoff-receipt.json",
|
|
123
|
+
"recovered Foundation state receipt",
|
|
124
|
+
)
|
|
125
|
+
state_digest = state.pop("receipt_digest", None)
|
|
126
|
+
handoff = read_private_bytes(
|
|
127
|
+
recovery_directory / "recovery-private-handoff.json",
|
|
128
|
+
max_bytes=1024 * 1024,
|
|
129
|
+
)
|
|
130
|
+
status = _private_record(foundation_root / "status.json", "source Foundation status")
|
|
131
|
+
required_state = (
|
|
132
|
+
"effect_verified",
|
|
133
|
+
"runner_attested",
|
|
134
|
+
"remote_backend_authority_verified",
|
|
135
|
+
"zero_change_verified",
|
|
136
|
+
"remote_transient_deleted",
|
|
137
|
+
"local_state_deleted",
|
|
138
|
+
)
|
|
139
|
+
if (
|
|
140
|
+
canonical_digest(marker) != marker_digest
|
|
141
|
+
or marker.get("source_commit") != prepared["source_commit"]
|
|
142
|
+
or marker.get("run_binding") != progress.get("run_binding")
|
|
143
|
+
or marker.get("target_binding") != progress.get("target_binding")
|
|
144
|
+
or canonical_digest(recovery) != recovery_digest
|
|
145
|
+
or recovery.get("schema_version") != "fdai.foundation-recovery-receipt.v1"
|
|
146
|
+
or recovery.get("state") != "verified"
|
|
147
|
+
or recovery.get("source_commit") != prepared["source_commit"]
|
|
148
|
+
or recovery.get("control_plane_readback_verified") is not True
|
|
149
|
+
or recovery.get("zero_change_verified") is not True
|
|
150
|
+
or recovery_digest != progress.get("foundation_receipt_digest")
|
|
151
|
+
or canonical_digest(enrollment) != enrollment_digest
|
|
152
|
+
or enrollment.get("schema_version") != "fdai.genesis-runner-enrollment-receipt.v1"
|
|
153
|
+
or enrollment.get("state") != "attested"
|
|
154
|
+
or enrollment.get("foundation_receipt_digest") != recovery_digest
|
|
155
|
+
or enrollment_digest != progress.get("enrollment_receipt_digest")
|
|
156
|
+
or canonical_digest(state) != state_digest
|
|
157
|
+
or state.get("schema_version") != "fdai.genesis-foundation-state-handoff-receipt.v1"
|
|
158
|
+
or state.get("state") != "verified"
|
|
159
|
+
or state.get("source_commit") != prepared["source_commit"]
|
|
160
|
+
or state.get("target_binding") != progress.get("target_binding")
|
|
161
|
+
or state.get("foundation_receipt_digest") != recovery_digest
|
|
162
|
+
or state.get("enrollment_receipt_digest") != enrollment_digest
|
|
163
|
+
or any(state.get(field) is not True for field in required_state)
|
|
164
|
+
or state_digest != progress.get("state_handoff_digest")
|
|
165
|
+
or canonical_digest(load_json_object(handoff, label="recovered Foundation handoff"))
|
|
166
|
+
!= recovery.get("handoff_digest")
|
|
167
|
+
):
|
|
168
|
+
raise ValueError("recovered source application evidence differs from the verified handoff")
|
|
169
|
+
return RecoveredApplicationEvidence(
|
|
170
|
+
status=status,
|
|
171
|
+
run_binding=str(marker["run_binding"]),
|
|
172
|
+
state_handoff_digest=str(state_digest),
|
|
173
|
+
source_snapshot_digest=str(prepared["source_snapshot_digest"]),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _private_record(path: Path, label: str) -> dict[str, object]:
|
|
178
|
+
return load_json_object(
|
|
179
|
+
read_private_bytes(path, max_bytes=1024 * 1024),
|
|
180
|
+
label=label,
|
|
181
|
+
max_bytes=1024 * 1024,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def resume_source_installation(
|
|
186
|
+
*,
|
|
187
|
+
source_root: Path,
|
|
188
|
+
work_dir: Path,
|
|
189
|
+
recovery_directory: Path,
|
|
190
|
+
runtime_profile: RuntimeDeploymentProfile,
|
|
191
|
+
region: str,
|
|
192
|
+
monthly_cost_ceiling: int,
|
|
193
|
+
timeout_seconds: int,
|
|
194
|
+
approval_file: Path | None,
|
|
195
|
+
) -> dict[str, object]:
|
|
196
|
+
"""Invoke current recovery code while retaining the original application's exact source.
|
|
197
|
+
|
|
198
|
+
This explicit route never starts a new installation, renews an approval, or
|
|
199
|
+
repeats Foundation apply. Only receipt-bound enrollment and migration may advance.
|
|
200
|
+
"""
|
|
201
|
+
from fdai_deployment_cli.source_azure import _capture
|
|
202
|
+
|
|
203
|
+
deadline = DeploymentDeadline(timeout_seconds)
|
|
204
|
+
source = inspect_source(source_root)
|
|
205
|
+
work_dir, recovery_directory = work_dir.absolute(), recovery_directory.absolute()
|
|
206
|
+
prepared = retained_source_run(
|
|
207
|
+
work_dir,
|
|
208
|
+
runtime_profile_digest=runtime_profile.digest,
|
|
209
|
+
region=region,
|
|
210
|
+
monthly_cost_ceiling=monthly_cost_ceiling,
|
|
211
|
+
)
|
|
212
|
+
environment = {
|
|
213
|
+
key: value
|
|
214
|
+
for key, value in os.environ.items()
|
|
215
|
+
if key
|
|
216
|
+
in {"HOME", "PATH", "AZURE_CONFIG_DIR", "LANG", "SSL_CERT_FILE", "REQUESTS_CA_BUNDLE"}
|
|
217
|
+
}
|
|
218
|
+
environment["PYTHONPATH"] = str(source.root / "packages/deployment-cli/src")
|
|
219
|
+
source.reverify()
|
|
220
|
+
result = _capture(
|
|
221
|
+
(
|
|
222
|
+
sys.executable,
|
|
223
|
+
str(source.root / "scripts/deployment/azure/source_recovery.py"),
|
|
224
|
+
"--work-dir",
|
|
225
|
+
str(work_dir),
|
|
226
|
+
"--recovery-directory",
|
|
227
|
+
str(recovery_directory),
|
|
228
|
+
"--runtime-profile-digest",
|
|
229
|
+
runtime_profile.digest,
|
|
230
|
+
"--region",
|
|
231
|
+
region,
|
|
232
|
+
"--monthly-cost-ceiling",
|
|
233
|
+
str(monthly_cost_ceiling),
|
|
234
|
+
"--timeout-seconds",
|
|
235
|
+
str(deadline.remaining(14400)),
|
|
236
|
+
*(
|
|
237
|
+
("--approval-file", str(approval_file.absolute()))
|
|
238
|
+
if approval_file is not None
|
|
239
|
+
else ()
|
|
240
|
+
),
|
|
241
|
+
),
|
|
242
|
+
source.root,
|
|
243
|
+
environment,
|
|
244
|
+
deadline.remaining(14400),
|
|
245
|
+
)
|
|
246
|
+
source.reverify()
|
|
247
|
+
if (
|
|
248
|
+
result.get("schema_version") != "fdai.source-recovery-progress.v1"
|
|
249
|
+
or result.get("state") != "review"
|
|
250
|
+
or result.get("source_commit") != prepared["source_commit"]
|
|
251
|
+
or result.get("execution_source_commit") != source.commit
|
|
252
|
+
or result.get("runtime_profile_digest") != runtime_profile.digest
|
|
253
|
+
or result.get("provenance") != "operator-selected-source"
|
|
254
|
+
or any(
|
|
255
|
+
result.get(key) is not False
|
|
256
|
+
for key in ("apply_authorized", "deployment_ready", "release_signature_verified")
|
|
257
|
+
)
|
|
258
|
+
or canonical_digest(
|
|
259
|
+
{key: value for key, value in result.items() if key != "receipt_digest"}
|
|
260
|
+
)
|
|
261
|
+
!= result.get("receipt_digest")
|
|
262
|
+
):
|
|
263
|
+
raise ValueError("source recovery returned progress for a different execution context")
|
|
264
|
+
return result
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Verify source-bound runtime content without release trust or execution authority."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from fdai_deployment_cli.contracts import canonical_digest
|
|
9
|
+
from fdai_deployment_cli.runtime_build import _regular_digest
|
|
10
|
+
from fdai_deployment_cli.runtime_release import load_runtime_release, validate_runtime_images
|
|
11
|
+
from fdai_deployment_cli.source_snapshot import verify_source_snapshot
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def verify_source_runtime(
|
|
15
|
+
*,
|
|
16
|
+
snapshot: Path,
|
|
17
|
+
snapshot_digest: str,
|
|
18
|
+
runtime_root: Path,
|
|
19
|
+
runtime_digest: str,
|
|
20
|
+
deployment_bundle: Path,
|
|
21
|
+
bundle_digest: str,
|
|
22
|
+
platform_tag: str,
|
|
23
|
+
) -> dict[str, object]:
|
|
24
|
+
"""Return no-authority content evidence for independently pinned local inputs.
|
|
25
|
+
|
|
26
|
+
This read-only check rejects changed snapshots, catalogs, payloads, revision,
|
|
27
|
+
platform or bundle bytes. OCI validation checks all service images and ClamAV;
|
|
28
|
+
other payloads remain opaque. No signature, bundle-source correspondence,
|
|
29
|
+
toolchain, support semantics, host identity or execution eligibility is proved.
|
|
30
|
+
Callers must preserve exclusive ownership and reverify inputs before later use.
|
|
31
|
+
"""
|
|
32
|
+
for digest in (snapshot_digest, runtime_digest, bundle_digest):
|
|
33
|
+
if not isinstance(digest, str) or re.fullmatch(r"[0-9a-f]{64}", digest) is None:
|
|
34
|
+
raise ValueError("source runtime requires independently pinned SHA-256 digests")
|
|
35
|
+
if any(not path.is_absolute() for path in (snapshot, runtime_root, deployment_bundle)):
|
|
36
|
+
raise ValueError("source runtime paths must be absolute")
|
|
37
|
+
source = verify_source_snapshot(snapshot, expected_digest=snapshot_digest)
|
|
38
|
+
commit = source.get("source_commit")
|
|
39
|
+
if not isinstance(commit, str) or re.fullmatch(r"[0-9a-f]{40}", commit) is None:
|
|
40
|
+
raise ValueError("source runtime source commit is invalid")
|
|
41
|
+
release = load_runtime_release(
|
|
42
|
+
runtime_root, expected_source_commit=commit, expected_platform_tag=platform_tag
|
|
43
|
+
)
|
|
44
|
+
if release.digest != runtime_digest:
|
|
45
|
+
raise ValueError("source runtime catalog differs from the retained digest")
|
|
46
|
+
if (
|
|
47
|
+
release.deployment_bundle_sha256 != bundle_digest
|
|
48
|
+
or _regular_digest(deployment_bundle, label="source deployment bundle") != bundle_digest
|
|
49
|
+
):
|
|
50
|
+
raise ValueError("source runtime deployment bundle differs from the retained digest")
|
|
51
|
+
images = validate_runtime_images(runtime_root, release)
|
|
52
|
+
observed = load_runtime_release(
|
|
53
|
+
runtime_root, expected_source_commit=commit, expected_platform_tag=platform_tag
|
|
54
|
+
)
|
|
55
|
+
if (
|
|
56
|
+
observed.digest != runtime_digest
|
|
57
|
+
or verify_source_snapshot(snapshot, expected_digest=snapshot_digest) != source
|
|
58
|
+
or _regular_digest(deployment_bundle, label="source deployment bundle") != bundle_digest
|
|
59
|
+
):
|
|
60
|
+
raise ValueError("source runtime inputs changed during verification")
|
|
61
|
+
result: dict[str, object] = {
|
|
62
|
+
"schema_version": "fdai.source-runtime-content.v1",
|
|
63
|
+
"state": "content-verified",
|
|
64
|
+
"source_commit": commit,
|
|
65
|
+
"source_snapshot_digest": snapshot_digest,
|
|
66
|
+
"runtime_release_digest": runtime_digest,
|
|
67
|
+
"deployment_bundle_sha256": bundle_digest,
|
|
68
|
+
"platform_tag": platform_tag,
|
|
69
|
+
"artifact_count": len(release.artifact_paths),
|
|
70
|
+
"image_content_digests": images,
|
|
71
|
+
"provenance": "operator-selected-source",
|
|
72
|
+
"release_signature_verified": False,
|
|
73
|
+
"bundle_source_verified": False,
|
|
74
|
+
"support_contents_verified": False,
|
|
75
|
+
"registry_published": False,
|
|
76
|
+
"apply_authorized": False,
|
|
77
|
+
"deployment_ready": False,
|
|
78
|
+
"mutation_performed": False,
|
|
79
|
+
}
|
|
80
|
+
result["receipt_digest"] = canonical_digest(result)
|
|
81
|
+
return result
|