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,586 @@
|
|
|
1
|
+
"""Supervise connected source Foundation with exact human checkpoint approvals."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import importlib
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from types import SimpleNamespace
|
|
14
|
+
|
|
15
|
+
from fdai_service_contracts.product_profile import ProductAddOn
|
|
16
|
+
|
|
17
|
+
from fdai_deployment_cli.aks_preflight import current_target_binding, inspect_aks_target
|
|
18
|
+
from fdai_deployment_cli.bootstrap_approval import selected_source
|
|
19
|
+
from fdai_deployment_cli.catalog_review_profile import CatalogReviewDeploymentProfile
|
|
20
|
+
from fdai_deployment_cli.contracts import load_json_object
|
|
21
|
+
from fdai_deployment_cli.deployment_cost import inspect_aks_compute_cost
|
|
22
|
+
from fdai_deployment_cli.deployment_deadline import DeploymentDeadline
|
|
23
|
+
from fdai_deployment_cli.deployment_progress import (
|
|
24
|
+
begin_stage,
|
|
25
|
+
installer_activity,
|
|
26
|
+
progress_detail,
|
|
27
|
+
)
|
|
28
|
+
from fdai_deployment_cli.development_authority_profile import (
|
|
29
|
+
DevelopmentAuthorityDeploymentProfile,
|
|
30
|
+
)
|
|
31
|
+
from fdai_deployment_cli.hub_image_trust import HubImageSelection, load_selection
|
|
32
|
+
from fdai_deployment_cli.installation_scope import (
|
|
33
|
+
InstallationOptions,
|
|
34
|
+
confirm_installation_scope,
|
|
35
|
+
started_installation_matches,
|
|
36
|
+
)
|
|
37
|
+
from fdai_deployment_cli.notification_recipients_input import (
|
|
38
|
+
NotificationRecipientsDeploymentInput,
|
|
39
|
+
)
|
|
40
|
+
from fdai_deployment_cli.operational_evidence_verifier_input import (
|
|
41
|
+
OperationalEvidenceVerifierDeploymentInput,
|
|
42
|
+
)
|
|
43
|
+
from fdai_deployment_cli.private_output import read_private_bytes
|
|
44
|
+
from fdai_deployment_cli.runtime_profile import RuntimeDeploymentProfile
|
|
45
|
+
from fdai_deployment_cli.source_deploy import prepare_source_deployment
|
|
46
|
+
from fdai_deployment_cli.source_foundation import _copy_terraform
|
|
47
|
+
from fdai_deployment_cli.source_input import inspect_source
|
|
48
|
+
from fdai_deployment_cli.source_terraform import download_pinned_terraform
|
|
49
|
+
from fdai_deployment_cli.standalone_application_completion import complete_application
|
|
50
|
+
from fdai_deployment_cli.standalone_deploy import (
|
|
51
|
+
_current_operator_object_id as current_operator_object_id,
|
|
52
|
+
)
|
|
53
|
+
from fdai_deployment_cli.standalone_status import current_status, prior_attempt
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def plan_source_installation(
|
|
57
|
+
*,
|
|
58
|
+
source_root: Path,
|
|
59
|
+
work_dir: Path,
|
|
60
|
+
runtime_profile: RuntimeDeploymentProfile,
|
|
61
|
+
region: str,
|
|
62
|
+
monthly_cost_ceiling: int,
|
|
63
|
+
timeout_seconds: int,
|
|
64
|
+
foundation_workload: str = "fdai",
|
|
65
|
+
interactive: bool = False,
|
|
66
|
+
approval_file: Path | None = None,
|
|
67
|
+
installation_options: InstallationOptions | None = None,
|
|
68
|
+
confirm_initial: bool = False,
|
|
69
|
+
foundation_recovery_directory: Path | None = None,
|
|
70
|
+
operational_evidence_verifier_input: OperationalEvidenceVerifierDeploymentInput | None = None,
|
|
71
|
+
notification_recipients: NotificationRecipientsDeploymentInput | None = None,
|
|
72
|
+
development_authority_profile: DevelopmentAuthorityDeploymentProfile | None = None,
|
|
73
|
+
clear_development_authority_profile: bool = False,
|
|
74
|
+
hub_selection: HubImageSelection | None = None,
|
|
75
|
+
) -> dict[str, object]:
|
|
76
|
+
"""Advance source Foundation through exact human approvals, or stop for review.
|
|
77
|
+
|
|
78
|
+
No kit construction, signature fallback or licence activation occurs here.
|
|
79
|
+
Noninteractive use passes only an explicitly supplied approval to the shared
|
|
80
|
+
verifier and never prompts or adopts an ambient approval. Foundation handoff
|
|
81
|
+
alone keeps deployment_ready=false until application acceptance is connected.
|
|
82
|
+
"""
|
|
83
|
+
if interactive and approval_file is not None:
|
|
84
|
+
raise ValueError("explicit source approval cannot be combined with interactive approval")
|
|
85
|
+
if hub_selection is None and (work_dir / "hub-images").exists():
|
|
86
|
+
raise ValueError("source builds cannot resume a Hub-image deployment")
|
|
87
|
+
hub_target_binding = None
|
|
88
|
+
if hub_selection is not None:
|
|
89
|
+
from fdai_deployment_cli.aks_preflight import _active_human_target
|
|
90
|
+
from fdai_deployment_cli.hub_image_deploy import record_deployment_request
|
|
91
|
+
from fdai_deployment_cli.target import compute_target_binding
|
|
92
|
+
|
|
93
|
+
hub_selection = load_selection(hub_selection.path, hub_selection.digest)
|
|
94
|
+
inspect_source(source_root, expected_commit=hub_selection.source_commit)
|
|
95
|
+
subscription_id, tenant_id = _active_human_target(
|
|
96
|
+
DeploymentDeadline(min(timeout_seconds, 60))
|
|
97
|
+
)
|
|
98
|
+
hub_target_binding = compute_target_binding(
|
|
99
|
+
subscription_id=subscription_id, tenant_id=tenant_id
|
|
100
|
+
)
|
|
101
|
+
record_deployment_request(hub_selection, subscription_id=subscription_id, region=region)
|
|
102
|
+
if foundation_recovery_directory is not None:
|
|
103
|
+
if hub_selection is not None:
|
|
104
|
+
from fdai_deployment_cli.hub_image_deploy import require_retained_hub_recovery
|
|
105
|
+
|
|
106
|
+
assert hub_target_binding is not None
|
|
107
|
+
require_retained_hub_recovery(
|
|
108
|
+
hub_selection, work_dir=work_dir, target_binding=hub_target_binding
|
|
109
|
+
)
|
|
110
|
+
from fdai_deployment_cli.source_recovery import (
|
|
111
|
+
recovered_application_evidence,
|
|
112
|
+
resume_source_installation,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if interactive or confirm_initial or installation_options is not None:
|
|
116
|
+
raise ValueError("source recovery cannot replace the retained initial scope or prompt")
|
|
117
|
+
if operational_evidence_verifier_input is not None:
|
|
118
|
+
raise ValueError("source recovery cannot add a new operational evidence verifier input")
|
|
119
|
+
if notification_recipients is not None:
|
|
120
|
+
raise ValueError("source recovery cannot add notification recipients")
|
|
121
|
+
if development_authority_profile is not None:
|
|
122
|
+
raise ValueError("source recovery cannot add a development authority profile")
|
|
123
|
+
if clear_development_authority_profile:
|
|
124
|
+
raise ValueError("source recovery cannot clear a development authority profile")
|
|
125
|
+
work_dir = work_dir.absolute()
|
|
126
|
+
foundation_recovery_directory = foundation_recovery_directory.absolute()
|
|
127
|
+
progress = resume_source_installation(
|
|
128
|
+
source_root=source_root,
|
|
129
|
+
work_dir=work_dir,
|
|
130
|
+
recovery_directory=foundation_recovery_directory,
|
|
131
|
+
runtime_profile=runtime_profile,
|
|
132
|
+
region=region,
|
|
133
|
+
monthly_cost_ceiling=monthly_cost_ceiling,
|
|
134
|
+
timeout_seconds=timeout_seconds,
|
|
135
|
+
approval_file=approval_file,
|
|
136
|
+
)
|
|
137
|
+
if hub_target_binding is not None and progress.get("target_binding") != hub_target_binding:
|
|
138
|
+
raise ValueError("Hub request target differs from retained Foundation recovery")
|
|
139
|
+
if progress.get("stage") != "application-plan":
|
|
140
|
+
return progress
|
|
141
|
+
evidence = recovered_application_evidence(
|
|
142
|
+
work_dir=work_dir,
|
|
143
|
+
recovery_directory=foundation_recovery_directory,
|
|
144
|
+
progress=progress,
|
|
145
|
+
runtime_profile_digest=runtime_profile.digest,
|
|
146
|
+
region=region,
|
|
147
|
+
monthly_cost_ceiling=monthly_cost_ceiling,
|
|
148
|
+
)
|
|
149
|
+
execution_source = inspect_source(source_root)
|
|
150
|
+
prepared_source = SimpleNamespace(
|
|
151
|
+
root=work_dir / "foundation",
|
|
152
|
+
ssh_private_key=work_dir / "foundation/runner_ed25519",
|
|
153
|
+
target_binding=progress["target_binding"],
|
|
154
|
+
source_commit=progress["source_commit"],
|
|
155
|
+
run_binding=evidence.run_binding,
|
|
156
|
+
)
|
|
157
|
+
application = complete_application(
|
|
158
|
+
kit=None,
|
|
159
|
+
prepared=prepared_source,
|
|
160
|
+
status=evidence.status,
|
|
161
|
+
scripts=execution_source.root / "scripts/deployment/azure",
|
|
162
|
+
deadline=DeploymentDeadline(timeout_seconds),
|
|
163
|
+
selected_runtime=runtime_profile,
|
|
164
|
+
trial_token=None,
|
|
165
|
+
application_state_adoption=None,
|
|
166
|
+
foundation_state_receipt_digest=evidence.state_handoff_digest,
|
|
167
|
+
catalog_review_profile=CatalogReviewDeploymentProfile.unselected(),
|
|
168
|
+
current_operator_object_id=current_operator_object_id,
|
|
169
|
+
source_snapshot=work_dir / "source-snapshot",
|
|
170
|
+
source_snapshot_digest=evidence.source_snapshot_digest,
|
|
171
|
+
source_root=execution_source.root,
|
|
172
|
+
foundation_recovery_directory=foundation_recovery_directory,
|
|
173
|
+
hub_selection=hub_selection,
|
|
174
|
+
)
|
|
175
|
+
return {
|
|
176
|
+
**application,
|
|
177
|
+
"foundation_recovery_receipt_digest": progress["foundation_receipt_digest"],
|
|
178
|
+
"source_recovery_progress_digest": progress["receipt_digest"],
|
|
179
|
+
"recovery_execution_source_commit": progress["execution_source_commit"],
|
|
180
|
+
}
|
|
181
|
+
deadline = DeploymentDeadline(timeout_seconds)
|
|
182
|
+
begin_stage("discovery")
|
|
183
|
+
with installer_activity("Preparing the clean source snapshot"):
|
|
184
|
+
prepared = prepare_source_deployment(
|
|
185
|
+
source_root=source_root,
|
|
186
|
+
work_dir=work_dir,
|
|
187
|
+
runtime_profile=runtime_profile,
|
|
188
|
+
region=region,
|
|
189
|
+
monthly_cost_ceiling=monthly_cost_ceiling,
|
|
190
|
+
hub_selection_digest=hub_selection.digest if hub_selection is not None else None,
|
|
191
|
+
)
|
|
192
|
+
with installer_activity("Verifying source and tenant-local identity names"):
|
|
193
|
+
source = inspect_source(source_root, expected_commit=str(prepared["source_commit"]))
|
|
194
|
+
_require_unique_entra_display_names(
|
|
195
|
+
runtime_profile,
|
|
196
|
+
source.root / "scripts/deployment/azure",
|
|
197
|
+
target_binding=current_target_binding(timeout_seconds=deadline.remaining(60)),
|
|
198
|
+
)
|
|
199
|
+
with installer_activity("Checking AKS SKU, quota, and PostgreSQL availability"):
|
|
200
|
+
preflight = inspect_aks_target(
|
|
201
|
+
profile=runtime_profile,
|
|
202
|
+
region=region,
|
|
203
|
+
timeout_seconds=deadline.remaining(180),
|
|
204
|
+
owned_resource_group=_retained_application_group(work_dir, foundation_workload),
|
|
205
|
+
)
|
|
206
|
+
if hub_target_binding is not None and preflight.get("target_binding") != hub_target_binding:
|
|
207
|
+
raise ValueError("Azure target changed after the Hub deployment request")
|
|
208
|
+
work_dir = work_dir.absolute()
|
|
209
|
+
retained_binding = {
|
|
210
|
+
"source_commit": source.commit,
|
|
211
|
+
"target_binding": preflight.get("target_binding"),
|
|
212
|
+
}
|
|
213
|
+
if preflight["state"] != "feasible" and not started_installation_matches(
|
|
214
|
+
work_dir,
|
|
215
|
+
retained_binding,
|
|
216
|
+
):
|
|
217
|
+
return {**preflight, "stage": "aks-preflight"}
|
|
218
|
+
if installation_options is not None:
|
|
219
|
+
progress_detail("Waiting for the one-time installation scope confirmation")
|
|
220
|
+
confirmation = confirm_installation_scope(
|
|
221
|
+
work_dir=work_dir,
|
|
222
|
+
binding={
|
|
223
|
+
"source_commit": source.commit,
|
|
224
|
+
"target_binding": preflight["target_binding"],
|
|
225
|
+
"preparation_digest": prepared["receipt_digest"],
|
|
226
|
+
"runtime_profile_digest": runtime_profile.digest,
|
|
227
|
+
"region": region,
|
|
228
|
+
"monthly_cost_ceiling": monthly_cost_ceiling,
|
|
229
|
+
},
|
|
230
|
+
runtime_profile=runtime_profile,
|
|
231
|
+
options=installation_options,
|
|
232
|
+
interactive=confirm_initial,
|
|
233
|
+
timeout_seconds=deadline.remaining(86400),
|
|
234
|
+
)
|
|
235
|
+
source.reverify()
|
|
236
|
+
if confirmation["state"] != "confirmed":
|
|
237
|
+
return {**confirmation, "source_commit": source.commit}
|
|
238
|
+
with installer_activity("Checking the public compute estimate"):
|
|
239
|
+
cost_review = inspect_aks_compute_cost(
|
|
240
|
+
profile=runtime_profile,
|
|
241
|
+
region=region,
|
|
242
|
+
monthly_cost_ceiling=monthly_cost_ceiling,
|
|
243
|
+
timeout_seconds=deadline.remaining(120),
|
|
244
|
+
)
|
|
245
|
+
source.reverify()
|
|
246
|
+
if cost_review["state"] == "blocked":
|
|
247
|
+
return {**cost_review, "source_commit": source.commit}
|
|
248
|
+
tools = work_dir / "source-tools"
|
|
249
|
+
tools.mkdir(mode=0o700, exist_ok=True)
|
|
250
|
+
terraform = tools / "terraform"
|
|
251
|
+
toolchain = load_json_object(
|
|
252
|
+
(source.root / "infra/genesis-runner-image/toolchain.json").read_bytes(),
|
|
253
|
+
label="source toolchain",
|
|
254
|
+
)
|
|
255
|
+
digest = toolchain.get("terraform_binary_sha256")
|
|
256
|
+
with installer_activity("Preparing the checksum-pinned Terraform executable"):
|
|
257
|
+
if terraform.exists():
|
|
258
|
+
if hashlib.sha256(terraform.read_bytes()).hexdigest() != digest:
|
|
259
|
+
raise ValueError("retained source Terraform differs; preserve the run")
|
|
260
|
+
else:
|
|
261
|
+
installed = shutil.which("terraform")
|
|
262
|
+
try:
|
|
263
|
+
if installed is None:
|
|
264
|
+
raise ValueError("no Terraform on PATH")
|
|
265
|
+
_copy_terraform(Path(installed), terraform, expected_digest=digest)
|
|
266
|
+
except (OSError, ValueError):
|
|
267
|
+
download_pinned_terraform(
|
|
268
|
+
toolchain=toolchain,
|
|
269
|
+
destination=terraform,
|
|
270
|
+
timeout_seconds=deadline.remaining(300),
|
|
271
|
+
)
|
|
272
|
+
scripts = source.root / "scripts/deployment/azure"
|
|
273
|
+
foundation = work_dir / "foundation"
|
|
274
|
+
begin_stage("foundation")
|
|
275
|
+
progress_detail("Preparing Foundation inputs and the retained execution context")
|
|
276
|
+
environment = {
|
|
277
|
+
key: value
|
|
278
|
+
for key, value in os.environ.items()
|
|
279
|
+
if key
|
|
280
|
+
in {"HOME", "PATH", "AZURE_CONFIG_DIR", "LANG", "SSL_CERT_FILE", "REQUESTS_CA_BUNDLE"}
|
|
281
|
+
}
|
|
282
|
+
environment["PYTHONPATH"] = str(source.root / "packages/deployment-cli/src")
|
|
283
|
+
preparation = _capture(
|
|
284
|
+
(
|
|
285
|
+
sys.executable,
|
|
286
|
+
str(scripts / "source_genesis.py"),
|
|
287
|
+
"--work-dir",
|
|
288
|
+
str(foundation),
|
|
289
|
+
"--source-commit",
|
|
290
|
+
source.commit,
|
|
291
|
+
"--target-binding",
|
|
292
|
+
str(preflight["target_binding"]),
|
|
293
|
+
"--region",
|
|
294
|
+
region,
|
|
295
|
+
"--monthly-cost-ceiling",
|
|
296
|
+
str(monthly_cost_ceiling),
|
|
297
|
+
"--workload",
|
|
298
|
+
foundation_workload,
|
|
299
|
+
"--register-providers",
|
|
300
|
+
),
|
|
301
|
+
source.root,
|
|
302
|
+
environment,
|
|
303
|
+
deadline.remaining(900),
|
|
304
|
+
)
|
|
305
|
+
values = load_json_object(
|
|
306
|
+
read_private_bytes(foundation / "foundation-variables.json", max_bytes=65536),
|
|
307
|
+
label="source Foundation variables",
|
|
308
|
+
)
|
|
309
|
+
environment["AZURE_SUBSCRIPTION_ID"] = str(values["subscription_id"])
|
|
310
|
+
environment["AZURE_TENANT_ID"] = str(values["tenant_id"])
|
|
311
|
+
run_binding = preparation.get("run_binding")
|
|
312
|
+
if not isinstance(run_binding, str):
|
|
313
|
+
raise ValueError( # noqa: TRY004 - stable CLI invariant error, not caller type misuse
|
|
314
|
+
"source Foundation preparation has no retained run binding"
|
|
315
|
+
)
|
|
316
|
+
approval = (
|
|
317
|
+
approval_file.absolute()
|
|
318
|
+
if approval_file is not None
|
|
319
|
+
else foundation / "current-source-approval.json"
|
|
320
|
+
)
|
|
321
|
+
status_path = foundation / "status.json"
|
|
322
|
+
failed_advance_recovery_used = False
|
|
323
|
+
while True:
|
|
324
|
+
source.reverify()
|
|
325
|
+
previous = prior_attempt(status_path)
|
|
326
|
+
try:
|
|
327
|
+
result = _capture(
|
|
328
|
+
(
|
|
329
|
+
sys.executable,
|
|
330
|
+
str(scripts / "source_genesis.py"),
|
|
331
|
+
"--advance",
|
|
332
|
+
"--work-dir",
|
|
333
|
+
str(foundation),
|
|
334
|
+
"--source-commit",
|
|
335
|
+
source.commit,
|
|
336
|
+
"--target-binding",
|
|
337
|
+
str(preflight["target_binding"]),
|
|
338
|
+
"--region",
|
|
339
|
+
region,
|
|
340
|
+
"--monthly-cost-ceiling",
|
|
341
|
+
str(monthly_cost_ceiling),
|
|
342
|
+
"--workload",
|
|
343
|
+
foundation_workload,
|
|
344
|
+
"--register-providers",
|
|
345
|
+
"--source-snapshot",
|
|
346
|
+
str(work_dir / "source-snapshot"),
|
|
347
|
+
"--source-snapshot-digest",
|
|
348
|
+
str(prepared["source_snapshot_digest"]),
|
|
349
|
+
"--terraform",
|
|
350
|
+
str(terraform),
|
|
351
|
+
"--timeout-seconds",
|
|
352
|
+
str(deadline.remaining(14400)),
|
|
353
|
+
*(
|
|
354
|
+
("--approval-file", str(approval))
|
|
355
|
+
if approval_file is not None
|
|
356
|
+
or ((interactive or selected_source() is not None) and approval.exists())
|
|
357
|
+
else ()
|
|
358
|
+
),
|
|
359
|
+
),
|
|
360
|
+
source.root,
|
|
361
|
+
environment,
|
|
362
|
+
deadline.remaining(14400),
|
|
363
|
+
)
|
|
364
|
+
except ValueError:
|
|
365
|
+
if failed_advance_recovery_used or not _claimed_foundation_apply_requires_recovery(
|
|
366
|
+
foundation
|
|
367
|
+
):
|
|
368
|
+
raise
|
|
369
|
+
failed_advance_recovery_used = True
|
|
370
|
+
progress_detail("Recovering the claimed Foundation apply without repeating its effect")
|
|
371
|
+
continue
|
|
372
|
+
source.reverify()
|
|
373
|
+
if (
|
|
374
|
+
result.get("schema_version") != "fdai.source-foundation-progress.v1"
|
|
375
|
+
or result.get("state") != "review"
|
|
376
|
+
or result.get("source_commit") != source.commit
|
|
377
|
+
or result.get("run_binding") != run_binding
|
|
378
|
+
or result.get("provenance") != "operator-selected-source"
|
|
379
|
+
or result.get("release_signature_verified") is not False
|
|
380
|
+
or result.get("apply_authorized") is not False
|
|
381
|
+
or type(result.get("mutation_performed")) is not bool
|
|
382
|
+
or result.get("deployment_ready") is not False
|
|
383
|
+
):
|
|
384
|
+
raise ValueError("source Foundation did not return a bound review result")
|
|
385
|
+
status = current_status(
|
|
386
|
+
status_path, previous=previous, source_commit=source.commit, run_binding=run_binding
|
|
387
|
+
)
|
|
388
|
+
if (
|
|
389
|
+
result.get("attempt") != status["attempt"]
|
|
390
|
+
or result.get("stage") != status.get("current_stage")
|
|
391
|
+
or result["mutation_performed"] != status.get("mutation_performed")
|
|
392
|
+
):
|
|
393
|
+
raise ValueError("source Foundation progress differs from its current status")
|
|
394
|
+
if result["stage"] == "application-plan":
|
|
395
|
+
report = status.get("foundation_report")
|
|
396
|
+
handoff = report.get("state_handoff") if isinstance(report, dict) else None
|
|
397
|
+
plan = report.get("foundation_plan") if isinstance(report, dict) else None
|
|
398
|
+
plan_ref = plan.get("plan_ref") if isinstance(plan, dict) else None
|
|
399
|
+
completed = status.get("completed_stages")
|
|
400
|
+
if (
|
|
401
|
+
not isinstance(handoff, dict)
|
|
402
|
+
or not isinstance(plan_ref, str)
|
|
403
|
+
or re.fullmatch(r"foundation-plan-attempt-[1-9][0-9]*", plan_ref) is None
|
|
404
|
+
):
|
|
405
|
+
raise ValueError("source application transfer requires verified Foundation handoff")
|
|
406
|
+
expected_handoff_digest = handoff.get("receipt_digest")
|
|
407
|
+
handoff = load_json_object(
|
|
408
|
+
read_private_bytes(
|
|
409
|
+
foundation / plan_ref / "foundation-state-handoff-receipt.json",
|
|
410
|
+
max_bytes=1024 * 1024,
|
|
411
|
+
),
|
|
412
|
+
label="source Foundation handoff receipt",
|
|
413
|
+
max_bytes=1024 * 1024,
|
|
414
|
+
)
|
|
415
|
+
if (
|
|
416
|
+
status.get("route") != "private-runner"
|
|
417
|
+
or not isinstance(completed, list)
|
|
418
|
+
or "foundation-state" not in completed
|
|
419
|
+
or not isinstance(handoff, dict)
|
|
420
|
+
or handoff.get("schema_version")
|
|
421
|
+
!= "fdai.genesis-foundation-state-handoff-receipt.v1"
|
|
422
|
+
or handoff.get("state") != "verified"
|
|
423
|
+
or handoff.get("source_commit") != source.commit
|
|
424
|
+
or handoff.get("target_binding") != preflight["target_binding"]
|
|
425
|
+
or any(
|
|
426
|
+
handoff.get(key) is not True
|
|
427
|
+
for key in (
|
|
428
|
+
"effect_verified",
|
|
429
|
+
"runner_attested",
|
|
430
|
+
"remote_backend_authority_verified",
|
|
431
|
+
"zero_change_verified",
|
|
432
|
+
"remote_transient_deleted",
|
|
433
|
+
)
|
|
434
|
+
)
|
|
435
|
+
):
|
|
436
|
+
raise ValueError("source application transfer requires verified Foundation handoff")
|
|
437
|
+
from fdai_deployment_cli.contracts import canonical_digest
|
|
438
|
+
|
|
439
|
+
if (
|
|
440
|
+
canonical_digest(
|
|
441
|
+
{key: value for key, value in handoff.items() if key != "receipt_digest"}
|
|
442
|
+
)
|
|
443
|
+
!= expected_handoff_digest
|
|
444
|
+
or handoff.get("receipt_digest") != expected_handoff_digest
|
|
445
|
+
):
|
|
446
|
+
raise ValueError("source Foundation handoff digest differs")
|
|
447
|
+
prepared_source = SimpleNamespace(
|
|
448
|
+
root=foundation,
|
|
449
|
+
ssh_private_key=foundation / "runner_ed25519",
|
|
450
|
+
target_binding=preflight["target_binding"],
|
|
451
|
+
source_commit=source.commit,
|
|
452
|
+
run_binding=run_binding,
|
|
453
|
+
)
|
|
454
|
+
application = complete_application(
|
|
455
|
+
kit=None,
|
|
456
|
+
prepared=prepared_source,
|
|
457
|
+
status=status,
|
|
458
|
+
scripts=scripts,
|
|
459
|
+
deadline=deadline,
|
|
460
|
+
selected_runtime=runtime_profile,
|
|
461
|
+
trial_token=None,
|
|
462
|
+
application_state_adoption=None,
|
|
463
|
+
foundation_state_receipt_digest=str(expected_handoff_digest),
|
|
464
|
+
catalog_review_profile=CatalogReviewDeploymentProfile.unselected(),
|
|
465
|
+
operational_evidence_verifier_input=operational_evidence_verifier_input,
|
|
466
|
+
notification_recipients=notification_recipients,
|
|
467
|
+
development_authority_profile=development_authority_profile,
|
|
468
|
+
clear_development_authority_profile=clear_development_authority_profile,
|
|
469
|
+
current_operator_object_id=current_operator_object_id,
|
|
470
|
+
source_snapshot=work_dir / "source-snapshot",
|
|
471
|
+
source_snapshot_digest=str(prepared["source_snapshot_digest"]),
|
|
472
|
+
source_root=source.root,
|
|
473
|
+
hub_selection=hub_selection,
|
|
474
|
+
)
|
|
475
|
+
return {**application, "cost_review": cost_review}
|
|
476
|
+
if (not interactive and selected_source() is None) or result["stage"] not in {
|
|
477
|
+
"runner-image-apply",
|
|
478
|
+
"foundation-apply",
|
|
479
|
+
"runner-enrollment",
|
|
480
|
+
"foundation-state",
|
|
481
|
+
}:
|
|
482
|
+
return {**result, "cost_review": cost_review}
|
|
483
|
+
approval.unlink(missing_ok=True)
|
|
484
|
+
if selected_source() is not None:
|
|
485
|
+
sys.path.insert(0, str(scripts))
|
|
486
|
+
try:
|
|
487
|
+
approval_prompt = importlib.import_module("genesis_approval_prompt")
|
|
488
|
+
approval_prompt.approve_status(status_path, approval)
|
|
489
|
+
finally:
|
|
490
|
+
sys.path.remove(str(scripts))
|
|
491
|
+
continue
|
|
492
|
+
prompt = subprocess.run(
|
|
493
|
+
(
|
|
494
|
+
sys.executable,
|
|
495
|
+
str(scripts / "genesis_approval_prompt.py"),
|
|
496
|
+
"--status",
|
|
497
|
+
str(status_path),
|
|
498
|
+
"--output",
|
|
499
|
+
str(approval),
|
|
500
|
+
),
|
|
501
|
+
cwd=source.root,
|
|
502
|
+
env=environment,
|
|
503
|
+
stdout=sys.stderr,
|
|
504
|
+
check=False,
|
|
505
|
+
timeout=deadline.remaining(600),
|
|
506
|
+
)
|
|
507
|
+
if prompt.returncode != 0:
|
|
508
|
+
raise ValueError("source Foundation exact approval was not granted")
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _retained_application_group(work_dir: Path, workload: str) -> str | None:
|
|
512
|
+
"""Name the resource group a resumed run already owns, from its retained variables."""
|
|
513
|
+
|
|
514
|
+
path = work_dir.absolute() / "foundation" / "foundation-variables.json"
|
|
515
|
+
if not path.is_file() or path.is_symlink():
|
|
516
|
+
return None
|
|
517
|
+
try:
|
|
518
|
+
values = load_json_object(
|
|
519
|
+
read_private_bytes(path, max_bytes=65536), label="retained Foundation variables"
|
|
520
|
+
)
|
|
521
|
+
except (OSError, ValueError):
|
|
522
|
+
return None
|
|
523
|
+
region_short = values.get("region_short")
|
|
524
|
+
if (
|
|
525
|
+
values.get("workload") != workload
|
|
526
|
+
or not isinstance(region_short, str)
|
|
527
|
+
or re.fullmatch(r"[a-z][a-z0-9]{1,11}", region_short) is None
|
|
528
|
+
or re.fullmatch(r"[a-z][a-z0-9-]{1,30}", workload) is None
|
|
529
|
+
):
|
|
530
|
+
return None
|
|
531
|
+
return f"rg-{workload}-dev-{region_short}"
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _capture(
|
|
535
|
+
command: tuple[str, ...], root: Path, environment: dict[str, str], timeout: int
|
|
536
|
+
) -> dict[str, object]:
|
|
537
|
+
result = subprocess.run(
|
|
538
|
+
command,
|
|
539
|
+
cwd=root,
|
|
540
|
+
env=environment,
|
|
541
|
+
stdin=subprocess.DEVNULL,
|
|
542
|
+
stdout=subprocess.PIPE,
|
|
543
|
+
stderr=sys.stderr,
|
|
544
|
+
check=False,
|
|
545
|
+
timeout=timeout,
|
|
546
|
+
)
|
|
547
|
+
if result.returncode != 0:
|
|
548
|
+
raise ValueError(
|
|
549
|
+
"source planning stage failed; preserve private state and do not repeat effects"
|
|
550
|
+
)
|
|
551
|
+
return load_json_object(result.stdout, label="source planning result", max_bytes=1024 * 1024)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _claimed_foundation_apply_requires_recovery(foundation: Path) -> bool:
|
|
555
|
+
claims = sorted(foundation.glob("foundation-plan-attempt-*/foundation-apply-claim.json"))
|
|
556
|
+
if len(claims) != 1 or claims[0].is_symlink():
|
|
557
|
+
return False
|
|
558
|
+
receipt = claims[0].with_name("foundation-apply-receipt.json")
|
|
559
|
+
return not receipt.exists() and not receipt.is_symlink()
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _require_unique_entra_display_names(
|
|
563
|
+
profile: RuntimeDeploymentProfile,
|
|
564
|
+
scripts: Path,
|
|
565
|
+
*,
|
|
566
|
+
target_binding: str,
|
|
567
|
+
) -> None:
|
|
568
|
+
product = profile.product_profile
|
|
569
|
+
if not (
|
|
570
|
+
product.selects(ProductAddOn.READ_ONLY_CONSOLE)
|
|
571
|
+
or product.selects(ProductAddOn.ENTERPRISE_IDENTITY_GOVERNANCE)
|
|
572
|
+
):
|
|
573
|
+
return
|
|
574
|
+
sys.path.insert(0, str(scripts))
|
|
575
|
+
try:
|
|
576
|
+
genesis_entra = importlib.import_module("genesis_entra")
|
|
577
|
+
spa_display_name = genesis_entra.scoped_spa_display_name(target_binding)
|
|
578
|
+
check = getattr(genesis_entra, "check_unique_display_names", None)
|
|
579
|
+
if callable(check):
|
|
580
|
+
check(spa_display_name=spa_display_name)
|
|
581
|
+
return
|
|
582
|
+
plan_entra = getattr(genesis_entra, "plan_entra", None)
|
|
583
|
+
if callable(plan_entra):
|
|
584
|
+
plan_entra(spa_display_name=spa_display_name)
|
|
585
|
+
finally:
|
|
586
|
+
sys.path.remove(str(scripts))
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""CLI adapters for changed-source planning, execution, and timing evidence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import Callable, Mapping
|
|
8
|
+
from datetime import UTC, datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from fdai_deployment_cli.deployment_progress import DeploymentProgress
|
|
13
|
+
from fdai_deployment_cli.private_output import write_private_output
|
|
14
|
+
from fdai_deployment_cli.source_change_plan import plan_git_changes
|
|
15
|
+
from fdai_deployment_cli.source_changed_update import deploy_changed_source
|
|
16
|
+
from fdai_deployment_cli.source_service_update import deploy_source_service_update
|
|
17
|
+
from fdai_deployment_cli.source_service_update_cli import source_service_update_inputs
|
|
18
|
+
|
|
19
|
+
PrintMapping = Callable[..., None]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def provision_plan(args: argparse.Namespace, print_mapping: PrintMapping) -> int:
|
|
23
|
+
result = plan_git_changes(args.source, args.base, args.head)
|
|
24
|
+
print_mapping(
|
|
25
|
+
result,
|
|
26
|
+
output=args.output,
|
|
27
|
+
text=(
|
|
28
|
+
f"source change lane: {result['lane']}; "
|
|
29
|
+
f"reason={result['reason_code']}; no deployment performed"
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
return 0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def provision_update(
|
|
36
|
+
args: argparse.Namespace,
|
|
37
|
+
print_mapping: PrintMapping,
|
|
38
|
+
*,
|
|
39
|
+
deploy: Callable[..., dict[str, object]] = deploy_changed_source,
|
|
40
|
+
) -> int:
|
|
41
|
+
result = deploy(
|
|
42
|
+
source_root=args.source,
|
|
43
|
+
base=args.base,
|
|
44
|
+
application_work_dir=args.application_work_dir,
|
|
45
|
+
work_dir=args.work_dir,
|
|
46
|
+
timeout_seconds=args.timeout_seconds,
|
|
47
|
+
)
|
|
48
|
+
services = result.get("services")
|
|
49
|
+
if not isinstance(services, list) or any(not isinstance(item, str) for item in services):
|
|
50
|
+
raise ValueError("source changed update returned an invalid service list")
|
|
51
|
+
print_mapping(
|
|
52
|
+
result,
|
|
53
|
+
output=args.output,
|
|
54
|
+
text=f"source changed update: {result['state']}; services={','.join(services) or 'none'}",
|
|
55
|
+
)
|
|
56
|
+
return 0 if result["state"] in {"applied", "no-deploy"} else 2
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def provision_service_update(args: argparse.Namespace) -> int:
|
|
60
|
+
source, application, selected, development_profile, clear_development_profile = (
|
|
61
|
+
source_service_update_inputs(args)
|
|
62
|
+
)
|
|
63
|
+
result = deploy_source_service_update(
|
|
64
|
+
source_root=source,
|
|
65
|
+
application_work_dir=application,
|
|
66
|
+
work_dir=selected,
|
|
67
|
+
service=args.service,
|
|
68
|
+
timeout_seconds=args.timeout_seconds,
|
|
69
|
+
adopt_historical_binding=args.adopt_historical_binding,
|
|
70
|
+
adopt_historical_state=args.adopt_historical_state,
|
|
71
|
+
adopt_historical_variables=args.adopt_historical_variables,
|
|
72
|
+
adopt_historical_live=args.adopt_historical_live,
|
|
73
|
+
adopt_historical_plan=args.adopt_historical_plan,
|
|
74
|
+
development_authority_profile=development_profile,
|
|
75
|
+
clear_development_authority_profile=clear_development_profile,
|
|
76
|
+
)
|
|
77
|
+
print(
|
|
78
|
+
json.dumps(result, sort_keys=True, separators=(",", ":"))
|
|
79
|
+
if args.output == "json"
|
|
80
|
+
else f"source service update applied: {result['service']} at {result['source_commit']}"
|
|
81
|
+
)
|
|
82
|
+
return 0
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def attach_timing(
|
|
86
|
+
result: Mapping[str, Any],
|
|
87
|
+
*,
|
|
88
|
+
progress: DeploymentProgress,
|
|
89
|
+
work_dir: Path,
|
|
90
|
+
) -> dict[str, Any]:
|
|
91
|
+
timing = progress.timing_receipt(
|
|
92
|
+
lane="source-install",
|
|
93
|
+
outcome=str(result.get("state", "unknown")),
|
|
94
|
+
recorded_at=datetime.now(UTC).isoformat().replace("+00:00", "Z"),
|
|
95
|
+
)
|
|
96
|
+
timing_directory = work_dir / "timing"
|
|
97
|
+
timing_directory.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
98
|
+
timing_path = timing_directory / f"{timing['receipt_digest']}.json"
|
|
99
|
+
if not timing_path.exists():
|
|
100
|
+
write_private_output(
|
|
101
|
+
timing_path,
|
|
102
|
+
json.dumps(timing, sort_keys=True, separators=(",", ":")) + "\n",
|
|
103
|
+
)
|
|
104
|
+
return {**result, "timing_receipt_digest": timing["receipt_digest"]}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
__all__ = ["attach_timing", "provision_plan", "provision_service_update", "provision_update"]
|