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,157 @@
|
|
|
1
|
+
"""Bind a verified Foundation adoption to managed-host kit preparation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from fdai_deployment_cli.deployment_kit import DeploymentKit
|
|
12
|
+
from fdai_deployment_cli.foundation_adoption_evidence import (
|
|
13
|
+
validate_foundation_adoption_receipt,
|
|
14
|
+
)
|
|
15
|
+
from fdai_deployment_cli.private_output import read_private_bytes
|
|
16
|
+
from fdai_deployment_cli.contracts import load_json_object
|
|
17
|
+
from fdai_deployment_cli.target import compute_target_binding
|
|
18
|
+
|
|
19
|
+
_COMMIT = re.compile(r"[0-9a-f]{40}")
|
|
20
|
+
_GUID = re.compile(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class FoundationHostAdoption:
|
|
25
|
+
"""Current application source and optional no-effect adoption evidence."""
|
|
26
|
+
|
|
27
|
+
source_commit: str
|
|
28
|
+
digest: str
|
|
29
|
+
receipt: dict[str, Any] | None
|
|
30
|
+
|
|
31
|
+
def require_kit(self, kit: DeploymentKit) -> None:
|
|
32
|
+
"""Require one verified kit to match the selected application revision."""
|
|
33
|
+
|
|
34
|
+
if kit.source_commit != self.source_commit:
|
|
35
|
+
raise ValueError("Foundation and deployment kit source revisions differ")
|
|
36
|
+
if self.receipt is not None and (
|
|
37
|
+
self.receipt.get("kit_manifest_digest") != kit.verification.manifest_digest
|
|
38
|
+
or self.receipt.get("runtime_release_digest") != kit.runtime.digest
|
|
39
|
+
):
|
|
40
|
+
raise ValueError("Foundation adoption differs from the verified deployment kit")
|
|
41
|
+
|
|
42
|
+
def require_context(self, context: dict[str, Any]) -> None:
|
|
43
|
+
"""Require retained host preparation to preserve the exact adoption."""
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
context.get("source_commit") != self.source_commit
|
|
47
|
+
or context.get("foundation_adoption_digest", "") != self.digest
|
|
48
|
+
or (
|
|
49
|
+
self.receipt is not None
|
|
50
|
+
and (
|
|
51
|
+
context.get("kit_manifest_digest") != self.receipt.get("kit_manifest_digest")
|
|
52
|
+
or context.get("runtime_release_digest")
|
|
53
|
+
!= self.receipt.get("runtime_release_digest")
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
):
|
|
57
|
+
raise ValueError("standalone host retained Foundation adoption differs")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True, slots=True)
|
|
61
|
+
class FoundationHostContext:
|
|
62
|
+
"""Verified target, runner identity, and optional adoption."""
|
|
63
|
+
|
|
64
|
+
subscription_id: str
|
|
65
|
+
tenant_id: str
|
|
66
|
+
target_binding: str
|
|
67
|
+
client_id: str
|
|
68
|
+
principal_id: str
|
|
69
|
+
adoption: FoundationHostAdoption
|
|
70
|
+
|
|
71
|
+
def login(
|
|
72
|
+
self,
|
|
73
|
+
operation: Callable[[str, str, str, str, Path], None],
|
|
74
|
+
work_dir: Path,
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Run the existing managed-identity login with verified target fields."""
|
|
77
|
+
|
|
78
|
+
operation(
|
|
79
|
+
self.subscription_id,
|
|
80
|
+
self.tenant_id,
|
|
81
|
+
self.client_id,
|
|
82
|
+
self.principal_id,
|
|
83
|
+
work_dir,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def load_foundation_host_context(
|
|
88
|
+
path: Path | None,
|
|
89
|
+
*,
|
|
90
|
+
handoff: dict[str, Any],
|
|
91
|
+
runner: dict[str, Any],
|
|
92
|
+
) -> FoundationHostContext:
|
|
93
|
+
"""Load the target and adoption fields needed by managed-host preparation."""
|
|
94
|
+
|
|
95
|
+
subscription_id = _guid(handoff, "subscription_id")
|
|
96
|
+
tenant_id = _guid(handoff, "tenant_id")
|
|
97
|
+
target_binding = compute_target_binding(
|
|
98
|
+
tenant_id=tenant_id,
|
|
99
|
+
subscription_id=subscription_id,
|
|
100
|
+
)
|
|
101
|
+
return FoundationHostContext(
|
|
102
|
+
subscription_id=subscription_id,
|
|
103
|
+
tenant_id=tenant_id,
|
|
104
|
+
target_binding=target_binding,
|
|
105
|
+
client_id=_guid(runner, "client_id"),
|
|
106
|
+
principal_id=_guid(runner, "principal_id"),
|
|
107
|
+
adoption=load_foundation_host_adoption(
|
|
108
|
+
path,
|
|
109
|
+
handoff=handoff,
|
|
110
|
+
target_binding=target_binding,
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def load_foundation_host_adoption(
|
|
116
|
+
path: Path | None,
|
|
117
|
+
*,
|
|
118
|
+
handoff: dict[str, Any],
|
|
119
|
+
target_binding: str,
|
|
120
|
+
) -> FoundationHostAdoption:
|
|
121
|
+
"""Load optional adoption evidence while preserving the original handoff."""
|
|
122
|
+
|
|
123
|
+
foundation_source_commit = handoff.get("source_commit")
|
|
124
|
+
if (
|
|
125
|
+
not isinstance(foundation_source_commit, str)
|
|
126
|
+
or _COMMIT.fullmatch(foundation_source_commit) is None
|
|
127
|
+
):
|
|
128
|
+
raise ValueError("Foundation handoff source revision is invalid")
|
|
129
|
+
if path is None:
|
|
130
|
+
return FoundationHostAdoption(
|
|
131
|
+
source_commit=foundation_source_commit,
|
|
132
|
+
digest="",
|
|
133
|
+
receipt=None,
|
|
134
|
+
)
|
|
135
|
+
raw = read_private_bytes(path, max_bytes=65_536)
|
|
136
|
+
receipt = load_json_object(raw, label="Foundation adoption")
|
|
137
|
+
application_source_commit = str(receipt.get("application_source_commit", ""))
|
|
138
|
+
digest = validate_foundation_adoption_receipt(
|
|
139
|
+
receipt,
|
|
140
|
+
handoff=handoff,
|
|
141
|
+
target_binding=target_binding,
|
|
142
|
+
application_source_commit=application_source_commit,
|
|
143
|
+
kit_manifest_digest=str(receipt.get("kit_manifest_digest", "")),
|
|
144
|
+
runtime_release_digest=str(receipt.get("runtime_release_digest", "")),
|
|
145
|
+
)
|
|
146
|
+
return FoundationHostAdoption(
|
|
147
|
+
source_commit=application_source_commit,
|
|
148
|
+
digest=digest,
|
|
149
|
+
receipt=receipt,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _guid(value: dict[str, Any], field: str) -> str:
|
|
154
|
+
item = value.get(field)
|
|
155
|
+
if not isinstance(item, str) or _GUID.fullmatch(item) is None:
|
|
156
|
+
raise ValueError(f"{field} is not a canonical GUID")
|
|
157
|
+
return item
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Transfer Foundation context and optional adoption evidence to the managed host."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PrivateTransport(Protocol):
|
|
11
|
+
"""Minimum managed-host file transport used by Foundation staging."""
|
|
12
|
+
|
|
13
|
+
def copy_to(self, source: Path, destination: str, *, timeout: int) -> None:
|
|
14
|
+
"""Copy one validated private file."""
|
|
15
|
+
...
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class FoundationTransport:
|
|
20
|
+
"""Remote Foundation input paths and optional host CLI arguments."""
|
|
21
|
+
|
|
22
|
+
handoff: str
|
|
23
|
+
entra: str | None
|
|
24
|
+
adoption_arguments: tuple[str, ...]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def stage_foundation_context(
|
|
28
|
+
tunnel: PrivateTransport,
|
|
29
|
+
handoff_path: Path,
|
|
30
|
+
remote_handoff: str,
|
|
31
|
+
entra_path: Path | None,
|
|
32
|
+
remote_entra: str | None,
|
|
33
|
+
remote_root: str,
|
|
34
|
+
) -> FoundationTransport:
|
|
35
|
+
"""Copy exact handoff inputs and return optional adoption arguments."""
|
|
36
|
+
|
|
37
|
+
tunnel.copy_to(handoff_path, remote_handoff, timeout=120)
|
|
38
|
+
if (entra_path is None) != (remote_entra is None):
|
|
39
|
+
raise ValueError("Entra transport inputs must be selected together")
|
|
40
|
+
if entra_path is not None and remote_entra is not None:
|
|
41
|
+
tunnel.copy_to(entra_path, remote_entra, timeout=120)
|
|
42
|
+
adoption = handoff_path.parent.parent / "foundation-adoption-receipt.json"
|
|
43
|
+
if not adoption.exists() and not adoption.is_symlink():
|
|
44
|
+
return FoundationTransport(remote_handoff, remote_entra, ())
|
|
45
|
+
remote_adoption = f"{remote_root}/foundation-adoption.json"
|
|
46
|
+
tunnel.copy_to(adoption, remote_adoption, timeout=120)
|
|
47
|
+
return FoundationTransport(
|
|
48
|
+
remote_handoff,
|
|
49
|
+
remote_entra,
|
|
50
|
+
("--foundation-adoption", remote_adoption),
|
|
51
|
+
)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Describe a failed Foundation attempt without accepting it as handoff or authority."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, NoReturn
|
|
8
|
+
|
|
9
|
+
from fdai_deployment_cli.private_output import read_private_bytes
|
|
10
|
+
|
|
11
|
+
_GENERIC = "standalone Foundation orchestration failed; inspect retained status before recovery"
|
|
12
|
+
_IMAGE_INCOMPLETE = (
|
|
13
|
+
"standalone Foundation orchestration failed at Runner image exact apply (08/15).\n"
|
|
14
|
+
"Image creation or independent verification is incomplete.\n"
|
|
15
|
+
"Preserve the work directory and its state, plans, and apply claims. "
|
|
16
|
+
"A retained claim permits verification only; recovery needs a separately reviewed exact "
|
|
17
|
+
"plan and new approval."
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def foundation_failure_summary(
|
|
22
|
+
path: Path, *, previous: int, source_commit: str, run_binding: str
|
|
23
|
+
) -> str:
|
|
24
|
+
"""Return fixed guidance only for a current, context-matched failed child attempt.
|
|
25
|
+
|
|
26
|
+
Call only after the child has exited unsuccessfully. This bounded private read never accepts
|
|
27
|
+
a success record, returns operational state, prints payload values, or changes execution.
|
|
28
|
+
Missing, malformed, stale, unreadable, or unrecognized evidence retains the generic failure.
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
value = json.loads(
|
|
32
|
+
read_private_bytes(path, max_bytes=1_048_576).decode("utf-8"),
|
|
33
|
+
object_pairs_hook=_unique_object,
|
|
34
|
+
parse_constant=_reject_constant,
|
|
35
|
+
)
|
|
36
|
+
except (OSError, ValueError, RecursionError):
|
|
37
|
+
return _GENERIC
|
|
38
|
+
if not isinstance(value, dict):
|
|
39
|
+
return _GENERIC
|
|
40
|
+
attempt, sequence = value.get("attempt"), value.get("sequence")
|
|
41
|
+
if (
|
|
42
|
+
value.get("schema_version") != "fdai.genesis-orchestration-status.v2"
|
|
43
|
+
or type(attempt) is not int
|
|
44
|
+
or attempt != previous + 1
|
|
45
|
+
or type(sequence) is not int
|
|
46
|
+
or sequence < 1
|
|
47
|
+
or value.get("source_commit") != source_commit
|
|
48
|
+
or value.get("target_binding") != run_binding
|
|
49
|
+
or value.get("mode") != "apply"
|
|
50
|
+
or value.get("state") not in ("blocked", "failed")
|
|
51
|
+
or value.get("route") != "private-runner"
|
|
52
|
+
):
|
|
53
|
+
return _GENERIC
|
|
54
|
+
if (
|
|
55
|
+
value.get("current_stage") == "runner-image-apply"
|
|
56
|
+
and value.get("reason_code") == "runner_image_apply_or_verification_failed"
|
|
57
|
+
):
|
|
58
|
+
return _IMAGE_INCOMPLETE
|
|
59
|
+
return _GENERIC
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _unique_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
63
|
+
result: dict[str, Any] = {}
|
|
64
|
+
for key, value in pairs:
|
|
65
|
+
if key in result:
|
|
66
|
+
raise ValueError("duplicate Foundation diagnostic key")
|
|
67
|
+
result[key] = value
|
|
68
|
+
return result
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _reject_constant(_value: str) -> NoReturn:
|
|
72
|
+
raise ValueError("nonfinite Foundation diagnostic value")
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Read-only validation for the exact managed image consumed by Foundation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
|
|
9
|
+
from fdai_deployment_cli.contracts import canonical_digest
|
|
10
|
+
|
|
11
|
+
RunCommand = Callable[..., subprocess.CompletedProcess[str]]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def verify_foundation_runner_image(
|
|
15
|
+
variables: dict[str, object], *, expected_region: str, run: RunCommand = subprocess.run
|
|
16
|
+
) -> str:
|
|
17
|
+
"""Verify exact image provenance through ARM and return a content-free observation digest.
|
|
18
|
+
|
|
19
|
+
This read grants no image, Foundation, apply, or readiness authority. Provider output is
|
|
20
|
+
never returned or included in an error.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
mode = variables.get("runner_bootstrap_mode", "offline")
|
|
24
|
+
image_id = variables.get("runner_source_image_id")
|
|
25
|
+
marketplace_version = variables.get("runner_marketplace_image_version", "")
|
|
26
|
+
source_commit = variables.get("runner_image_source_commit", variables.get("source_commit"))
|
|
27
|
+
toolchain_digest = variables.get("runner_image_toolchain_digest")
|
|
28
|
+
if mode == "online":
|
|
29
|
+
return _verify_marketplace_image(
|
|
30
|
+
version=marketplace_version,
|
|
31
|
+
toolchain_digest=toolchain_digest,
|
|
32
|
+
expected_region=expected_region,
|
|
33
|
+
run=run,
|
|
34
|
+
)
|
|
35
|
+
if (
|
|
36
|
+
not isinstance(image_id, str)
|
|
37
|
+
or not image_id
|
|
38
|
+
or not isinstance(source_commit, str)
|
|
39
|
+
or not source_commit
|
|
40
|
+
or not isinstance(toolchain_digest, str)
|
|
41
|
+
or not toolchain_digest
|
|
42
|
+
):
|
|
43
|
+
raise ValueError("Foundation runner image provenance input is incomplete")
|
|
44
|
+
completed = run(
|
|
45
|
+
[
|
|
46
|
+
"az",
|
|
47
|
+
"resource",
|
|
48
|
+
"show",
|
|
49
|
+
"--ids",
|
|
50
|
+
image_id,
|
|
51
|
+
"--query",
|
|
52
|
+
"{id:id,type:type,location:location,provisioningState:properties.provisioningState,managedOsType:properties.storageProfile.osDisk.osType,galleryOsType:properties.storageProfile.osDiskImage.operatingSystem,tags:tags}",
|
|
53
|
+
"--output",
|
|
54
|
+
"json",
|
|
55
|
+
"--only-show-errors",
|
|
56
|
+
],
|
|
57
|
+
capture_output=True,
|
|
58
|
+
text=True,
|
|
59
|
+
check=False,
|
|
60
|
+
timeout=60,
|
|
61
|
+
)
|
|
62
|
+
if completed.returncode != 0:
|
|
63
|
+
raise ValueError("Foundation runner image is unavailable")
|
|
64
|
+
try:
|
|
65
|
+
observed = json.loads(completed.stdout)
|
|
66
|
+
except json.JSONDecodeError as exc:
|
|
67
|
+
raise ValueError("Foundation runner image observation is invalid") from exc
|
|
68
|
+
if not isinstance(observed, dict):
|
|
69
|
+
raise TypeError("Foundation runner image observation is invalid")
|
|
70
|
+
tags = observed.get("tags")
|
|
71
|
+
image_type = str(observed.get("type", "")).casefold()
|
|
72
|
+
os_type = observed.get("managedOsType") or observed.get("galleryOsType")
|
|
73
|
+
if (
|
|
74
|
+
str(observed.get("id", "")).casefold() != image_id.casefold()
|
|
75
|
+
or image_type
|
|
76
|
+
not in {
|
|
77
|
+
"microsoft.compute/images",
|
|
78
|
+
"microsoft.compute/galleries/images/versions",
|
|
79
|
+
}
|
|
80
|
+
or str(observed.get("location", "")).casefold() != expected_region.casefold()
|
|
81
|
+
or observed.get("provisioningState") != "Succeeded"
|
|
82
|
+
or str(os_type).casefold() != "linux"
|
|
83
|
+
or not isinstance(tags, dict)
|
|
84
|
+
or tags.get("fdai:source-commit") != source_commit
|
|
85
|
+
or tags.get("fdai:toolchain-digest") != toolchain_digest
|
|
86
|
+
):
|
|
87
|
+
raise ValueError("Foundation runner image provenance does not match the reviewed input")
|
|
88
|
+
return canonical_digest(
|
|
89
|
+
{
|
|
90
|
+
"schema_version": "fdai.foundation-runner-image-observation.v1",
|
|
91
|
+
"image_id_digest": canonical_digest(image_id.casefold()),
|
|
92
|
+
"image_type": image_type,
|
|
93
|
+
"location": expected_region,
|
|
94
|
+
"provisioning_state": "Succeeded",
|
|
95
|
+
"os_type": "Linux",
|
|
96
|
+
"source_commit": source_commit,
|
|
97
|
+
"toolchain_digest": toolchain_digest,
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _verify_marketplace_image(
|
|
103
|
+
*,
|
|
104
|
+
version: object,
|
|
105
|
+
toolchain_digest: object,
|
|
106
|
+
expected_region: str,
|
|
107
|
+
run: RunCommand,
|
|
108
|
+
) -> str:
|
|
109
|
+
if (
|
|
110
|
+
not isinstance(version, str)
|
|
111
|
+
or not version
|
|
112
|
+
or not isinstance(toolchain_digest, str)
|
|
113
|
+
or not toolchain_digest
|
|
114
|
+
):
|
|
115
|
+
raise ValueError("Foundation Marketplace image input is incomplete")
|
|
116
|
+
urn = f"Canonical:ubuntu-24_04-lts:server:{version}"
|
|
117
|
+
completed = run(
|
|
118
|
+
[
|
|
119
|
+
"az",
|
|
120
|
+
"vm",
|
|
121
|
+
"image",
|
|
122
|
+
"show",
|
|
123
|
+
"--location",
|
|
124
|
+
expected_region,
|
|
125
|
+
"--urn",
|
|
126
|
+
urn,
|
|
127
|
+
"--query",
|
|
128
|
+
(
|
|
129
|
+
"{version:name,location:location,"
|
|
130
|
+
"osType:osDiskImage.operatingSystem,"
|
|
131
|
+
"hyperVGeneration:hyperVGeneration}"
|
|
132
|
+
),
|
|
133
|
+
"--output",
|
|
134
|
+
"json",
|
|
135
|
+
"--only-show-errors",
|
|
136
|
+
],
|
|
137
|
+
capture_output=True,
|
|
138
|
+
text=True,
|
|
139
|
+
check=False,
|
|
140
|
+
timeout=60,
|
|
141
|
+
)
|
|
142
|
+
if completed.returncode != 0:
|
|
143
|
+
raise ValueError("Foundation Marketplace image is unavailable")
|
|
144
|
+
try:
|
|
145
|
+
observed = json.loads(completed.stdout)
|
|
146
|
+
except json.JSONDecodeError as exc:
|
|
147
|
+
raise ValueError("Foundation Marketplace image observation is invalid") from exc
|
|
148
|
+
if (
|
|
149
|
+
not isinstance(observed, dict)
|
|
150
|
+
or observed.get("version") != version
|
|
151
|
+
or str(observed.get("location", "")).casefold() != expected_region.casefold()
|
|
152
|
+
or observed.get("osType") != "Linux"
|
|
153
|
+
or observed.get("hyperVGeneration") != "V2"
|
|
154
|
+
):
|
|
155
|
+
raise ValueError("Foundation Marketplace image does not match the reviewed input")
|
|
156
|
+
return canonical_digest(
|
|
157
|
+
{
|
|
158
|
+
"schema_version": "fdai.foundation-marketplace-image-observation.v1",
|
|
159
|
+
"publisher": "Canonical",
|
|
160
|
+
"offer": "ubuntu-24_04-lts",
|
|
161
|
+
"sku": "server",
|
|
162
|
+
"version": version,
|
|
163
|
+
"region": expected_region,
|
|
164
|
+
"os_type": "Linux",
|
|
165
|
+
"hyper_v_generation": "V2",
|
|
166
|
+
"toolchain_digest": toolchain_digest,
|
|
167
|
+
}
|
|
168
|
+
)
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Secret-free inputs for planning the ARM-only genesis foundation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ipaddress
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from fdai_deployment_cli.plan_input import PlanInputContext, read_plan_input, write_plan_input
|
|
10
|
+
from fdai_deployment_cli.target import compute_target_binding
|
|
11
|
+
|
|
12
|
+
_STRINGS = {
|
|
13
|
+
"workload": r"[a-z][a-z0-9]{1,11}",
|
|
14
|
+
"region_short": r"[a-z][a-z0-9]{1,7}",
|
|
15
|
+
"state_storage_account_name": r"[a-z0-9]{3,24}",
|
|
16
|
+
"runner_ssh_public_key": r"ssh-(?:ed25519|rsa) [A-Za-z0-9+/]+={0,2}",
|
|
17
|
+
"source_commit": r"[0-9a-f]{40}",
|
|
18
|
+
"run_digest": r"[0-9a-f]{64}",
|
|
19
|
+
"foundation_context_digest": r"[0-9a-f]{64}",
|
|
20
|
+
"runner_image_toolchain_digest": r"[0-9a-f]{64}",
|
|
21
|
+
}
|
|
22
|
+
_NETWORKS = ("ops_address_space", "runner_subnet_prefix", "pe_subnet_prefix")
|
|
23
|
+
_RUNNER_IMAGE_NETWORKS = (
|
|
24
|
+
"build_address_space",
|
|
25
|
+
"build_subnet_prefix",
|
|
26
|
+
"firewall_subnet_prefix",
|
|
27
|
+
"firewall_management_subnet_prefix",
|
|
28
|
+
)
|
|
29
|
+
_OPTIONAL_STRINGS = {
|
|
30
|
+
"application_workload": r"[a-z][a-z0-9]{1,11}",
|
|
31
|
+
"runner_image_source_commit": r"[0-9a-f]{40}",
|
|
32
|
+
"runner_image_verified_source_commit": r"[0-9a-f]{40}",
|
|
33
|
+
"runner_image_run_digest": r"[0-9a-f]{64}",
|
|
34
|
+
"runner_image_receipt_digest": r"[0-9a-f]{64}",
|
|
35
|
+
}
|
|
36
|
+
_REQUIRED = (
|
|
37
|
+
frozenset(_STRINGS)
|
|
38
|
+
| frozenset(_NETWORKS)
|
|
39
|
+
| {
|
|
40
|
+
"tenant_id",
|
|
41
|
+
"subscription_id",
|
|
42
|
+
"target_binding",
|
|
43
|
+
"region",
|
|
44
|
+
"runner_source_image_id",
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
_OPTIONAL = frozenset(
|
|
48
|
+
{
|
|
49
|
+
"state_retention_days",
|
|
50
|
+
"operations_public_ip_tags",
|
|
51
|
+
"runner_vm_size",
|
|
52
|
+
"runner_admin_username",
|
|
53
|
+
"runner_parallelism",
|
|
54
|
+
"enable_public_egress",
|
|
55
|
+
"enable_bastion",
|
|
56
|
+
"bastion_subnet_prefix",
|
|
57
|
+
"execution_transport",
|
|
58
|
+
"runner_bootstrap_mode",
|
|
59
|
+
"runner_marketplace_image_version",
|
|
60
|
+
*_OPTIONAL_STRINGS,
|
|
61
|
+
*_RUNNER_IMAGE_NETWORKS,
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
_UUID = r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}"
|
|
65
|
+
_SEGMENT = r"[A-Za-z0-9._()-]+"
|
|
66
|
+
_IMAGE = re.compile(
|
|
67
|
+
rf"/subscriptions/{_UUID}/resourceGroups/{_SEGMENT}/providers/Microsoft\.Compute/"
|
|
68
|
+
rf"(?:images/{_SEGMENT}|galleries/{_SEGMENT}/images/{_SEGMENT}/versions/[0-9]+\.[0-9]+\.[0-9]+)",
|
|
69
|
+
re.IGNORECASE,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def snapshot_foundation_input(
|
|
74
|
+
source: Path,
|
|
75
|
+
destination: Path,
|
|
76
|
+
*,
|
|
77
|
+
expected_target_binding: str,
|
|
78
|
+
expected_region: str,
|
|
79
|
+
expected_environment: str,
|
|
80
|
+
preserve_runner_image_networks: bool = False,
|
|
81
|
+
) -> PlanInputContext:
|
|
82
|
+
"""Snapshot only reviewed foundation inputs; never accept credentials.
|
|
83
|
+
|
|
84
|
+
This validates a dry-run input, not image trust, network policy, source
|
|
85
|
+
eligibility, approval, or existing-resource adoption. The protected executor
|
|
86
|
+
must independently bind those before applying a separately sealed plan.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
values = read_plan_input(source)
|
|
90
|
+
if not _REQUIRED <= values.keys() or values.keys() - (_REQUIRED | _OPTIONAL):
|
|
91
|
+
raise ValueError("foundation plan input fields do not match the secret-free schema")
|
|
92
|
+
tenant = values["tenant_id"]
|
|
93
|
+
subscription = values["subscription_id"]
|
|
94
|
+
if (
|
|
95
|
+
not isinstance(tenant, str)
|
|
96
|
+
or not isinstance(subscription, str)
|
|
97
|
+
or not tenant
|
|
98
|
+
or not subscription
|
|
99
|
+
):
|
|
100
|
+
raise ValueError("foundation plan target values MUST be strings")
|
|
101
|
+
binding = compute_target_binding(tenant_id=tenant, subscription_id=subscription)
|
|
102
|
+
if values["target_binding"] != binding or binding != expected_target_binding:
|
|
103
|
+
raise ValueError("foundation plan target binding does not match the profile")
|
|
104
|
+
if values["region"] != expected_region:
|
|
105
|
+
raise ValueError("foundation plan region does not match the profile")
|
|
106
|
+
if expected_environment not in {"dev", "staging", "prod"}:
|
|
107
|
+
raise ValueError("foundation plan environment is unsupported")
|
|
108
|
+
for name, pattern in _STRINGS.items():
|
|
109
|
+
value = values[name]
|
|
110
|
+
if not isinstance(value, str) or re.fullmatch(pattern, value) is None:
|
|
111
|
+
raise ValueError(f"foundation plan {name} is invalid")
|
|
112
|
+
for name, pattern in _OPTIONAL_STRINGS.items():
|
|
113
|
+
value = values.get(name)
|
|
114
|
+
if value is not None and (
|
|
115
|
+
not isinstance(value, str) or re.fullmatch(pattern, value) is None
|
|
116
|
+
):
|
|
117
|
+
raise ValueError(f"foundation plan {name} is invalid")
|
|
118
|
+
image = values["runner_source_image_id"]
|
|
119
|
+
marketplace_version = values.get("runner_marketplace_image_version", "")
|
|
120
|
+
mode = values.get("runner_bootstrap_mode", "offline")
|
|
121
|
+
if mode not in {"online", "offline"}:
|
|
122
|
+
raise ValueError("foundation plan runner bootstrap mode is invalid")
|
|
123
|
+
if mode == "online":
|
|
124
|
+
if (
|
|
125
|
+
image != ""
|
|
126
|
+
or not isinstance(marketplace_version, str)
|
|
127
|
+
or re.fullmatch(r"[0-9]+(?:\.[0-9]+)+", marketplace_version) is None
|
|
128
|
+
):
|
|
129
|
+
raise ValueError("online foundation requires an exact Marketplace image version")
|
|
130
|
+
elif not isinstance(image, str) or _IMAGE.fullmatch(image) is None or marketplace_version != "":
|
|
131
|
+
raise ValueError("offline foundation requires an exact managed image or gallery version")
|
|
132
|
+
_validate_networks(values)
|
|
133
|
+
_validate_runner_image_networks(values)
|
|
134
|
+
if values.get("operations_public_ip_tags", {}) not in (
|
|
135
|
+
{},
|
|
136
|
+
{"FirstPartyUsage": "/Unprivileged"},
|
|
137
|
+
):
|
|
138
|
+
raise ValueError("foundation plan operations public IP policy tags are unsupported")
|
|
139
|
+
retention = values.get("state_retention_days", 30)
|
|
140
|
+
if type(retention) is not int or not 1 <= retention <= 365:
|
|
141
|
+
raise ValueError("foundation plan retention MUST be an integer from 1 through 365")
|
|
142
|
+
size = values.get("runner_vm_size", "Standard_D8ds_v5")
|
|
143
|
+
if not isinstance(size, str) or re.fullmatch(r"Standard_[A-Za-z0-9_]{1,64}", size) is None:
|
|
144
|
+
raise ValueError("foundation plan runner size is invalid")
|
|
145
|
+
username = values.get("runner_admin_username", "fdairunner")
|
|
146
|
+
if not isinstance(username, str) or re.fullmatch(r"[a-z_][a-z0-9_-]{0,30}", username) is None:
|
|
147
|
+
raise ValueError("foundation plan runner username is invalid")
|
|
148
|
+
parallelism = values.get("runner_parallelism", 1)
|
|
149
|
+
if type(parallelism) is not int or not 1 <= parallelism <= 5:
|
|
150
|
+
raise ValueError("foundation plan runner parallelism MUST be an integer from 1 through 5")
|
|
151
|
+
if values.get("execution_transport", "manual") != "manual":
|
|
152
|
+
raise ValueError("foundation plan execution transport must be manual")
|
|
153
|
+
if type(values.get("enable_public_egress", False)) is not bool:
|
|
154
|
+
raise ValueError("foundation plan public egress selection MUST be boolean")
|
|
155
|
+
enable_bastion = values.get("enable_bastion", False)
|
|
156
|
+
if type(enable_bastion) is not bool:
|
|
157
|
+
raise ValueError("foundation plan Bastion selection MUST be boolean")
|
|
158
|
+
_validate_bastion_network(values, enabled=enable_bastion)
|
|
159
|
+
excluded = {"target_binding"}
|
|
160
|
+
if not preserve_runner_image_networks:
|
|
161
|
+
excluded.update(_RUNNER_IMAGE_NETWORKS)
|
|
162
|
+
terraform_values = {key: value for key, value in values.items() if key not in excluded}
|
|
163
|
+
terraform_values.setdefault("runner_bootstrap_mode", "offline")
|
|
164
|
+
terraform_values.setdefault("runner_marketplace_image_version", "")
|
|
165
|
+
terraform_values["env"] = expected_environment
|
|
166
|
+
write_plan_input(destination, terraform_values)
|
|
167
|
+
return PlanInputContext(subscription_id=subscription, tenant_id=tenant)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _validate_networks(values: dict[str, object]) -> None:
|
|
171
|
+
networks: list[ipaddress.IPv4Network] = []
|
|
172
|
+
for name in _NETWORKS:
|
|
173
|
+
value = values[name]
|
|
174
|
+
if not isinstance(value, str) or "/" not in value:
|
|
175
|
+
raise ValueError("foundation network prefixes MUST be IPv4 CIDRs")
|
|
176
|
+
try:
|
|
177
|
+
network = ipaddress.IPv4Network(value)
|
|
178
|
+
except ValueError:
|
|
179
|
+
raise ValueError("foundation network prefixes MUST be canonical IPv4 CIDRs") from None
|
|
180
|
+
networks.append(network)
|
|
181
|
+
hub, runner, endpoint = networks
|
|
182
|
+
if not runner.subnet_of(hub) or not endpoint.subnet_of(hub) or runner.overlaps(endpoint):
|
|
183
|
+
raise ValueError("foundation subnets MUST be disjoint and contained in the ops network")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _validate_bastion_network(values: dict[str, object], *, enabled: bool) -> None:
|
|
187
|
+
value = values.get("bastion_subnet_prefix")
|
|
188
|
+
if not enabled:
|
|
189
|
+
if value is not None:
|
|
190
|
+
raise ValueError("foundation Bastion subnet requires Bastion to be enabled")
|
|
191
|
+
return
|
|
192
|
+
if not isinstance(value, str) or "/" not in value:
|
|
193
|
+
raise ValueError("foundation Bastion requires an IPv4 /26-or-larger subnet")
|
|
194
|
+
try:
|
|
195
|
+
bastion = ipaddress.IPv4Network(value)
|
|
196
|
+
hub = ipaddress.IPv4Network(str(values["ops_address_space"]))
|
|
197
|
+
runner = ipaddress.IPv4Network(str(values["runner_subnet_prefix"]))
|
|
198
|
+
endpoint = ipaddress.IPv4Network(str(values["pe_subnet_prefix"]))
|
|
199
|
+
except ValueError:
|
|
200
|
+
raise ValueError("foundation Bastion subnet MUST be a canonical IPv4 CIDR") from None
|
|
201
|
+
if (
|
|
202
|
+
bastion.prefixlen > 26
|
|
203
|
+
or not bastion.subnet_of(hub)
|
|
204
|
+
or bastion.overlaps(runner)
|
|
205
|
+
or bastion.overlaps(endpoint)
|
|
206
|
+
):
|
|
207
|
+
raise ValueError("foundation Bastion subnet MUST be disjoint, contained, and at least /26")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _validate_runner_image_networks(values: dict[str, object]) -> None:
|
|
211
|
+
present = set(_RUNNER_IMAGE_NETWORKS) & values.keys()
|
|
212
|
+
if not present:
|
|
213
|
+
return
|
|
214
|
+
if present != set(_RUNNER_IMAGE_NETWORKS):
|
|
215
|
+
raise ValueError("runner image network fields MUST be supplied together")
|
|
216
|
+
try:
|
|
217
|
+
build, builder, firewall, firewall_management = (
|
|
218
|
+
ipaddress.IPv4Network(str(values[name])) for name in _RUNNER_IMAGE_NETWORKS
|
|
219
|
+
)
|
|
220
|
+
ops = ipaddress.IPv4Network(str(values["ops_address_space"]))
|
|
221
|
+
except ValueError:
|
|
222
|
+
raise ValueError("runner image network prefixes MUST be canonical IPv4 CIDRs") from None
|
|
223
|
+
if (
|
|
224
|
+
not build.is_private
|
|
225
|
+
or build.overlaps(ops)
|
|
226
|
+
or not all(subnet.subnet_of(build) for subnet in (builder, firewall, firewall_management))
|
|
227
|
+
or builder.overlaps(firewall)
|
|
228
|
+
or builder.overlaps(firewall_management)
|
|
229
|
+
or firewall.overlaps(firewall_management)
|
|
230
|
+
or firewall.prefixlen > 26
|
|
231
|
+
or firewall_management.prefixlen > 26
|
|
232
|
+
):
|
|
233
|
+
raise ValueError(
|
|
234
|
+
"runner image subnets MUST be private, disjoint, contained, and policy compatible"
|
|
235
|
+
)
|