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,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
|
+
]
|