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.
Files changed (203) hide show
  1. fdai_deployment_cli/__about__.py +3 -0
  2. fdai_deployment_cli/__init__.py +5 -0
  3. fdai_deployment_cli/__main__.py +5 -0
  4. fdai_deployment_cli/acr_publish.py +585 -0
  5. fdai_deployment_cli/aks_catalog_review.py +137 -0
  6. fdai_deployment_cli/aks_historical_reconciliation.py +732 -0
  7. fdai_deployment_cli/aks_job_environment.py +59 -0
  8. fdai_deployment_cli/aks_job_execution.py +794 -0
  9. fdai_deployment_cli/aks_kubernetes_defaults.py +66 -0
  10. fdai_deployment_cli/aks_preflight.py +422 -0
  11. fdai_deployment_cli/aks_readiness.py +220 -0
  12. fdai_deployment_cli/aks_service_update.py +180 -0
  13. fdai_deployment_cli/aks_workload_jobs.py +281 -0
  14. fdai_deployment_cli/application_state_adoption.py +347 -0
  15. fdai_deployment_cli/azure_actor.py +61 -0
  16. fdai_deployment_cli/azure_naming.py +252 -0
  17. fdai_deployment_cli/azure_retail_prices.py +173 -0
  18. fdai_deployment_cli/bootstrap_approval.py +99 -0
  19. fdai_deployment_cli/bootstrap_channel.py +321 -0
  20. fdai_deployment_cli/bootstrap_executor.py +311 -0
  21. fdai_deployment_cli/bootstrap_plan.py +237 -0
  22. fdai_deployment_cli/bootstrap_reconcile.py +402 -0
  23. fdai_deployment_cli/bootstrap_support.py +138 -0
  24. fdai_deployment_cli/bootstrap_worker.py +448 -0
  25. fdai_deployment_cli/bundle.py +352 -0
  26. fdai_deployment_cli/capacity.py +143 -0
  27. fdai_deployment_cli/catalog_review_profile.py +306 -0
  28. fdai_deployment_cli/cli.py +798 -0
  29. fdai_deployment_cli/cli_help.py +214 -0
  30. fdai_deployment_cli/cli_parser.py +791 -0
  31. fdai_deployment_cli/cli_plan.py +450 -0
  32. fdai_deployment_cli/compiler.py +99 -0
  33. fdai_deployment_cli/console_artifact.py +218 -0
  34. fdai_deployment_cli/console_config.py +214 -0
  35. fdai_deployment_cli/console_update.py +832 -0
  36. fdai_deployment_cli/contracts.py +307 -0
  37. fdai_deployment_cli/control_package.py +140 -0
  38. fdai_deployment_cli/deadline_transport.py +70 -0
  39. fdai_deployment_cli/deployment_cost.py +131 -0
  40. fdai_deployment_cli/deployment_deadline.py +28 -0
  41. fdai_deployment_cli/deployment_kit.py +790 -0
  42. fdai_deployment_cli/deployment_kit_cache.py +191 -0
  43. fdai_deployment_cli/deployment_progress.py +648 -0
  44. fdai_deployment_cli/development_authority_cli.py +56 -0
  45. fdai_deployment_cli/development_authority_profile.py +319 -0
  46. fdai_deployment_cli/doctor.py +136 -0
  47. fdai_deployment_cli/entitlement_preflight.py +68 -0
  48. fdai_deployment_cli/entitlement_transport.py +49 -0
  49. fdai_deployment_cli/entra_bootstrap.py +579 -0
  50. fdai_deployment_cli/entra_control_profile.py +214 -0
  51. fdai_deployment_cli/entra_graph.py +349 -0
  52. fdai_deployment_cli/entra_profiles.py +215 -0
  53. fdai_deployment_cli/entra_source.py +244 -0
  54. fdai_deployment_cli/execution_bundle.py +478 -0
  55. fdai_deployment_cli/execution_copies.py +63 -0
  56. fdai_deployment_cli/foundation_adoption.py +438 -0
  57. fdai_deployment_cli/foundation_adoption_evidence.py +245 -0
  58. fdai_deployment_cli/foundation_adoption_host.py +157 -0
  59. fdai_deployment_cli/foundation_adoption_transport.py +51 -0
  60. fdai_deployment_cli/foundation_failure.py +72 -0
  61. fdai_deployment_cli/foundation_image.py +168 -0
  62. fdai_deployment_cli/foundation_input.py +235 -0
  63. fdai_deployment_cli/foundation_output.py +154 -0
  64. fdai_deployment_cli/foundation_plan.py +381 -0
  65. fdai_deployment_cli/foundation_process.py +64 -0
  66. fdai_deployment_cli/foundation_progress.py +231 -0
  67. fdai_deployment_cli/github_actions.py +171 -0
  68. fdai_deployment_cli/github_apply_artifact.py +259 -0
  69. fdai_deployment_cli/github_artifact_io.py +48 -0
  70. fdai_deployment_cli/github_dispatch.py +248 -0
  71. fdai_deployment_cli/github_plan_artifact.py +181 -0
  72. fdai_deployment_cli/github_provider_artifact.py +170 -0
  73. fdai_deployment_cli/github_transport.py +46 -0
  74. fdai_deployment_cli/github_workflow_values.py +312 -0
  75. fdai_deployment_cli/hub_image_deploy.py +354 -0
  76. fdai_deployment_cli/hub_image_stage.py +162 -0
  77. fdai_deployment_cli/hub_image_trust.py +281 -0
  78. fdai_deployment_cli/hub_ownership.py +161 -0
  79. fdai_deployment_cli/hub_workload_handoff.py +264 -0
  80. fdai_deployment_cli/hub_workload_handoff_inputs.py +227 -0
  81. fdai_deployment_cli/identity_profile.py +227 -0
  82. fdai_deployment_cli/image_targets.py +197 -0
  83. fdai_deployment_cli/installation_scope.py +280 -0
  84. fdai_deployment_cli/license.py +311 -0
  85. fdai_deployment_cli/license_issue.py +239 -0
  86. fdai_deployment_cli/lifecycle_authorization.py +252 -0
  87. fdai_deployment_cli/lifecycle_commands.py +173 -0
  88. fdai_deployment_cli/lifecycle_configuration.py +722 -0
  89. fdai_deployment_cli/lifecycle_configuration_artifact.py +45 -0
  90. fdai_deployment_cli/lifecycle_configuration_cli.py +324 -0
  91. fdai_deployment_cli/lifecycle_configuration_document.py +74 -0
  92. fdai_deployment_cli/lifecycle_configuration_entities.py +146 -0
  93. fdai_deployment_cli/lifecycle_configuration_package.py +492 -0
  94. fdai_deployment_cli/lifecycle_configuration_publication.py +277 -0
  95. fdai_deployment_cli/lifecycle_configuration_publication_cli.py +191 -0
  96. fdai_deployment_cli/lifecycle_configuration_sealing.py +127 -0
  97. fdai_deployment_cli/lifecycle_enrollment.py +45 -0
  98. fdai_deployment_cli/lifecycle_infrastructure_agent.py +174 -0
  99. fdai_deployment_cli/lifecycle_installation_binding.py +65 -0
  100. fdai_deployment_cli/lifecycle_native.py +247 -0
  101. fdai_deployment_cli/lifecycle_native_reconciliation.py +568 -0
  102. fdai_deployment_cli/lifecycle_native_transition.py +339 -0
  103. fdai_deployment_cli/lifecycle_ownership_exclusion.py +78 -0
  104. fdai_deployment_cli/lifecycle_plan.py +800 -0
  105. fdai_deployment_cli/lifecycle_promotion.py +185 -0
  106. fdai_deployment_cli/lifecycle_release.py +308 -0
  107. fdai_deployment_cli/lifecycle_self_upgrade.py +202 -0
  108. fdai_deployment_cli/lifecycle_upgrade_bundle.py +176 -0
  109. fdai_deployment_cli/notification_recipients_input.py +222 -0
  110. fdai_deployment_cli/observer_artifact.py +106 -0
  111. fdai_deployment_cli/oci_archive.py +353 -0
  112. fdai_deployment_cli/offline_kit.py +797 -0
  113. fdai_deployment_cli/offline_prepare.py +135 -0
  114. fdai_deployment_cli/operational_evidence_verifier_input.py +575 -0
  115. fdai_deployment_cli/plan_input.py +101 -0
  116. fdai_deployment_cli/private_output.py +192 -0
  117. fdai_deployment_cli/profile.py +70 -0
  118. fdai_deployment_cli/progress.py +183 -0
  119. fdai_deployment_cli/readiness.py +286 -0
  120. fdai_deployment_cli/run_command_receiver.py +418 -0
  121. fdai_deployment_cli/runtime_build.py +386 -0
  122. fdai_deployment_cli/runtime_profile.py +287 -0
  123. fdai_deployment_cli/runtime_release.py +745 -0
  124. fdai_deployment_cli/runtime_stage.py +82 -0
  125. fdai_deployment_cli/runtime_support_installation.py +215 -0
  126. fdai_deployment_cli/simulation.py +80 -0
  127. fdai_deployment_cli/source_application_inputs.py +397 -0
  128. fdai_deployment_cli/source_azure.py +586 -0
  129. fdai_deployment_cli/source_change_cli.py +107 -0
  130. fdai_deployment_cli/source_change_parser.py +75 -0
  131. fdai_deployment_cli/source_change_plan.py +181 -0
  132. fdai_deployment_cli/source_changed_update.py +196 -0
  133. fdai_deployment_cli/source_deploy.py +170 -0
  134. fdai_deployment_cli/source_foundation.py +174 -0
  135. fdai_deployment_cli/source_host_tools.py +163 -0
  136. fdai_deployment_cli/source_image_stage.py +400 -0
  137. fdai_deployment_cli/source_input.py +173 -0
  138. fdai_deployment_cli/source_projection.py +164 -0
  139. fdai_deployment_cli/source_receiver.py +76 -0
  140. fdai_deployment_cli/source_recovery.py +264 -0
  141. fdai_deployment_cli/source_runtime.py +81 -0
  142. fdai_deployment_cli/source_runtime_support.py +368 -0
  143. fdai_deployment_cli/source_service_update.py +726 -0
  144. fdai_deployment_cli/source_service_update_cli.py +47 -0
  145. fdai_deployment_cli/source_snapshot.py +178 -0
  146. fdai_deployment_cli/source_teardown.py +574 -0
  147. fdai_deployment_cli/source_terraform.py +85 -0
  148. fdai_deployment_cli/source_transport.py +399 -0
  149. fdai_deployment_cli/standalone_aks_inventory.py +558 -0
  150. fdai_deployment_cli/standalone_aks_job_execution.py +769 -0
  151. fdai_deployment_cli/standalone_aks_nodepool_guard.py +141 -0
  152. fdai_deployment_cli/standalone_application.py +977 -0
  153. fdai_deployment_cli/standalone_application_completion.py +151 -0
  154. fdai_deployment_cli/standalone_application_input_recovery.py +107 -0
  155. fdai_deployment_cli/standalone_application_recovery.py +116 -0
  156. fdai_deployment_cli/standalone_approval_input.py +24 -0
  157. fdai_deployment_cli/standalone_catalog_checkpoint.py +122 -0
  158. fdai_deployment_cli/standalone_catalog_review.py +251 -0
  159. fdai_deployment_cli/standalone_checkpoint_failure.py +127 -0
  160. fdai_deployment_cli/standalone_console_publish.py +174 -0
  161. fdai_deployment_cli/standalone_database_principals.py +41 -0
  162. fdai_deployment_cli/standalone_deploy.py +638 -0
  163. fdai_deployment_cli/standalone_development_authority.py +166 -0
  164. fdai_deployment_cli/standalone_foundation_adoption.py +87 -0
  165. fdai_deployment_cli/standalone_foundation_transition.py +767 -0
  166. fdai_deployment_cli/standalone_host.py +4608 -0
  167. fdai_deployment_cli/standalone_host_rebind.py +226 -0
  168. fdai_deployment_cli/standalone_host_residual.py +104 -0
  169. fdai_deployment_cli/standalone_host_state.py +145 -0
  170. fdai_deployment_cli/standalone_host_values.py +563 -0
  171. fdai_deployment_cli/standalone_kubernetes_tools.py +100 -0
  172. fdai_deployment_cli/standalone_license_installation.py +177 -0
  173. fdai_deployment_cli/standalone_management_egress.py +83 -0
  174. fdai_deployment_cli/standalone_migration_evidence.py +38 -0
  175. fdai_deployment_cli/standalone_operational_evidence.py +356 -0
  176. fdai_deployment_cli/standalone_planned_outputs.py +147 -0
  177. fdai_deployment_cli/standalone_private_access_probe.py +100 -0
  178. fdai_deployment_cli/standalone_product_profile.py +110 -0
  179. fdai_deployment_cli/standalone_remote_prepare.py +442 -0
  180. fdai_deployment_cli/standalone_residual_apply.py +540 -0
  181. fdai_deployment_cli/standalone_residual_successor.py +431 -0
  182. fdai_deployment_cli/standalone_review.py +266 -0
  183. fdai_deployment_cli/standalone_service_update_finalization.py +116 -0
  184. fdai_deployment_cli/standalone_service_update_recovery.py +277 -0
  185. fdai_deployment_cli/standalone_stage_targets.py +325 -0
  186. fdai_deployment_cli/standalone_status.py +67 -0
  187. fdai_deployment_cli/standalone_terraform_environment.py +237 -0
  188. fdai_deployment_cli/standalone_transfer_cleanup.py +237 -0
  189. fdai_deployment_cli/standalone_trial_activation.py +193 -0
  190. fdai_deployment_cli/state.py +434 -0
  191. fdai_deployment_cli/state_handoff.py +253 -0
  192. fdai_deployment_cli/status_projection.py +97 -0
  193. fdai_deployment_cli/support_install.py +257 -0
  194. fdai_deployment_cli/target.py +46 -0
  195. fdai_deployment_cli/trust/capabilities.json +62 -0
  196. fdai_deployment_cli/trust/deployment-bundle-root.pub +3 -0
  197. fdai_deployment_cli/trust/deployment-release-root.pub +3 -0
  198. fdai_deployment_cli/trust/upstream-signing-key.pub +3 -0
  199. fdai_deployment_cli/trust_roots.py +50 -0
  200. fdai_deployment_cli-0.1.2.dist-info/METADATA +112 -0
  201. fdai_deployment_cli-0.1.2.dist-info/RECORD +203 -0
  202. fdai_deployment_cli-0.1.2.dist-info/WHEEL +4 -0
  203. 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()}