fdai-deployment-cli 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fdai_deployment_cli/__about__.py +3 -0
- fdai_deployment_cli/__init__.py +5 -0
- fdai_deployment_cli/__main__.py +5 -0
- fdai_deployment_cli/acr_publish.py +585 -0
- fdai_deployment_cli/aks_catalog_review.py +137 -0
- fdai_deployment_cli/aks_historical_reconciliation.py +732 -0
- fdai_deployment_cli/aks_job_environment.py +59 -0
- fdai_deployment_cli/aks_job_execution.py +794 -0
- fdai_deployment_cli/aks_kubernetes_defaults.py +66 -0
- fdai_deployment_cli/aks_preflight.py +422 -0
- fdai_deployment_cli/aks_readiness.py +220 -0
- fdai_deployment_cli/aks_service_update.py +180 -0
- fdai_deployment_cli/aks_workload_jobs.py +281 -0
- fdai_deployment_cli/application_state_adoption.py +347 -0
- fdai_deployment_cli/azure_actor.py +61 -0
- fdai_deployment_cli/azure_naming.py +252 -0
- fdai_deployment_cli/azure_retail_prices.py +173 -0
- fdai_deployment_cli/bootstrap_approval.py +99 -0
- fdai_deployment_cli/bootstrap_channel.py +321 -0
- fdai_deployment_cli/bootstrap_executor.py +311 -0
- fdai_deployment_cli/bootstrap_plan.py +237 -0
- fdai_deployment_cli/bootstrap_reconcile.py +402 -0
- fdai_deployment_cli/bootstrap_support.py +138 -0
- fdai_deployment_cli/bootstrap_worker.py +448 -0
- fdai_deployment_cli/bundle.py +352 -0
- fdai_deployment_cli/capacity.py +143 -0
- fdai_deployment_cli/catalog_review_profile.py +306 -0
- fdai_deployment_cli/cli.py +798 -0
- fdai_deployment_cli/cli_help.py +214 -0
- fdai_deployment_cli/cli_parser.py +791 -0
- fdai_deployment_cli/cli_plan.py +450 -0
- fdai_deployment_cli/compiler.py +99 -0
- fdai_deployment_cli/console_artifact.py +218 -0
- fdai_deployment_cli/console_config.py +214 -0
- fdai_deployment_cli/console_update.py +832 -0
- fdai_deployment_cli/contracts.py +307 -0
- fdai_deployment_cli/control_package.py +140 -0
- fdai_deployment_cli/deadline_transport.py +70 -0
- fdai_deployment_cli/deployment_cost.py +131 -0
- fdai_deployment_cli/deployment_deadline.py +28 -0
- fdai_deployment_cli/deployment_kit.py +790 -0
- fdai_deployment_cli/deployment_kit_cache.py +191 -0
- fdai_deployment_cli/deployment_progress.py +648 -0
- fdai_deployment_cli/development_authority_cli.py +56 -0
- fdai_deployment_cli/development_authority_profile.py +319 -0
- fdai_deployment_cli/doctor.py +136 -0
- fdai_deployment_cli/entitlement_preflight.py +68 -0
- fdai_deployment_cli/entitlement_transport.py +49 -0
- fdai_deployment_cli/entra_bootstrap.py +579 -0
- fdai_deployment_cli/entra_control_profile.py +214 -0
- fdai_deployment_cli/entra_graph.py +349 -0
- fdai_deployment_cli/entra_profiles.py +215 -0
- fdai_deployment_cli/entra_source.py +244 -0
- fdai_deployment_cli/execution_bundle.py +478 -0
- fdai_deployment_cli/execution_copies.py +63 -0
- fdai_deployment_cli/foundation_adoption.py +438 -0
- fdai_deployment_cli/foundation_adoption_evidence.py +245 -0
- fdai_deployment_cli/foundation_adoption_host.py +157 -0
- fdai_deployment_cli/foundation_adoption_transport.py +51 -0
- fdai_deployment_cli/foundation_failure.py +72 -0
- fdai_deployment_cli/foundation_image.py +168 -0
- fdai_deployment_cli/foundation_input.py +235 -0
- fdai_deployment_cli/foundation_output.py +154 -0
- fdai_deployment_cli/foundation_plan.py +381 -0
- fdai_deployment_cli/foundation_process.py +64 -0
- fdai_deployment_cli/foundation_progress.py +231 -0
- fdai_deployment_cli/github_actions.py +171 -0
- fdai_deployment_cli/github_apply_artifact.py +259 -0
- fdai_deployment_cli/github_artifact_io.py +48 -0
- fdai_deployment_cli/github_dispatch.py +248 -0
- fdai_deployment_cli/github_plan_artifact.py +181 -0
- fdai_deployment_cli/github_provider_artifact.py +170 -0
- fdai_deployment_cli/github_transport.py +46 -0
- fdai_deployment_cli/github_workflow_values.py +312 -0
- fdai_deployment_cli/hub_image_deploy.py +354 -0
- fdai_deployment_cli/hub_image_stage.py +162 -0
- fdai_deployment_cli/hub_image_trust.py +281 -0
- fdai_deployment_cli/hub_ownership.py +161 -0
- fdai_deployment_cli/hub_workload_handoff.py +264 -0
- fdai_deployment_cli/hub_workload_handoff_inputs.py +227 -0
- fdai_deployment_cli/identity_profile.py +227 -0
- fdai_deployment_cli/image_targets.py +197 -0
- fdai_deployment_cli/installation_scope.py +280 -0
- fdai_deployment_cli/license.py +311 -0
- fdai_deployment_cli/license_issue.py +239 -0
- fdai_deployment_cli/lifecycle_authorization.py +252 -0
- fdai_deployment_cli/lifecycle_commands.py +173 -0
- fdai_deployment_cli/lifecycle_configuration.py +722 -0
- fdai_deployment_cli/lifecycle_configuration_artifact.py +45 -0
- fdai_deployment_cli/lifecycle_configuration_cli.py +324 -0
- fdai_deployment_cli/lifecycle_configuration_document.py +74 -0
- fdai_deployment_cli/lifecycle_configuration_entities.py +146 -0
- fdai_deployment_cli/lifecycle_configuration_package.py +492 -0
- fdai_deployment_cli/lifecycle_configuration_publication.py +277 -0
- fdai_deployment_cli/lifecycle_configuration_publication_cli.py +191 -0
- fdai_deployment_cli/lifecycle_configuration_sealing.py +127 -0
- fdai_deployment_cli/lifecycle_enrollment.py +45 -0
- fdai_deployment_cli/lifecycle_infrastructure_agent.py +174 -0
- fdai_deployment_cli/lifecycle_installation_binding.py +65 -0
- fdai_deployment_cli/lifecycle_native.py +247 -0
- fdai_deployment_cli/lifecycle_native_reconciliation.py +568 -0
- fdai_deployment_cli/lifecycle_native_transition.py +339 -0
- fdai_deployment_cli/lifecycle_ownership_exclusion.py +78 -0
- fdai_deployment_cli/lifecycle_plan.py +800 -0
- fdai_deployment_cli/lifecycle_promotion.py +185 -0
- fdai_deployment_cli/lifecycle_release.py +308 -0
- fdai_deployment_cli/lifecycle_self_upgrade.py +202 -0
- fdai_deployment_cli/lifecycle_upgrade_bundle.py +176 -0
- fdai_deployment_cli/notification_recipients_input.py +222 -0
- fdai_deployment_cli/observer_artifact.py +106 -0
- fdai_deployment_cli/oci_archive.py +353 -0
- fdai_deployment_cli/offline_kit.py +797 -0
- fdai_deployment_cli/offline_prepare.py +135 -0
- fdai_deployment_cli/operational_evidence_verifier_input.py +575 -0
- fdai_deployment_cli/plan_input.py +101 -0
- fdai_deployment_cli/private_output.py +192 -0
- fdai_deployment_cli/profile.py +70 -0
- fdai_deployment_cli/progress.py +183 -0
- fdai_deployment_cli/readiness.py +286 -0
- fdai_deployment_cli/run_command_receiver.py +418 -0
- fdai_deployment_cli/runtime_build.py +386 -0
- fdai_deployment_cli/runtime_profile.py +287 -0
- fdai_deployment_cli/runtime_release.py +745 -0
- fdai_deployment_cli/runtime_stage.py +82 -0
- fdai_deployment_cli/runtime_support_installation.py +215 -0
- fdai_deployment_cli/simulation.py +80 -0
- fdai_deployment_cli/source_application_inputs.py +397 -0
- fdai_deployment_cli/source_azure.py +586 -0
- fdai_deployment_cli/source_change_cli.py +107 -0
- fdai_deployment_cli/source_change_parser.py +75 -0
- fdai_deployment_cli/source_change_plan.py +181 -0
- fdai_deployment_cli/source_changed_update.py +196 -0
- fdai_deployment_cli/source_deploy.py +170 -0
- fdai_deployment_cli/source_foundation.py +174 -0
- fdai_deployment_cli/source_host_tools.py +163 -0
- fdai_deployment_cli/source_image_stage.py +400 -0
- fdai_deployment_cli/source_input.py +173 -0
- fdai_deployment_cli/source_projection.py +164 -0
- fdai_deployment_cli/source_receiver.py +76 -0
- fdai_deployment_cli/source_recovery.py +264 -0
- fdai_deployment_cli/source_runtime.py +81 -0
- fdai_deployment_cli/source_runtime_support.py +368 -0
- fdai_deployment_cli/source_service_update.py +726 -0
- fdai_deployment_cli/source_service_update_cli.py +47 -0
- fdai_deployment_cli/source_snapshot.py +178 -0
- fdai_deployment_cli/source_teardown.py +574 -0
- fdai_deployment_cli/source_terraform.py +85 -0
- fdai_deployment_cli/source_transport.py +399 -0
- fdai_deployment_cli/standalone_aks_inventory.py +558 -0
- fdai_deployment_cli/standalone_aks_job_execution.py +769 -0
- fdai_deployment_cli/standalone_aks_nodepool_guard.py +141 -0
- fdai_deployment_cli/standalone_application.py +977 -0
- fdai_deployment_cli/standalone_application_completion.py +151 -0
- fdai_deployment_cli/standalone_application_input_recovery.py +107 -0
- fdai_deployment_cli/standalone_application_recovery.py +116 -0
- fdai_deployment_cli/standalone_approval_input.py +24 -0
- fdai_deployment_cli/standalone_catalog_checkpoint.py +122 -0
- fdai_deployment_cli/standalone_catalog_review.py +251 -0
- fdai_deployment_cli/standalone_checkpoint_failure.py +127 -0
- fdai_deployment_cli/standalone_console_publish.py +174 -0
- fdai_deployment_cli/standalone_database_principals.py +41 -0
- fdai_deployment_cli/standalone_deploy.py +638 -0
- fdai_deployment_cli/standalone_development_authority.py +166 -0
- fdai_deployment_cli/standalone_foundation_adoption.py +87 -0
- fdai_deployment_cli/standalone_foundation_transition.py +767 -0
- fdai_deployment_cli/standalone_host.py +4608 -0
- fdai_deployment_cli/standalone_host_rebind.py +226 -0
- fdai_deployment_cli/standalone_host_residual.py +104 -0
- fdai_deployment_cli/standalone_host_state.py +145 -0
- fdai_deployment_cli/standalone_host_values.py +563 -0
- fdai_deployment_cli/standalone_kubernetes_tools.py +100 -0
- fdai_deployment_cli/standalone_license_installation.py +177 -0
- fdai_deployment_cli/standalone_management_egress.py +83 -0
- fdai_deployment_cli/standalone_migration_evidence.py +38 -0
- fdai_deployment_cli/standalone_operational_evidence.py +356 -0
- fdai_deployment_cli/standalone_planned_outputs.py +147 -0
- fdai_deployment_cli/standalone_private_access_probe.py +100 -0
- fdai_deployment_cli/standalone_product_profile.py +110 -0
- fdai_deployment_cli/standalone_remote_prepare.py +442 -0
- fdai_deployment_cli/standalone_residual_apply.py +540 -0
- fdai_deployment_cli/standalone_residual_successor.py +431 -0
- fdai_deployment_cli/standalone_review.py +266 -0
- fdai_deployment_cli/standalone_service_update_finalization.py +116 -0
- fdai_deployment_cli/standalone_service_update_recovery.py +277 -0
- fdai_deployment_cli/standalone_stage_targets.py +325 -0
- fdai_deployment_cli/standalone_status.py +67 -0
- fdai_deployment_cli/standalone_terraform_environment.py +237 -0
- fdai_deployment_cli/standalone_transfer_cleanup.py +237 -0
- fdai_deployment_cli/standalone_trial_activation.py +193 -0
- fdai_deployment_cli/state.py +434 -0
- fdai_deployment_cli/state_handoff.py +253 -0
- fdai_deployment_cli/status_projection.py +97 -0
- fdai_deployment_cli/support_install.py +257 -0
- fdai_deployment_cli/target.py +46 -0
- fdai_deployment_cli/trust/capabilities.json +62 -0
- fdai_deployment_cli/trust/deployment-bundle-root.pub +3 -0
- fdai_deployment_cli/trust/deployment-release-root.pub +3 -0
- fdai_deployment_cli/trust/upstream-signing-key.pub +3 -0
- fdai_deployment_cli/trust_roots.py +50 -0
- fdai_deployment_cli-0.1.2.dist-info/METADATA +112 -0
- fdai_deployment_cli-0.1.2.dist-info/RECORD +203 -0
- fdai_deployment_cli-0.1.2.dist-info/WHEEL +4 -0
- fdai_deployment_cli-0.1.2.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,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)
|