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,151 @@
1
+ """Complete standalone identity and application deployment from verified Foundation context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import sys
7
+ from collections.abc import Callable
8
+ from contextlib import redirect_stdout
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from fdai_service_contracts.product_profile import ProductAddOn
13
+
14
+ from fdai_deployment_cli.application_state_adoption import ApplicationStateAdoption
15
+ from fdai_deployment_cli.catalog_review_profile import CatalogReviewDeploymentProfile
16
+ from fdai_deployment_cli.control_package import ControlPackage
17
+ from fdai_deployment_cli.deployment_deadline import DeploymentDeadline
18
+ from fdai_deployment_cli.deployment_kit import DeploymentKit
19
+ from fdai_deployment_cli.deployment_progress import begin_stage, terminal_output
20
+ from fdai_deployment_cli.development_authority_profile import (
21
+ DevelopmentAuthorityDeploymentProfile,
22
+ )
23
+ from fdai_deployment_cli.hub_image_trust import HubImageSelection
24
+ from fdai_deployment_cli.notification_recipients_input import (
25
+ NotificationRecipientsDeploymentInput,
26
+ )
27
+ from fdai_deployment_cli.operational_evidence_verifier_input import (
28
+ OperationalEvidenceVerifierDeploymentInput,
29
+ )
30
+ from fdai_deployment_cli.runtime_profile import RuntimeDeploymentProfile
31
+ from fdai_deployment_cli.standalone_application import deploy_standalone_application
32
+
33
+
34
+ def complete_application(
35
+ *,
36
+ kit: DeploymentKit | None,
37
+ prepared: Any,
38
+ status: dict[str, Any],
39
+ scripts: Path,
40
+ deadline: DeploymentDeadline,
41
+ selected_runtime: RuntimeDeploymentProfile,
42
+ trial_token: Path | None,
43
+ application_state_adoption: ApplicationStateAdoption | None,
44
+ foundation_state_receipt_digest: str,
45
+ current_operator_object_id: Callable[[], str],
46
+ foundation_adoption_receipt_digest: str | None = None,
47
+ catalog_review_profile: CatalogReviewDeploymentProfile | None = None,
48
+ operational_evidence_verifier_input: OperationalEvidenceVerifierDeploymentInput | None = None,
49
+ notification_recipients: NotificationRecipientsDeploymentInput | None = None,
50
+ development_authority_profile: DevelopmentAuthorityDeploymentProfile | None = None,
51
+ clear_development_authority_profile: bool = False,
52
+ control_package: ControlPackage | None = None,
53
+ source_snapshot: Path | None = None,
54
+ source_snapshot_digest: str | None = None,
55
+ source_root: Path | None = None,
56
+ foundation_recovery_directory: Path | None = None,
57
+ hub_selection: HubImageSelection | None = None,
58
+ ) -> dict[str, object]:
59
+ """Configure identity and complete one exact standalone application deployment."""
60
+
61
+ source_commit = str(getattr(prepared, "source_commit", "") or getattr(kit, "source_commit", ""))
62
+ if not source_commit:
63
+ raise ValueError("application source commit is unavailable")
64
+ entra_bindings: dict[str, str] | None = None
65
+ if selected_runtime.product_profile.selects(ProductAddOn.ENTERPRISE_IDENTITY_GOVERNANCE):
66
+ begin_stage("identity")
67
+ sys.path.insert(0, str(scripts))
68
+ try:
69
+ supervisor = importlib.import_module("genesis_supervisor")
70
+ entra = importlib.import_module("genesis_entra")
71
+ approval_prompt = importlib.import_module("genesis_approval_prompt")
72
+ actor_digest = approval_prompt.current_actor_digest(prepared.run_binding)
73
+ with (
74
+ terminal_output("Identity configuration and any required approval"),
75
+ redirect_stdout(sys.stderr),
76
+ ):
77
+ entra_bindings = supervisor._configure_entra(
78
+ prepared=prepared,
79
+ status=status,
80
+ actor_digest=actor_digest,
81
+ plan=entra.plan_entra(
82
+ spa_display_name=entra.scoped_spa_display_name(str(prepared.target_binding))
83
+ ),
84
+ foundation_handoff_path=(
85
+ foundation_recovery_directory / "recovery-private-handoff.json"
86
+ if foundation_recovery_directory is not None
87
+ else None
88
+ ),
89
+ )
90
+ entra_bindings["CURRENT_OPERATOR_OBJECT_ID"] = current_operator_object_id()
91
+ finally:
92
+ sys.path.remove(str(scripts))
93
+ application = deploy_standalone_application(
94
+ kit=kit,
95
+ prepared=prepared,
96
+ foundation_status=status,
97
+ entra_bindings=entra_bindings,
98
+ scripts=scripts,
99
+ trial_token=trial_token,
100
+ timeout_seconds=deadline.remaining(),
101
+ runtime_profile=selected_runtime,
102
+ application_state_adoption=application_state_adoption,
103
+ catalog_review_profile=(
104
+ catalog_review_profile or CatalogReviewDeploymentProfile.unselected()
105
+ ),
106
+ operational_evidence_verifier_input=operational_evidence_verifier_input,
107
+ notification_recipients=notification_recipients,
108
+ development_authority_profile=development_authority_profile,
109
+ clear_development_authority_profile=clear_development_authority_profile,
110
+ control_package=control_package,
111
+ source_snapshot=source_snapshot,
112
+ source_snapshot_digest=source_snapshot_digest,
113
+ source_root=source_root,
114
+ foundation_recovery_directory=foundation_recovery_directory,
115
+ **({"hub_selection": hub_selection} if hub_selection is not None else {}),
116
+ )
117
+ deadline.remaining()
118
+ result: dict[str, object] = {
119
+ "schema_version": "fdai.standalone-azure-deployment.v2",
120
+ "state": "deployment-ready",
121
+ "source_commit": source_commit,
122
+ "foundation_state_receipt_digest": foundation_state_receipt_digest,
123
+ "application_receipt_digest": application["receipt_digest"],
124
+ "catalog_review_receipt_digest": application["catalog_review_receipt_digest"],
125
+ "catalog_review_state": application["catalog_review_state"],
126
+ "runtime_profile_digest": selected_runtime.digest,
127
+ "runtime_platform": selected_runtime.runtime_platform.value,
128
+ "database_placement": selected_runtime.database_placement.value,
129
+ "product_profile": selected_runtime.product_profile.model_dump(mode="json"),
130
+ "application_converged": True,
131
+ "deployment_ready": True,
132
+ "inventory_ready": application.get("inventory_ready") is True,
133
+ "license_mode": application["license_mode"],
134
+ "mutation_performed": True,
135
+ "subscription_ready": False,
136
+ }
137
+ if source_snapshot_digest is not None:
138
+ result["source_snapshot_digest"] = source_snapshot_digest
139
+ result["provenance"] = "operator-selected-source"
140
+ result["release_signature_verified"] = False
141
+ elif kit is not None:
142
+ result["kit_manifest_digest"] = kit.verification.manifest_digest
143
+ result["runtime_release_digest"] = kit.runtime.digest
144
+ if control_package is not None:
145
+ result["control_package_digest"] = control_package.archive_digest
146
+ result["control_package_version"] = control_package.version
147
+ if foundation_adoption_receipt_digest is not None:
148
+ result["foundation_adoption_receipt_digest"] = foundation_adoption_receipt_digest
149
+ if hub_selection is not None:
150
+ result.update(hub_selection.metadata())
151
+ return result
@@ -0,0 +1,107 @@
1
+ """Recover one authoritative AKS application input after an ambiguous apply."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from fdai_deployment_cli.contracts import canonical_digest
9
+ from fdai_deployment_cli.private_output import write_private_output
10
+ from fdai_deployment_cli.standalone_host_state import (
11
+ private_json,
12
+ replace_private_json,
13
+ )
14
+
15
+
16
+ def bind_authoritative_backend_nsg(
17
+ *,
18
+ work_dir: Path,
19
+ retained: dict[str, object],
20
+ current: dict[str, object],
21
+ ) -> bool:
22
+ """Bind the sole empty-to-authoritative NSG transition under retained claims."""
23
+
24
+ if retained == current:
25
+ return False
26
+ original_paths = ("application-claim.json", "application-receipt.json")
27
+ residual_paths = (
28
+ "application-claim.json",
29
+ "application-residual-claim.json",
30
+ "application-residual-review.json",
31
+ )
32
+ ordinary_applied = all((work_dir / name).is_file() for name in original_paths)
33
+ residual_claimed = all((work_dir / name).is_file() for name in residual_paths)
34
+ if not ordinary_applied and not residual_claimed:
35
+ raise ValueError("retained workloads.auto.tfvars.json differs")
36
+ old_gateway = retained.get("browser_gateway")
37
+ new_gateway = current.get("browser_gateway")
38
+ if not isinstance(old_gateway, dict) or not isinstance(new_gateway, dict):
39
+ raise TypeError("retained workloads.auto.tfvars.json differs")
40
+ old_nsg = old_gateway.get("backend_nsg_id")
41
+ new_nsg = new_gateway.get("backend_nsg_id")
42
+ expected = {
43
+ **retained,
44
+ "browser_gateway": {**old_gateway, "backend_nsg_id": new_nsg},
45
+ }
46
+ if old_nsg != "" or not isinstance(new_nsg, str) or not new_nsg or expected != current:
47
+ raise ValueError("retained workloads.auto.tfvars.json differs")
48
+ original_claim = private_json(work_dir / "application-claim.json", "application claim")
49
+ claim = {
50
+ "schema_version": "fdai.application-input-rebind-claim.v1",
51
+ "field": "browser_gateway.backend_nsg_id",
52
+ "prior_values_digest": canonical_digest(retained),
53
+ "current_values_digest": canonical_digest(current),
54
+ "original_claim_digest": canonical_digest(original_claim),
55
+ "mutation_performed": False,
56
+ }
57
+ if residual_claimed:
58
+ claim["residual_claim_digest"] = canonical_digest(
59
+ private_json(
60
+ work_dir / "application-residual-claim.json",
61
+ "application residual claim",
62
+ )
63
+ )
64
+ else:
65
+ claim["application_receipt_digest"] = canonical_digest(
66
+ private_json(work_dir / "application-receipt.json", "application receipt")
67
+ )
68
+ claim["claim_digest"] = canonical_digest(claim)
69
+ claim_path = work_dir / "application-input-rebind-claim.json"
70
+ if claim_path.exists():
71
+ if private_json(claim_path, "application input rebind claim") != claim:
72
+ raise ValueError("application input rebind claim differs")
73
+ else:
74
+ write_private_output(
75
+ claim_path,
76
+ json.dumps(claim, sort_keys=True, separators=(",", ":")) + "\n",
77
+ )
78
+ replace_private_json(work_dir / "workloads.auto.tfvars.json", current)
79
+ receipt = {
80
+ "schema_version": "fdai.application-input-rebind-receipt.v1",
81
+ "claim_digest": claim["claim_digest"],
82
+ "field": claim["field"],
83
+ "authoritative_value_digest": canonical_digest({"backend_nsg_id": new_nsg}),
84
+ "mutation_performed": True,
85
+ "subscription_ready": False,
86
+ }
87
+ receipt["receipt_digest"] = canonical_digest(receipt)
88
+ replace_private_json(work_dir / "application-input-rebind-receipt.json", receipt)
89
+ return True
90
+
91
+
92
+ def successor_prerequisite_verified(work_dir: Path, stage: str) -> bool:
93
+ """Return whether the stage has its exact recovery prerequisite receipt."""
94
+
95
+ if stage != "application":
96
+ return False
97
+ path = work_dir / "application-input-rebind-receipt.json"
98
+ if not path.is_file() or path.is_symlink():
99
+ return False
100
+ receipt = private_json(path, "application input rebind receipt")
101
+ digest = receipt.pop("receipt_digest", None)
102
+ return (
103
+ canonical_digest(receipt) == digest
104
+ and receipt.get("schema_version") == "fdai.application-input-rebind-receipt.v1"
105
+ and receipt.get("field") == "browser_gateway.backend_nsg_id"
106
+ and receipt.get("mutation_performed") is True
107
+ )
@@ -0,0 +1,116 @@
1
+ """Bounded same-invocation recovery for managed-host application stages."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from fdai_deployment_cli.deployment_deadline import DeploymentDeadline
10
+
11
+ RemoteJson = Callable[..., dict[str, Any]]
12
+ PlanAfterRecovery = Callable[..., tuple[dict[str, Any], str]]
13
+ ApprovePlan = Callable[..., Path]
14
+ RequireReceipt = Callable[[dict[str, Any], str], None]
15
+
16
+
17
+ def converge_focused_private_access(
18
+ tunnel: Any,
19
+ *,
20
+ remote_root: str,
21
+ app_work: str,
22
+ prepared_root: Path,
23
+ remote_approval: str,
24
+ deadline: DeploymentDeadline,
25
+ remote_json: RemoteJson,
26
+ plan_after_recovery: PlanAfterRecovery,
27
+ approve_plan: ApprovePlan,
28
+ require_receipt: RequireReceipt,
29
+ ) -> dict[str, Any]:
30
+ recovery = remote_json(
31
+ tunnel,
32
+ remote_root,
33
+ app_work,
34
+ ("recover-apply", "--stage", "access"),
35
+ timeout=deadline.remaining(3600),
36
+ )
37
+ if recovery.get("state") == "applied":
38
+ require_receipt(recovery, "access")
39
+ return recovery
40
+ review, command = plan_after_recovery(
41
+ tunnel,
42
+ remote_root,
43
+ app_work,
44
+ recovery,
45
+ stage="access",
46
+ timeout=deadline.remaining(3600),
47
+ )
48
+ approval = approve_plan(prepared_root, review, deadline=deadline)
49
+ tunnel.ssh(("rm", "-f", "--", remote_approval), timeout=60)
50
+ tunnel.copy_to(approval, remote_approval, timeout=120)
51
+ try:
52
+ receipt = remote_json(
53
+ tunnel,
54
+ remote_root,
55
+ app_work,
56
+ (command, "--stage", "access", "--approval", remote_approval),
57
+ timeout=deadline.remaining(7200),
58
+ )
59
+ finally:
60
+ approval.unlink(missing_ok=True)
61
+ tunnel.ssh(("rm", "-f", "--", remote_approval), timeout=60)
62
+ require_receipt(receipt, "access")
63
+ return receipt
64
+
65
+
66
+ def recover_failed_exact_apply(
67
+ tunnel: Any,
68
+ *,
69
+ remote_root: str,
70
+ app_work: str,
71
+ prepared_root: Path,
72
+ remote_approval: str,
73
+ stage: str,
74
+ deadline: DeploymentDeadline,
75
+ remote_json: RemoteJson,
76
+ plan_after_recovery: PlanAfterRecovery,
77
+ approve_plan: ApprovePlan,
78
+ ) -> dict[str, Any]:
79
+ """Verify a failed exact apply, then apply only its separately approved residual."""
80
+ for phase in range(2):
81
+ recovery = remote_json(
82
+ tunnel,
83
+ remote_root,
84
+ app_work,
85
+ ("recover-apply", "--stage", stage),
86
+ timeout=deadline.remaining(3600),
87
+ )
88
+ if recovery.get("state") == "applied":
89
+ return recovery
90
+ review, command = plan_after_recovery(
91
+ tunnel,
92
+ remote_root,
93
+ app_work,
94
+ recovery,
95
+ stage=stage,
96
+ timeout=deadline.remaining(3600),
97
+ )
98
+ if command != "apply-residual":
99
+ raise ValueError("failed exact apply did not produce bounded residual recovery")
100
+ approval = approve_plan(prepared_root, review, deadline=deadline)
101
+ tunnel.copy_to(approval, remote_approval, timeout=120)
102
+ try:
103
+ return remote_json(
104
+ tunnel,
105
+ remote_root,
106
+ app_work,
107
+ (command, "--stage", stage, "--approval", remote_approval),
108
+ timeout=deadline.remaining(7200),
109
+ )
110
+ except ValueError:
111
+ if phase == 1:
112
+ raise
113
+ finally:
114
+ approval.unlink(missing_ok=True)
115
+ tunnel.ssh(("rm", "-f", "--", remote_approval), timeout=60)
116
+ raise ValueError("failed exact apply exceeded its bounded residual recovery")
@@ -0,0 +1,24 @@
1
+ """Bounded interactive-input readiness for standalone plan approval."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import select
6
+ import sys
7
+
8
+
9
+ def wait_for_approval_input(timeout_seconds: int) -> None:
10
+ """Wait only on a real terminal within the invocation's remaining budget."""
11
+
12
+ if timeout_seconds <= 0:
13
+ raise TimeoutError("standalone approval deadline expired; no approval was granted")
14
+ if not sys.stdin.isatty():
15
+ raise ValueError("standalone approval requires an interactive terminal")
16
+ try:
17
+ readable, _, _ = select.select([sys.stdin], [], [], timeout_seconds)
18
+ except (OSError, ValueError):
19
+ raise ValueError("standalone approval input is unavailable") from None
20
+ if not readable:
21
+ raise TimeoutError("standalone approval input timed out; no approval was granted")
22
+
23
+
24
+ __all__ = ["wait_for_approval_input"]
@@ -0,0 +1,122 @@
1
+ """Post-application inventory and catalog-review checkpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Callable
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+
10
+ from fdai_deployment_cli.contracts import canonical_digest
11
+ from fdai_deployment_cli.deployment_progress import begin_stage, progress_detail
12
+ from fdai_deployment_cli.standalone_aks_inventory import require_initial_inventory_receipt
13
+ from fdai_deployment_cli.standalone_trial_activation import require_trial_activation_receipt
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class PostApplicationReceipts:
18
+ inventory: dict[str, Any]
19
+ catalog_review: dict[str, Any]
20
+ trial_activation: dict[str, Any] | None = None
21
+
22
+ @property
23
+ def trial_open(self) -> bool:
24
+ """Whether the read-back Trial window is open under a trusted clock."""
25
+
26
+ trial = self.trial_activation
27
+ return trial is not None and not trial["window_ended"] and not trial["clock_blocked"]
28
+
29
+
30
+ def run_post_application_checkpoints(
31
+ tunnel: Any,
32
+ *,
33
+ remote_root: str,
34
+ app_work: str,
35
+ runtime_platform: str,
36
+ remote_json: Callable[..., dict[str, Any]],
37
+ ) -> PostApplicationReceipts:
38
+ """Open the AKS Trial window, then run inventory and the selected-or-skipped review."""
39
+
40
+ trial_activation = None
41
+ if runtime_platform == "aks":
42
+ progress_detail("Opening the Trial window at the installation's anchored creation time")
43
+ trial_activation = require_trial_activation_receipt(
44
+ remote_json(tunnel, remote_root, app_work, ("activate-trial",), timeout=900)
45
+ )
46
+ begin_stage("initial-inventory")
47
+ progress_detail("Collecting and independently reading back the initial inventory")
48
+ inventory = remote_json(
49
+ tunnel,
50
+ remote_root,
51
+ app_work,
52
+ ("initial-inventory",),
53
+ timeout=4200,
54
+ )
55
+ require_initial_inventory_receipt(inventory, runtime_platform=runtime_platform)
56
+ begin_stage("catalog-review")
57
+ progress_detail("Publishing or explicitly skipping the frozen catalog review")
58
+ catalog_review = remote_json(
59
+ tunnel,
60
+ remote_root,
61
+ app_work,
62
+ ("catalog-review",),
63
+ timeout=600,
64
+ )
65
+ require_catalog_review_receipt(catalog_review)
66
+ return PostApplicationReceipts(
67
+ inventory=inventory,
68
+ catalog_review=catalog_review,
69
+ trial_activation=trial_activation,
70
+ )
71
+
72
+
73
+ def require_catalog_review_receipt(value: dict[str, Any]) -> None:
74
+ """Accept only an explicit skip or independently observed draft publication."""
75
+
76
+ supplied_digest = value.get("receipt_digest")
77
+ material = {key: item for key, item in value.items() if key != "receipt_digest"}
78
+ if supplied_digest != canonical_digest(material):
79
+ raise ValueError("standalone catalog review receipt digest is invalid")
80
+ if value.get("state") == "skipped":
81
+ if (
82
+ value.get("selected") is not False
83
+ or value.get("reason") != "not_selected"
84
+ or value.get("mutation_performed") is not False
85
+ ):
86
+ raise ValueError("standalone catalog review skip receipt is invalid")
87
+ return
88
+ required_labels = value.get("required_labels")
89
+ observed_labels = value.get("observed_labels")
90
+ if (
91
+ value.get("state") != "draft-review-published"
92
+ or value.get("selected") is not True
93
+ or not isinstance(required_labels, list)
94
+ or any(not isinstance(label, str) for label in required_labels)
95
+ or len(required_labels) != len(set(required_labels))
96
+ or required_labels != sorted(required_labels)
97
+ or observed_labels != required_labels
98
+ or not {"draft", "shadow", "governance", "catalog-review"}.issubset(required_labels)
99
+ or sum(label.startswith("rule:") for label in required_labels) != 1
100
+ or sum(label.startswith("action:") for label in required_labels) != 1
101
+ or value.get("independent_pr_observation_verified") is not True
102
+ or value.get("independent_observation_scope") != "gitops-pr"
103
+ or value.get("durable_intent_verified") is not True
104
+ or value.get("durable_terminal_verified") is not True
105
+ or value.get("code_path_merge_authority") is not False
106
+ or value.get("managed_resource_mutation_status") != "unknown"
107
+ or value.get("managed_resource_mutation_performed") is not None
108
+ or re.fullmatch(
109
+ r"(?:[0-9a-f]{40}|[0-9a-f]{64})",
110
+ str(value.get("publication_head_sha", "")),
111
+ )
112
+ is None
113
+ or re.fullmatch(r"[0-9a-f]{64}", str(value.get("review_document_digest", ""))) is None
114
+ ):
115
+ raise ValueError("standalone selected catalog review is incomplete")
116
+
117
+
118
+ __all__ = [
119
+ "PostApplicationReceipts",
120
+ "require_catalog_review_receipt",
121
+ "run_post_application_checkpoints",
122
+ ]