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.
Files changed (203) hide show
  1. fdai_deployment_cli/__about__.py +3 -0
  2. fdai_deployment_cli/__init__.py +5 -0
  3. fdai_deployment_cli/__main__.py +5 -0
  4. fdai_deployment_cli/acr_publish.py +585 -0
  5. fdai_deployment_cli/aks_catalog_review.py +137 -0
  6. fdai_deployment_cli/aks_historical_reconciliation.py +732 -0
  7. fdai_deployment_cli/aks_job_environment.py +59 -0
  8. fdai_deployment_cli/aks_job_execution.py +794 -0
  9. fdai_deployment_cli/aks_kubernetes_defaults.py +66 -0
  10. fdai_deployment_cli/aks_preflight.py +422 -0
  11. fdai_deployment_cli/aks_readiness.py +220 -0
  12. fdai_deployment_cli/aks_service_update.py +180 -0
  13. fdai_deployment_cli/aks_workload_jobs.py +281 -0
  14. fdai_deployment_cli/application_state_adoption.py +347 -0
  15. fdai_deployment_cli/azure_actor.py +61 -0
  16. fdai_deployment_cli/azure_naming.py +252 -0
  17. fdai_deployment_cli/azure_retail_prices.py +173 -0
  18. fdai_deployment_cli/bootstrap_approval.py +99 -0
  19. fdai_deployment_cli/bootstrap_channel.py +321 -0
  20. fdai_deployment_cli/bootstrap_executor.py +311 -0
  21. fdai_deployment_cli/bootstrap_plan.py +237 -0
  22. fdai_deployment_cli/bootstrap_reconcile.py +402 -0
  23. fdai_deployment_cli/bootstrap_support.py +138 -0
  24. fdai_deployment_cli/bootstrap_worker.py +448 -0
  25. fdai_deployment_cli/bundle.py +352 -0
  26. fdai_deployment_cli/capacity.py +143 -0
  27. fdai_deployment_cli/catalog_review_profile.py +306 -0
  28. fdai_deployment_cli/cli.py +798 -0
  29. fdai_deployment_cli/cli_help.py +214 -0
  30. fdai_deployment_cli/cli_parser.py +791 -0
  31. fdai_deployment_cli/cli_plan.py +450 -0
  32. fdai_deployment_cli/compiler.py +99 -0
  33. fdai_deployment_cli/console_artifact.py +218 -0
  34. fdai_deployment_cli/console_config.py +214 -0
  35. fdai_deployment_cli/console_update.py +832 -0
  36. fdai_deployment_cli/contracts.py +307 -0
  37. fdai_deployment_cli/control_package.py +140 -0
  38. fdai_deployment_cli/deadline_transport.py +70 -0
  39. fdai_deployment_cli/deployment_cost.py +131 -0
  40. fdai_deployment_cli/deployment_deadline.py +28 -0
  41. fdai_deployment_cli/deployment_kit.py +790 -0
  42. fdai_deployment_cli/deployment_kit_cache.py +191 -0
  43. fdai_deployment_cli/deployment_progress.py +648 -0
  44. fdai_deployment_cli/development_authority_cli.py +56 -0
  45. fdai_deployment_cli/development_authority_profile.py +319 -0
  46. fdai_deployment_cli/doctor.py +136 -0
  47. fdai_deployment_cli/entitlement_preflight.py +68 -0
  48. fdai_deployment_cli/entitlement_transport.py +49 -0
  49. fdai_deployment_cli/entra_bootstrap.py +579 -0
  50. fdai_deployment_cli/entra_control_profile.py +214 -0
  51. fdai_deployment_cli/entra_graph.py +349 -0
  52. fdai_deployment_cli/entra_profiles.py +215 -0
  53. fdai_deployment_cli/entra_source.py +244 -0
  54. fdai_deployment_cli/execution_bundle.py +478 -0
  55. fdai_deployment_cli/execution_copies.py +63 -0
  56. fdai_deployment_cli/foundation_adoption.py +438 -0
  57. fdai_deployment_cli/foundation_adoption_evidence.py +245 -0
  58. fdai_deployment_cli/foundation_adoption_host.py +157 -0
  59. fdai_deployment_cli/foundation_adoption_transport.py +51 -0
  60. fdai_deployment_cli/foundation_failure.py +72 -0
  61. fdai_deployment_cli/foundation_image.py +168 -0
  62. fdai_deployment_cli/foundation_input.py +235 -0
  63. fdai_deployment_cli/foundation_output.py +154 -0
  64. fdai_deployment_cli/foundation_plan.py +381 -0
  65. fdai_deployment_cli/foundation_process.py +64 -0
  66. fdai_deployment_cli/foundation_progress.py +231 -0
  67. fdai_deployment_cli/github_actions.py +171 -0
  68. fdai_deployment_cli/github_apply_artifact.py +259 -0
  69. fdai_deployment_cli/github_artifact_io.py +48 -0
  70. fdai_deployment_cli/github_dispatch.py +248 -0
  71. fdai_deployment_cli/github_plan_artifact.py +181 -0
  72. fdai_deployment_cli/github_provider_artifact.py +170 -0
  73. fdai_deployment_cli/github_transport.py +46 -0
  74. fdai_deployment_cli/github_workflow_values.py +312 -0
  75. fdai_deployment_cli/hub_image_deploy.py +354 -0
  76. fdai_deployment_cli/hub_image_stage.py +162 -0
  77. fdai_deployment_cli/hub_image_trust.py +281 -0
  78. fdai_deployment_cli/hub_ownership.py +161 -0
  79. fdai_deployment_cli/hub_workload_handoff.py +264 -0
  80. fdai_deployment_cli/hub_workload_handoff_inputs.py +227 -0
  81. fdai_deployment_cli/identity_profile.py +227 -0
  82. fdai_deployment_cli/image_targets.py +197 -0
  83. fdai_deployment_cli/installation_scope.py +280 -0
  84. fdai_deployment_cli/license.py +311 -0
  85. fdai_deployment_cli/license_issue.py +239 -0
  86. fdai_deployment_cli/lifecycle_authorization.py +252 -0
  87. fdai_deployment_cli/lifecycle_commands.py +173 -0
  88. fdai_deployment_cli/lifecycle_configuration.py +722 -0
  89. fdai_deployment_cli/lifecycle_configuration_artifact.py +45 -0
  90. fdai_deployment_cli/lifecycle_configuration_cli.py +324 -0
  91. fdai_deployment_cli/lifecycle_configuration_document.py +74 -0
  92. fdai_deployment_cli/lifecycle_configuration_entities.py +146 -0
  93. fdai_deployment_cli/lifecycle_configuration_package.py +492 -0
  94. fdai_deployment_cli/lifecycle_configuration_publication.py +277 -0
  95. fdai_deployment_cli/lifecycle_configuration_publication_cli.py +191 -0
  96. fdai_deployment_cli/lifecycle_configuration_sealing.py +127 -0
  97. fdai_deployment_cli/lifecycle_enrollment.py +45 -0
  98. fdai_deployment_cli/lifecycle_infrastructure_agent.py +174 -0
  99. fdai_deployment_cli/lifecycle_installation_binding.py +65 -0
  100. fdai_deployment_cli/lifecycle_native.py +247 -0
  101. fdai_deployment_cli/lifecycle_native_reconciliation.py +568 -0
  102. fdai_deployment_cli/lifecycle_native_transition.py +339 -0
  103. fdai_deployment_cli/lifecycle_ownership_exclusion.py +78 -0
  104. fdai_deployment_cli/lifecycle_plan.py +800 -0
  105. fdai_deployment_cli/lifecycle_promotion.py +185 -0
  106. fdai_deployment_cli/lifecycle_release.py +308 -0
  107. fdai_deployment_cli/lifecycle_self_upgrade.py +202 -0
  108. fdai_deployment_cli/lifecycle_upgrade_bundle.py +176 -0
  109. fdai_deployment_cli/notification_recipients_input.py +222 -0
  110. fdai_deployment_cli/observer_artifact.py +106 -0
  111. fdai_deployment_cli/oci_archive.py +353 -0
  112. fdai_deployment_cli/offline_kit.py +797 -0
  113. fdai_deployment_cli/offline_prepare.py +135 -0
  114. fdai_deployment_cli/operational_evidence_verifier_input.py +575 -0
  115. fdai_deployment_cli/plan_input.py +101 -0
  116. fdai_deployment_cli/private_output.py +192 -0
  117. fdai_deployment_cli/profile.py +70 -0
  118. fdai_deployment_cli/progress.py +183 -0
  119. fdai_deployment_cli/readiness.py +286 -0
  120. fdai_deployment_cli/run_command_receiver.py +418 -0
  121. fdai_deployment_cli/runtime_build.py +386 -0
  122. fdai_deployment_cli/runtime_profile.py +287 -0
  123. fdai_deployment_cli/runtime_release.py +745 -0
  124. fdai_deployment_cli/runtime_stage.py +82 -0
  125. fdai_deployment_cli/runtime_support_installation.py +215 -0
  126. fdai_deployment_cli/simulation.py +80 -0
  127. fdai_deployment_cli/source_application_inputs.py +397 -0
  128. fdai_deployment_cli/source_azure.py +586 -0
  129. fdai_deployment_cli/source_change_cli.py +107 -0
  130. fdai_deployment_cli/source_change_parser.py +75 -0
  131. fdai_deployment_cli/source_change_plan.py +181 -0
  132. fdai_deployment_cli/source_changed_update.py +196 -0
  133. fdai_deployment_cli/source_deploy.py +170 -0
  134. fdai_deployment_cli/source_foundation.py +174 -0
  135. fdai_deployment_cli/source_host_tools.py +163 -0
  136. fdai_deployment_cli/source_image_stage.py +400 -0
  137. fdai_deployment_cli/source_input.py +173 -0
  138. fdai_deployment_cli/source_projection.py +164 -0
  139. fdai_deployment_cli/source_receiver.py +76 -0
  140. fdai_deployment_cli/source_recovery.py +264 -0
  141. fdai_deployment_cli/source_runtime.py +81 -0
  142. fdai_deployment_cli/source_runtime_support.py +368 -0
  143. fdai_deployment_cli/source_service_update.py +726 -0
  144. fdai_deployment_cli/source_service_update_cli.py +47 -0
  145. fdai_deployment_cli/source_snapshot.py +178 -0
  146. fdai_deployment_cli/source_teardown.py +574 -0
  147. fdai_deployment_cli/source_terraform.py +85 -0
  148. fdai_deployment_cli/source_transport.py +399 -0
  149. fdai_deployment_cli/standalone_aks_inventory.py +558 -0
  150. fdai_deployment_cli/standalone_aks_job_execution.py +769 -0
  151. fdai_deployment_cli/standalone_aks_nodepool_guard.py +141 -0
  152. fdai_deployment_cli/standalone_application.py +977 -0
  153. fdai_deployment_cli/standalone_application_completion.py +151 -0
  154. fdai_deployment_cli/standalone_application_input_recovery.py +107 -0
  155. fdai_deployment_cli/standalone_application_recovery.py +116 -0
  156. fdai_deployment_cli/standalone_approval_input.py +24 -0
  157. fdai_deployment_cli/standalone_catalog_checkpoint.py +122 -0
  158. fdai_deployment_cli/standalone_catalog_review.py +251 -0
  159. fdai_deployment_cli/standalone_checkpoint_failure.py +127 -0
  160. fdai_deployment_cli/standalone_console_publish.py +174 -0
  161. fdai_deployment_cli/standalone_database_principals.py +41 -0
  162. fdai_deployment_cli/standalone_deploy.py +638 -0
  163. fdai_deployment_cli/standalone_development_authority.py +166 -0
  164. fdai_deployment_cli/standalone_foundation_adoption.py +87 -0
  165. fdai_deployment_cli/standalone_foundation_transition.py +767 -0
  166. fdai_deployment_cli/standalone_host.py +4608 -0
  167. fdai_deployment_cli/standalone_host_rebind.py +226 -0
  168. fdai_deployment_cli/standalone_host_residual.py +104 -0
  169. fdai_deployment_cli/standalone_host_state.py +145 -0
  170. fdai_deployment_cli/standalone_host_values.py +563 -0
  171. fdai_deployment_cli/standalone_kubernetes_tools.py +100 -0
  172. fdai_deployment_cli/standalone_license_installation.py +177 -0
  173. fdai_deployment_cli/standalone_management_egress.py +83 -0
  174. fdai_deployment_cli/standalone_migration_evidence.py +38 -0
  175. fdai_deployment_cli/standalone_operational_evidence.py +356 -0
  176. fdai_deployment_cli/standalone_planned_outputs.py +147 -0
  177. fdai_deployment_cli/standalone_private_access_probe.py +100 -0
  178. fdai_deployment_cli/standalone_product_profile.py +110 -0
  179. fdai_deployment_cli/standalone_remote_prepare.py +442 -0
  180. fdai_deployment_cli/standalone_residual_apply.py +540 -0
  181. fdai_deployment_cli/standalone_residual_successor.py +431 -0
  182. fdai_deployment_cli/standalone_review.py +266 -0
  183. fdai_deployment_cli/standalone_service_update_finalization.py +116 -0
  184. fdai_deployment_cli/standalone_service_update_recovery.py +277 -0
  185. fdai_deployment_cli/standalone_stage_targets.py +325 -0
  186. fdai_deployment_cli/standalone_status.py +67 -0
  187. fdai_deployment_cli/standalone_terraform_environment.py +237 -0
  188. fdai_deployment_cli/standalone_transfer_cleanup.py +237 -0
  189. fdai_deployment_cli/standalone_trial_activation.py +193 -0
  190. fdai_deployment_cli/state.py +434 -0
  191. fdai_deployment_cli/state_handoff.py +253 -0
  192. fdai_deployment_cli/status_projection.py +97 -0
  193. fdai_deployment_cli/support_install.py +257 -0
  194. fdai_deployment_cli/target.py +46 -0
  195. fdai_deployment_cli/trust/capabilities.json +62 -0
  196. fdai_deployment_cli/trust/deployment-bundle-root.pub +3 -0
  197. fdai_deployment_cli/trust/deployment-release-root.pub +3 -0
  198. fdai_deployment_cli/trust/upstream-signing-key.pub +3 -0
  199. fdai_deployment_cli/trust_roots.py +50 -0
  200. fdai_deployment_cli-0.1.2.dist-info/METADATA +112 -0
  201. fdai_deployment_cli-0.1.2.dist-info/RECORD +203 -0
  202. fdai_deployment_cli-0.1.2.dist-info/WHEEL +4 -0
  203. 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
+ ]