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,226 @@
1
+ """Retained managed-host input rebind operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import copy
7
+ import re
8
+ from collections.abc import Callable
9
+ from pathlib import Path
10
+
11
+ from fdai_deployment_cli import standalone_planned_outputs, standalone_terraform_environment
12
+ from fdai_deployment_cli.contracts import canonical_digest
13
+ from fdai_deployment_cli.standalone_host_state import private_json, replace_private_json
14
+ from fdai_deployment_cli.standalone_license_installation import aks_license_environment
15
+ from fdai_deployment_cli.standalone_stage_targets import (
16
+ focused_private_access,
17
+ reconcile_retained_private_access,
18
+ )
19
+
20
+ ManagedIdentityLogin = Callable[[dict[str, object], Path], None]
21
+
22
+
23
+ def reconcile_retained_aks_license_binding(
24
+ retained_values: dict[str, object],
25
+ current_values: dict[str, object],
26
+ ) -> bool:
27
+ """Admit only the first verified license binding into a retained Core workload."""
28
+ retained_workloads = retained_values.get("workloads")
29
+ current_workloads = current_values.get("workloads")
30
+ if not isinstance(retained_workloads, dict) or not isinstance(current_workloads, dict):
31
+ raise TypeError("retained AKS workload inventory is invalid")
32
+ retained_core = retained_workloads.get("core-control-plane")
33
+ current_core = current_workloads.get("core-control-plane")
34
+ if not isinstance(retained_core, dict) or not isinstance(current_core, dict):
35
+ raise TypeError("retained Core workload is invalid")
36
+ environment_keys = {"FDAI_LICENSE_IMAGE_DIGEST", "FDAI_LICENSE_TOKEN_REVISION"}
37
+ secret_keys = {"FDAI_LICENSE_TOKEN"}
38
+ changed = False
39
+ for field, allowed in (
40
+ ("environment", environment_keys),
41
+ ("secret_environment", secret_keys),
42
+ ):
43
+ retained = retained_core.get(field)
44
+ current = current_core.get(field)
45
+ if not isinstance(retained, dict) or not isinstance(current, dict):
46
+ raise TypeError("retained Core license binding is invalid")
47
+ previous = {key: retained.get(key) for key in allowed if key in retained}
48
+ selected = {key: current.get(key) for key in allowed if key in current}
49
+ if previous and previous != selected:
50
+ raise ValueError("retained Core license binding differs")
51
+ if not previous and selected:
52
+ retained.update(selected)
53
+ changed = True
54
+ if changed:
55
+ image_digest = retained_core["environment"].get("FDAI_LICENSE_IMAGE_DIGEST")
56
+ revision = retained_core["environment"].get("FDAI_LICENSE_TOKEN_REVISION")
57
+ token_secret = retained_core["secret_environment"].get("FDAI_LICENSE_TOKEN")
58
+ if (
59
+ not isinstance(image_digest, str)
60
+ or re.fullmatch(r"[0-9a-f]{64}", image_digest) is None
61
+ or not isinstance(revision, str)
62
+ or re.fullmatch(r"[0-9a-f]{64}", revision) is None
63
+ or token_secret != "fdai-capability-license"
64
+ ):
65
+ raise ValueError("new Core license binding is invalid")
66
+ return changed
67
+
68
+
69
+ def bind_installed_license(
70
+ _args: argparse.Namespace,
71
+ work_dir: Path,
72
+ ) -> dict[str, object]:
73
+ """Bind an already verified entitlement to the retained Core workload contract."""
74
+ application_values = private_json(
75
+ work_dir / "application.auto.tfvars.json",
76
+ "application variables",
77
+ )
78
+ environment, secrets = aks_license_environment(application_values)
79
+ if not environment or secrets != {"FDAI_LICENSE_TOKEN": "fdai-capability-license"}:
80
+ raise ValueError("verified installed license binding is unavailable")
81
+ workloads_path = work_dir / "workloads.auto.tfvars.json"
82
+ context_path = work_dir / "context.json"
83
+ retained_values = private_json(workloads_path, "AKS workload variables")
84
+ current_values = copy.deepcopy(retained_values)
85
+ current_workloads = current_values.get("workloads")
86
+ if not isinstance(current_workloads, dict):
87
+ raise TypeError("AKS workload inventory is invalid")
88
+ current_core = current_workloads.get("core-control-plane")
89
+ if not isinstance(current_core, dict):
90
+ raise TypeError("Core workload is invalid")
91
+ current_environment = current_core.get("environment")
92
+ current_secrets = current_core.get("secret_environment")
93
+ if not isinstance(current_environment, dict) or not isinstance(current_secrets, dict):
94
+ raise TypeError("Core workload environment is invalid")
95
+ current_environment.update(environment)
96
+ current_secrets.update(secrets)
97
+ changed = reconcile_retained_aks_license_binding(retained_values, current_values)
98
+
99
+ context = private_json(context_path, "standalone host context")
100
+ expected = context.get("expected_workloads")
101
+ if not isinstance(expected, dict):
102
+ raise TypeError("expected AKS workload inventory is invalid")
103
+ expected_core = expected.get("core-control-plane")
104
+ if not isinstance(expected_core, dict):
105
+ raise TypeError("expected Core workload is invalid")
106
+ expected_secrets = expected_core.get("secret_environment")
107
+ if not isinstance(expected_secrets, dict):
108
+ raise TypeError("expected Core secret environment is invalid")
109
+ retained_secret = expected_secrets.get("FDAI_LICENSE_TOKEN")
110
+ if retained_secret not in {None, secrets["FDAI_LICENSE_TOKEN"]}:
111
+ raise ValueError("expected Core license binding differs")
112
+ expected_secrets.update(secrets)
113
+
114
+ if changed:
115
+ replace_private_json(workloads_path, retained_values)
116
+ replace_private_json(context_path, context)
117
+ receipt: dict[str, object] = {
118
+ "schema_version": "fdai.aks-license-workload-binding.v1",
119
+ "state": "bound",
120
+ "workloads_digest": canonical_digest(retained_values),
121
+ "expected_workloads_digest": canonical_digest(expected),
122
+ "mutation_performed": changed,
123
+ "subscription_ready": False,
124
+ }
125
+ receipt["receipt_digest"] = canonical_digest(receipt)
126
+ replace_private_json(work_dir / "license-workload-binding-receipt.json", receipt)
127
+ return receipt
128
+
129
+
130
+ def refresh_private_access(
131
+ _args: argparse.Namespace,
132
+ work_dir: Path,
133
+ *,
134
+ login: ManagedIdentityLogin,
135
+ ) -> dict[str, object]:
136
+ """Refresh only policy-forced private-access selectors after a claimed substrate failure."""
137
+ context_path = work_dir / "context.json"
138
+ variables_path = work_dir / "application.auto.tfvars.json"
139
+ context = private_json(context_path, "standalone host context")
140
+ variables = private_json(variables_path, "standalone host Terraform variables")
141
+ login(context, work_dir)
142
+ key_vault_private_access = (
143
+ standalone_terraform_environment.requires_aks_key_vault_private_access(
144
+ subscription_id=str(context["subscription_id"]),
145
+ resource_group_name=str(context["resource_group_name"]),
146
+ vault_name=str(context["key_vault_name"]),
147
+ work_dir=work_dir,
148
+ )
149
+ )
150
+ document_storage_private_access = (
151
+ standalone_terraform_environment.requires_aks_document_storage_private_access(
152
+ subscription_id=str(context["subscription_id"]),
153
+ resource_group_name=str(context["resource_group_name"]),
154
+ account_name=str(context["document_storage_account_name"]),
155
+ work_dir=work_dir,
156
+ )
157
+ )
158
+ changed = reconcile_retained_private_access(
159
+ context,
160
+ variables,
161
+ key_vault_private_access,
162
+ document_storage_private_access,
163
+ )
164
+ if changed:
165
+ replace_private_json(context_path, context)
166
+ replace_private_json(variables_path, variables)
167
+ return {
168
+ "schema_version": "fdai.standalone-private-access-refresh.v1",
169
+ "state": "refreshed",
170
+ "focused_private_access": focused_private_access(context),
171
+ "mutation_performed": False,
172
+ "subscription_ready": False,
173
+ }
174
+
175
+
176
+ def prepare_managed_identity_login(
177
+ context: dict[str, object],
178
+ work_dir: Path,
179
+ *,
180
+ login: Callable[[str, str, str, str, Path], None],
181
+ ) -> None:
182
+ standalone_terraform_environment.configure_terraform(context)
183
+ standalone_planned_outputs.bind(
184
+ Path(str(context["infra"])),
185
+ variables=work_dir / "application.auto.tfvars.json",
186
+ data_dir=Path(str(context.get("terraform_data", work_dir / "terraform-data"))),
187
+ )
188
+ login(
189
+ str(context["subscription_id"]),
190
+ str(context["tenant_id"]),
191
+ str(context["client_id"]),
192
+ str(context["principal_id"]),
193
+ work_dir,
194
+ )
195
+
196
+
197
+ def pending_application_operation(
198
+ context: dict[str, object],
199
+ work_dir: Path,
200
+ *,
201
+ input_rebind_pending: bool,
202
+ ) -> str | None:
203
+ if (
204
+ input_rebind_pending
205
+ and not (work_dir / "application-input-rebind-update-receipt.json").is_file()
206
+ ):
207
+ return "application-input-rebind-update"
208
+ if (work_dir / "license-application-update-receipt.json").is_file():
209
+ return None
210
+ path = work_dir / "license-workload-binding-receipt.json"
211
+ if not path.is_file() or path.is_symlink():
212
+ return None
213
+ receipt = private_json(path, "license workload binding receipt")
214
+ digest = receipt.pop("receipt_digest", None)
215
+ expected = context.get("expected_workloads")
216
+ workloads = private_json(work_dir / "workloads.auto.tfvars.json", "AKS workload variables")
217
+ if (
218
+ canonical_digest(receipt) != digest
219
+ or receipt.get("schema_version") != "fdai.aks-license-workload-binding.v1"
220
+ or receipt.get("state") != "bound"
221
+ or receipt.get("workloads_digest") != canonical_digest(workloads)
222
+ or not isinstance(expected, dict)
223
+ or receipt.get("expected_workloads_digest") != canonical_digest(expected)
224
+ ):
225
+ raise ValueError("license workload binding receipt is invalid")
226
+ return "license-application-update"
@@ -0,0 +1,104 @@
1
+ """Dispatch managed-host residual and residual-successor applies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from collections.abc import Callable
7
+ from pathlib import Path
8
+
9
+ from fdai_deployment_cli.standalone_residual_apply import apply_residual_plan
10
+ from fdai_deployment_cli.standalone_residual_successor import apply_residual_successor
11
+
12
+
13
+ def _original_operation(stage: str, work_dir: Path) -> str:
14
+ """Name the claimed apply a residual review extends.
15
+
16
+ A fresh AKS runtime is applied as ``runtime-cluster`` before the ordinary ``runtime``
17
+ operation, so its retained review and claim carry that name until it receives a receipt.
18
+ """
19
+
20
+ if (
21
+ stage == "runtime"
22
+ and (work_dir / "runtime-cluster-claim.json").exists()
23
+ and not (work_dir / "runtime-cluster-receipt.json").exists()
24
+ ):
25
+ return "runtime-cluster"
26
+ return stage
27
+
28
+
29
+ def apply_residual_checkpoint(
30
+ args: argparse.Namespace,
31
+ work_dir: Path,
32
+ *,
33
+ private_json: Callable[[Path, str], dict[str, object]],
34
+ validate_approval: Callable[..., None],
35
+ managed_identity_login: Callable[[dict[str, object], Path], None],
36
+ stage_paths: Callable[[str, dict[str, object], Path], tuple[Path, Path]],
37
+ activate_stage: Callable[[str, dict[str, object], Path], None],
38
+ stage_targets: Callable[[str, dict[str, object]], tuple[str, ...]],
39
+ guard_runtime: Callable[[dict[str, object], Path], None],
40
+ readback_stage: Callable[[str, dict[str, object]], bool],
41
+ absolute_path: Callable[[Path], Path],
42
+ ) -> dict[str, object]:
43
+ """Apply the one current residual review under the shared host boundary."""
44
+
45
+ stage = str(args.stage)
46
+ context = private_json(work_dir / "context.json", "standalone host context")
47
+ receipt_path = work_dir / f"{stage}-receipt.json"
48
+ residual_review_path = work_dir / f"{stage}-residual-review.json"
49
+ if receipt_path.exists() and not residual_review_path.exists():
50
+ return private_json(receipt_path, "standalone residual apply receipt")
51
+ original = _original_operation(stage, work_dir)
52
+ original_review = private_json(
53
+ work_dir / f"{original}-review.json", "standalone original plan review"
54
+ )
55
+ original_claim = private_json(
56
+ work_dir / f"{original}-claim.json", "standalone original apply claim"
57
+ )
58
+ residual_review = private_json(residual_review_path, "standalone residual plan review")
59
+ approval = private_json(absolute_path(args.approval), "standalone residual plan approval")
60
+ successor_path = work_dir / f"{stage}-residual-successor-review.json"
61
+ validate_approval(
62
+ private_json(successor_path, "standalone residual successor review")
63
+ if successor_path.exists()
64
+ else residual_review,
65
+ approval,
66
+ context=context,
67
+ )
68
+ managed_identity_login(context, work_dir)
69
+ infra, variables = stage_paths(stage, context, work_dir)
70
+ activate_stage(stage, context, work_dir)
71
+ targets = stage_targets(stage, context)
72
+ if stage == "runtime":
73
+ guard_runtime(context, work_dir)
74
+ if successor_path.exists():
75
+ return apply_residual_successor(
76
+ work_dir=work_dir,
77
+ stage=stage,
78
+ context=context,
79
+ infra=infra,
80
+ variables=variables,
81
+ targets=targets,
82
+ original_review=original_review,
83
+ original_claim=original_claim,
84
+ residual_review=residual_review,
85
+ residual_claim=private_json(
86
+ work_dir / f"{stage}-residual-claim.json", "standalone residual claim"
87
+ ),
88
+ successor_review=private_json(successor_path, "standalone residual successor review"),
89
+ approval=approval,
90
+ effect_readback=lambda: readback_stage(stage, context),
91
+ )
92
+ return apply_residual_plan(
93
+ work_dir=work_dir,
94
+ stage=stage,
95
+ context=context,
96
+ infra=infra,
97
+ variables=variables,
98
+ targets=targets,
99
+ original_review=original_review,
100
+ original_claim=original_claim,
101
+ residual_review=residual_review,
102
+ approval=approval,
103
+ effect_readback=lambda: readback_stage(stage, context),
104
+ )
@@ -0,0 +1,145 @@
1
+ """Private filesystem state primitives for standalone deployment checkpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import fcntl
6
+ import hashlib
7
+ import json
8
+ import os
9
+ import secrets
10
+ import stat
11
+ from datetime import UTC, datetime
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from fdai_deployment_cli.private_output import read_private_bytes, write_private_output
16
+
17
+
18
+ def private_json(path: Path, label: str) -> dict[str, Any]:
19
+ """Read one bounded private JSON object through descriptor-safe I/O."""
20
+ try:
21
+ value = json.loads(read_private_bytes(path, max_bytes=4 * 1024 * 1024))
22
+ except json.JSONDecodeError as exc:
23
+ raise ValueError(f"{label} is invalid") from exc
24
+ if not isinstance(value, dict):
25
+ raise ValueError( # noqa: TRY004 - normalize untrusted JSON into a stable CLI error
26
+ f"{label} is invalid"
27
+ )
28
+ return {str(key): item for key, item in value.items()}
29
+
30
+
31
+ def replace_private_json(path: Path, value: dict[str, object]) -> None:
32
+ """Atomically replace one private JSON object without reusing a temporary path."""
33
+ temporary = path.parent / f".{path.name}.tmp-{os.getpid()}-{secrets.token_hex(8)}"
34
+ published = False
35
+ try:
36
+ write_private_output(
37
+ temporary,
38
+ json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n",
39
+ )
40
+ os.replace(temporary, path)
41
+ path.chmod(0o600)
42
+ published = True
43
+ finally:
44
+ if not published:
45
+ temporary.unlink(missing_ok=True)
46
+
47
+
48
+ def replace_or_verify_private_json(path: Path, value: dict[str, object]) -> None:
49
+ """Create one private JSON object or verify its exact retained value."""
50
+ if path.exists():
51
+ if private_json(path, path.name) != value:
52
+ raise ValueError(f"retained {path.name} differs")
53
+ return
54
+ replace_private_json(path, value)
55
+
56
+
57
+ def file_digest(path: Path) -> str:
58
+ """Hash one bounded private artifact without following symlinks."""
59
+ return hashlib.sha256(read_private_bytes(path, max_bytes=512 * 1024 * 1024)).hexdigest()
60
+
61
+
62
+ def executable_digest(path: Path) -> str:
63
+ """Hash one current-UID immutable executable through a held descriptor."""
64
+ descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
65
+ try:
66
+ details = os.fstat(descriptor)
67
+ mode = stat.S_IMODE(details.st_mode)
68
+ if (
69
+ not stat.S_ISREG(details.st_mode)
70
+ or details.st_uid != os.geteuid()
71
+ or details.st_nlink != 1
72
+ or not mode & stat.S_IXUSR
73
+ or mode & 0o022
74
+ or details.st_size > 512 * 1024 * 1024
75
+ ):
76
+ raise PermissionError("verified executable permissions are invalid")
77
+ digest = hashlib.sha256()
78
+ while chunk := os.read(descriptor, 1024 * 1024):
79
+ digest.update(chunk)
80
+ return digest.hexdigest()
81
+ finally:
82
+ os.close(descriptor)
83
+
84
+
85
+ def private_directory(path: Path) -> None:
86
+ """Require one absolute current-UID mode-0700 directory."""
87
+ path.mkdir(parents=True, mode=0o700, exist_ok=True)
88
+ details = path.lstat()
89
+ if (
90
+ not path.is_absolute()
91
+ or not stat.S_ISDIR(details.st_mode)
92
+ or stat.S_IMODE(details.st_mode) != 0o700
93
+ or details.st_uid != os.geteuid()
94
+ ):
95
+ raise PermissionError("standalone host work directory must be current-UID mode 0700")
96
+
97
+
98
+ def acquire_checkpoint_lock(work_dir: Path) -> int:
99
+ """Acquire one nonblocking lock for all stateful managed-host checkpoints."""
100
+ directory = os.open(work_dir, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
101
+ try:
102
+ descriptor = os.open(
103
+ ".checkpoint.lock",
104
+ os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW,
105
+ 0o600,
106
+ dir_fd=directory,
107
+ )
108
+ finally:
109
+ os.close(directory)
110
+ details = os.fstat(descriptor)
111
+ if (
112
+ not stat.S_ISREG(details.st_mode)
113
+ or stat.S_IMODE(details.st_mode) != 0o600
114
+ or details.st_uid != os.geteuid()
115
+ or details.st_nlink != 1
116
+ ):
117
+ os.close(descriptor)
118
+ raise PermissionError("standalone checkpoint lock is not a private regular file")
119
+ try:
120
+ fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
121
+ except BlockingIOError as exc:
122
+ os.close(descriptor)
123
+ raise ValueError("another standalone checkpoint is already running") from exc
124
+ return descriptor
125
+
126
+
127
+ def absolute(path: Path) -> Path:
128
+ """Resolve a CLI path relative to the current working directory without dereferencing it."""
129
+ return path if path.is_absolute() else Path.cwd() / path
130
+
131
+
132
+ def moment(value: datetime) -> str:
133
+ """Render one UTC instant in the stable machine format."""
134
+ return value.astimezone(UTC).isoformat().replace("+00:00", "Z")
135
+
136
+
137
+ def parse_moment(value: str) -> datetime:
138
+ """Parse one timezone-aware approval instant and normalize it to UTC."""
139
+ try:
140
+ result = datetime.fromisoformat(value)
141
+ except ValueError as exc:
142
+ raise ValueError("approval expiry is invalid") from exc
143
+ if result.tzinfo is None:
144
+ raise ValueError("approval expiry is invalid")
145
+ return result.astimezone(UTC)