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,311 @@
1
+ """Customer-local adapter over the existing AKS source and signed-image coordinator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import json
7
+ import os
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+ from collections.abc import Callable
12
+ from pathlib import Path
13
+ from typing import Any
14
+ from urllib.parse import urlsplit
15
+ from uuid import UUID
16
+
17
+ from fdai_deployment_cli.bootstrap_approval import approval_source
18
+ from fdai_deployment_cli.bootstrap_channel import _read, _write, prepare_directory
19
+
20
+
21
+ class BootstrapBlocked(ValueError):
22
+ """A safe, actionable prerequisite failure, with no raw process output."""
23
+
24
+
25
+ class BootstrapExecutor:
26
+ """One bounded first-install precursor, not a lifecycle reconciliation loop."""
27
+
28
+ def __init__(
29
+ self,
30
+ area: Path,
31
+ decide: Callable[[dict[str, object]], dict[str, object]],
32
+ *,
33
+ run: Callable[..., Any] = subprocess.run,
34
+ planner: Callable[..., Any] | None = None,
35
+ hub_source: Callable[..., Any] | None = None,
36
+ ):
37
+ self.area, self.decide, self.run = area, decide, run
38
+ self.planner, self.hub_source = planner, hub_source
39
+ prepare_directory(area)
40
+ self.azure = area / "azure"
41
+ prepare_directory(self.azure)
42
+ self.environment = {
43
+ key: value
44
+ for key, value in os.environ.items()
45
+ if key
46
+ in {
47
+ "HOME",
48
+ "PATH",
49
+ "LANG",
50
+ "SSL_CERT_FILE",
51
+ "REQUESTS_CA_BUNDLE",
52
+ "DISPLAY",
53
+ "WAYLAND_DISPLAY",
54
+ "XDG_RUNTIME_DIR",
55
+ "DBUS_SESSION_BUS_ADDRESS",
56
+ }
57
+ }
58
+ self.environment.update(
59
+ AZURE_CONFIG_DIR=str(self.azure),
60
+ AZURE_CORE_COLLECT_TELEMETRY="false",
61
+ AZURE_CORE_ONLY_SHOW_ERRORS="true",
62
+ AZURE_CORE_LOGIN_EXPERIENCE_V2="off",
63
+ )
64
+
65
+ def _az(self, *args: str, timeout: int = 60) -> object:
66
+ if self.run is subprocess.run and shutil.which("az") is None:
67
+ raise BootstrapBlocked(
68
+ "Install Azure CLI on this customer machine, then retry sign-in."
69
+ )
70
+ result = self.run(
71
+ ["az", *args, "--output", "json", "--only-show-errors"],
72
+ env=self.environment,
73
+ stdin=subprocess.DEVNULL,
74
+ capture_output=True,
75
+ text=True,
76
+ timeout=timeout,
77
+ check=False,
78
+ )
79
+ if result.returncode:
80
+ raise BootstrapBlocked(
81
+ "Azure CLI operation failed. Sign in locally and inspect private diagnostics."
82
+ )
83
+ if args[0] == "login":
84
+ # Login output can contain identity and authorization data; never retain it.
85
+ return None
86
+ try:
87
+ return json.loads(result.stdout or "null")
88
+ except ValueError as exc:
89
+ raise BootstrapBlocked(
90
+ "Azure CLI returned an invalid response; no deployment was started."
91
+ ) from exc
92
+
93
+ def accounts(self) -> list[dict[str, str]]:
94
+ result = self._az(
95
+ "account",
96
+ "list",
97
+ "--query",
98
+ "[?state=='Enabled'].{id:id,name:name,tenantId:tenantId,state:state}",
99
+ )
100
+ if not isinstance(result, list) or len(result) > 100:
101
+ raise BootstrapBlocked("Azure subscription listing is invalid or exceeds 100 entries.")
102
+ accounts = []
103
+ for item in result:
104
+ if not isinstance(item, dict) or item.get("state") != "Enabled":
105
+ continue
106
+ try:
107
+ account_id, tenant = str(UUID(item["id"])), str(UUID(item["tenantId"]))
108
+ except (ValueError, KeyError, TypeError, AttributeError) as exc:
109
+ raise BootstrapBlocked(
110
+ "Azure subscription listing contains invalid identifiers."
111
+ ) from exc
112
+ name = item.get("name")
113
+ if not isinstance(name, str) or len(name) > 256 or any(ord(char) < 32 for char in name):
114
+ raise BootstrapBlocked(
115
+ "Azure subscription listing contains an invalid display name."
116
+ )
117
+ accounts.append({"id": account_id, "name": name, "tenant_id": tenant})
118
+ return accounts
119
+
120
+ def _target(self, subscription: str, tenant: str | None = None) -> dict[str, str]:
121
+ value = self._az(
122
+ "account",
123
+ "show",
124
+ "--query",
125
+ "{id:id,tenantId:tenantId,user:{type:user.type}}",
126
+ )
127
+ if (
128
+ not isinstance(value, dict)
129
+ or value.get("id", "").lower() != subscription.lower()
130
+ or not isinstance(value.get("user"), dict)
131
+ or value["user"].get("type") != "user"
132
+ or (tenant is not None and value.get("tenantId") != tenant)
133
+ ):
134
+ raise BootstrapBlocked(
135
+ "Azure human identity or selected subscription changed. No approval was granted."
136
+ )
137
+ try:
138
+ tenant_id = str(UUID(value["tenantId"]))
139
+ except (ValueError, KeyError, TypeError, AttributeError) as exc:
140
+ raise BootstrapBlocked("Azure tenant identity is unavailable.") from exc
141
+ return {"subscription_id": subscription, "tenant_id": tenant_id}
142
+
143
+ def execute(self, action: str, payload: dict[str, object]) -> dict[str, object]:
144
+ if action == "login":
145
+ self._az("login", timeout=600)
146
+ return {
147
+ "accounts": self.accounts(),
148
+ "message": "Signed in. Select the intended subscription.",
149
+ }
150
+ if action == "accounts":
151
+ return {
152
+ "accounts": self.accounts(),
153
+ "message": "Available Azure subscriptions refreshed.",
154
+ }
155
+ if action == "inspect":
156
+ retained = _read(self.area, "configuration.json")
157
+ receipt = _read(self.area, "acceptance.json")
158
+ if retained:
159
+ self._target(str(retained["subscription_id"]), str(retained["tenant_id"]))
160
+ # A saved success is historical, not an independent current health observation.
161
+ return {
162
+ "message": "Retained installation exists. Explicitly resume to refresh plans and health."
163
+ if retained
164
+ else "No first-install configuration has been retained.",
165
+ "retained_acceptance": receipt.get("deployment_ready") is True,
166
+ }
167
+ return self._install(payload, resume=action == "resume")
168
+
169
+ def _install(self, payload: dict[str, object], *, resume: bool) -> dict[str, object]:
170
+ retained = _read(self.area, "configuration.json")
171
+ if resume:
172
+ if not retained:
173
+ raise BootstrapBlocked("No retained installation exists to resume.")
174
+ config = retained
175
+ self._target(str(config["subscription_id"]), str(config["tenant_id"]))
176
+ else:
177
+ if retained:
178
+ raise BootstrapBlocked(
179
+ "An installation is retained. Use resume without replacing its inputs."
180
+ )
181
+ config = dict(payload)
182
+ subscription = str(config["subscription_id"])
183
+ accounts = self.accounts()
184
+ if not any(item["id"] == subscription.lower() for item in accounts):
185
+ raise BootstrapBlocked(
186
+ "Selected subscription is not available to the signed-in user."
187
+ )
188
+ self._az("account", "set", "--subscription", subscription)
189
+ config.update(self._target(subscription))
190
+ _write(self.area, config, "configuration.json")
191
+ if (self.area / "native-ownership.json").exists():
192
+ raise BootstrapBlocked(
193
+ "Native lifecycle ownership was accepted; Bootstrap cannot mutate this installation."
194
+ )
195
+ mode = config["artifact_source"]
196
+ selection = None
197
+ if mode == "hub":
198
+ from fdai_deployment_cli.bootstrap_support import resolve_hub_source
199
+
200
+ try:
201
+ source, selection = (self.hub_source or resolve_hub_source)(
202
+ self.area,
203
+ str(config["hub_public_key"]),
204
+ config.get("source_root"),
205
+ )
206
+ except (ValueError, OSError) as exc:
207
+ raise BootstrapBlocked(
208
+ "Signed Hub selection or pinned support source is unavailable. "
209
+ "Verify independent Release trust and support compatibility; no source-build fallback."
210
+ ) from exc
211
+ else:
212
+ source = Path(str(config["source_root"])).absolute()
213
+ from fdai_deployment_cli.source_input import inspect_source
214
+
215
+ try:
216
+ snapshot = inspect_source(source, expected_commit=config.get("source_commit"))
217
+ except (ValueError, OSError) as exc:
218
+ raise BootstrapBlocked(
219
+ "Provide the same clean committed FDAI checkout at the retained source root. "
220
+ "Source revision changes cannot resume this installation."
221
+ ) from exc
222
+ scripts = source / "scripts/deployment/azure"
223
+ sys.path.insert(0, str(scripts))
224
+ try:
225
+ prompt = importlib.import_module("genesis_approval_prompt")
226
+ if (
227
+ getattr(prompt, "BOOTSTRAP_APPROVAL_PROTOCOL", None) != 1
228
+ or Path(str(prompt.__file__)).resolve().parent != scripts.resolve()
229
+ ):
230
+ raise BootstrapBlocked(
231
+ "Selected support source lacks the Bootstrap approval adapter. "
232
+ "Use compatible pinned support source; no terminal approval fallback is allowed."
233
+ )
234
+ finally:
235
+ sys.path.remove(str(scripts))
236
+ config.update(source_root=str(source), source_commit=snapshot.commit)
237
+ _write(self.area, config, "configuration.json")
238
+ from fdai_deployment_cli.runtime_profile import RuntimeDeploymentProfile
239
+ from fdai_deployment_cli.source_azure import plan_source_installation
240
+
241
+ profile = RuntimeDeploymentProfile.create(
242
+ runtime_platform="aks",
243
+ database_placement="postgres-flex",
244
+ product_add_ons=("read-only-console", "enterprise-identity-governance"),
245
+ )
246
+
247
+ def exact_decision(review: dict[str, object]) -> dict[str, object]:
248
+ self._target(str(config["subscription_id"]), str(config["tenant_id"]))
249
+ if (self.area / "native-ownership.json").exists():
250
+ raise BootstrapBlocked("Native lifecycle ownership superseded Bootstrap.")
251
+ result = self.decide(review)
252
+ self._target(str(config["subscription_id"]), str(config["tenant_id"]))
253
+ return result
254
+
255
+ original_environment = dict(os.environ)
256
+ try:
257
+ os.environ.clear()
258
+ os.environ.update(self.environment)
259
+ with approval_source(exact_decision):
260
+ result = (self.planner or plan_source_installation)(
261
+ source_root=source,
262
+ work_dir=self.area / "deployment",
263
+ runtime_profile=profile,
264
+ region=str(config["region"]),
265
+ monthly_cost_ceiling=int(config["monthly_cost_ceiling"]),
266
+ timeout_seconds=14400,
267
+ hub_selection=selection,
268
+ )
269
+ finally:
270
+ os.environ.clear()
271
+ os.environ.update(original_environment)
272
+ self._target(str(config["subscription_id"]), str(config["tenant_id"]))
273
+ ready = result.get("deployment_ready") is True
274
+ console = result.get("console_url") if ready else None
275
+ if ready and console is None and result.get("browser_access_verified") is True:
276
+ from fdai_deployment_cli.contracts import canonical_digest
277
+
278
+ publication = _read(
279
+ self.area / "deployment/foundation", "console-publication-receipt.json"
280
+ )
281
+ digest = publication.pop("receipt_digest", None)
282
+ if (
283
+ digest != result.get("console_publication_receipt_digest")
284
+ or canonical_digest(publication) != digest
285
+ ):
286
+ raise BootstrapBlocked(
287
+ "Console publication receipt differs from independent acceptance."
288
+ )
289
+ console = publication.get("console_origin")
290
+ if isinstance(console, str):
291
+ parsed = urlsplit(console)
292
+ if (
293
+ parsed.scheme != "https"
294
+ or not parsed.hostname
295
+ or parsed.username
296
+ or parsed.password
297
+ or parsed.query
298
+ or parsed.fragment
299
+ ):
300
+ console = None
301
+ else:
302
+ console = None
303
+ safe: dict[str, object] = {
304
+ "deployment_ready": ready,
305
+ "console_url": console,
306
+ "message": "Independent health and zero-change acceptance completed."
307
+ if ready
308
+ else "Installation is not accepted. Inspect retained private evidence before resuming.",
309
+ }
310
+ _write(self.area, safe, "acceptance.json")
311
+ return safe
@@ -0,0 +1,237 @@
1
+ """Sealed intent and sanitized observations for Azure bootstrap reconciliation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Mapping
7
+ from dataclasses import dataclass
8
+ from datetime import UTC, datetime
9
+ from enum import StrEnum
10
+
11
+ from fdai_deployment_cli.contracts import ProvisionProfile, canonical_digest
12
+
13
+ FOUNDATION_PROVIDER_NAMESPACES = (
14
+ "Microsoft.Authorization",
15
+ "Microsoft.Compute",
16
+ "Microsoft.DevTestLab",
17
+ "Microsoft.EventGrid",
18
+ "Microsoft.ManagedIdentity",
19
+ "Microsoft.Network",
20
+ "Microsoft.Resources",
21
+ "Microsoft.Storage",
22
+ )
23
+
24
+ _RESOURCE_GROUP_NAME = re.compile(r"^[A-Za-z0-9._()\-]{1,90}$")
25
+ _STORAGE_ACCOUNT_NAME = re.compile(r"^[a-z0-9]{3,24}$")
26
+ _SHA = re.compile(r"^[0-9a-f]{40}$")
27
+
28
+
29
+ class Classification(StrEnum):
30
+ """Disposition of one read-only bootstrap observation."""
31
+
32
+ COMPATIBLE = "compatible"
33
+ MISSING = "missing"
34
+ CONFLICT = "conflict"
35
+ INDETERMINATE = "indeterminate"
36
+
37
+
38
+ @dataclass(frozen=True, slots=True)
39
+ class BootstrapObservation:
40
+ """Sanitized classification that contains no Azure resource identifiers."""
41
+
42
+ entry_id: str
43
+ classification: Classification
44
+ reason_code: str
45
+
46
+ def to_mapping(self) -> dict[str, str]:
47
+ """Return canonical observation data."""
48
+
49
+ return {
50
+ "entry_id": self.entry_id,
51
+ "classification": self.classification.value,
52
+ "reason_code": self.reason_code,
53
+ }
54
+
55
+
56
+ @dataclass(frozen=True, slots=True)
57
+ class BootstrapReconcileResult:
58
+ """Sealed bootstrap intent plus separate time-bound observations."""
59
+
60
+ intent: Mapping[str, object]
61
+ observations: tuple[BootstrapObservation, ...]
62
+ created_at: str
63
+ expires_at: str
64
+
65
+ @property
66
+ def plan_digest(self) -> str:
67
+ """Return the replay-stable digest of bootstrap intent only."""
68
+
69
+ return canonical_digest(self.intent)
70
+
71
+ @property
72
+ def observation_digest(self) -> str:
73
+ """Return the digest of the time-bound observed state."""
74
+
75
+ return canonical_digest(
76
+ {
77
+ "created_at": self.created_at,
78
+ "expires_at": self.expires_at,
79
+ "observations": [item.to_mapping() for item in self.observations],
80
+ }
81
+ )
82
+
83
+ @property
84
+ def blockers(self) -> tuple[str, ...]:
85
+ """Return conflicts and unknown state that block approval."""
86
+
87
+ return tuple(
88
+ item.reason_code
89
+ for item in self.observations
90
+ if item.classification in {Classification.CONFLICT, Classification.INDETERMINATE}
91
+ )
92
+
93
+ def to_mapping(self) -> dict[str, object]:
94
+ """Return a private artifact without raw tenant or subscription identifiers."""
95
+
96
+ return {
97
+ "schema_version": "fdai.bootstrap-reconcile-plan.v1",
98
+ "state": "review" if not self.blockers else "incomplete",
99
+ "plan": dict(self.intent),
100
+ "plan_digest": self.plan_digest,
101
+ "observation": {
102
+ "created_at": self.created_at,
103
+ "expires_at": self.expires_at,
104
+ "digest": self.observation_digest,
105
+ "entries": [item.to_mapping() for item in self.observations],
106
+ },
107
+ "reason_codes": list(self.blockers),
108
+ "mutation_performed": False,
109
+ }
110
+
111
+
112
+ def build_intent(
113
+ *,
114
+ profile: ProvisionProfile,
115
+ source_commit: str,
116
+ ops_resource_group: str,
117
+ app_resource_group: str,
118
+ state_storage_account: str,
119
+ ) -> dict[str, object]:
120
+ """Build deterministic foundation intent without live observations."""
121
+
122
+ operations = [
123
+ {
124
+ "entry_id": f"provider.{namespace.casefold()}",
125
+ "action": "ensure_registered",
126
+ "postcondition": "provider.registered",
127
+ "rollback_ref": "rollback.provider-registration",
128
+ }
129
+ for namespace in FOUNDATION_PROVIDER_NAMESPACES
130
+ ]
131
+ operations.extend(
132
+ (
133
+ {
134
+ "entry_id": "ops-resource-group",
135
+ "action": "ensure",
136
+ "postcondition": "resource-group.compatible",
137
+ "rollback_ref": "rollback.ops-resource-group",
138
+ },
139
+ {
140
+ "entry_id": "application-resource-group",
141
+ "action": "ensure",
142
+ "postcondition": "resource-group.compatible",
143
+ "rollback_ref": "rollback.application-resource-group",
144
+ },
145
+ {
146
+ "entry_id": "state-storage",
147
+ "action": "ensure",
148
+ "postcondition": "state-storage.private-keyless-versioned",
149
+ "rollback_ref": "rollback.state-storage",
150
+ },
151
+ {
152
+ "entry_id": "ops-network",
153
+ "action": "ensure",
154
+ "postcondition": "ops-network.private-runner-ready",
155
+ "rollback_ref": "rollback.ops-network",
156
+ },
157
+ {
158
+ "entry_id": "deploy-identity",
159
+ "action": "ensure",
160
+ "postcondition": "deploy-identity.roles-effective",
161
+ "rollback_ref": "rollback.deploy-identity",
162
+ },
163
+ {
164
+ "entry_id": "runner",
165
+ "action": "ensure",
166
+ "postcondition": "runner.attested",
167
+ "rollback_ref": "rollback.runner",
168
+ },
169
+ {
170
+ "entry_id": "state-containers",
171
+ "action": "ensure",
172
+ "postcondition": "state-containers.private",
173
+ "rollback_ref": "rollback.state-containers",
174
+ },
175
+ {
176
+ "entry_id": "state-handoff",
177
+ "action": "verify",
178
+ "postcondition": "state-handoff.zero-change",
179
+ "rollback_ref": "rollback.state-handoff",
180
+ },
181
+ )
182
+ )
183
+ return {
184
+ "schema_version": "fdai.bootstrap-reconcile-intent.v1",
185
+ "source_commit": source_commit,
186
+ "profile_digest": canonical_digest(profile.to_mapping()),
187
+ "target_binding": profile.target_binding,
188
+ "environment": profile.environment,
189
+ "region": profile.region,
190
+ "resource_names": {
191
+ "ops_resource_group": ops_resource_group,
192
+ "application_resource_group": app_resource_group,
193
+ "state_storage_account": state_storage_account,
194
+ },
195
+ "state_storage_posture": {
196
+ "kind": "StorageV2",
197
+ "sku": "Standard_LRS",
198
+ "minimum_tls_version": "TLS1_2",
199
+ "public_network_access": "Disabled",
200
+ "shared_key_access": False,
201
+ "blob_public_access": False,
202
+ "cross_tenant_replication": False,
203
+ "blob_versioning": True,
204
+ "blob_delete_retention": True,
205
+ "container_delete_retention": True,
206
+ "containers": ["tfstate", "deployment-plans"],
207
+ },
208
+ "provider_namespaces": list(FOUNDATION_PROVIDER_NAMESPACES),
209
+ "operations": operations,
210
+ }
211
+
212
+
213
+ def validate_inputs(
214
+ *,
215
+ source_commit: str,
216
+ ops_resource_group: str,
217
+ app_resource_group: str,
218
+ state_storage_account: str,
219
+ ttl_seconds: int,
220
+ ) -> None:
221
+ """Reject names and bounds that Azure cannot safely apply later."""
222
+
223
+ if _SHA.fullmatch(source_commit) is None:
224
+ raise ValueError("source_commit MUST be a lowercase 40-character SHA")
225
+ for value in (ops_resource_group, app_resource_group):
226
+ if _RESOURCE_GROUP_NAME.fullmatch(value) is None or value.endswith("."):
227
+ raise ValueError("Azure resource group name is invalid")
228
+ if _STORAGE_ACCOUNT_NAME.fullmatch(state_storage_account) is None:
229
+ raise ValueError("Azure storage account name is invalid")
230
+ if not 60 <= ttl_seconds <= 3600:
231
+ raise ValueError("bootstrap plan ttl MUST be from 60 through 3600 seconds")
232
+
233
+
234
+ def timestamp(moment: datetime) -> str:
235
+ """Render one canonical UTC timestamp."""
236
+
237
+ return moment.astimezone(UTC).isoformat().replace("+00:00", "Z")