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,147 @@
|
|
|
1
|
+
"""Read shared-root Terraform outputs that targeted applies do not persist.
|
|
2
|
+
|
|
3
|
+
Terraform writes a root output during ``apply -target`` only when that output depends on a
|
|
4
|
+
targeted resource. Outputs derived only from variables and locals, such as logical topic names,
|
|
5
|
+
stay absent from state even though their values are fully determined by the verified
|
|
6
|
+
configuration. When state lacks such an output, this module evaluates one refresh-free,
|
|
7
|
+
lock-free, non-targeted plan of the same root and accepts only outputs whose planned values are
|
|
8
|
+
known. Unknown values mean the backing resource was never applied, so readback fails closed.
|
|
9
|
+
The plan is read-only, private, cached per process, and deleted after evaluation.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import shutil
|
|
17
|
+
import subprocess
|
|
18
|
+
import tempfile
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
_MISSING = "not found"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class _Source:
|
|
27
|
+
variables: Path
|
|
28
|
+
data_dir: Path
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_SOURCES: dict[Path, _Source] = {}
|
|
32
|
+
_CACHE: dict[Path, dict[str, object]] = {}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def bind(infra: Path, *, variables: Path, data_dir: Path) -> None:
|
|
36
|
+
"""Allow planned-output evaluation for one shared root with its exact inputs."""
|
|
37
|
+
|
|
38
|
+
_SOURCES[infra.resolve()] = _Source(variables=variables, data_dir=data_dir)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def read_output(infra: Path, name: str, *, raw: bool, reason: str) -> object:
|
|
42
|
+
"""Return a state output, or a known planned value when state never recorded it."""
|
|
43
|
+
|
|
44
|
+
command = ("terraform", "output", "-raw" if raw else "-json", name)
|
|
45
|
+
result = subprocess.run(
|
|
46
|
+
command, cwd=infra, check=False, capture_output=True, text=True, timeout=120
|
|
47
|
+
)
|
|
48
|
+
if result.returncode == 0:
|
|
49
|
+
return result.stdout.strip() if raw else _json(result.stdout)
|
|
50
|
+
source = _SOURCES.get(infra.resolve())
|
|
51
|
+
if source is None or _MISSING not in result.stderr:
|
|
52
|
+
raise ValueError(reason)
|
|
53
|
+
outputs = _CACHE.get(infra.resolve())
|
|
54
|
+
if outputs is None:
|
|
55
|
+
outputs = _planned_outputs(infra, source)
|
|
56
|
+
_CACHE[infra.resolve()] = outputs
|
|
57
|
+
if name not in outputs:
|
|
58
|
+
raise ValueError(f"{reason}: output is unknown until its resource is applied")
|
|
59
|
+
value = outputs[name]
|
|
60
|
+
if not raw:
|
|
61
|
+
return value
|
|
62
|
+
if isinstance(value, bool):
|
|
63
|
+
return "true" if value else "false"
|
|
64
|
+
if isinstance(value, str | int | float):
|
|
65
|
+
return str(value)
|
|
66
|
+
raise ValueError(f"{reason}: output is not a primitive value")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _planned_outputs(infra: Path, source: _Source) -> dict[str, object]:
|
|
70
|
+
environment = {**os.environ, "TF_DATA_DIR": str(source.data_dir)}
|
|
71
|
+
scratch = Path(tempfile.mkdtemp(prefix="planned-outputs-", dir=source.variables.parent))
|
|
72
|
+
plan = scratch / "outputs.tfplan"
|
|
73
|
+
try:
|
|
74
|
+
planned = subprocess.run(
|
|
75
|
+
(
|
|
76
|
+
"terraform",
|
|
77
|
+
"plan",
|
|
78
|
+
"-refresh=false",
|
|
79
|
+
"-lock=false",
|
|
80
|
+
"-input=false",
|
|
81
|
+
"-no-color",
|
|
82
|
+
f"-var-file={source.variables}",
|
|
83
|
+
f"-out={plan}",
|
|
84
|
+
),
|
|
85
|
+
cwd=infra,
|
|
86
|
+
env=environment,
|
|
87
|
+
check=False,
|
|
88
|
+
capture_output=True,
|
|
89
|
+
text=True,
|
|
90
|
+
timeout=1800,
|
|
91
|
+
)
|
|
92
|
+
if planned.returncode != 0:
|
|
93
|
+
raise ValueError("planned Terraform output evaluation failed")
|
|
94
|
+
shown = subprocess.run(
|
|
95
|
+
("terraform", "show", "-json", str(plan)),
|
|
96
|
+
cwd=infra,
|
|
97
|
+
env=environment,
|
|
98
|
+
check=False,
|
|
99
|
+
capture_output=True,
|
|
100
|
+
text=True,
|
|
101
|
+
timeout=300,
|
|
102
|
+
)
|
|
103
|
+
if shown.returncode != 0:
|
|
104
|
+
raise ValueError("planned Terraform output evaluation failed")
|
|
105
|
+
document = _json(shown.stdout)
|
|
106
|
+
finally:
|
|
107
|
+
shutil.rmtree(scratch, ignore_errors=True)
|
|
108
|
+
if not isinstance(document, dict):
|
|
109
|
+
raise ValueError("planned Terraform output document is invalid")
|
|
110
|
+
return known_planned_outputs(document)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def known_planned_outputs(document: dict[str, object]) -> dict[str, object]:
|
|
114
|
+
"""Select planned root outputs whose values are fully known."""
|
|
115
|
+
|
|
116
|
+
planned = document.get("planned_values")
|
|
117
|
+
changes = document.get("output_changes")
|
|
118
|
+
outputs = planned.get("outputs") if isinstance(planned, dict) else None
|
|
119
|
+
if not isinstance(outputs, dict) or not isinstance(changes, dict):
|
|
120
|
+
raise ValueError("planned Terraform output document is invalid")
|
|
121
|
+
known: dict[str, object] = {}
|
|
122
|
+
for name, output in outputs.items():
|
|
123
|
+
change = changes.get(name)
|
|
124
|
+
if not isinstance(output, dict) or "value" not in output:
|
|
125
|
+
continue
|
|
126
|
+
if isinstance(change, dict) and _has_unknown(change.get("after_unknown")):
|
|
127
|
+
continue
|
|
128
|
+
known[str(name)] = output["value"]
|
|
129
|
+
return known
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _has_unknown(marker: object) -> bool:
|
|
133
|
+
if isinstance(marker, dict):
|
|
134
|
+
return any(_has_unknown(item) for item in marker.values())
|
|
135
|
+
if isinstance(marker, list):
|
|
136
|
+
return any(_has_unknown(item) for item in marker)
|
|
137
|
+
return marker is True
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _json(raw: str) -> object:
|
|
141
|
+
try:
|
|
142
|
+
return json.loads(raw)
|
|
143
|
+
except json.JSONDecodeError as exc:
|
|
144
|
+
raise ValueError("Terraform JSON output is invalid") from exc
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
__all__ = ["bind", "known_planned_outputs", "read_output"]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Wait for the managed host's selected private application data planes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
_RESOURCE_NAME = re.compile(r"[a-z0-9-]{3,63}")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def wait_for_private_data_planes(
|
|
15
|
+
*,
|
|
16
|
+
context: dict[str, object],
|
|
17
|
+
work_dir: Path,
|
|
18
|
+
timeout_seconds: float = 600,
|
|
19
|
+
interval_seconds: float = 15,
|
|
20
|
+
clock: Callable[[], float] = time.monotonic,
|
|
21
|
+
sleep: Callable[[float], None] = time.sleep,
|
|
22
|
+
run: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run,
|
|
23
|
+
) -> dict[str, object]:
|
|
24
|
+
"""Require Key Vault and Data Lake access before a residual can consume authority."""
|
|
25
|
+
|
|
26
|
+
vault = _name(context, "key_vault_name")
|
|
27
|
+
storage = _name(context, "document_storage_account_name")
|
|
28
|
+
if (
|
|
29
|
+
context.get("key_vault_private_access") is not True
|
|
30
|
+
or context.get("document_storage_private_access") is not True
|
|
31
|
+
or timeout_seconds <= 0
|
|
32
|
+
or interval_seconds <= 0
|
|
33
|
+
):
|
|
34
|
+
raise ValueError("private application data-plane probe context is invalid")
|
|
35
|
+
commands = (
|
|
36
|
+
(
|
|
37
|
+
"az",
|
|
38
|
+
"keyvault",
|
|
39
|
+
"secret",
|
|
40
|
+
"list",
|
|
41
|
+
"--vault-name",
|
|
42
|
+
vault,
|
|
43
|
+
"--query",
|
|
44
|
+
"length(@)",
|
|
45
|
+
"--output",
|
|
46
|
+
"tsv",
|
|
47
|
+
"--only-show-errors",
|
|
48
|
+
),
|
|
49
|
+
(
|
|
50
|
+
"az",
|
|
51
|
+
"storage",
|
|
52
|
+
"fs",
|
|
53
|
+
"list",
|
|
54
|
+
"--account-name",
|
|
55
|
+
storage,
|
|
56
|
+
"--auth-mode",
|
|
57
|
+
"login",
|
|
58
|
+
"--query",
|
|
59
|
+
"length(@)",
|
|
60
|
+
"--output",
|
|
61
|
+
"tsv",
|
|
62
|
+
"--only-show-errors",
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
deadline = clock() + timeout_seconds
|
|
66
|
+
attempts = 0
|
|
67
|
+
while True:
|
|
68
|
+
attempts += 1
|
|
69
|
+
results = [
|
|
70
|
+
run(
|
|
71
|
+
command,
|
|
72
|
+
cwd=work_dir,
|
|
73
|
+
check=False,
|
|
74
|
+
capture_output=True,
|
|
75
|
+
text=True,
|
|
76
|
+
timeout=60,
|
|
77
|
+
)
|
|
78
|
+
for command in commands
|
|
79
|
+
]
|
|
80
|
+
if all(result.returncode == 0 and result.stdout.strip().isdigit() for result in results):
|
|
81
|
+
return {
|
|
82
|
+
"schema_version": "fdai.private-data-plane-probe.v1",
|
|
83
|
+
"state": "ready",
|
|
84
|
+
"key_vault_verified": True,
|
|
85
|
+
"document_storage_verified": True,
|
|
86
|
+
"attempts": attempts,
|
|
87
|
+
"mutation_performed": False,
|
|
88
|
+
"subscription_ready": False,
|
|
89
|
+
}
|
|
90
|
+
remaining = deadline - clock()
|
|
91
|
+
if remaining <= 0:
|
|
92
|
+
raise ValueError("private application data planes did not become ready")
|
|
93
|
+
sleep(min(interval_seconds, remaining))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _name(context: dict[str, object], field: str) -> str:
|
|
97
|
+
value = context.get(field)
|
|
98
|
+
if not isinstance(value, str) or _RESOURCE_NAME.fullmatch(value) is None:
|
|
99
|
+
raise ValueError("private application data-plane resource name is invalid")
|
|
100
|
+
return value
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Compile one product profile into authority-neutral Terraform values."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from fdai_service_contracts.product_profile import (
|
|
10
|
+
AzureObservationRole,
|
|
11
|
+
ProductAddOn,
|
|
12
|
+
ProductProfile,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from fdai_deployment_cli.runtime_profile import RuntimeDeploymentProfile
|
|
16
|
+
|
|
17
|
+
_STEWARD_AGENT_NAMES = (
|
|
18
|
+
"Odin",
|
|
19
|
+
"Thor",
|
|
20
|
+
"Forseti",
|
|
21
|
+
"Huginn",
|
|
22
|
+
"Heimdall",
|
|
23
|
+
"Vidar",
|
|
24
|
+
"Var",
|
|
25
|
+
"Bragi",
|
|
26
|
+
"Saga",
|
|
27
|
+
"Mimir",
|
|
28
|
+
"Muninn",
|
|
29
|
+
"Norns",
|
|
30
|
+
"Njord",
|
|
31
|
+
"Freyr",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def product_terraform_values(
|
|
36
|
+
runtime_profile: RuntimeDeploymentProfile,
|
|
37
|
+
entra: dict[str, Any],
|
|
38
|
+
*,
|
|
39
|
+
require_guid: Callable[[dict[str, Any], str], str],
|
|
40
|
+
) -> dict[str, object]:
|
|
41
|
+
"""Return only bindings selected by the immutable product profile."""
|
|
42
|
+
|
|
43
|
+
profile = runtime_profile.product_profile
|
|
44
|
+
console = profile.selects(ProductAddOn.READ_ONLY_CONSOLE)
|
|
45
|
+
governed_execution = profile.selects(ProductAddOn.GOVERNED_EXECUTION)
|
|
46
|
+
notifications = profile.selects(ProductAddOn.NOTIFICATIONS)
|
|
47
|
+
enterprise_identity = profile.selects(ProductAddOn.ENTERPRISE_IDENTITY_GOVERNANCE)
|
|
48
|
+
permissions = profile.observation_permissions
|
|
49
|
+
values: dict[str, object] = {
|
|
50
|
+
"enable_console": console,
|
|
51
|
+
"enable_operator_api": console or enterprise_identity,
|
|
52
|
+
"enable_isolated_executor": governed_execution,
|
|
53
|
+
"enable_governed_execution": governed_execution,
|
|
54
|
+
"enable_email_notifications": notifications,
|
|
55
|
+
"enable_document_ingestion": console and runtime_profile.runtime_platform.value == "aks",
|
|
56
|
+
"enable_inventory_monitoring_reader": permissions.selects(
|
|
57
|
+
AzureObservationRole.MONITORING_READER
|
|
58
|
+
),
|
|
59
|
+
"enable_inventory_log_analytics_reader": permissions.selects(
|
|
60
|
+
AzureObservationRole.LOG_ANALYTICS_READER
|
|
61
|
+
),
|
|
62
|
+
"enable_inventory_cost_management_reader": permissions.selects(
|
|
63
|
+
AzureObservationRole.COST_MANAGEMENT_READER
|
|
64
|
+
),
|
|
65
|
+
"enable_inventory_aks_reader": permissions.selects(AzureObservationRole.AKS_READ_ONLY),
|
|
66
|
+
"inventory_kubernetes_subscription_discovery_enabled": permissions.selects(
|
|
67
|
+
AzureObservationRole.AKS_READ_ONLY
|
|
68
|
+
),
|
|
69
|
+
"enable_inventory_evidence_store_reader": permissions.selects(
|
|
70
|
+
AzureObservationRole.EVIDENCE_STORE_DATA_READER
|
|
71
|
+
),
|
|
72
|
+
"product_profile_json": json.dumps(
|
|
73
|
+
profile.model_dump(mode="json"),
|
|
74
|
+
sort_keys=True,
|
|
75
|
+
separators=(",", ":"),
|
|
76
|
+
),
|
|
77
|
+
}
|
|
78
|
+
if runtime_profile.database_sku is not None:
|
|
79
|
+
values["postgres_sku_name"] = runtime_profile.database_sku
|
|
80
|
+
if not enterprise_identity:
|
|
81
|
+
return values
|
|
82
|
+
operator_id = require_guid(entra, "CURRENT_OPERATOR_OBJECT_ID")
|
|
83
|
+
values.update(
|
|
84
|
+
operator_api_audience=str(entra["OPERATOR_API_AUDIENCE"]),
|
|
85
|
+
rbac_readers_group_id=str(entra["RBAC_READERS_GROUP_ID"]),
|
|
86
|
+
rbac_contributors_group_id=str(entra["RBAC_CONTRIBUTORS_GROUP_ID"]),
|
|
87
|
+
rbac_approvers_group_id=str(entra["RBAC_APPROVERS_GROUP_ID"]),
|
|
88
|
+
rbac_owners_group_id=str(entra["RBAC_OWNERS_GROUP_ID"]),
|
|
89
|
+
rbac_break_glass_group_id=str(entra["RBAC_BREAK_GLASS_GROUP_ID"]),
|
|
90
|
+
stewardship_maintainers=operator_id,
|
|
91
|
+
stewardship_agent_bindings={name: f"user:{operator_id}" for name in _STEWARD_AGENT_NAMES},
|
|
92
|
+
)
|
|
93
|
+
return values
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def context_selects_add_on(context: dict[str, Any], add_on: ProductAddOn) -> bool:
|
|
97
|
+
"""Report whether the recorded deployment context explicitly selected one add-on."""
|
|
98
|
+
|
|
99
|
+
runtime = context.get("runtime_profile")
|
|
100
|
+
if not isinstance(runtime, dict):
|
|
101
|
+
return False
|
|
102
|
+
product = runtime.get("product_profile")
|
|
103
|
+
if product is None:
|
|
104
|
+
return False
|
|
105
|
+
if not isinstance(product, dict):
|
|
106
|
+
raise ValueError("product profile is invalid")
|
|
107
|
+
return ProductProfile.model_validate(product).selects(add_on)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
__all__ = ["context_selects_add_on", "product_terraform_values"]
|