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,306 @@
|
|
|
1
|
+
"""Private, content-addressed standalone catalog-review deployment profile."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from fdai_deployment_cli.contracts import canonical_digest
|
|
13
|
+
from fdai_deployment_cli.private_output import read_private_bytes
|
|
14
|
+
from fdai_deployment_cli.standalone_host_state import replace_or_verify_private_json
|
|
15
|
+
|
|
16
|
+
_SCHEMA = "fdai.catalog-review-deployment-profile.v1"
|
|
17
|
+
_STAGED_SCHEMA = "fdai.catalog-review-staged-profile.v1"
|
|
18
|
+
_NAME = re.compile(r"^[A-Za-z0-9](?:[A-Za-z0-9_.-]{0,99})$")
|
|
19
|
+
_BRANCH = re.compile(r"^[A-Za-z0-9](?:[A-Za-z0-9._/-]{0,254})$")
|
|
20
|
+
_MAX_PROFILE_BYTES = 64 * 1024
|
|
21
|
+
_MAX_PRIVATE_KEY_BYTES = 64 * 1024
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class CatalogReviewDeploymentProfile:
|
|
26
|
+
"""Reviewed selection plus a private GitHub App key file reference."""
|
|
27
|
+
|
|
28
|
+
selected: bool
|
|
29
|
+
owner: str = ""
|
|
30
|
+
repo: str = ""
|
|
31
|
+
default_branch: str = ""
|
|
32
|
+
app_client_id: str = ""
|
|
33
|
+
app_installation_id: str = ""
|
|
34
|
+
private_key_path: Path | None = None
|
|
35
|
+
private_key_sha256: str = ""
|
|
36
|
+
profile_digest: str = ""
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def unselected(cls) -> CatalogReviewDeploymentProfile:
|
|
40
|
+
material = {"schema_version": _SCHEMA, "selected": False}
|
|
41
|
+
return cls(selected=False, profile_digest=canonical_digest(material))
|
|
42
|
+
|
|
43
|
+
def staged_mapping(self) -> dict[str, object]:
|
|
44
|
+
material = self._material(schema_version=_STAGED_SCHEMA)
|
|
45
|
+
return {**material, "profile_digest": self.profile_digest}
|
|
46
|
+
|
|
47
|
+
def private_key_text(self) -> str:
|
|
48
|
+
if not self.selected or self.private_key_path is None:
|
|
49
|
+
raise ValueError("selected catalog review profile has no private key reference")
|
|
50
|
+
content = read_private_bytes(
|
|
51
|
+
self.private_key_path,
|
|
52
|
+
max_bytes=_MAX_PRIVATE_KEY_BYTES,
|
|
53
|
+
)
|
|
54
|
+
if hashlib.sha256(content).hexdigest() != self.private_key_sha256:
|
|
55
|
+
raise ValueError("catalog review GitHub App private key changed after profile review")
|
|
56
|
+
try:
|
|
57
|
+
return content.decode("ascii")
|
|
58
|
+
except UnicodeDecodeError as exc:
|
|
59
|
+
raise ValueError("catalog review GitHub App private key MUST be ASCII PEM") from exc
|
|
60
|
+
|
|
61
|
+
def _material(self, *, schema_version: str) -> dict[str, object]:
|
|
62
|
+
if not self.selected:
|
|
63
|
+
return {"schema_version": schema_version, "selected": False}
|
|
64
|
+
return {
|
|
65
|
+
"schema_version": schema_version,
|
|
66
|
+
"selected": True,
|
|
67
|
+
"owner": self.owner,
|
|
68
|
+
"repo": self.repo,
|
|
69
|
+
"default_branch": self.default_branch,
|
|
70
|
+
"app_client_id": self.app_client_id,
|
|
71
|
+
"app_installation_id": self.app_installation_id,
|
|
72
|
+
"private_key_sha256": self.private_key_sha256,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def load_catalog_review_profile(path: Path) -> CatalogReviewDeploymentProfile:
|
|
77
|
+
"""Load one operator-supplied private profile and its referenced private key."""
|
|
78
|
+
|
|
79
|
+
value = _read_json(path, label="catalog review deployment profile")
|
|
80
|
+
if value.get("schema_version") != _SCHEMA or not isinstance(value.get("selected"), bool):
|
|
81
|
+
raise ValueError("catalog review deployment profile schema is invalid")
|
|
82
|
+
if value["selected"] is False:
|
|
83
|
+
if set(value) != {"schema_version", "selected"}:
|
|
84
|
+
raise ValueError("unselected catalog review profile MUST contain no binding")
|
|
85
|
+
return CatalogReviewDeploymentProfile.unselected()
|
|
86
|
+
expected = {
|
|
87
|
+
"schema_version",
|
|
88
|
+
"selected",
|
|
89
|
+
"owner",
|
|
90
|
+
"repo",
|
|
91
|
+
"default_branch",
|
|
92
|
+
"app_client_id",
|
|
93
|
+
"app_installation_id",
|
|
94
|
+
"app_private_key_file",
|
|
95
|
+
}
|
|
96
|
+
if set(value) != expected:
|
|
97
|
+
raise ValueError("selected catalog review deployment profile is incomplete")
|
|
98
|
+
owner = _name(value, "owner")
|
|
99
|
+
repo = _name(value, "repo")
|
|
100
|
+
default_branch = value.get("default_branch")
|
|
101
|
+
client_id = value.get("app_client_id")
|
|
102
|
+
installation_id = value.get("app_installation_id")
|
|
103
|
+
key_reference = value.get("app_private_key_file")
|
|
104
|
+
if (
|
|
105
|
+
not isinstance(default_branch, str)
|
|
106
|
+
or _BRANCH.fullmatch(default_branch) is None
|
|
107
|
+
or not isinstance(client_id, str)
|
|
108
|
+
or not client_id.strip()
|
|
109
|
+
or len(client_id) > 256
|
|
110
|
+
or not isinstance(installation_id, str)
|
|
111
|
+
or re.fullmatch(r"[1-9][0-9]*", installation_id) is None
|
|
112
|
+
or not isinstance(key_reference, str)
|
|
113
|
+
or not key_reference
|
|
114
|
+
):
|
|
115
|
+
raise ValueError("catalog review GitHub App binding is invalid")
|
|
116
|
+
key_path = Path(key_reference)
|
|
117
|
+
if not key_path.is_absolute():
|
|
118
|
+
key_path = path.parent / key_path
|
|
119
|
+
key_content = read_private_bytes(key_path, max_bytes=_MAX_PRIVATE_KEY_BYTES)
|
|
120
|
+
key_digest = hashlib.sha256(key_content).hexdigest()
|
|
121
|
+
material = {
|
|
122
|
+
"schema_version": _SCHEMA,
|
|
123
|
+
"selected": True,
|
|
124
|
+
"owner": owner,
|
|
125
|
+
"repo": repo,
|
|
126
|
+
"default_branch": default_branch,
|
|
127
|
+
"app_client_id": client_id.strip(),
|
|
128
|
+
"app_installation_id": installation_id,
|
|
129
|
+
"private_key_sha256": key_digest,
|
|
130
|
+
}
|
|
131
|
+
return CatalogReviewDeploymentProfile(
|
|
132
|
+
selected=True,
|
|
133
|
+
owner=owner,
|
|
134
|
+
repo=repo,
|
|
135
|
+
default_branch=default_branch,
|
|
136
|
+
app_client_id=client_id.strip(),
|
|
137
|
+
app_installation_id=installation_id,
|
|
138
|
+
private_key_path=key_path,
|
|
139
|
+
private_key_sha256=key_digest,
|
|
140
|
+
profile_digest=canonical_digest(material),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def load_staged_catalog_review_profile(
|
|
145
|
+
path: Path,
|
|
146
|
+
*,
|
|
147
|
+
private_key_path: Path | None,
|
|
148
|
+
) -> CatalogReviewDeploymentProfile:
|
|
149
|
+
"""Verify the remote staged profile and separately transferred key bytes."""
|
|
150
|
+
|
|
151
|
+
value = _read_json(path, label="staged catalog review profile")
|
|
152
|
+
supplied_digest = value.pop("profile_digest", None)
|
|
153
|
+
if value.get("schema_version") != _STAGED_SCHEMA or not isinstance(value.get("selected"), bool):
|
|
154
|
+
raise ValueError("staged catalog review profile schema is invalid")
|
|
155
|
+
if value["selected"] is False:
|
|
156
|
+
if set(value) != {"schema_version", "selected"} or private_key_path is not None:
|
|
157
|
+
raise ValueError("unselected staged catalog review profile has unexpected binding")
|
|
158
|
+
expected = CatalogReviewDeploymentProfile.unselected()
|
|
159
|
+
if supplied_digest != expected.profile_digest:
|
|
160
|
+
raise ValueError("staged catalog review profile digest differs")
|
|
161
|
+
return expected
|
|
162
|
+
expected_fields = {
|
|
163
|
+
"schema_version",
|
|
164
|
+
"selected",
|
|
165
|
+
"owner",
|
|
166
|
+
"repo",
|
|
167
|
+
"default_branch",
|
|
168
|
+
"app_client_id",
|
|
169
|
+
"app_installation_id",
|
|
170
|
+
"private_key_sha256",
|
|
171
|
+
}
|
|
172
|
+
if set(value) != expected_fields or private_key_path is None:
|
|
173
|
+
raise ValueError("selected staged catalog review profile is incomplete")
|
|
174
|
+
private_key_sha256 = str(value.get("private_key_sha256", ""))
|
|
175
|
+
default_branch = value.get("default_branch")
|
|
176
|
+
app_client_id = value.get("app_client_id")
|
|
177
|
+
app_installation_id = value.get("app_installation_id")
|
|
178
|
+
if (
|
|
179
|
+
not isinstance(default_branch, str)
|
|
180
|
+
or _BRANCH.fullmatch(default_branch) is None
|
|
181
|
+
or not isinstance(app_client_id, str)
|
|
182
|
+
or not app_client_id.strip()
|
|
183
|
+
or len(app_client_id) > 256
|
|
184
|
+
or not isinstance(app_installation_id, str)
|
|
185
|
+
or re.fullmatch(r"[1-9][0-9]*", app_installation_id) is None
|
|
186
|
+
or re.fullmatch(r"[0-9a-f]{64}", private_key_sha256) is None
|
|
187
|
+
):
|
|
188
|
+
raise ValueError("staged catalog review GitHub App binding is invalid")
|
|
189
|
+
content = read_private_bytes(private_key_path, max_bytes=_MAX_PRIVATE_KEY_BYTES)
|
|
190
|
+
if hashlib.sha256(content).hexdigest() != private_key_sha256:
|
|
191
|
+
raise ValueError("staged catalog review GitHub App private key digest differs")
|
|
192
|
+
material = {
|
|
193
|
+
"schema_version": _SCHEMA,
|
|
194
|
+
"selected": True,
|
|
195
|
+
"owner": _name(value, "owner"),
|
|
196
|
+
"repo": _name(value, "repo"),
|
|
197
|
+
"default_branch": default_branch,
|
|
198
|
+
"app_client_id": app_client_id.strip(),
|
|
199
|
+
"app_installation_id": app_installation_id,
|
|
200
|
+
"private_key_sha256": private_key_sha256,
|
|
201
|
+
}
|
|
202
|
+
if canonical_digest(material) != supplied_digest:
|
|
203
|
+
raise ValueError("staged catalog review profile digest differs")
|
|
204
|
+
return CatalogReviewDeploymentProfile(
|
|
205
|
+
selected=True,
|
|
206
|
+
owner=str(material["owner"]),
|
|
207
|
+
repo=str(material["repo"]),
|
|
208
|
+
default_branch=str(material["default_branch"]),
|
|
209
|
+
app_client_id=str(material["app_client_id"]),
|
|
210
|
+
app_installation_id=str(material["app_installation_id"]),
|
|
211
|
+
private_key_path=private_key_path,
|
|
212
|
+
private_key_sha256=private_key_sha256,
|
|
213
|
+
profile_digest=str(supplied_digest),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def staged_catalog_review_profile_from_paths(
|
|
218
|
+
profile_path: Path | None,
|
|
219
|
+
private_key_path: Path | None,
|
|
220
|
+
) -> CatalogReviewDeploymentProfile:
|
|
221
|
+
"""Resolve optional staged paths into an explicit selected or skipped profile."""
|
|
222
|
+
|
|
223
|
+
if profile_path is None:
|
|
224
|
+
if private_key_path is not None:
|
|
225
|
+
raise ValueError("catalog review private key requires its reviewed profile")
|
|
226
|
+
return CatalogReviewDeploymentProfile.unselected()
|
|
227
|
+
return load_staged_catalog_review_profile(
|
|
228
|
+
profile_path,
|
|
229
|
+
private_key_path=private_key_path,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def catalog_review_terraform_values(
|
|
234
|
+
profile: CatalogReviewDeploymentProfile,
|
|
235
|
+
) -> dict[str, object]:
|
|
236
|
+
"""Return private Terraform inputs without placing values in process arguments."""
|
|
237
|
+
|
|
238
|
+
values: dict[str, object] = {"enable_catalog_review": profile.selected}
|
|
239
|
+
if profile.selected:
|
|
240
|
+
values.update(
|
|
241
|
+
gitops_owner=profile.owner,
|
|
242
|
+
gitops_repo=profile.repo,
|
|
243
|
+
catalog_review_default_branch=profile.default_branch,
|
|
244
|
+
github_app_client_id=profile.app_client_id,
|
|
245
|
+
github_app_installation_id=profile.app_installation_id,
|
|
246
|
+
github_app_private_key=profile.private_key_text(),
|
|
247
|
+
)
|
|
248
|
+
return values
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def stage_catalog_review_profile(
|
|
252
|
+
profile: CatalogReviewDeploymentProfile,
|
|
253
|
+
*,
|
|
254
|
+
tunnel: Any,
|
|
255
|
+
prepared_root: Path,
|
|
256
|
+
remote_root: str,
|
|
257
|
+
) -> tuple[str, ...]:
|
|
258
|
+
"""Copy private profile records and return value-free prepare arguments."""
|
|
259
|
+
|
|
260
|
+
local_profile = prepared_root / "catalog-review-profile.json"
|
|
261
|
+
remote_profile = f"{remote_root}/catalog-review-profile.json"
|
|
262
|
+
replace_or_verify_private_json(local_profile, profile.staged_mapping())
|
|
263
|
+
remote_key = f"{remote_root}/catalog-review-private-key.pem"
|
|
264
|
+
try:
|
|
265
|
+
tunnel.copy_to(local_profile, remote_profile, timeout=120)
|
|
266
|
+
arguments: tuple[str, ...] = ("--catalog-review-profile", remote_profile)
|
|
267
|
+
if not profile.selected:
|
|
268
|
+
return arguments
|
|
269
|
+
if profile.private_key_path is None:
|
|
270
|
+
raise ValueError("selected catalog review profile has no private key reference")
|
|
271
|
+
tunnel.copy_to(profile.private_key_path, remote_key, timeout=120)
|
|
272
|
+
return (*arguments, "--catalog-review-private-key", remote_key)
|
|
273
|
+
except BaseException:
|
|
274
|
+
tunnel.ssh(
|
|
275
|
+
("rm", "-f", "--", remote_profile, remote_key),
|
|
276
|
+
timeout=60,
|
|
277
|
+
)
|
|
278
|
+
raise
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _read_json(path: Path, *, label: str) -> dict[str, object]:
|
|
282
|
+
content = read_private_bytes(path, max_bytes=_MAX_PROFILE_BYTES)
|
|
283
|
+
try:
|
|
284
|
+
value = json.loads(content)
|
|
285
|
+
except json.JSONDecodeError as exc:
|
|
286
|
+
raise ValueError(f"{label} is invalid JSON") from exc
|
|
287
|
+
if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
|
|
288
|
+
raise ValueError(f"{label} MUST be an object")
|
|
289
|
+
return {str(key): item for key, item in value.items()}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _name(value: dict[str, object], field: str) -> str:
|
|
293
|
+
item = value.get(field)
|
|
294
|
+
if not isinstance(item, str) or _NAME.fullmatch(item) is None:
|
|
295
|
+
raise ValueError(f"catalog review GitOps {field} is invalid")
|
|
296
|
+
return item
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
__all__ = [
|
|
300
|
+
"CatalogReviewDeploymentProfile",
|
|
301
|
+
"catalog_review_terraform_values",
|
|
302
|
+
"load_catalog_review_profile",
|
|
303
|
+
"load_staged_catalog_review_profile",
|
|
304
|
+
"staged_catalog_review_profile_from_paths",
|
|
305
|
+
"stage_catalog_review_profile",
|
|
306
|
+
]
|