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,325 @@
1
+ """Select bounded Terraform targets for standalone deployment stages."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import subprocess
7
+ from pathlib import Path
8
+ from typing import Final
9
+
10
+ _SUBSTRATE_TARGETS: Final = (
11
+ "module.resource_group",
12
+ # Every later stage reads installation_binding, and on AKS no untargeted apply runs.
13
+ "terraform_data.installation",
14
+ "module.log_analytics",
15
+ "azurerm_application_insights.core",
16
+ "module.network",
17
+ "module.container_registry",
18
+ "module.acr_private_endpoint",
19
+ "azurerm_role_assignment.deploy_runner_acr_push",
20
+ "module.identity",
21
+ "module.identity_change",
22
+ "module.identity_resilience",
23
+ "module.identity_finops",
24
+ "module.command_api_identity",
25
+ "module.inventory_identity",
26
+ "module.canary_identity",
27
+ "module.operational_evidence_verifier_identity",
28
+ "module.operator_api_identity",
29
+ "module.isolated_executor_identity",
30
+ "module.ingestion_identity",
31
+ "module.ingestion_worker_identity",
32
+ "module.key_vault",
33
+ "azurerm_role_assignment.kv_officer_self",
34
+ "module.kv_private_endpoint",
35
+ "module.state_store",
36
+ "module.postgres_public_mode_private_endpoint",
37
+ "module.event_bus",
38
+ "module.event_bus_auxiliary",
39
+ "module.event_bus_private_endpoint",
40
+ "azurerm_key_vault_secret.state_store_dsn",
41
+ "azurerm_key_vault_secret.application_insights_connection_string",
42
+ "azurerm_role_assignment.core_application_insights_secret_reader",
43
+ "azurerm_key_vault_secret.github_app_private_key",
44
+ "azurerm_role_assignment.core_github_app_private_key_reader",
45
+ "azurerm_role_assignment.command_api_eventhubs_sender",
46
+ "azurerm_role_assignment.command_api_eventhubs_receiver",
47
+ "azurerm_role_assignment.inventory_reader",
48
+ "azurerm_role_assignment.inventory_monitoring_reader",
49
+ "azurerm_role_assignment.inventory_log_analytics_reader",
50
+ "azurerm_role_assignment.inventory_cost_reader",
51
+ "azurerm_role_assignment.inventory_kubernetes_reader",
52
+ "azurerm_role_assignment.inventory_eventhubs_sender",
53
+ "azurerm_role_assignment.inventory_stage_sender",
54
+ "azurerm_role_assignment.inventory_eventhubs_raw_sender",
55
+ "azurerm_role_assignment.canary_eventhubs_sender",
56
+ "azurerm_role_assignment.operational_evidence_verifier_acr_pull",
57
+ "azurerm_role_assignment.operational_evidence_verifier_state_store_secret_reader",
58
+ "azurerm_role_assignment.operational_evidence_verifier_monitoring_reader",
59
+ "azurerm_role_assignment.inventory_kv_secrets_user",
60
+ "azurerm_role_assignment.operator_api_kv_secrets_user",
61
+ "azurerm_role_assignment.isolated_executor_kv_secrets_user",
62
+ "module.document_storage",
63
+ "module.document_blob_private_endpoint",
64
+ "module.document_dfs_private_endpoint",
65
+ "azurerm_private_dns_a_record.document_blob_ops",
66
+ "azurerm_role_assignment.ingestion_document_data",
67
+ "azurerm_role_assignment.ingestion_worker_document_data",
68
+ "azurerm_key_vault_secret.ingestion_api_dsn",
69
+ "azurerm_key_vault_secret.ingestion_worker_dsn",
70
+ "azurerm_role_assignment.ingestion_api_kv_secrets_user",
71
+ "azurerm_role_assignment.ingestion_worker_kv_secrets_user",
72
+ "azurerm_role_assignment.ingestion_aks_eventhubs_sender",
73
+ "azurerm_role_assignment.ingestion_worker_aks_eventhubs_receiver",
74
+ "azurerm_role_assignment.ingestion_worker_eventhubs_sender",
75
+ "azurerm_role_assignment.ingestion_worker_pantheon_receiver",
76
+ "azurerm_role_assignment.executor_eventhubs_data_owner",
77
+ "azurerm_role_assignment.isolated_executor_command_receiver",
78
+ "azurerm_role_assignment.isolated_executor_receipt_sender",
79
+ "azurerm_role_assignment.ingestion_eventhubs_sender",
80
+ "azurerm_role_assignment.operator_api_reader",
81
+ "module.console",
82
+ "module.operational_history_storage",
83
+ "random_id.cost_pseudonym_key",
84
+ "azurerm_key_vault_secret.cost_pseudonym_key",
85
+ "azurerm_role_assignment.operator_cost_pseudonym_secret_reader",
86
+ "random_id.operator_request_core_signing_seed",
87
+ "azurerm_key_vault_secret.operator_request_core_signing_seed",
88
+ "random_id.operator_request_operator_signing_seed",
89
+ "azurerm_key_vault_secret.operator_request_operator_signing_seed",
90
+ "azurerm_role_assignment.core_operator_request_core_seed_reader",
91
+ "azurerm_role_assignment.core_operator_request_operator_seed_reader",
92
+ "azurerm_role_assignment.operator_api_operator_request_seed_reader",
93
+ # Email notifications; every address is count 0 unless the add-on is selected.
94
+ "module.notification_identity",
95
+ "azurerm_communication_service.notifications",
96
+ "azurerm_email_communication_service.notifications",
97
+ "azurerm_email_communication_service_domain.notifications",
98
+ "azurerm_communication_service_email_domain_association.notifications",
99
+ "azurerm_role_assignment.notification_email_sender",
100
+ )
101
+ CONTAINER_INSIGHTS_ASSOCIATION_TARGET: Final = (
102
+ "azurerm_monitor_data_collection_rule_association.container_insights"
103
+ )
104
+ RUNTIME_CLUSTER_STATE_TARGET: Final = "azurerm_kubernetes_cluster.runtime"
105
+ _AKS_RUNTIME_TARGETS: Final = (
106
+ "azurerm_public_ip.egress",
107
+ "azurerm_nat_gateway.egress",
108
+ "azurerm_nat_gateway_public_ip_association.egress",
109
+ "azurerm_subnet_nat_gateway_association.egress",
110
+ "azurerm_user_assigned_identity.cluster",
111
+ "azurerm_role_assignment.cluster_network",
112
+ "azurerm_role_assignment.cluster_api_network",
113
+ RUNTIME_CLUSTER_STATE_TARGET,
114
+ "azurerm_monitor_data_collection_rule.container_insights",
115
+ CONTAINER_INSIGHTS_ASSOCIATION_TARGET,
116
+ "azurerm_kubernetes_cluster_node_pool.user",
117
+ "azurerm_role_assignment.managed_host_cluster_user",
118
+ "azurerm_role_assignment.managed_host_cluster_admin",
119
+ "azurerm_role_assignment.kubelet_acr_pull",
120
+ )
121
+
122
+
123
+ def database_placement(context: dict[str, object]) -> str:
124
+ value = context.get("runtime_profile")
125
+ if value is None:
126
+ return "postgres-flex"
127
+ if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
128
+ raise ValueError("runtime deployment profile is invalid")
129
+ placement = value.get("database_placement")
130
+ if placement not in {"postgres-flex", "postgres-aks"}:
131
+ raise ValueError("database placement is invalid")
132
+ return str(placement)
133
+
134
+
135
+ _MOVED = re.compile(r"moved\s*\{\s*from\s*=\s*(\S+)\s+to\s*=\s*(\S+)\s*\}")
136
+
137
+
138
+ def stage_targets(stage: str, context: dict[str, object]) -> tuple[str, ...]:
139
+ if stage == "access":
140
+ base = focused_access_targets(context)
141
+ elif stage == "substrate":
142
+ base = substrate_targets(context)
143
+ else:
144
+ return ()
145
+ infra = context.get("infra")
146
+ moved = moved_state_targets(Path(infra)) if isinstance(infra, str) else ()
147
+ return base + tuple(target for target in moved if target not in base)
148
+
149
+
150
+ def fresh_aks_runtime_targets() -> tuple[str, ...]:
151
+ """Return the first fresh-runtime target set without the Container Insights association."""
152
+
153
+ return tuple(
154
+ target for target in _AKS_RUNTIME_TARGETS if target != CONTAINER_INSIGHTS_ASSOCIATION_TARGET
155
+ )
156
+
157
+
158
+ def runtime_operation(work_dir: Path, infra: Path) -> str:
159
+ if (work_dir / "runtime-cluster-review.json").exists() and not (
160
+ work_dir / "runtime-cluster-receipt.json"
161
+ ).exists():
162
+ return "runtime-cluster"
163
+ if terraform_state_contains(infra, RUNTIME_CLUSTER_STATE_TARGET):
164
+ return "runtime"
165
+ return "runtime-cluster"
166
+
167
+
168
+ def terraform_state_contains(infra: Path, address: str) -> bool:
169
+ completed = subprocess.run(
170
+ ("terraform", "state", "list", address),
171
+ cwd=infra,
172
+ check=False,
173
+ capture_output=True,
174
+ text=True,
175
+ timeout=300,
176
+ )
177
+ return completed.returncode == 0 and address in completed.stdout.splitlines()
178
+
179
+
180
+ def operation_targets(stage: str, context: dict[str, object], operation: str) -> tuple[str, ...]:
181
+ if operation == "runtime-cluster":
182
+ return fresh_aks_runtime_targets()
183
+ return stage_targets(stage, context)
184
+
185
+
186
+ def not_required_recovery(stage: str) -> dict[str, object]:
187
+ return {
188
+ "schema_version": "fdai.standalone-application-recovery.v1",
189
+ "state": "not-required",
190
+ "stage": stage,
191
+ "mutation_performed": False,
192
+ "subscription_ready": False,
193
+ }
194
+
195
+
196
+ def moved_state_targets(infra: Path) -> tuple[str, ...]:
197
+ """Return `moved` source and destination addresses whose source is still in Terraform state.
198
+
199
+ Terraform rejects a targeted plan that leaves such instances out, so every targeted stage
200
+ must include them. Without readable state, no destination is added and Terraform fails closed.
201
+ """
202
+
203
+ blocks = [
204
+ match.groups()
205
+ for path in sorted(infra.glob("*.tf"))
206
+ for match in _MOVED.finditer(path.read_text(encoding="utf-8"))
207
+ ]
208
+ if not blocks:
209
+ return ()
210
+ listed = subprocess.run(
211
+ ("terraform", "state", "list"),
212
+ cwd=infra,
213
+ check=False,
214
+ capture_output=True,
215
+ text=True,
216
+ timeout=300,
217
+ )
218
+ if listed.returncode != 0:
219
+ return ()
220
+ state = set(listed.stdout.split())
221
+ return tuple(
222
+ sorted(
223
+ {
224
+ address
225
+ for source, destination in blocks
226
+ if any(
227
+ item == source or item.startswith((f"{source}.", f"{source}["))
228
+ for item in state
229
+ )
230
+ for address in (source, destination)
231
+ }
232
+ )
233
+ )
234
+
235
+
236
+ def focused_private_access(context: dict[str, object]) -> bool:
237
+ key_vault = context.get("key_vault_private_access")
238
+ document_storage = context.get("document_storage_private_access")
239
+ if type(key_vault) is not bool or type(document_storage) is not bool:
240
+ raise ValueError("focused private-access context is invalid")
241
+ return key_vault or document_storage
242
+
243
+
244
+ def reconcile_retained_private_access(
245
+ retained: dict[str, object],
246
+ retained_values: dict[str, object],
247
+ key_vault_private_access: bool,
248
+ document_storage_private_access: bool,
249
+ ) -> bool:
250
+ changed = False
251
+ for key, variable, observed in (
252
+ (
253
+ "key_vault_private_access",
254
+ "enable_aks_key_vault_private_access",
255
+ key_vault_private_access,
256
+ ),
257
+ (
258
+ "document_storage_private_access",
259
+ "enable_aks_document_storage_private_access",
260
+ document_storage_private_access,
261
+ ),
262
+ ):
263
+ previous = retained.get(key)
264
+ if type(previous) is not bool:
265
+ raise ValueError("standalone host retained context differs")
266
+ if previous and not observed:
267
+ raise ValueError(f"standalone host retained private-access posture loosened: {key}")
268
+ tightened = previous or observed
269
+ if retained.get(key) is not tightened:
270
+ retained[key] = tightened
271
+ retained_values[variable] = tightened
272
+ changed = True
273
+ if changed:
274
+ retained["private_access_posture_transition"] = True
275
+ return changed
276
+
277
+
278
+ def focused_access_targets(context: dict[str, object]) -> tuple[str, ...]:
279
+ if not focused_private_access(context):
280
+ raise ValueError("focused private-access plan was not selected")
281
+ targets: list[str] = []
282
+ if context["key_vault_private_access"] is True:
283
+ targets.extend(
284
+ (
285
+ "module.kv_private_endpoint",
286
+ "azurerm_role_assignment.kv_officer_self",
287
+ )
288
+ )
289
+ if context["document_storage_private_access"] is True:
290
+ targets.extend(
291
+ (
292
+ "module.document_blob_private_endpoint",
293
+ "module.document_dfs_private_endpoint",
294
+ "module.document_storage[0].azurerm_role_assignment.deployer_data_owner",
295
+ "azurerm_private_dns_a_record.document_blob_ops",
296
+ )
297
+ )
298
+ targets.extend(
299
+ (
300
+ "azurerm_virtual_network_peering.spoke_to_hub",
301
+ "azurerm_virtual_network_peering.hub_to_spoke",
302
+ )
303
+ )
304
+ return tuple(targets)
305
+
306
+
307
+ def substrate_targets(context: dict[str, object]) -> tuple[str, ...]:
308
+ if database_placement(context) == "postgres-flex":
309
+ return _SUBSTRATE_TARGETS
310
+ # -target keeps every configuration dependency even at count 0, so the ingestion DSN
311
+ # secrets and their readers would plan the Flexible Server through module.state_store.
312
+ excluded = {
313
+ "module.state_store",
314
+ "module.postgres_public_mode_private_endpoint",
315
+ "azurerm_key_vault_secret.state_store_dsn",
316
+ "azurerm_key_vault_secret.ingestion_api_dsn",
317
+ "azurerm_key_vault_secret.ingestion_worker_dsn",
318
+ "azurerm_role_assignment.ingestion_api_kv_secrets_user",
319
+ "azurerm_role_assignment.ingestion_worker_kv_secrets_user",
320
+ "azurerm_role_assignment.inventory_kv_secrets_user",
321
+ "azurerm_role_assignment.operator_api_kv_secrets_user",
322
+ "azurerm_role_assignment.isolated_executor_kv_secrets_user",
323
+ "azurerm_role_assignment.operational_evidence_verifier_state_store_secret_reader",
324
+ }
325
+ return tuple(target for target in _SUBSTRATE_TARGETS if target not in excluded)
@@ -0,0 +1,67 @@
1
+ """Bind Foundation supervision to a fresh, target-matched private status snapshot."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from fdai_deployment_cli.private_output import read_private_bytes
10
+
11
+
12
+ def prior_attempt(path: Path) -> int:
13
+ """Read the existing attempt without treating absence as successful Foundation evidence."""
14
+
15
+ try:
16
+ value = _read(path)
17
+ except FileNotFoundError:
18
+ return 0
19
+ return _attempt(value)
20
+
21
+
22
+ def current_status(
23
+ path: Path, *, previous: int, source_commit: str, run_binding: str
24
+ ) -> dict[str, Any]:
25
+ """Require a newer writer attempt and exact execution context after a successful child exit.
26
+
27
+ This validates the status handoff, not operational effects. Existing exact plan, approval,
28
+ receipt, and independent verification checks remain required by the caller.
29
+ """
30
+
31
+ try:
32
+ value = _read(path)
33
+ except FileNotFoundError as exc:
34
+ raise ValueError(
35
+ "orchestrator did not publish current Foundation status; inspect retained evidence"
36
+ ) from exc
37
+ sequence = value.get("sequence")
38
+ state = value.get("state")
39
+ if (
40
+ value.get("schema_version") != "fdai.genesis-orchestration-status.v2"
41
+ or _attempt(value) != previous + 1
42
+ or type(sequence) is not int
43
+ or sequence < 1
44
+ or value.get("source_commit") != source_commit
45
+ or value.get("target_binding") != run_binding
46
+ or value.get("mode") != "apply"
47
+ or not isinstance(state, str)
48
+ or state not in {"waiting", "complete"}
49
+ ):
50
+ raise ValueError(
51
+ "current Foundation status is stale or mismatched; inspect retained evidence"
52
+ )
53
+ return value
54
+
55
+
56
+ def _attempt(value: dict[str, Any]) -> int:
57
+ attempt = value.get("attempt")
58
+ if type(attempt) is not int or attempt < 1:
59
+ raise ValueError("current Foundation status has an invalid attempt")
60
+ return attempt
61
+
62
+
63
+ def _read(path: Path) -> dict[str, Any]:
64
+ value = json.loads(read_private_bytes(path, max_bytes=1_048_576))
65
+ if not isinstance(value, dict):
66
+ raise ValueError("current Foundation status must be an object")
67
+ return {str(key): item for key, item in value.items()}
@@ -0,0 +1,237 @@
1
+ """Bind standalone Terraform execution to verified local artifacts and one exact UAMI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import shutil
9
+ import subprocess
10
+ from pathlib import Path
11
+
12
+ from fdai_deployment_cli.private_output import read_private_bytes
13
+
14
+ _CONFLICTING_AUTH = (
15
+ "ARM_USE_CLI",
16
+ "ARM_USE_OIDC",
17
+ "ARM_OIDC_TOKEN",
18
+ "ARM_CLIENT_SECRET",
19
+ "ARM_CLIENT_CERTIFICATE_PATH",
20
+ "ARM_CLIENT_CERTIFICATE_PASSWORD",
21
+ "ARM_USERNAME",
22
+ "ARM_PASSWORD",
23
+ "ARM_USE_AKS_WORKLOAD_IDENTITY",
24
+ "ARM_MSI_ENDPOINT",
25
+ )
26
+ _GUID = re.compile(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}")
27
+
28
+
29
+ def configure_terraform(context: dict[str, object]) -> None:
30
+ terraform = Path(str(context["terraform"]))
31
+ config = Path(str(context["terraform_config"]))
32
+ data = Path(str(context["terraform_data"]))
33
+ if not terraform.is_file() or not config.is_file():
34
+ raise ValueError("verified Terraform execution context is unavailable")
35
+ provider_mode = str(context.get("provider_installation", "filesystem_mirror"))
36
+ if provider_mode == "direct":
37
+ expected_config = source_terraform_configuration()
38
+ else:
39
+ provider_mirror = Path(str(context["provider_mirror"]))
40
+ expected_config = terraform_configuration(provider_mirror)
41
+ if read_private_bytes(config, max_bytes=16_384).decode("utf-8") != expected_config:
42
+ raise ValueError("Terraform provider configuration differs from the verified input")
43
+ data.mkdir(mode=0o700, exist_ok=True)
44
+ kit_bin = Path(str(context.get("kit_bin", terraform.parent)))
45
+ os.environ["PATH"] = os.pathsep.join(
46
+ (str(terraform.parent), str(kit_bin), "/usr/local/bin", "/usr/bin", "/bin")
47
+ )
48
+ os.environ["TF_CLI_CONFIG_FILE"] = str(config)
49
+ os.environ["TF_DATA_DIR"] = str(data)
50
+ os.environ["TF_IN_AUTOMATION"] = "1"
51
+ for variable in _CONFLICTING_AUTH:
52
+ os.environ.pop(variable, None)
53
+ os.environ["ARM_SUBSCRIPTION_ID"] = str(context["subscription_id"])
54
+ os.environ["ARM_TENANT_ID"] = str(context["tenant_id"])
55
+ os.environ["ARM_USE_MSI"] = "true"
56
+ os.environ["ARM_CLIENT_ID"] = str(context["client_id"])
57
+ os.environ["ARM_RESOURCE_PROVIDER_REGISTRATIONS"] = "none"
58
+
59
+
60
+ def terraform_configuration(provider_mirror: Path) -> str:
61
+ return (
62
+ "provider_installation {\n"
63
+ " filesystem_mirror {\n"
64
+ f' path = "{provider_mirror}"\n'
65
+ ' include = ["*/*"]\n'
66
+ " }\n"
67
+ " direct {\n"
68
+ ' exclude = ["*/*"]\n'
69
+ " }\n"
70
+ "}\n"
71
+ )
72
+
73
+
74
+ def source_terraform_configuration() -> str:
75
+ """Resolve providers from public registries under committed lock files."""
76
+
77
+ return "provider_installation {\n direct {}\n}\n"
78
+
79
+
80
+ def requires_aks_key_vault_private_access(
81
+ *,
82
+ subscription_id: str,
83
+ resource_group_name: str,
84
+ vault_name: str,
85
+ work_dir: Path,
86
+ ) -> bool:
87
+ """Select only the focused private path required by an existing policy-locked vault."""
88
+
89
+ if (
90
+ _GUID.fullmatch(subscription_id) is None
91
+ or re.fullmatch(r"rg-[a-z0-9-]{3,80}", resource_group_name) is None
92
+ or re.fullmatch(r"[a-z0-9-]{3,24}", vault_name) is None
93
+ ):
94
+ raise ValueError("Key Vault private-access readback target is invalid")
95
+ return _requires_private_access(
96
+ subscription_id=subscription_id,
97
+ resource_group_name=resource_group_name,
98
+ resource_name=vault_name,
99
+ list_command=(
100
+ "az",
101
+ "keyvault",
102
+ "list",
103
+ "--subscription",
104
+ subscription_id,
105
+ "--resource-group",
106
+ resource_group_name,
107
+ "--output",
108
+ "json",
109
+ "--only-show-errors",
110
+ ),
111
+ label="Key Vault",
112
+ work_dir=work_dir,
113
+ )
114
+
115
+
116
+ def requires_aks_document_storage_private_access(
117
+ *,
118
+ subscription_id: str,
119
+ resource_group_name: str,
120
+ account_name: str,
121
+ work_dir: Path,
122
+ ) -> bool:
123
+ """Select focused Blob/DFS access for one existing policy-locked document account."""
124
+
125
+ if (
126
+ _GUID.fullmatch(subscription_id) is None
127
+ or re.fullmatch(r"rg-[a-z0-9-]{3,80}", resource_group_name) is None
128
+ or re.fullmatch(r"[a-z0-9]{3,24}", account_name) is None
129
+ ):
130
+ raise ValueError("document storage private-access readback target is invalid")
131
+ return _requires_private_access(
132
+ subscription_id=subscription_id,
133
+ resource_group_name=resource_group_name,
134
+ resource_name=account_name,
135
+ list_command=(
136
+ "az",
137
+ "storage",
138
+ "account",
139
+ "list",
140
+ "--subscription",
141
+ subscription_id,
142
+ "--resource-group",
143
+ resource_group_name,
144
+ "--output",
145
+ "json",
146
+ "--only-show-errors",
147
+ ),
148
+ label="document storage",
149
+ work_dir=work_dir,
150
+ )
151
+
152
+
153
+ def _requires_private_access(
154
+ *,
155
+ subscription_id: str,
156
+ resource_group_name: str,
157
+ resource_name: str,
158
+ list_command: tuple[str, ...],
159
+ label: str,
160
+ work_dir: Path,
161
+ ) -> bool:
162
+ group_exists = _capture(
163
+ (
164
+ "az",
165
+ "group",
166
+ "exists",
167
+ "--subscription",
168
+ subscription_id,
169
+ "--name",
170
+ resource_group_name,
171
+ "--output",
172
+ "tsv",
173
+ "--only-show-errors",
174
+ ),
175
+ cwd=work_dir,
176
+ reason="application resource-group readback failed",
177
+ )
178
+ if group_exists == "false":
179
+ return False
180
+ if group_exists != "true":
181
+ raise ValueError("application resource-group readback is invalid")
182
+ raw = _capture(list_command, cwd=work_dir, reason=f"{label} public-access readback failed")
183
+ if len(raw.encode()) > 4 * 1024 * 1024:
184
+ raise ValueError(f"{label} public-access readback exceeds its size limit")
185
+ try:
186
+ rows = json.loads(raw)
187
+ except json.JSONDecodeError as exc:
188
+ raise ValueError(f"{label} public-access readback is invalid") from exc
189
+ if not isinstance(rows, list):
190
+ raise TypeError(f"{label} public-access readback is invalid")
191
+ matches = [row for row in rows if isinstance(row, dict) and row.get("name") == resource_name]
192
+ if not matches:
193
+ return False
194
+ if len(matches) != 1:
195
+ raise ValueError(f"{label} public-access readback is ambiguous")
196
+ public_access = matches[0].get("publicNetworkAccess")
197
+ if not isinstance(public_access, str):
198
+ properties = matches[0].get("properties")
199
+ public_access = (
200
+ properties.get("publicNetworkAccess") if isinstance(properties, dict) else None
201
+ )
202
+ if public_access == "Disabled":
203
+ return True
204
+ if public_access == "Enabled":
205
+ return False
206
+ raise ValueError(f"{label} public-access readback is invalid")
207
+
208
+
209
+ def _capture(command: tuple[str, ...], *, cwd: Path, reason: str) -> str:
210
+ result = subprocess.run(
211
+ command,
212
+ cwd=cwd,
213
+ check=False,
214
+ capture_output=True,
215
+ text=True,
216
+ timeout=60,
217
+ umask=0o077,
218
+ )
219
+ if result.returncode != 0:
220
+ raise ValueError(reason)
221
+ return result.stdout.strip()
222
+
223
+
224
+ __all__ = [
225
+ "configure_terraform",
226
+ "requires_aks_document_storage_private_access",
227
+ "requires_aks_key_vault_private_access",
228
+ "terraform_configuration",
229
+ ]
230
+
231
+
232
+ def ensure_backend(infra: Path) -> None:
233
+ """Recreate the generated backend file that a re-extracted source snapshot lacks."""
234
+ backend = infra / "backend.tf"
235
+ if not backend.exists():
236
+ shutil.copyfile(infra / "backend.azurerm.tf.example", backend)
237
+ backend.chmod(0o600)