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,397 @@
|
|
|
1
|
+
"""Source-only inputs for standalone application continuation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import hashlib
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import zipapp
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from tempfile import TemporaryDirectory
|
|
13
|
+
from typing import Any, Protocol
|
|
14
|
+
|
|
15
|
+
from fdai_service_contracts.product_profile import ObservationDataSource, ProductAddOn
|
|
16
|
+
|
|
17
|
+
from fdai_deployment_cli.contracts import canonical_digest
|
|
18
|
+
from fdai_deployment_cli.source_host_tools import install_kubernetes_tools
|
|
19
|
+
from fdai_deployment_cli.source_image_stage import (
|
|
20
|
+
AzureRegistryBuildService,
|
|
21
|
+
SourceImageSnapshot,
|
|
22
|
+
SourceImageStageStopped,
|
|
23
|
+
SourceImageTarget,
|
|
24
|
+
run_source_image_stage,
|
|
25
|
+
)
|
|
26
|
+
from fdai_deployment_cli.source_receiver import prepare_source_receiver
|
|
27
|
+
from fdai_deployment_cli.source_runtime_support import (
|
|
28
|
+
INPUT_NAME,
|
|
29
|
+
install_source_runtime_support,
|
|
30
|
+
prepare_runtime_support_input,
|
|
31
|
+
)
|
|
32
|
+
from fdai_deployment_cli.source_snapshot import verify_source_snapshot
|
|
33
|
+
from fdai_deployment_cli.source_transport import prepare_source_transport
|
|
34
|
+
from fdai_deployment_cli.standalone_terraform_environment import source_terraform_configuration
|
|
35
|
+
from fdai_deployment_cli.standalone_terraform_environment import (
|
|
36
|
+
terraform_configuration as mirror_terraform_configuration,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
_PLACEHOLDER_DIGEST = "sha256:" + ("0" * 64)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Subparsers(Protocol):
|
|
43
|
+
def add_parser(self, name: str) -> Any: ...
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True, slots=True)
|
|
47
|
+
class SourceTransferInputs:
|
|
48
|
+
archive: Path
|
|
49
|
+
archive_digest: str
|
|
50
|
+
receiver: Path
|
|
51
|
+
receiver_digest: str
|
|
52
|
+
runtime_support: Path
|
|
53
|
+
runtime_support_digest: str
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, slots=True)
|
|
57
|
+
class SourceHostArtifacts:
|
|
58
|
+
source_commit: str
|
|
59
|
+
artifact_root: Path
|
|
60
|
+
infra: Path
|
|
61
|
+
terraform: Path
|
|
62
|
+
kit_bin: Path
|
|
63
|
+
digest: str
|
|
64
|
+
|
|
65
|
+
def snapshot_metadata(self) -> dict[str, object]:
|
|
66
|
+
return {
|
|
67
|
+
"source_snapshot": str(self.artifact_root.parent),
|
|
68
|
+
"source_snapshot_digest": self.digest,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def absolute_input_path(path: Path) -> Path:
|
|
73
|
+
return path if path.is_absolute() else Path.cwd() / path
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def prepare_host_artifacts(
|
|
77
|
+
args: argparse.Namespace, work_dir: Path, source_commit: str
|
|
78
|
+
) -> SourceHostArtifacts:
|
|
79
|
+
return source_host_artifacts(
|
|
80
|
+
source_snapshot=absolute_input_path(args.source_snapshot),
|
|
81
|
+
snapshot_digest=str(args.source_snapshot_digest),
|
|
82
|
+
expected_source_commit=source_commit,
|
|
83
|
+
work_dir=work_dir,
|
|
84
|
+
runtime_support=absolute_input_path(args.runtime_support),
|
|
85
|
+
runtime_support_digest=str(args.runtime_support_digest),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def add_prepare_source_parser(subcommands: Subparsers, handler: object) -> None:
|
|
90
|
+
"""Register the managed-host source preparation command."""
|
|
91
|
+
|
|
92
|
+
prepare_source = subcommands.add_parser("prepare-source")
|
|
93
|
+
prepare_source.add_argument("--source-snapshot", type=Path, required=True)
|
|
94
|
+
prepare_source.add_argument("--source-snapshot-digest", required=True)
|
|
95
|
+
prepare_source.add_argument("--runtime-support", type=Path, required=True)
|
|
96
|
+
prepare_source.add_argument("--runtime-support-digest", required=True)
|
|
97
|
+
prepare_source.add_argument("--hub-selection", type=Path)
|
|
98
|
+
prepare_source.add_argument("--hub-selection-digest")
|
|
99
|
+
prepare_source.add_argument("--handoff", type=Path, required=True)
|
|
100
|
+
prepare_source.add_argument("--entra", type=Path)
|
|
101
|
+
prepare_source.add_argument("--foundation-adoption", type=Path)
|
|
102
|
+
prepare_source.add_argument("--adoption-state", type=Path)
|
|
103
|
+
prepare_source.add_argument("--adoption-models", type=Path)
|
|
104
|
+
prepare_source.add_argument("--adoption-descriptor", type=Path)
|
|
105
|
+
prepare_source.add_argument("--catalog-review-profile", type=Path)
|
|
106
|
+
prepare_source.add_argument("--catalog-review-private-key", type=Path)
|
|
107
|
+
prepare_source.add_argument("--operational-evidence-verifier-input", type=Path)
|
|
108
|
+
prepare_source.add_argument("--notification-recipients", type=Path)
|
|
109
|
+
prepare_source.add_argument("--development-authority-profile", type=Path)
|
|
110
|
+
prepare_source.add_argument(
|
|
111
|
+
"--clear-development-profile",
|
|
112
|
+
action="store_true",
|
|
113
|
+
dest="clear_development_authority_profile",
|
|
114
|
+
)
|
|
115
|
+
prepare_source.add_argument("--runtime-platform", default="aks")
|
|
116
|
+
prepare_source.add_argument("--database-placement", default="postgres-flex")
|
|
117
|
+
prepare_source.add_argument("--system-node-count", type=int, default=3)
|
|
118
|
+
prepare_source.add_argument("--system-node-sku", default=None)
|
|
119
|
+
prepare_source.add_argument("--user-node-min-count", type=int, default=3)
|
|
120
|
+
prepare_source.add_argument("--user-node-max-count", type=int, default=5)
|
|
121
|
+
prepare_source.add_argument("--user-node-sku", default="Standard_D4ds_v4")
|
|
122
|
+
prepare_source.add_argument("--database-sku")
|
|
123
|
+
prepare_source.add_argument(
|
|
124
|
+
"--product-add-on",
|
|
125
|
+
action="append",
|
|
126
|
+
choices=tuple(item.value for item in ProductAddOn),
|
|
127
|
+
default=[],
|
|
128
|
+
)
|
|
129
|
+
prepare_source.add_argument(
|
|
130
|
+
"--observation-source",
|
|
131
|
+
action="append",
|
|
132
|
+
choices=tuple(item.value for item in ObservationDataSource),
|
|
133
|
+
default=[],
|
|
134
|
+
)
|
|
135
|
+
prepare_source.set_defaults(handler=handler)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def placeholder_image_refs(login_server: str, *, include_pgvector: bool) -> dict[str, str]:
|
|
139
|
+
refs = {
|
|
140
|
+
name: f"{login_server}/{name}@{_PLACEHOLDER_DIGEST}"
|
|
141
|
+
for name in (
|
|
142
|
+
"core-control-plane",
|
|
143
|
+
"operator-service",
|
|
144
|
+
"document-ingestion-api",
|
|
145
|
+
"document-processing-worker",
|
|
146
|
+
"isolated-executor",
|
|
147
|
+
"clamav",
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
if include_pgvector:
|
|
151
|
+
refs["pgvector"] = f"{login_server}/pgvector@{_PLACEHOLDER_DIGEST}"
|
|
152
|
+
return refs
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def source_tree_copy(snapshot_tree: Path, work_dir: Path) -> Path:
|
|
156
|
+
"""Return a private working copy of the verified snapshot tree.
|
|
157
|
+
|
|
158
|
+
Terraform writes backend and lock files into its roots and Python may write bytecode beside
|
|
159
|
+
migration code. Doing that inside the snapshot would change the file set that later stages
|
|
160
|
+
verify, so every host step that runs from the tree uses this copy instead.
|
|
161
|
+
"""
|
|
162
|
+
copy = work_dir / "source-tree"
|
|
163
|
+
if not copy.exists():
|
|
164
|
+
staging = work_dir / ".source-tree-staging"
|
|
165
|
+
shutil.rmtree(staging, ignore_errors=True)
|
|
166
|
+
shutil.copytree(snapshot_tree, staging, symlinks=True)
|
|
167
|
+
staging.chmod(0o700)
|
|
168
|
+
staging.rename(copy)
|
|
169
|
+
elif copy.is_symlink() or not copy.is_dir():
|
|
170
|
+
raise ValueError("source working tree is invalid")
|
|
171
|
+
return copy
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def rebind_source_tree(context: dict[str, object], work_dir: Path) -> bool:
|
|
175
|
+
"""Move a retained source context's working paths out of the snapshot."""
|
|
176
|
+
if context.get("artifact_source") not in {"operator-selected-source", "hub-signed-images"}:
|
|
177
|
+
return False
|
|
178
|
+
snapshot_tree = Path(str(context["source_snapshot"])) / "tree"
|
|
179
|
+
prefix = str(snapshot_tree)
|
|
180
|
+
copy = str(source_tree_copy(snapshot_tree, work_dir))
|
|
181
|
+
changed = False
|
|
182
|
+
for key, value in list(context.items()):
|
|
183
|
+
if isinstance(value, str) and (value == prefix or value.startswith(prefix + "/")):
|
|
184
|
+
context[key] = copy + value[len(prefix) :]
|
|
185
|
+
changed = True
|
|
186
|
+
return changed
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def source_host_artifacts(
|
|
190
|
+
*,
|
|
191
|
+
source_snapshot: Path,
|
|
192
|
+
snapshot_digest: str,
|
|
193
|
+
expected_source_commit: str,
|
|
194
|
+
work_dir: Path,
|
|
195
|
+
runtime_support: Path,
|
|
196
|
+
runtime_support_digest: str,
|
|
197
|
+
) -> SourceHostArtifacts:
|
|
198
|
+
source_record = verify_source_snapshot(source_snapshot, expected_digest=snapshot_digest)
|
|
199
|
+
source_commit = str(source_record["source_commit"])
|
|
200
|
+
if source_commit != expected_source_commit:
|
|
201
|
+
raise ValueError("Foundation and source snapshot revisions differ")
|
|
202
|
+
artifact_root = source_snapshot / "tree"
|
|
203
|
+
working_tree = source_tree_copy(artifact_root, work_dir)
|
|
204
|
+
install_source_runtime_support(
|
|
205
|
+
work_dir,
|
|
206
|
+
source_root=working_tree,
|
|
207
|
+
archive=runtime_support,
|
|
208
|
+
archive_digest=runtime_support_digest,
|
|
209
|
+
snapshot_digest=snapshot_digest,
|
|
210
|
+
)
|
|
211
|
+
terraform_path = shutil.which("terraform")
|
|
212
|
+
if terraform_path is None:
|
|
213
|
+
raise ValueError("source deployment requires Terraform on the managed host")
|
|
214
|
+
terraform = Path(terraform_path)
|
|
215
|
+
return SourceHostArtifacts(
|
|
216
|
+
source_commit=source_commit,
|
|
217
|
+
artifact_root=artifact_root,
|
|
218
|
+
infra=working_tree / "infra",
|
|
219
|
+
terraform=terraform,
|
|
220
|
+
kit_bin=install_kubernetes_tools(work_dir / "source-tools"),
|
|
221
|
+
digest=snapshot_digest,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def source_transfer_inputs(
|
|
226
|
+
*, prepared_root: Path, source_snapshot: Path, source_snapshot_digest: str
|
|
227
|
+
) -> SourceTransferInputs:
|
|
228
|
+
transfer = prepare_source_transport(
|
|
229
|
+
source_snapshot,
|
|
230
|
+
prepared_root,
|
|
231
|
+
snapshot_digest=source_snapshot_digest,
|
|
232
|
+
)
|
|
233
|
+
receiver_digest = prepare_source_receiver(
|
|
234
|
+
source_snapshot,
|
|
235
|
+
prepared_root,
|
|
236
|
+
snapshot_digest=source_snapshot_digest,
|
|
237
|
+
)
|
|
238
|
+
return SourceTransferInputs(
|
|
239
|
+
archive=prepared_root / str(transfer["archive_ref"]),
|
|
240
|
+
archive_digest=str(transfer["archive_digest"]),
|
|
241
|
+
receiver=prepared_root / "source-receiver.pyz",
|
|
242
|
+
receiver_digest=receiver_digest,
|
|
243
|
+
runtime_support=prepared_root / INPUT_NAME,
|
|
244
|
+
runtime_support_digest=prepare_runtime_support_input(
|
|
245
|
+
source_snapshot / "tree", prepared_root
|
|
246
|
+
),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def prepare_source_recovery_host(*, source_root: Path, prepared_root: Path) -> tuple[Path, str]:
|
|
251
|
+
"""Build current recovery-only host tooling without replacing application source."""
|
|
252
|
+
|
|
253
|
+
package_root = source_root / "packages/deployment-cli/src"
|
|
254
|
+
package = package_root / "fdai_deployment_cli"
|
|
255
|
+
if (
|
|
256
|
+
not source_root.is_absolute()
|
|
257
|
+
or not package.is_dir()
|
|
258
|
+
or package.is_symlink()
|
|
259
|
+
or not prepared_root.is_absolute()
|
|
260
|
+
):
|
|
261
|
+
raise ValueError("source recovery host input is invalid")
|
|
262
|
+
output = prepared_root / "source-recovery-host.pyz"
|
|
263
|
+
temporary = prepared_root / ".source-recovery-host.pyz"
|
|
264
|
+
if output.is_symlink() or temporary.exists() or temporary.is_symlink():
|
|
265
|
+
raise ValueError("source recovery host output is invalid")
|
|
266
|
+
with TemporaryDirectory(prefix=".source-recovery-host-", dir=prepared_root) as staging:
|
|
267
|
+
staging_root = Path(staging)
|
|
268
|
+
shutil.copytree(package, staging_root / "fdai_deployment_cli")
|
|
269
|
+
(staging_root / "__main__.py").write_text(
|
|
270
|
+
"from fdai_deployment_cli.standalone_host import main\nraise SystemExit(main())\n",
|
|
271
|
+
encoding="utf-8",
|
|
272
|
+
)
|
|
273
|
+
zipapp.create_archive(staging_root, target=temporary, compressed=True)
|
|
274
|
+
temporary.chmod(0o600)
|
|
275
|
+
os.replace(temporary, output)
|
|
276
|
+
return output, hashlib.sha256(output.read_bytes()).hexdigest()
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def build_source_console(
|
|
280
|
+
*,
|
|
281
|
+
source_root: Path,
|
|
282
|
+
source_commit: str,
|
|
283
|
+
prepared_root: Path,
|
|
284
|
+
timeout_seconds: int,
|
|
285
|
+
) -> tuple[Path, str]:
|
|
286
|
+
from fdai_deployment_cli.console_artifact import build_console_update_artifact
|
|
287
|
+
|
|
288
|
+
output = prepared_root / "source-console-artifact"
|
|
289
|
+
# A rerun rebuilds the same pinned source; publication verifies the new archive digest.
|
|
290
|
+
if output.is_symlink():
|
|
291
|
+
raise ValueError("source Console artifact directory is invalid")
|
|
292
|
+
if output.exists():
|
|
293
|
+
shutil.rmtree(output)
|
|
294
|
+
artifact = build_console_update_artifact(
|
|
295
|
+
source_root=source_root,
|
|
296
|
+
revision=source_commit,
|
|
297
|
+
output_dir=output,
|
|
298
|
+
timeout_seconds=timeout_seconds,
|
|
299
|
+
require_protected_revision=False,
|
|
300
|
+
)
|
|
301
|
+
return Path(str(artifact["artifact_directory"])) / "console.tar.gz", str(
|
|
302
|
+
artifact["archive_sha256"]
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def source_image_import_receipt(
|
|
307
|
+
*, context: dict[str, object], work_dir: Path
|
|
308
|
+
) -> tuple[dict[str, object], dict[str, object]]:
|
|
309
|
+
snapshot_path = Path(str(context.get("source_snapshot", "")))
|
|
310
|
+
snapshot_digest = str(context.get("source_snapshot_digest", ""))
|
|
311
|
+
source_record = verify_source_snapshot(snapshot_path, expected_digest=snapshot_digest)
|
|
312
|
+
commit = str(source_record["source_commit"])
|
|
313
|
+
target = SourceImageTarget(
|
|
314
|
+
subscription_id=str(context["subscription_id"]),
|
|
315
|
+
registry_name=str(context["registry_name"]),
|
|
316
|
+
)
|
|
317
|
+
try:
|
|
318
|
+
if context.get("artifact_source") == "hub-signed-images":
|
|
319
|
+
from fdai_deployment_cli.hub_image_stage import run_hub_image_stage
|
|
320
|
+
from fdai_deployment_cli.hub_image_trust import load_selection
|
|
321
|
+
|
|
322
|
+
selection = load_selection(
|
|
323
|
+
Path(str(context["hub_selection"])), str(context["hub_selection_digest"])
|
|
324
|
+
)
|
|
325
|
+
if selection.source_commit != commit:
|
|
326
|
+
raise ValueError("Hub image selection differs from the support snapshot")
|
|
327
|
+
receipt = run_hub_image_stage(
|
|
328
|
+
selection=selection,
|
|
329
|
+
target=target,
|
|
330
|
+
registry=AzureRegistryBuildService(),
|
|
331
|
+
work_dir=work_dir / "hub-image-stage",
|
|
332
|
+
)
|
|
333
|
+
elif context.get("artifact_source") == "operator-selected-source":
|
|
334
|
+
if (work_dir / "hub-selection.json").exists():
|
|
335
|
+
raise ValueError("source builds cannot reuse Hub image inputs")
|
|
336
|
+
receipt = run_source_image_stage(
|
|
337
|
+
snapshot=SourceImageSnapshot(
|
|
338
|
+
root=snapshot_path / "tree",
|
|
339
|
+
commit=commit,
|
|
340
|
+
digest=snapshot_digest,
|
|
341
|
+
),
|
|
342
|
+
target=target,
|
|
343
|
+
builder=AzureRegistryBuildService(),
|
|
344
|
+
work_dir=work_dir / "source-image-stage",
|
|
345
|
+
)
|
|
346
|
+
else:
|
|
347
|
+
raise ValueError("image artifact source is unknown; no build fallback")
|
|
348
|
+
except SourceImageStageStopped as exc:
|
|
349
|
+
raise ValueError(exc.reason_code) from exc
|
|
350
|
+
image_refs = _mapping(receipt.get("image_refs"), "source image stage receipt")
|
|
351
|
+
context["image_refs"] = image_refs
|
|
352
|
+
result: dict[str, object] = {
|
|
353
|
+
"schema_version": "fdai.source-image-import-receipt.v1",
|
|
354
|
+
"state": "imported",
|
|
355
|
+
"source_image_stage_receipt_digest": receipt["receipt_digest"],
|
|
356
|
+
"image_digests": _mapping(receipt.get("image_digests"), "source image stage receipt"),
|
|
357
|
+
"provenance": "operator-selected-source",
|
|
358
|
+
"release_signature_verified": False,
|
|
359
|
+
"effect_verified": True,
|
|
360
|
+
"mutation_performed": True,
|
|
361
|
+
"subscription_ready": False,
|
|
362
|
+
}
|
|
363
|
+
result["receipt_digest"] = canonical_digest(result)
|
|
364
|
+
if context.get("artifact_source") == "hub-signed-images":
|
|
365
|
+
result.update(selection.metadata())
|
|
366
|
+
result["schema_version"] = "fdai.hub-image-import-receipt.v1"
|
|
367
|
+
result.pop("source_image_stage_receipt_digest", None)
|
|
368
|
+
result["hub_image_stage_receipt_digest"] = receipt["receipt_digest"]
|
|
369
|
+
result.pop("receipt_digest", None)
|
|
370
|
+
result["receipt_digest"] = canonical_digest(result)
|
|
371
|
+
return result, context
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def terraform_configuration(*, source_mode: bool, provider_mirror: Path | None) -> str:
|
|
375
|
+
if source_mode:
|
|
376
|
+
return source_terraform_configuration()
|
|
377
|
+
if provider_mirror is None:
|
|
378
|
+
raise ValueError("signed-kit provider mirror is unavailable")
|
|
379
|
+
return mirror_terraform_configuration(provider_mirror)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def write_source_image_import_receipt(
|
|
383
|
+
*,
|
|
384
|
+
context: dict[str, object],
|
|
385
|
+
work_dir: Path,
|
|
386
|
+
write_json: Any,
|
|
387
|
+
) -> dict[str, object]:
|
|
388
|
+
result, updated_context = source_image_import_receipt(context=context, work_dir=work_dir)
|
|
389
|
+
for name, value in (("context.json", updated_context), ("image-import-receipt.json", result)):
|
|
390
|
+
write_json(work_dir / name, value)
|
|
391
|
+
return result
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _mapping(value: object, label: str) -> dict[str, Any]:
|
|
395
|
+
if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
|
|
396
|
+
raise ValueError(f"{label} is invalid")
|
|
397
|
+
return {str(key): item for key, item in value.items()}
|