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,174 @@
|
|
|
1
|
+
"""Create source-bound Foundation plans with locked tools and no kit or apply fallback."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import os
|
|
7
|
+
import stat
|
|
8
|
+
import subprocess
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from tempfile import TemporaryDirectory
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from fdai_deployment_cli.contracts import ProvisionProfile, canonical_digest, load_json_object
|
|
15
|
+
from fdai_deployment_cli.foundation_image import verify_foundation_runner_image
|
|
16
|
+
from fdai_deployment_cli.foundation_input import snapshot_foundation_input
|
|
17
|
+
from fdai_deployment_cli.foundation_plan import save_foundation_plan, source_foundation_plan_context
|
|
18
|
+
from fdai_deployment_cli.plan_input import read_plan_input
|
|
19
|
+
from fdai_deployment_cli.private_output import write_private_bytes
|
|
20
|
+
from fdai_deployment_cli.source_snapshot import verify_source_snapshot
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def prepare_source_foundation_plan(
|
|
24
|
+
*,
|
|
25
|
+
snapshot: Path,
|
|
26
|
+
snapshot_digest: str,
|
|
27
|
+
terraform: Path,
|
|
28
|
+
profile: ProvisionProfile,
|
|
29
|
+
variables_file: Path,
|
|
30
|
+
work_dir: Path,
|
|
31
|
+
environment_builder: Callable[..., dict[str, str]],
|
|
32
|
+
) -> dict[str, object]:
|
|
33
|
+
"""Save one exact source plan after input, target and image verification.
|
|
34
|
+
|
|
35
|
+
The caller validates the active human target and creates an empty private work
|
|
36
|
+
directory. Provider installation is connected and lockfile-readonly; no source,
|
|
37
|
+
Terraform binary, provider lock or approval is selected from a release fallback.
|
|
38
|
+
"""
|
|
39
|
+
if (
|
|
40
|
+
profile.environment != "dev"
|
|
41
|
+
or profile.connectivity != "online"
|
|
42
|
+
or profile.transport != "manual"
|
|
43
|
+
):
|
|
44
|
+
raise ValueError("source Foundation requires connected manual development")
|
|
45
|
+
source = verify_source_snapshot(snapshot, expected_digest=snapshot_digest)
|
|
46
|
+
root = snapshot / "tree"
|
|
47
|
+
infra = root / "infra/genesis-foundation"
|
|
48
|
+
lock = infra / ".terraform.lock.hcl"
|
|
49
|
+
toolchain = load_json_object(
|
|
50
|
+
(root / "infra/genesis-runner-image/toolchain.json").read_bytes(), label="source toolchain"
|
|
51
|
+
)
|
|
52
|
+
tool_digest = toolchain.get("terraform_binary_sha256")
|
|
53
|
+
executable = _copy_terraform(terraform, work_dir / "terraform", expected_digest=tool_digest)
|
|
54
|
+
config = work_dir / "source.tfrc"
|
|
55
|
+
write_private_bytes(
|
|
56
|
+
config, b'provider_installation { direct { include = ["registry.terraform.io/*/*"] } }\n'
|
|
57
|
+
)
|
|
58
|
+
normalized = work_dir / "variables.json"
|
|
59
|
+
binding = snapshot_foundation_input(
|
|
60
|
+
variables_file,
|
|
61
|
+
normalized,
|
|
62
|
+
expected_target_binding=profile.target_binding,
|
|
63
|
+
expected_region=profile.region,
|
|
64
|
+
expected_environment=profile.environment,
|
|
65
|
+
)
|
|
66
|
+
try:
|
|
67
|
+
values = read_plan_input(normalized)
|
|
68
|
+
if values["source_commit"] != source.get("source_commit"):
|
|
69
|
+
raise ValueError("source Foundation variables do not match the selected source")
|
|
70
|
+
if profile.access_method == "bastion" and values.get("enable_bastion") is not True:
|
|
71
|
+
raise ValueError("source Foundation requires its selected Bastion path")
|
|
72
|
+
import shutil
|
|
73
|
+
|
|
74
|
+
environment = environment_builder(
|
|
75
|
+
work_dir=work_dir,
|
|
76
|
+
config=config,
|
|
77
|
+
source=os.environ,
|
|
78
|
+
subscription_id=binding.subscription_id,
|
|
79
|
+
tenant_id=binding.tenant_id,
|
|
80
|
+
azure_cli_path=Path(azure_cli) if (azure_cli := shutil.which("az")) else None,
|
|
81
|
+
)
|
|
82
|
+
image_observation = verify_foundation_runner_image(values, expected_region=profile.region)
|
|
83
|
+
provider_lock = lock.read_bytes()
|
|
84
|
+
context = source_foundation_plan_context(
|
|
85
|
+
profile=profile,
|
|
86
|
+
variables=values,
|
|
87
|
+
source_input_digest=canonical_digest(source),
|
|
88
|
+
source_snapshot_digest=snapshot_digest,
|
|
89
|
+
terraform_digest=str(tool_digest),
|
|
90
|
+
provider_lock=provider_lock,
|
|
91
|
+
runner_image_observation_digest=image_observation,
|
|
92
|
+
)
|
|
93
|
+
for arguments in (
|
|
94
|
+
("init", "-backend=false", "-input=false", "-lockfile=readonly"),
|
|
95
|
+
("validate", "-no-color"),
|
|
96
|
+
):
|
|
97
|
+
_run((str(executable), *arguments), infra, environment)
|
|
98
|
+
with TemporaryDirectory(prefix="source-plan-", dir=work_dir) as temporary:
|
|
99
|
+
plan = Path(temporary) / "plan.tfplan"
|
|
100
|
+
_run(
|
|
101
|
+
(
|
|
102
|
+
str(executable),
|
|
103
|
+
"plan",
|
|
104
|
+
"-input=false",
|
|
105
|
+
"-no-color",
|
|
106
|
+
f"-var-file={normalized}",
|
|
107
|
+
f"-out={plan}",
|
|
108
|
+
),
|
|
109
|
+
infra,
|
|
110
|
+
environment,
|
|
111
|
+
)
|
|
112
|
+
if lock.read_bytes() != provider_lock:
|
|
113
|
+
raise ValueError("source Foundation provider lock changed")
|
|
114
|
+
verify_source_snapshot(snapshot, expected_digest=snapshot_digest)
|
|
115
|
+
if hashlib.sha256(executable.read_bytes()).hexdigest() != tool_digest:
|
|
116
|
+
raise ValueError("source Foundation Terraform changed during planning")
|
|
117
|
+
review = save_foundation_plan(
|
|
118
|
+
plan=plan,
|
|
119
|
+
destination=work_dir,
|
|
120
|
+
terraform=executable,
|
|
121
|
+
root=infra,
|
|
122
|
+
environment=environment,
|
|
123
|
+
context=context,
|
|
124
|
+
variables=values,
|
|
125
|
+
)
|
|
126
|
+
return {
|
|
127
|
+
"schema_version": "fdai.provision-source-plan.v1",
|
|
128
|
+
"stage": "foundation",
|
|
129
|
+
"state": "review",
|
|
130
|
+
"saved_plan": review,
|
|
131
|
+
"source_snapshot_digest": snapshot_digest,
|
|
132
|
+
"provenance": "operator-selected-source",
|
|
133
|
+
"release_signature_verified": False,
|
|
134
|
+
"apply_authorized": False,
|
|
135
|
+
"mutation_performed": False,
|
|
136
|
+
"deployment_ready": False,
|
|
137
|
+
"subscription_ready": False,
|
|
138
|
+
}
|
|
139
|
+
finally:
|
|
140
|
+
normalized.unlink(missing_ok=True)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _copy_terraform(source: Path, destination: Path, *, expected_digest: Any) -> Path:
|
|
144
|
+
descriptor = os.open(source, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
|
|
145
|
+
with os.fdopen(descriptor, "rb") as stream:
|
|
146
|
+
details = os.fstat(stream.fileno())
|
|
147
|
+
if (
|
|
148
|
+
not stat.S_ISREG(details.st_mode)
|
|
149
|
+
or details.st_mode & 0o022
|
|
150
|
+
or details.st_uid not in {0, os.geteuid()}
|
|
151
|
+
or not 0 < details.st_size <= 128 * 1024 * 1024
|
|
152
|
+
):
|
|
153
|
+
raise ValueError("source Terraform must be an owned bounded non-writable executable")
|
|
154
|
+
data = stream.read(128 * 1024 * 1024 + 1)
|
|
155
|
+
if len(data) != details.st_size or hashlib.sha256(data).hexdigest() != expected_digest:
|
|
156
|
+
raise ValueError("source Terraform does not match the committed toolchain digest")
|
|
157
|
+
write_private_bytes(destination, data)
|
|
158
|
+
destination.chmod(0o700)
|
|
159
|
+
return destination
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _run(command: tuple[str, ...], root: Path, environment: dict[str, str]) -> None:
|
|
163
|
+
result = subprocess.run(
|
|
164
|
+
command,
|
|
165
|
+
cwd=root,
|
|
166
|
+
env=environment,
|
|
167
|
+
stdin=subprocess.DEVNULL,
|
|
168
|
+
capture_output=True,
|
|
169
|
+
check=False,
|
|
170
|
+
timeout=900,
|
|
171
|
+
umask=0o077,
|
|
172
|
+
)
|
|
173
|
+
if result.returncode != 0:
|
|
174
|
+
raise ValueError(f"source Foundation {command[1]} failed; preserve private plan evidence")
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Acquire the pinned Kubernetes client tools on a source-deployment managed host.
|
|
2
|
+
|
|
3
|
+
A signed kit ships ``kubectl`` and ``kubelogin`` in its ``bin`` directory. A source deployment
|
|
4
|
+
downloads the same pinned releases on the connected managed host and keeps them only when the
|
|
5
|
+
digests committed here match. A consistency test keeps these pins equal to the kit build script.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import http.client
|
|
12
|
+
import io
|
|
13
|
+
import json
|
|
14
|
+
import platform
|
|
15
|
+
import stat
|
|
16
|
+
import urllib.error
|
|
17
|
+
import urllib.parse
|
|
18
|
+
import urllib.request
|
|
19
|
+
import zipfile
|
|
20
|
+
from pathlib import Path, PurePosixPath
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from fdai_deployment_cli.deployment_deadline import DeploymentDeadline
|
|
24
|
+
from fdai_deployment_cli.private_output import (
|
|
25
|
+
read_private_bytes,
|
|
26
|
+
write_private_bytes,
|
|
27
|
+
write_private_output,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
KUBECTL_VERSION = "1.31.14"
|
|
31
|
+
KUBECTL_SHA256 = "8791ec7c8966b61420d55103a5fb948de9f0ca3d7306d789734975ad9704bdb0"
|
|
32
|
+
KUBELOGIN_VERSION = "0.2.19"
|
|
33
|
+
KUBELOGIN_ARCHIVE_SHA256 = "ebaeff02aa899c5cae6a2b954b64fc02738185319df2570f7dc053451efa4b2f"
|
|
34
|
+
_KUBECTL_URL = f"https://dl.k8s.io/release/v{KUBECTL_VERSION}/bin/linux/amd64/kubectl"
|
|
35
|
+
_KUBELOGIN_URL = (
|
|
36
|
+
"https://github.com/Azure/kubelogin/releases/download/"
|
|
37
|
+
f"v{KUBELOGIN_VERSION}/kubelogin-linux-amd64.zip"
|
|
38
|
+
)
|
|
39
|
+
_MAX_BYTES = 256 * 1024 * 1024
|
|
40
|
+
_RECEIPT = "kubernetes-tools.json"
|
|
41
|
+
_SCHEMA = "fdai.source-kubernetes-tools.v1"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class _HttpsOnlyRedirect(urllib.request.HTTPRedirectHandler):
|
|
45
|
+
max_redirections = 3
|
|
46
|
+
|
|
47
|
+
def redirect_request(
|
|
48
|
+
self, request: Any, fp: Any, code: int, msg: str, headers: Any, newurl: str
|
|
49
|
+
) -> Any:
|
|
50
|
+
if urllib.parse.urlsplit(newurl).scheme != "https":
|
|
51
|
+
raise ValueError("Kubernetes tool downloads accept HTTPS redirects only")
|
|
52
|
+
return super().redirect_request(request, fp, code, msg, headers, newurl)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def install_kubernetes_tools(directory: Path, *, timeout_seconds: int = 600) -> Path:
|
|
56
|
+
"""Return a private directory holding verified ``kubectl`` and ``kubelogin``.
|
|
57
|
+
|
|
58
|
+
The release hosts are not trusted: each download is kept only when its committed digest
|
|
59
|
+
matches. A rerun reuses the tools when their recorded binary digests still match.
|
|
60
|
+
"""
|
|
61
|
+
if platform.system() != "Linux" or platform.machine() not in {"x86_64", "AMD64"}:
|
|
62
|
+
raise ValueError("source Kubernetes tool pins cover linux_amd64 only")
|
|
63
|
+
directory.mkdir(mode=0o700, exist_ok=True)
|
|
64
|
+
details = directory.lstat()
|
|
65
|
+
if not stat.S_ISDIR(details.st_mode) or stat.S_IMODE(details.st_mode) != 0o700:
|
|
66
|
+
raise ValueError("source Kubernetes tool directory must be a private directory")
|
|
67
|
+
receipt_path = directory / _RECEIPT
|
|
68
|
+
if receipt_path.exists() and _retained_tools_match(directory, receipt_path):
|
|
69
|
+
return directory
|
|
70
|
+
deadline = DeploymentDeadline(timeout_seconds)
|
|
71
|
+
kubectl = _download(_KUBECTL_URL, deadline)
|
|
72
|
+
if hashlib.sha256(kubectl).hexdigest() != KUBECTL_SHA256:
|
|
73
|
+
raise ValueError("downloaded kubectl does not match the committed digest")
|
|
74
|
+
archive = _download(_KUBELOGIN_URL, deadline)
|
|
75
|
+
if hashlib.sha256(archive).hexdigest() != KUBELOGIN_ARCHIVE_SHA256:
|
|
76
|
+
raise ValueError("downloaded kubelogin archive does not match the committed digest")
|
|
77
|
+
kubelogin = _kubelogin_binary(archive)
|
|
78
|
+
binaries = {"kubectl": kubectl, "kubelogin": kubelogin}
|
|
79
|
+
for name, content in binaries.items():
|
|
80
|
+
path = directory / name
|
|
81
|
+
path.unlink(missing_ok=True)
|
|
82
|
+
write_private_bytes(path, content)
|
|
83
|
+
path.chmod(0o700)
|
|
84
|
+
receipt_path.unlink(missing_ok=True)
|
|
85
|
+
write_private_output(
|
|
86
|
+
receipt_path,
|
|
87
|
+
json.dumps(
|
|
88
|
+
{
|
|
89
|
+
"schema_version": _SCHEMA,
|
|
90
|
+
"kubectl_version": KUBECTL_VERSION,
|
|
91
|
+
"kubelogin_version": KUBELOGIN_VERSION,
|
|
92
|
+
"binary_sha256": {
|
|
93
|
+
name: hashlib.sha256(content).hexdigest()
|
|
94
|
+
for name, content in sorted(binaries.items())
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
sort_keys=True,
|
|
98
|
+
)
|
|
99
|
+
+ "\n",
|
|
100
|
+
)
|
|
101
|
+
return directory
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _retained_tools_match(directory: Path, receipt_path: Path) -> bool:
|
|
105
|
+
try:
|
|
106
|
+
receipt = json.loads(read_private_bytes(receipt_path, max_bytes=4096))
|
|
107
|
+
digests = receipt["binary_sha256"]
|
|
108
|
+
return (
|
|
109
|
+
receipt.get("schema_version") == _SCHEMA
|
|
110
|
+
and receipt.get("kubectl_version") == KUBECTL_VERSION
|
|
111
|
+
and receipt.get("kubelogin_version") == KUBELOGIN_VERSION
|
|
112
|
+
and digests.get("kubectl") == KUBECTL_SHA256
|
|
113
|
+
and all(
|
|
114
|
+
_binary_digest(directory / name) == digests.get(name)
|
|
115
|
+
for name in ("kubectl", "kubelogin")
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
except (OSError, ValueError, KeyError, TypeError, AttributeError):
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _binary_digest(path: Path) -> str:
|
|
123
|
+
details = path.lstat()
|
|
124
|
+
if (
|
|
125
|
+
not stat.S_ISREG(details.st_mode)
|
|
126
|
+
or stat.S_IMODE(details.st_mode) != 0o700
|
|
127
|
+
or not 0 < details.st_size <= _MAX_BYTES
|
|
128
|
+
):
|
|
129
|
+
raise ValueError("retained Kubernetes tool is not a private executable")
|
|
130
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _download(url: str, deadline: DeploymentDeadline) -> bytes:
|
|
134
|
+
opener = urllib.request.build_opener(_HttpsOnlyRedirect())
|
|
135
|
+
chunks = bytearray()
|
|
136
|
+
try:
|
|
137
|
+
with opener.open(url, timeout=deadline.remaining(60)) as response:
|
|
138
|
+
while len(chunks) <= _MAX_BYTES:
|
|
139
|
+
deadline.remaining()
|
|
140
|
+
chunk = response.read1(min(1024 * 1024, _MAX_BYTES + 1 - len(chunks)))
|
|
141
|
+
if not chunk:
|
|
142
|
+
break
|
|
143
|
+
chunks.extend(chunk)
|
|
144
|
+
except (OSError, urllib.error.URLError, http.client.HTTPException):
|
|
145
|
+
raise ValueError("pinned Kubernetes tool download is unavailable") from None
|
|
146
|
+
if len(chunks) > _MAX_BYTES:
|
|
147
|
+
raise ValueError("pinned Kubernetes tool download exceeds its bound")
|
|
148
|
+
return bytes(chunks)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _kubelogin_binary(archive: bytes) -> bytes:
|
|
152
|
+
try:
|
|
153
|
+
with zipfile.ZipFile(io.BytesIO(archive)) as bundle:
|
|
154
|
+
members = [item for item in bundle.infolist() if not item.is_dir()]
|
|
155
|
+
if (
|
|
156
|
+
len(members) != 1
|
|
157
|
+
or PurePosixPath(members[0].filename).name != "kubelogin"
|
|
158
|
+
or not 0 < members[0].file_size <= _MAX_BYTES
|
|
159
|
+
):
|
|
160
|
+
raise ValueError("kubelogin archive member set is invalid")
|
|
161
|
+
return bundle.read(members[0])
|
|
162
|
+
except zipfile.BadZipFile:
|
|
163
|
+
raise ValueError("kubelogin archive is invalid") from None
|