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,798 @@
|
|
|
1
|
+
"""Command-line facade for safe FDAI deployment preparation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import stat
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from fdai_deployment_cli import source_change_cli
|
|
15
|
+
from fdai_deployment_cli.__about__ import __version__
|
|
16
|
+
from fdai_deployment_cli.aks_preflight import inspect_aks_target
|
|
17
|
+
from fdai_deployment_cli.bootstrap_reconcile import reconcile_bootstrap
|
|
18
|
+
from fdai_deployment_cli.bundle import (
|
|
19
|
+
verify_bundle,
|
|
20
|
+
)
|
|
21
|
+
from fdai_deployment_cli.cli_parser import build_parser
|
|
22
|
+
from fdai_deployment_cli.cli_plan import (
|
|
23
|
+
_absolute_work_dir,
|
|
24
|
+
_create_private_work_dir,
|
|
25
|
+
_print_mapping,
|
|
26
|
+
_provision_plan,
|
|
27
|
+
_read_public_key,
|
|
28
|
+
_require_bundle_version, # noqa: F401 - compatibility seam
|
|
29
|
+
_runtime_platform_tag,
|
|
30
|
+
_safe_plan_error, # noqa: F401 - compatibility seam
|
|
31
|
+
_terraform_environment, # noqa: F401 - compatibility seam
|
|
32
|
+
_validate_plan_target, # noqa: F401 - compatibility seam
|
|
33
|
+
_write_private_text, # noqa: F401 - compatibility seam
|
|
34
|
+
read_plan_input, # noqa: F401 - compatibility seam
|
|
35
|
+
)
|
|
36
|
+
from fdai_deployment_cli.compiler import compile_manifest
|
|
37
|
+
from fdai_deployment_cli.console_artifact import build_console_update_artifact
|
|
38
|
+
from fdai_deployment_cli.console_config import configure_console
|
|
39
|
+
from fdai_deployment_cli.console_update import (
|
|
40
|
+
apply_console_update_plan,
|
|
41
|
+
load_console_update_plan,
|
|
42
|
+
prepare_console_update_plan,
|
|
43
|
+
)
|
|
44
|
+
from fdai_deployment_cli.contracts import ProvisionProfile, canonical_digest
|
|
45
|
+
from fdai_deployment_cli.deployment_cost import resolve_work_dir_monthly_cost_ceiling
|
|
46
|
+
from fdai_deployment_cli.deployment_progress import DeploymentProgress
|
|
47
|
+
from fdai_deployment_cli.doctor import (
|
|
48
|
+
azure_active_target_binding,
|
|
49
|
+
azure_cli_authenticated,
|
|
50
|
+
doctor_json,
|
|
51
|
+
inspect_tools,
|
|
52
|
+
)
|
|
53
|
+
from fdai_deployment_cli.entitlement_preflight import (
|
|
54
|
+
describe_entitlement_mode,
|
|
55
|
+
ensure_azure_session,
|
|
56
|
+
select_entitlement_mode,
|
|
57
|
+
)
|
|
58
|
+
from fdai_deployment_cli.entitlement_transport import seal_entitlement_command
|
|
59
|
+
from fdai_deployment_cli.entra_source import run_source_entra_operation
|
|
60
|
+
from fdai_deployment_cli.execution_copies import execution_copy_scope
|
|
61
|
+
from fdai_deployment_cli.license import (
|
|
62
|
+
INSTALLATION_ENTITLEMENT_SCHEMA,
|
|
63
|
+
inspect_installation_entitlement,
|
|
64
|
+
inspect_license,
|
|
65
|
+
signed_token_schema,
|
|
66
|
+
)
|
|
67
|
+
from fdai_deployment_cli.offline_prepare import prepare_offline_release
|
|
68
|
+
from fdai_deployment_cli.private_output import write_private_output
|
|
69
|
+
from fdai_deployment_cli.profile import load_profile, write_profile
|
|
70
|
+
from fdai_deployment_cli.runtime_profile import RuntimeDeploymentProfile
|
|
71
|
+
from fdai_deployment_cli.simulation import rehearse
|
|
72
|
+
from fdai_deployment_cli.source_azure import plan_source_installation
|
|
73
|
+
from fdai_deployment_cli.source_changed_update import deploy_changed_source
|
|
74
|
+
from fdai_deployment_cli.source_deploy import prepare_source_deployment
|
|
75
|
+
from fdai_deployment_cli.source_teardown import AzureResourceGroupClient, apply_source_teardown
|
|
76
|
+
from fdai_deployment_cli.standalone_deploy import deploy_azure_foundation
|
|
77
|
+
from fdai_deployment_cli.state import read_journal
|
|
78
|
+
from fdai_deployment_cli.status_projection import project_status
|
|
79
|
+
from fdai_deployment_cli.support_install import install_support
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def main(argv: list[str] | None = None) -> int:
|
|
83
|
+
"""Run one fdaictl command and map safe failures to stable exit codes."""
|
|
84
|
+
|
|
85
|
+
parser = _parser()
|
|
86
|
+
args = parser.parse_args(argv)
|
|
87
|
+
try:
|
|
88
|
+
return int(args.handler(args))
|
|
89
|
+
except ValueError as exc:
|
|
90
|
+
print(f"fdaictl: {exc}", file=sys.stderr)
|
|
91
|
+
return 3
|
|
92
|
+
except (OSError, subprocess.SubprocessError):
|
|
93
|
+
print(
|
|
94
|
+
"fdaictl: local or child operation failed; inspect retained evidence before recovery",
|
|
95
|
+
file=sys.stderr,
|
|
96
|
+
)
|
|
97
|
+
return 3
|
|
98
|
+
except EOFError:
|
|
99
|
+
print("fdaictl: approval input closed; no new approval was granted", file=sys.stderr)
|
|
100
|
+
return 3
|
|
101
|
+
except KeyboardInterrupt:
|
|
102
|
+
print("fdaictl: interrupted; inspect retained evidence before resuming", file=sys.stderr)
|
|
103
|
+
return 130
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _parser() -> argparse.ArgumentParser:
|
|
107
|
+
return build_parser(
|
|
108
|
+
{
|
|
109
|
+
"version": _version,
|
|
110
|
+
"doctor": _doctor,
|
|
111
|
+
"offline_prepare": _offline_prepare,
|
|
112
|
+
"offline_configure_console": _offline_configure_console,
|
|
113
|
+
"offline_install_support": _offline_install_support,
|
|
114
|
+
"provision_azure": _provision_azure,
|
|
115
|
+
"provision_entra": _provision_entra,
|
|
116
|
+
"provision_source_service_update": _provision_source_service_update,
|
|
117
|
+
"provision_source_change_plan": _provision_source_change_plan,
|
|
118
|
+
"provision_source_changed_update": _provision_source_changed_update,
|
|
119
|
+
"provision_console_update_build": _provision_console_update_build,
|
|
120
|
+
"provision_console_update_plan": _provision_console_update_plan,
|
|
121
|
+
"provision_console_update_apply": _provision_console_update_apply,
|
|
122
|
+
"provision_init": _provision_init,
|
|
123
|
+
"provision_inspect": _provision_inspect,
|
|
124
|
+
"provision_plan": _provision_plan,
|
|
125
|
+
"provision_bootstrap_reconcile": _provision_bootstrap_reconcile,
|
|
126
|
+
"bundle_verify": _bundle_verify,
|
|
127
|
+
"license_inspect": _license_inspect,
|
|
128
|
+
"license_seal_entitlement": seal_entitlement_command,
|
|
129
|
+
"onboard_guided": _onboard_guided,
|
|
130
|
+
"onboard_status": _onboard_status,
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _version(args: argparse.Namespace) -> int:
|
|
136
|
+
if args.output == "json":
|
|
137
|
+
print(
|
|
138
|
+
json.dumps(
|
|
139
|
+
{"schema_version": "fdai.version.v1", "version": __version__},
|
|
140
|
+
sort_keys=True,
|
|
141
|
+
separators=(",", ":"),
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
else:
|
|
145
|
+
print(__version__)
|
|
146
|
+
return 0
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _doctor(args: argparse.Namespace) -> int:
|
|
150
|
+
report = doctor_json(
|
|
151
|
+
inspect_tools(),
|
|
152
|
+
azure_authenticated=azure_cli_authenticated(),
|
|
153
|
+
)
|
|
154
|
+
payload = json.loads(report)
|
|
155
|
+
print(report if args.output == "json" else f"ready={str(payload['ready']).lower()}")
|
|
156
|
+
return 0 if payload["ready"] else 3
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _provision_init(args: argparse.Namespace) -> int:
|
|
160
|
+
profile = ProvisionProfile(
|
|
161
|
+
environment=args.environment,
|
|
162
|
+
region=args.region,
|
|
163
|
+
target_binding=args.target_binding,
|
|
164
|
+
connectivity=args.connectivity,
|
|
165
|
+
host=args.host,
|
|
166
|
+
transport=args.transport,
|
|
167
|
+
access_method=args.access_method,
|
|
168
|
+
shadow_only=True,
|
|
169
|
+
approval_quorum=args.approval_quorum,
|
|
170
|
+
monthly_cost_ceiling=args.monthly_cost_ceiling,
|
|
171
|
+
)
|
|
172
|
+
write_profile(args.profile, profile, force=args.force)
|
|
173
|
+
result = {
|
|
174
|
+
"schema_version": "fdai.provision-init.v1",
|
|
175
|
+
"profile_digest": canonical_digest(profile.to_mapping()),
|
|
176
|
+
"mutation_performed": False,
|
|
177
|
+
}
|
|
178
|
+
print(
|
|
179
|
+
json.dumps(result, sort_keys=True, separators=(",", ":"))
|
|
180
|
+
if args.output == "json"
|
|
181
|
+
else f"profile initialized: {result['profile_digest']}"
|
|
182
|
+
)
|
|
183
|
+
return 0
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _provision_azure(args: argparse.Namespace) -> int:
|
|
187
|
+
"""Run the standalone active-Azure-login deployment path."""
|
|
188
|
+
|
|
189
|
+
from fdai_deployment_cli.catalog_review_profile import (
|
|
190
|
+
CatalogReviewDeploymentProfile,
|
|
191
|
+
load_catalog_review_profile,
|
|
192
|
+
)
|
|
193
|
+
from fdai_deployment_cli.development_authority_cli import (
|
|
194
|
+
selected_development_authority,
|
|
195
|
+
validate_development_authority_command,
|
|
196
|
+
)
|
|
197
|
+
from fdai_deployment_cli import hub_image_deploy
|
|
198
|
+
from fdai_deployment_cli.installation_scope import InstallationOptions
|
|
199
|
+
from fdai_deployment_cli.notification_recipients_input import selected_notification_recipients
|
|
200
|
+
from fdai_deployment_cli.operational_evidence_verifier_input import (
|
|
201
|
+
load_operational_evidence_verifier_input,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
hub_mode, source_root, hub_key = hub_image_deploy.hub_source_inputs(args)
|
|
205
|
+
|
|
206
|
+
catalog_review_profile = (
|
|
207
|
+
CatalogReviewDeploymentProfile.unselected()
|
|
208
|
+
if args.catalog_review_profile is None
|
|
209
|
+
else load_catalog_review_profile(
|
|
210
|
+
args.catalog_review_profile
|
|
211
|
+
if args.catalog_review_profile.is_absolute()
|
|
212
|
+
else Path.cwd() / args.catalog_review_profile
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
operational_evidence_verifier_input = (
|
|
216
|
+
None
|
|
217
|
+
if args.operational_evidence_verifier_input is None
|
|
218
|
+
else load_operational_evidence_verifier_input(
|
|
219
|
+
args.operational_evidence_verifier_input
|
|
220
|
+
if args.operational_evidence_verifier_input.is_absolute()
|
|
221
|
+
else Path.cwd() / args.operational_evidence_verifier_input
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
development_authority_profile, clear_development_authority_profile = (
|
|
225
|
+
selected_development_authority(args, cwd=Path.cwd())
|
|
226
|
+
)
|
|
227
|
+
runtime_profile = RuntimeDeploymentProfile.from_cli_arguments(args)
|
|
228
|
+
notification_recipients = selected_notification_recipients(
|
|
229
|
+
args, runtime_profile=runtime_profile, cwd=Path.cwd()
|
|
230
|
+
)
|
|
231
|
+
if catalog_review_profile.selected and args.runtime != "aks":
|
|
232
|
+
raise ValueError("selected catalog review profile requires --runtime aks")
|
|
233
|
+
if operational_evidence_verifier_input is not None and args.runtime != "aks":
|
|
234
|
+
raise ValueError("operational evidence verifier input requires --runtime aks")
|
|
235
|
+
validate_development_authority_command(
|
|
236
|
+
args,
|
|
237
|
+
profile=development_authority_profile,
|
|
238
|
+
clear=clear_development_authority_profile,
|
|
239
|
+
)
|
|
240
|
+
if (args.prepare_only or args.preflight_only or args.teardown) and source_root is None:
|
|
241
|
+
raise ValueError("source-only preparation, preflight, or teardown requires --source")
|
|
242
|
+
if operational_evidence_verifier_input is not None and (
|
|
243
|
+
args.prepare_only or args.preflight_only or args.teardown
|
|
244
|
+
):
|
|
245
|
+
raise ValueError(
|
|
246
|
+
"operational evidence verifier input requires an application deployment run"
|
|
247
|
+
)
|
|
248
|
+
if args.teardown_confirmation is not None and not args.teardown:
|
|
249
|
+
raise ValueError("--teardown-confirmation requires --teardown")
|
|
250
|
+
if re.fullmatch(r"[a-z][a-z0-9]{1,11}", args.foundation_workload) is None:
|
|
251
|
+
raise ValueError(
|
|
252
|
+
"--foundation-workload must be a lowercase token of 2 through 12 characters"
|
|
253
|
+
)
|
|
254
|
+
if args.foundation_workload != "fdai" and source_root is None:
|
|
255
|
+
raise ValueError("--foundation-workload is supported only for source deployment")
|
|
256
|
+
if args.foundation_recovery_directory is not None and (
|
|
257
|
+
source_root is None or args.work_dir is None or args.prepare_only or args.preflight_only
|
|
258
|
+
):
|
|
259
|
+
raise ValueError(
|
|
260
|
+
"Foundation recovery requires --source and the original --work-dir without preparation or preflight"
|
|
261
|
+
)
|
|
262
|
+
if args.approval_file is not None and (
|
|
263
|
+
source_root is None or args.prepare_only or args.preflight_only or args.teardown
|
|
264
|
+
):
|
|
265
|
+
raise ValueError("--approval-file requires source deployment, not preparation or preflight")
|
|
266
|
+
initial_options_requested = (
|
|
267
|
+
args.setup_cost_ceiling is not None
|
|
268
|
+
or args.console_access is not None
|
|
269
|
+
or args.allow_dedicated_identities
|
|
270
|
+
or args.cleanup_temporary_resources
|
|
271
|
+
)
|
|
272
|
+
if initial_options_requested and (
|
|
273
|
+
source_root is None
|
|
274
|
+
or args.prepare_only
|
|
275
|
+
or args.preflight_only
|
|
276
|
+
or args.teardown
|
|
277
|
+
or args.approval_file is not None
|
|
278
|
+
or args.foundation_recovery_directory is not None
|
|
279
|
+
):
|
|
280
|
+
raise ValueError(
|
|
281
|
+
"initial scope options require a source install without an exact approval file"
|
|
282
|
+
)
|
|
283
|
+
work_dir = hub_image_deploy.deployment_work_dir(args)
|
|
284
|
+
args.monthly_cost_ceiling = resolve_work_dir_monthly_cost_ceiling(
|
|
285
|
+
args.monthly_cost_ceiling,
|
|
286
|
+
work_dir=work_dir,
|
|
287
|
+
adopted_foundation=args.adopt_foundation_directory,
|
|
288
|
+
)
|
|
289
|
+
adoption_paths = tuple(
|
|
290
|
+
getattr(args, name)
|
|
291
|
+
for name in (
|
|
292
|
+
"adopt_application_state",
|
|
293
|
+
"adopt_application_recovery",
|
|
294
|
+
"adopt_resolved_models",
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
if any(path is not None for path in adoption_paths) and not all(
|
|
298
|
+
path is not None for path in adoption_paths
|
|
299
|
+
):
|
|
300
|
+
raise ValueError("recovered public deployment requires all three adoption inputs")
|
|
301
|
+
foundation_adoption_paths = (args.adopt_foundation_directory, args.adopt_foundation_recovery)
|
|
302
|
+
if any(path is not None for path in foundation_adoption_paths) and not all(
|
|
303
|
+
path is not None for path in foundation_adoption_paths
|
|
304
|
+
):
|
|
305
|
+
raise ValueError("Foundation adoption requires both retained directories")
|
|
306
|
+
if args.control_package is not None and args.offline_kit is None:
|
|
307
|
+
raise ValueError("--control-package requires --offline-kit")
|
|
308
|
+
hub_selection = hub_image_deploy.prepare_cli_selection(args, work_dir, source_root, hub_key)
|
|
309
|
+
if not args.prepare_only:
|
|
310
|
+
entitlement_mode = select_entitlement_mode()
|
|
311
|
+
print(
|
|
312
|
+
f"fdaictl: entitlement: {describe_entitlement_mode(entitlement_mode)}",
|
|
313
|
+
file=sys.stderr,
|
|
314
|
+
)
|
|
315
|
+
ensure_azure_session()
|
|
316
|
+
if source_root is not None:
|
|
317
|
+
if catalog_review_profile.selected:
|
|
318
|
+
raise ValueError(
|
|
319
|
+
"selected catalog review profile requires signed-kit standalone deployment"
|
|
320
|
+
)
|
|
321
|
+
if any(path is not None for path in adoption_paths) or any(
|
|
322
|
+
path is not None for path in foundation_adoption_paths
|
|
323
|
+
):
|
|
324
|
+
raise ValueError("source deployment cannot reuse kit URLs or adoption inputs")
|
|
325
|
+
if args.teardown:
|
|
326
|
+
result = apply_source_teardown(
|
|
327
|
+
work_dir=work_dir,
|
|
328
|
+
source_root=source_root,
|
|
329
|
+
runtime_profile=runtime_profile,
|
|
330
|
+
region=args.region,
|
|
331
|
+
monthly_cost_ceiling=args.monthly_cost_ceiling,
|
|
332
|
+
confirmation=args.teardown_confirmation,
|
|
333
|
+
client=AzureResourceGroupClient(),
|
|
334
|
+
recovery_directory=args.foundation_recovery_directory,
|
|
335
|
+
)
|
|
336
|
+
_print_mapping(
|
|
337
|
+
result,
|
|
338
|
+
output=args.output,
|
|
339
|
+
text=(
|
|
340
|
+
f"source teardown: {result['state']}; "
|
|
341
|
+
f"reason={result.get('reason_code', 'complete')}"
|
|
342
|
+
),
|
|
343
|
+
)
|
|
344
|
+
return 0 if result["state"] == "torn-down" else 3
|
|
345
|
+
if not args.prepare_only and not args.preflight_only:
|
|
346
|
+
with DeploymentProgress.for_source(mode=args.progress, output=args.output) as progress:
|
|
347
|
+
if hub_mode:
|
|
348
|
+
progress.begin_hub()
|
|
349
|
+
else:
|
|
350
|
+
progress.begin_source()
|
|
351
|
+
result = plan_source_installation(
|
|
352
|
+
source_root=source_root,
|
|
353
|
+
work_dir=work_dir,
|
|
354
|
+
runtime_profile=runtime_profile,
|
|
355
|
+
region=args.region,
|
|
356
|
+
monthly_cost_ceiling=args.monthly_cost_ceiling,
|
|
357
|
+
foundation_workload=args.foundation_workload,
|
|
358
|
+
timeout_seconds=args.timeout_seconds,
|
|
359
|
+
approval_file=args.approval_file,
|
|
360
|
+
foundation_recovery_directory=args.foundation_recovery_directory,
|
|
361
|
+
operational_evidence_verifier_input=operational_evidence_verifier_input,
|
|
362
|
+
notification_recipients=notification_recipients,
|
|
363
|
+
development_authority_profile=development_authority_profile,
|
|
364
|
+
clear_development_authority_profile=clear_development_authority_profile,
|
|
365
|
+
interactive=args.approval_file is None,
|
|
366
|
+
installation_options=(
|
|
367
|
+
InstallationOptions(
|
|
368
|
+
setup_cost_ceiling=args.setup_cost_ceiling,
|
|
369
|
+
console_access=args.console_access or "none",
|
|
370
|
+
allow_dedicated_identities=args.allow_dedicated_identities,
|
|
371
|
+
cleanup_temporary_resources=args.cleanup_temporary_resources,
|
|
372
|
+
)
|
|
373
|
+
if args.approval_file is None and args.foundation_recovery_directory is None
|
|
374
|
+
else None
|
|
375
|
+
),
|
|
376
|
+
confirm_initial=args.foundation_recovery_directory is None
|
|
377
|
+
and args.output == "text"
|
|
378
|
+
and sys.stdin.isatty(),
|
|
379
|
+
**({"hub_selection": hub_selection} if hub_selection is not None else {}),
|
|
380
|
+
)
|
|
381
|
+
hub_image_deploy.bind_support_receipt(result, hub_selection)
|
|
382
|
+
ready = progress.finish_source(result)
|
|
383
|
+
result = source_change_cli.attach_timing(result, progress=progress, work_dir=work_dir)
|
|
384
|
+
_print_mapping(
|
|
385
|
+
result,
|
|
386
|
+
output=args.output,
|
|
387
|
+
text=(
|
|
388
|
+
f"{'Hub-image' if hub_mode else 'source'} Azure deployment ready; "
|
|
389
|
+
"subscription-wide assurance evidence remains open"
|
|
390
|
+
if ready
|
|
391
|
+
else f"{'Hub-image' if hub_mode else 'source'} deployment: {result['state']}; "
|
|
392
|
+
f"reason={result.get('reason_code', 'review_required')}; deployment is not ready"
|
|
393
|
+
),
|
|
394
|
+
)
|
|
395
|
+
return 0 if ready else 2 if result["state"] == "review" else 3
|
|
396
|
+
result = prepare_source_deployment(
|
|
397
|
+
source_root=source_root,
|
|
398
|
+
work_dir=work_dir,
|
|
399
|
+
runtime_profile=runtime_profile,
|
|
400
|
+
region=args.region,
|
|
401
|
+
monthly_cost_ceiling=args.monthly_cost_ceiling,
|
|
402
|
+
**({"hub_selection_digest": hub_selection.digest} if hub_selection is not None else {}),
|
|
403
|
+
)
|
|
404
|
+
hub_image_deploy.bind_support_receipt(result, hub_selection, require_receipt=True)
|
|
405
|
+
if args.preflight_only:
|
|
406
|
+
result = inspect_aks_target(
|
|
407
|
+
profile=runtime_profile,
|
|
408
|
+
region=args.region,
|
|
409
|
+
timeout_seconds=min(args.timeout_seconds, 180),
|
|
410
|
+
)
|
|
411
|
+
if hub_selection is not None:
|
|
412
|
+
result.update(hub_selection.metadata())
|
|
413
|
+
_print_mapping(
|
|
414
|
+
result,
|
|
415
|
+
output=args.output,
|
|
416
|
+
text=f"AKS capacity preflight: {result['state']}; no deployment performed",
|
|
417
|
+
)
|
|
418
|
+
return 0 if result["state"] == "feasible" else 3
|
|
419
|
+
_print_mapping(
|
|
420
|
+
result,
|
|
421
|
+
output=args.output,
|
|
422
|
+
text=(
|
|
423
|
+
"Hub signed images and matching checkout support prepared; no Azure access"
|
|
424
|
+
if hub_mode
|
|
425
|
+
else "source snapshot prepared; no Azure access or deployment performed"
|
|
426
|
+
),
|
|
427
|
+
)
|
|
428
|
+
return 0
|
|
429
|
+
|
|
430
|
+
def adoption_path(value: Path | None) -> Path | None:
|
|
431
|
+
return value if value is None or value.is_absolute() else Path.cwd() / value
|
|
432
|
+
|
|
433
|
+
mode = args.progress if args.output == "text" else "off"
|
|
434
|
+
with DeploymentProgress(mode=mode) as progress, execution_copy_scope():
|
|
435
|
+
result = deploy_azure_foundation(
|
|
436
|
+
work_dir=work_dir,
|
|
437
|
+
offline_kit=args.offline_kit,
|
|
438
|
+
region=args.region,
|
|
439
|
+
runtime_profile=runtime_profile,
|
|
440
|
+
monthly_cost_ceiling=args.monthly_cost_ceiling,
|
|
441
|
+
timeout_seconds=args.timeout_seconds,
|
|
442
|
+
trial_token=args.trial_token,
|
|
443
|
+
adopt_runner_image_receipt=adoption_path(args.adopt_runner_image_receipt),
|
|
444
|
+
adopt_application_state=adoption_path(args.adopt_application_state),
|
|
445
|
+
adopt_application_recovery=adoption_path(args.adopt_application_recovery),
|
|
446
|
+
adopt_resolved_models=adoption_path(args.adopt_resolved_models),
|
|
447
|
+
adopt_foundation_directory=adoption_path(args.adopt_foundation_directory),
|
|
448
|
+
adopt_foundation_recovery_directory=adoption_path(args.adopt_foundation_recovery),
|
|
449
|
+
catalog_review_profile=catalog_review_profile,
|
|
450
|
+
operational_evidence_verifier_input=operational_evidence_verifier_input,
|
|
451
|
+
control_package=adoption_path(args.control_package),
|
|
452
|
+
)
|
|
453
|
+
if result.get("deployment_ready") is not True:
|
|
454
|
+
raise ValueError("standalone deployment did not return verified deployment readiness")
|
|
455
|
+
progress.ready()
|
|
456
|
+
_print_mapping(
|
|
457
|
+
result,
|
|
458
|
+
output=args.output,
|
|
459
|
+
text=(
|
|
460
|
+
"standalone Azure deployment ready; subscription-wide assurance evidence remains open"
|
|
461
|
+
),
|
|
462
|
+
)
|
|
463
|
+
return 0
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _provision_entra(args: argparse.Namespace) -> int:
|
|
467
|
+
"""Run the source-owned identity operation without Foundation or application stages."""
|
|
468
|
+
|
|
469
|
+
return run_source_entra_operation(
|
|
470
|
+
source=args.source,
|
|
471
|
+
work_dir=args.work_dir,
|
|
472
|
+
target_profile_path=args.target_profile,
|
|
473
|
+
control_profile_path=args.control_profile,
|
|
474
|
+
apply=args.apply,
|
|
475
|
+
output=args.output,
|
|
476
|
+
timeout_seconds=args.timeout_seconds,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _provision_source_change_plan(args: argparse.Namespace) -> int:
|
|
481
|
+
return source_change_cli.provision_plan(args, _print_mapping)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _provision_source_changed_update(args: argparse.Namespace) -> int:
|
|
485
|
+
return source_change_cli.provision_update(
|
|
486
|
+
args,
|
|
487
|
+
_print_mapping,
|
|
488
|
+
deploy=deploy_changed_source,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _provision_source_service_update(args: argparse.Namespace) -> int:
|
|
493
|
+
return source_change_cli.provision_service_update(args)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _provision_console_update_plan(args: argparse.Namespace) -> int:
|
|
497
|
+
"""Create one private existing-development Console update plan."""
|
|
498
|
+
|
|
499
|
+
result = prepare_console_update_plan(
|
|
500
|
+
source_root=_absolute_work_dir(args.source),
|
|
501
|
+
candidate_archive=_absolute_work_dir(args.candidate_archive),
|
|
502
|
+
candidate_manifest=_absolute_work_dir(args.candidate_manifest),
|
|
503
|
+
rollback_archive=_absolute_work_dir(args.rollback_archive),
|
|
504
|
+
rollback_manifest=_absolute_work_dir(args.rollback_manifest),
|
|
505
|
+
target_file=_absolute_work_dir(args.target),
|
|
506
|
+
work_dir=_absolute_work_dir(args.work_dir),
|
|
507
|
+
ttl_seconds=args.ttl_seconds,
|
|
508
|
+
)
|
|
509
|
+
_print_mapping(
|
|
510
|
+
result,
|
|
511
|
+
output=args.output,
|
|
512
|
+
text=(f"Console update plan ready: {result['plan_digest']}; no Azure mutation performed"),
|
|
513
|
+
)
|
|
514
|
+
return 0
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _provision_console_update_build(args: argparse.Namespace) -> int:
|
|
518
|
+
"""Build a deterministic Console artifact from one protected source revision."""
|
|
519
|
+
|
|
520
|
+
result = build_console_update_artifact(
|
|
521
|
+
source_root=_absolute_work_dir(args.source),
|
|
522
|
+
revision=args.revision,
|
|
523
|
+
output_dir=_absolute_work_dir(args.output_dir),
|
|
524
|
+
timeout_seconds=args.timeout_seconds,
|
|
525
|
+
)
|
|
526
|
+
_print_mapping(
|
|
527
|
+
result,
|
|
528
|
+
output=args.output,
|
|
529
|
+
text=(f"Console artifact built: {result['archive_sha256']}; no Azure mutation performed"),
|
|
530
|
+
)
|
|
531
|
+
return 0
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _provision_console_update_apply(args: argparse.Namespace) -> int:
|
|
535
|
+
"""Apply one explicitly invoked Console update plan without redundant input."""
|
|
536
|
+
|
|
537
|
+
work_dir = _absolute_work_dir(args.work_dir)
|
|
538
|
+
plan = load_console_update_plan(
|
|
539
|
+
work_dir / "plan.json", allow_expired_claim=(work_dir / "claim.json").is_file()
|
|
540
|
+
)
|
|
541
|
+
if args.output == "text":
|
|
542
|
+
print(
|
|
543
|
+
"Console update exact plan\n"
|
|
544
|
+
f" source commit: {plan['source_commit']}\n"
|
|
545
|
+
f" target binding: {plan['target_binding']}\n"
|
|
546
|
+
f" candidate SHA-256: {plan['candidate_archive_sha256']}\n"
|
|
547
|
+
f" rollback SHA-256: {plan['rollback_archive_sha256']}\n"
|
|
548
|
+
f" plan digest: {plan['plan_digest']}\n"
|
|
549
|
+
"Applying the explicitly invoked non-destructive dev plan.",
|
|
550
|
+
file=sys.stderr,
|
|
551
|
+
)
|
|
552
|
+
source_root = _absolute_work_dir(args.source)
|
|
553
|
+
result = apply_console_update_plan(
|
|
554
|
+
source_root=source_root,
|
|
555
|
+
work_dir=work_dir,
|
|
556
|
+
approved_plan_digest=str(plan["plan_digest"]),
|
|
557
|
+
scripts=_console_update_scripts(source_root),
|
|
558
|
+
timeout_seconds=args.timeout_seconds,
|
|
559
|
+
)
|
|
560
|
+
_print_mapping(
|
|
561
|
+
result,
|
|
562
|
+
output=args.output,
|
|
563
|
+
text=f"Console update applied and read back: {result['receipt_digest']}",
|
|
564
|
+
)
|
|
565
|
+
return 0
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _console_update_scripts(source_root: Path) -> Path:
|
|
569
|
+
"""Prefer the current reviewed source-checkout controls over artifact-source scripts."""
|
|
570
|
+
|
|
571
|
+
checkout_scripts = Path(__file__).resolve().parents[4] / "scripts/deployment/azure"
|
|
572
|
+
if (checkout_scripts / "publish-console.sh").is_file():
|
|
573
|
+
return checkout_scripts
|
|
574
|
+
return source_root / "scripts/deployment/azure"
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def _offline_prepare(args: argparse.Namespace) -> int:
|
|
578
|
+
profile = load_profile(args.profile)
|
|
579
|
+
release_root = _read_public_key(args.release_root)
|
|
580
|
+
bundle_key = _read_public_key(args.bundle_public_key)
|
|
581
|
+
work_dir = _absolute_work_dir(args.work_dir)
|
|
582
|
+
_create_private_work_dir(work_dir)
|
|
583
|
+
result = prepare_offline_release(
|
|
584
|
+
args.offline_kit,
|
|
585
|
+
work_dir=work_dir,
|
|
586
|
+
profile=profile,
|
|
587
|
+
source_commit=args.source_commit,
|
|
588
|
+
release_root_pem=release_root,
|
|
589
|
+
bundle_public_key_pem=bundle_key,
|
|
590
|
+
cli_version=__version__,
|
|
591
|
+
platform_tag=_runtime_platform_tag(),
|
|
592
|
+
)
|
|
593
|
+
_print_mapping(
|
|
594
|
+
result,
|
|
595
|
+
output=args.output,
|
|
596
|
+
text="prepared: signed inputs only; subscription provisioning and approvals remain required",
|
|
597
|
+
)
|
|
598
|
+
return 0
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _offline_configure_console(args: argparse.Namespace) -> int:
|
|
602
|
+
result = configure_console(
|
|
603
|
+
_absolute_work_dir(args.directory), _absolute_work_dir(args.settings)
|
|
604
|
+
)
|
|
605
|
+
_print_mapping(
|
|
606
|
+
result,
|
|
607
|
+
output=args.output,
|
|
608
|
+
text="Console configured for Entra; publication and authenticated access remain unverified",
|
|
609
|
+
)
|
|
610
|
+
return 0
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _offline_install_support(args: argparse.Namespace) -> int:
|
|
614
|
+
public_key = _read_public_key(args.release_root)
|
|
615
|
+
work_dir = _absolute_work_dir(args.work_dir)
|
|
616
|
+
_create_private_work_dir(work_dir)
|
|
617
|
+
result = install_support(
|
|
618
|
+
args.offline_kit,
|
|
619
|
+
work_dir=work_dir,
|
|
620
|
+
release_root_pem=public_key,
|
|
621
|
+
cli_version=__version__,
|
|
622
|
+
platform_tag=_runtime_platform_tag(),
|
|
623
|
+
)
|
|
624
|
+
_print_mapping(
|
|
625
|
+
result,
|
|
626
|
+
output=args.output,
|
|
627
|
+
text="deployment support installed offline; runtime services have not been started",
|
|
628
|
+
)
|
|
629
|
+
return 0
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _provision_inspect(args: argparse.Namespace) -> int:
|
|
633
|
+
profile = load_profile(args.profile)
|
|
634
|
+
checks = inspect_tools(("az", "terraform"))
|
|
635
|
+
active_target = azure_active_target_binding()
|
|
636
|
+
authenticated = active_target is not None and azure_cli_authenticated()
|
|
637
|
+
target_matches = active_target == profile.target_binding
|
|
638
|
+
base_ready = all(check.available for check in checks) and authenticated and target_matches
|
|
639
|
+
state = "review" if base_ready else "incomplete"
|
|
640
|
+
reasons = (
|
|
641
|
+
["execution_host_identity_unverified"]
|
|
642
|
+
if base_ready
|
|
643
|
+
else [
|
|
644
|
+
*[f"tool_unavailable.{check.name}" for check in checks if not check.available],
|
|
645
|
+
*(["azure_authentication_missing"] if not authenticated else []),
|
|
646
|
+
*(["target_binding_mismatch"] if authenticated and not target_matches else []),
|
|
647
|
+
]
|
|
648
|
+
)
|
|
649
|
+
result = {
|
|
650
|
+
"schema_version": "fdai.provision-inspect.v1",
|
|
651
|
+
"state": state,
|
|
652
|
+
"profile_digest": canonical_digest(profile.to_mapping()),
|
|
653
|
+
"approval_quorum": profile.approval_quorum,
|
|
654
|
+
"azure_authenticated": authenticated,
|
|
655
|
+
"target_matches": target_matches,
|
|
656
|
+
"mutation_performed": False,
|
|
657
|
+
"missing_tools": [check.name for check in checks if not check.available],
|
|
658
|
+
"reason_codes": reasons,
|
|
659
|
+
}
|
|
660
|
+
print(
|
|
661
|
+
json.dumps(result, sort_keys=True, separators=(",", ":"))
|
|
662
|
+
if args.output == "json"
|
|
663
|
+
else f"state={result['state']}"
|
|
664
|
+
)
|
|
665
|
+
return 2 if base_ready else 3
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def _provision_bootstrap_reconcile(args: argparse.Namespace) -> int:
|
|
669
|
+
profile = load_profile(args.profile)
|
|
670
|
+
result = reconcile_bootstrap(
|
|
671
|
+
profile,
|
|
672
|
+
source_commit=args.source_commit,
|
|
673
|
+
ops_resource_group=args.ops_resource_group,
|
|
674
|
+
app_resource_group=args.app_resource_group,
|
|
675
|
+
state_storage_account=args.state_storage_account,
|
|
676
|
+
ttl_seconds=args.ttl_seconds,
|
|
677
|
+
)
|
|
678
|
+
output_plan = _absolute_work_dir(args.output_plan)
|
|
679
|
+
payload = result.to_mapping()
|
|
680
|
+
write_private_output(
|
|
681
|
+
output_plan,
|
|
682
|
+
json.dumps(payload, sort_keys=True, separators=(",", ":")) + "\n",
|
|
683
|
+
)
|
|
684
|
+
summary = {
|
|
685
|
+
"schema_version": "fdai.bootstrap-reconcile-result.v1",
|
|
686
|
+
"state": payload["state"],
|
|
687
|
+
"plan_digest": result.plan_digest,
|
|
688
|
+
"observation_digest": result.observation_digest,
|
|
689
|
+
"mutation_performed": False,
|
|
690
|
+
}
|
|
691
|
+
print(
|
|
692
|
+
json.dumps(summary, sort_keys=True, separators=(",", ":"))
|
|
693
|
+
if args.output == "json"
|
|
694
|
+
else f"state={summary['state']} plan_digest={result.plan_digest}"
|
|
695
|
+
)
|
|
696
|
+
return 2 if not result.blockers else 3
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def _bundle_verify(args: argparse.Namespace) -> int:
|
|
700
|
+
result = verify_bundle(
|
|
701
|
+
args.bundle,
|
|
702
|
+
public_key_pem=_read_public_key(args.public_key),
|
|
703
|
+
cli_version=__version__,
|
|
704
|
+
)
|
|
705
|
+
print(result.to_json() if args.output == "json" else f"verified {result.file_count} files")
|
|
706
|
+
return 0
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def _license_inspect(args: argparse.Namespace) -> int:
|
|
710
|
+
token = _read_private_license_token(args.token)
|
|
711
|
+
public_key_pem = _read_public_key(args.public_key)
|
|
712
|
+
if signed_token_schema(token) == INSTALLATION_ENTITLEMENT_SCHEMA:
|
|
713
|
+
if args.installation_binding is None or args.tenant_binding is None:
|
|
714
|
+
raise ValueError(
|
|
715
|
+
"an installation entitlement requires --installation-binding and --tenant-binding"
|
|
716
|
+
)
|
|
717
|
+
entitlement = inspect_installation_entitlement(
|
|
718
|
+
token,
|
|
719
|
+
public_key_pem=public_key_pem,
|
|
720
|
+
expected_installation_binding=args.installation_binding,
|
|
721
|
+
expected_deployment_binding=args.tenant_binding,
|
|
722
|
+
)
|
|
723
|
+
print(entitlement.to_json() if args.output == "json" else "active")
|
|
724
|
+
return 0
|
|
725
|
+
result = inspect_license(
|
|
726
|
+
token,
|
|
727
|
+
public_key_pem=public_key_pem,
|
|
728
|
+
expected_image_digest=args.image_digest,
|
|
729
|
+
expected_tenant_binding=args.tenant_binding,
|
|
730
|
+
)
|
|
731
|
+
print(result.to_json() if args.output == "json" else "active")
|
|
732
|
+
return 0
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def _read_private_license_token(path: Path) -> str:
|
|
736
|
+
descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
|
|
737
|
+
with os.fdopen(descriptor, "rb") as stream:
|
|
738
|
+
details = os.fstat(stream.fileno())
|
|
739
|
+
if (
|
|
740
|
+
not stat.S_ISREG(details.st_mode)
|
|
741
|
+
or stat.S_IMODE(details.st_mode) != 0o600
|
|
742
|
+
or details.st_size > 8192
|
|
743
|
+
):
|
|
744
|
+
raise ValueError("license token MUST be a mode-0600 regular file within 8192 bytes")
|
|
745
|
+
payload = stream.read(8193)
|
|
746
|
+
try:
|
|
747
|
+
token = payload.decode("ascii")
|
|
748
|
+
except UnicodeDecodeError as exc:
|
|
749
|
+
raise ValueError("license token MUST be ASCII") from exc
|
|
750
|
+
if token != token.strip():
|
|
751
|
+
raise ValueError("license token MUST NOT contain surrounding whitespace")
|
|
752
|
+
return token
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def _onboard_status(args: argparse.Namespace) -> int:
|
|
756
|
+
events = read_journal(args.journal)
|
|
757
|
+
result = project_status(events)
|
|
758
|
+
print(
|
|
759
|
+
json.dumps(result, sort_keys=True, separators=(",", ":"))
|
|
760
|
+
if args.output == "json"
|
|
761
|
+
else f"{result['state']}: {result['current_stage']}"
|
|
762
|
+
)
|
|
763
|
+
return 0
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def _onboard_guided(args: argparse.Namespace) -> int:
|
|
767
|
+
profile = load_profile(args.profile)
|
|
768
|
+
manifest = compile_manifest(profile, source_commit=args.source_commit)
|
|
769
|
+
if not args.simulate:
|
|
770
|
+
raise ValueError(
|
|
771
|
+
"live onboarding uses 'fdaictl provision azure'; "
|
|
772
|
+
"GitHub Actions deployment is not supported"
|
|
773
|
+
)
|
|
774
|
+
events = rehearse(
|
|
775
|
+
manifest,
|
|
776
|
+
run_id=args.run_id,
|
|
777
|
+
journal=args.journal,
|
|
778
|
+
interrupt_after=args.interrupt_after,
|
|
779
|
+
)
|
|
780
|
+
latest = events[-1]
|
|
781
|
+
result = {
|
|
782
|
+
"schema_version": "fdai.onboard-guided.v1",
|
|
783
|
+
"run_id": latest.run_id,
|
|
784
|
+
"manifest_digest": manifest.digest,
|
|
785
|
+
"state": latest.state.value,
|
|
786
|
+
"stage": latest.stage,
|
|
787
|
+
"sequence": latest.sequence,
|
|
788
|
+
"mutation_performed": False,
|
|
789
|
+
}
|
|
790
|
+
print(
|
|
791
|
+
json.dumps(result, sort_keys=True, separators=(",", ":"))
|
|
792
|
+
if args.output == "json"
|
|
793
|
+
else f"{latest.state.value}: {latest.stage}"
|
|
794
|
+
)
|
|
795
|
+
return 0
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
__all__ = ["main"]
|