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,215 @@
1
+ """Private target and reviewed-control profiles for bounded Entra convergence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import stat
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from fdai_deployment_cli.contracts import canonical_digest, load_json_object
14
+ from fdai_deployment_cli.entra_control_profile import validate_control_profile
15
+
16
+ _GUID = re.compile(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}")
17
+ _DIGEST = re.compile(r"[0-9a-f]{64}")
18
+ _NAME = re.compile(r"[A-Za-z0-9][A-Za-z0-9._ -]{0,127}")
19
+ _MAX_PROFILE_BYTES = 262_144
20
+
21
+
22
+ @dataclass(frozen=True, slots=True, repr=False)
23
+ class EntraTargetProfile:
24
+ """Private exact target and approved executor identity."""
25
+
26
+ environment: str
27
+ target_binding: str
28
+ executor_object_id: str = field(repr=False)
29
+ executor_client_id: str = field(repr=False)
30
+ executor_display_name: str = field(repr=False)
31
+ executor_azure_config_dir: Path = field(repr=False)
32
+ control_profile_digest: str
33
+
34
+ @classmethod
35
+ def from_mapping(cls, value: dict[str, Any]) -> EntraTargetProfile:
36
+ if (
37
+ set(value)
38
+ != {
39
+ "schema_version",
40
+ "environment",
41
+ "target_binding",
42
+ "executor_object_id",
43
+ "executor_client_id",
44
+ "executor_display_name",
45
+ "executor_azure_config_dir",
46
+ "control_profile_digest",
47
+ }
48
+ or value.get("schema_version") != "fdai.entra-target-profile.v2"
49
+ ):
50
+ raise ValueError("Entra target profile fields are invalid")
51
+ environment = _text(value, "environment")
52
+ target_binding = _text(value, "target_binding")
53
+ executor_object_id = _text(value, "executor_object_id")
54
+ executor_client_id = _text(value, "executor_client_id")
55
+ executor_display_name = _text(value, "executor_display_name")
56
+ executor_azure_config_dir = Path(_text(value, "executor_azure_config_dir"))
57
+ control_profile_digest = _text(value, "control_profile_digest")
58
+ if (
59
+ environment not in {"dev", "staging", "prod"}
60
+ or _DIGEST.fullmatch(target_binding) is None
61
+ or _GUID.fullmatch(executor_object_id) is None
62
+ or _GUID.fullmatch(executor_client_id) is None
63
+ or _NAME.fullmatch(executor_display_name) is None
64
+ or not executor_azure_config_dir.is_absolute()
65
+ or ".." in executor_azure_config_dir.parts
66
+ or _DIGEST.fullmatch(control_profile_digest) is None
67
+ ):
68
+ raise ValueError("Entra target profile values are invalid")
69
+ return cls(
70
+ environment=environment,
71
+ target_binding=target_binding,
72
+ executor_object_id=executor_object_id,
73
+ executor_client_id=executor_client_id,
74
+ executor_display_name=executor_display_name,
75
+ executor_azure_config_dir=executor_azure_config_dir,
76
+ control_profile_digest=control_profile_digest,
77
+ )
78
+
79
+ @property
80
+ def digest(self) -> str:
81
+ return canonical_digest(self.to_mapping())
82
+
83
+ def to_mapping(self) -> dict[str, object]:
84
+ return {
85
+ "schema_version": "fdai.entra-target-profile.v2",
86
+ "environment": self.environment,
87
+ "target_binding": self.target_binding,
88
+ "executor_object_id": self.executor_object_id,
89
+ "executor_client_id": self.executor_client_id,
90
+ "executor_display_name": self.executor_display_name,
91
+ "executor_azure_config_dir": str(self.executor_azure_config_dir),
92
+ "control_profile_digest": self.control_profile_digest,
93
+ }
94
+
95
+
96
+ @dataclass(frozen=True, slots=True, repr=False)
97
+ class EntraControlProfile:
98
+ value: dict[str, Any] = field(repr=False)
99
+
100
+ @classmethod
101
+ def from_mapping(cls, value: dict[str, Any]) -> EntraControlProfile:
102
+ normalized = json.loads(json.dumps(value))
103
+ if not isinstance(normalized, dict):
104
+ raise ValueError("Entra control profile is invalid")
105
+ validate_control_profile(normalized)
106
+ return cls(normalized)
107
+
108
+ @property
109
+ def digest(self) -> str:
110
+ return canonical_digest(self.value)
111
+
112
+ @property
113
+ def premium_service_plan(self) -> str:
114
+ return str(self.value["premium_service_plan"])
115
+
116
+ @property
117
+ def role_groups(self) -> dict[str, str]:
118
+ groups = self.value["role_groups"]
119
+ assert isinstance(groups, dict)
120
+ return {str(key): str(item) for key, item in groups.items()}
121
+
122
+ @property
123
+ def conditional_access_policies(self) -> list[dict[str, Any]]:
124
+ return _dict_list(self.value["conditional_access_policies"])
125
+
126
+ @property
127
+ def access_reviews(self) -> list[dict[str, Any]]:
128
+ return _dict_list(self.value["access_reviews"])
129
+
130
+ @property
131
+ def authentication_methods(self) -> list[dict[str, Any]]:
132
+ return _dict_list(self.value["authentication_methods"])
133
+
134
+ @property
135
+ def azure_policy_assignments(self) -> list[dict[str, Any]]:
136
+ return _dict_list(self.value["azure_policy_assignments"])
137
+
138
+
139
+ def load_target_profile(path: Path) -> EntraTargetProfile:
140
+ descriptor = open_private_profile(path)
141
+ try:
142
+ return load_target_profile_descriptor(descriptor)
143
+ finally:
144
+ os.close(descriptor)
145
+
146
+
147
+ def load_control_profile(path: Path) -> EntraControlProfile:
148
+ descriptor = open_private_profile(path)
149
+ try:
150
+ return load_control_profile_descriptor(descriptor)
151
+ finally:
152
+ os.close(descriptor)
153
+
154
+
155
+ def load_target_profile_descriptor(descriptor: int) -> EntraTargetProfile:
156
+ return EntraTargetProfile.from_mapping(_read_private_object(descriptor, "Entra target profile"))
157
+
158
+
159
+ def load_control_profile_descriptor(descriptor: int) -> EntraControlProfile:
160
+ return EntraControlProfile.from_mapping(
161
+ _read_private_object(descriptor, "Entra control profile")
162
+ )
163
+
164
+
165
+ def open_private_profile(path: Path) -> int:
166
+ if not path.is_absolute():
167
+ raise ValueError("Entra profile path MUST be absolute")
168
+ descriptor = os.open(path, os.O_RDONLY | os.O_NONBLOCK | os.O_NOFOLLOW)
169
+ try:
170
+ _validate_private_descriptor(descriptor)
171
+ return descriptor
172
+ except BaseException:
173
+ os.close(descriptor)
174
+ raise
175
+
176
+
177
+ def _read_private_object(descriptor: int, label: str) -> dict[str, Any]:
178
+ _validate_private_descriptor(descriptor)
179
+ os.lseek(descriptor, 0, os.SEEK_SET)
180
+ before = os.fstat(descriptor)
181
+ payload = os.read(descriptor, _MAX_PROFILE_BYTES + 1)
182
+ after = os.fstat(descriptor)
183
+ if (
184
+ not 0 < len(payload) <= _MAX_PROFILE_BYTES
185
+ or len(payload) != before.st_size
186
+ or before.st_size != after.st_size
187
+ or before.st_mtime_ns != after.st_mtime_ns
188
+ or before.st_ctime_ns != after.st_ctime_ns
189
+ ):
190
+ raise ValueError(f"{label} changed or exceeded its bound")
191
+ return load_json_object(payload, label=label, max_bytes=_MAX_PROFILE_BYTES)
192
+
193
+
194
+ def _validate_private_descriptor(descriptor: int) -> None:
195
+ details = os.fstat(descriptor)
196
+ if (
197
+ not stat.S_ISREG(details.st_mode)
198
+ or stat.S_IMODE(details.st_mode) != 0o600
199
+ or details.st_uid != os.geteuid()
200
+ or details.st_nlink != 1
201
+ ):
202
+ raise PermissionError("Entra profile MUST be a current-UID mode-0600 single-link file")
203
+
204
+
205
+ def _dict_list(value: object) -> list[dict[str, Any]]:
206
+ if not isinstance(value, list) or any(not isinstance(item, dict) for item in value):
207
+ raise ValueError("Entra control profile collection is invalid")
208
+ return value
209
+
210
+
211
+ def _text(value: dict[str, Any], key: str) -> str:
212
+ item = value.get(key)
213
+ if not isinstance(item, str):
214
+ raise ValueError(f"Entra profile {key} is invalid")
215
+ return item
@@ -0,0 +1,244 @@
1
+ """Launch bounded Entra convergence from a private tracked-only source snapshot."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import signal
8
+ import stat
9
+ import subprocess
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ from fdai_deployment_cli.contracts import load_json_object
14
+ from fdai_deployment_cli.doctor import azure_active_target_binding
15
+ from fdai_deployment_cli.entra_profiles import (
16
+ load_control_profile,
17
+ load_control_profile_descriptor,
18
+ load_target_profile,
19
+ load_target_profile_descriptor,
20
+ open_private_profile,
21
+ )
22
+ from fdai_deployment_cli.private_output import read_private_bytes, write_private_output
23
+ from fdai_deployment_cli.source_input import SourceDeploymentInput, inspect_source
24
+ from fdai_deployment_cli.source_snapshot import materialize_source, verify_source_snapshot
25
+
26
+ _SAFE_ENVIRONMENT_KEYS = frozenset(
27
+ {
28
+ "AZURE_CONFIG_DIR",
29
+ "CURL_CA_BUNDLE",
30
+ "HOME",
31
+ "LANG",
32
+ "LC_ALL",
33
+ "LC_CTYPE",
34
+ "LOGNAME",
35
+ "PATH",
36
+ "REQUESTS_CA_BUNDLE",
37
+ "SSL_CERT_FILE",
38
+ "TERM",
39
+ "TZ",
40
+ "USER",
41
+ }
42
+ )
43
+ _SNAPSHOT_CONTEXT = "entra-source-context.json"
44
+ _BOOTSTRAP = """
45
+ import os, runpy, sys
46
+ from pathlib import Path
47
+ from fdai_deployment_cli.source_snapshot import verify_source_snapshot
48
+ root = os.environ["FDAI_ENTRA_SNAPSHOT_ROOT"]
49
+ verify_source_snapshot(
50
+ Path(os.environ["FDAI_ENTRA_SNAPSHOT_DIRECTORY"]),
51
+ expected_digest=os.environ["FDAI_ENTRA_SNAPSHOT_DIGEST"],
52
+ )
53
+ sys.path[:0] = [
54
+ root + "/scripts/deployment/azure",
55
+ root + "/packages/deployment-cli/src",
56
+ ]
57
+ runpy.run_path(
58
+ root + "/scripts/deployment/azure/genesis_entra_operation.py",
59
+ run_name="__main__",
60
+ )
61
+ """.strip()
62
+
63
+
64
+ def run_source_entra_operation(
65
+ *,
66
+ source: Path,
67
+ work_dir: Path,
68
+ target_profile_path: Path,
69
+ control_profile_path: Path,
70
+ apply: bool,
71
+ output: str,
72
+ timeout_seconds: int,
73
+ ) -> int:
74
+ """Verify the target before creating state, then execute only snapshotted bytes."""
75
+
76
+ target_profile = load_target_profile(target_profile_path)
77
+ control_profile = load_control_profile(control_profile_path)
78
+ if target_profile.control_profile_digest != control_profile.digest:
79
+ raise ValueError("Entra target profile does not bind the reviewed control profile")
80
+ if apply and target_profile.environment != "dev":
81
+ raise ValueError("Entra apply is supported only for a profile-bound dev target")
82
+ active_binding = azure_active_target_binding()
83
+ if active_binding is None or active_binding != target_profile.target_binding:
84
+ raise ValueError("active Azure target does not match the private Entra target profile")
85
+ if not work_dir.is_absolute():
86
+ raise ValueError("Entra work directory MUST be absolute")
87
+ if output not in {"text", "json"}:
88
+ raise ValueError("Entra output format is invalid")
89
+ if not 30 <= timeout_seconds <= 1800:
90
+ raise ValueError("Entra operation timeout MUST be between 30 and 1800 seconds")
91
+ source_input = inspect_source(source)
92
+ _ensure_private_directory(work_dir)
93
+ snapshot_directory, snapshot_digest = _prepare_snapshot(
94
+ work_dir=work_dir,
95
+ source=source_input,
96
+ target_profile_digest=target_profile.digest,
97
+ control_profile_digest=control_profile.digest,
98
+ )
99
+ snapshot_tree = snapshot_directory / "tree"
100
+ entrypoint = snapshot_tree / "scripts/deployment/azure/genesis_entra_operation.py"
101
+ if not entrypoint.is_file() or entrypoint.is_symlink():
102
+ raise ValueError("verified Entra source snapshot is missing its identity entrypoint")
103
+
104
+ target_descriptor = open_private_profile(target_profile_path)
105
+ control_descriptor = open_private_profile(control_profile_path)
106
+ try:
107
+ if load_target_profile_descriptor(target_descriptor).digest != target_profile.digest:
108
+ raise ValueError("Entra target profile changed after target validation")
109
+ if load_control_profile_descriptor(control_descriptor).digest != control_profile.digest:
110
+ raise ValueError("Entra control profile changed after target validation")
111
+ command = [
112
+ sys.executable,
113
+ "-I",
114
+ "-c",
115
+ _BOOTSTRAP,
116
+ "--work-dir",
117
+ str(work_dir),
118
+ "--output",
119
+ output,
120
+ ]
121
+ if apply:
122
+ command.append("--apply")
123
+ process_environment = {
124
+ key: value for key, value in os.environ.items() if key in _SAFE_ENVIRONMENT_KEYS
125
+ }
126
+ process_environment.update(
127
+ {
128
+ "PYTHONNOUSERSITE": "1",
129
+ "FDAI_ENTRA_TARGET_PROFILE_FD": str(target_descriptor),
130
+ "FDAI_ENTRA_CONTROL_PROFILE_FD": str(control_descriptor),
131
+ "FDAI_ENTRA_SNAPSHOT_DIRECTORY": str(snapshot_directory),
132
+ "FDAI_ENTRA_SNAPSHOT_ROOT": str(snapshot_tree),
133
+ "FDAI_ENTRA_SNAPSHOT_DIGEST": snapshot_digest,
134
+ }
135
+ )
136
+ verify_source_snapshot(snapshot_directory, expected_digest=snapshot_digest)
137
+ return _run_identity_process(
138
+ command,
139
+ cwd=snapshot_tree,
140
+ environment=process_environment,
141
+ timeout_seconds=timeout_seconds,
142
+ pass_fds=(target_descriptor, control_descriptor),
143
+ )
144
+ finally:
145
+ os.close(target_descriptor)
146
+ os.close(control_descriptor)
147
+
148
+
149
+ def _prepare_snapshot(
150
+ *,
151
+ work_dir: Path,
152
+ source: SourceDeploymentInput,
153
+ target_profile_digest: str,
154
+ control_profile_digest: str,
155
+ ) -> tuple[Path, str]:
156
+ snapshot_directory = work_dir / "source-snapshot"
157
+ context_path = work_dir / _SNAPSHOT_CONTEXT
158
+ if context_path.exists():
159
+ context = load_json_object(
160
+ read_private_bytes(context_path, max_bytes=65_536),
161
+ label="Entra source context",
162
+ max_bytes=65_536,
163
+ )
164
+ if (
165
+ set(context)
166
+ != {
167
+ "schema_version",
168
+ "source_digest",
169
+ "snapshot_digest",
170
+ "target_profile_digest",
171
+ "control_profile_digest",
172
+ }
173
+ or context.get("schema_version") != "fdai.entra-source-context.v1"
174
+ or context.get("source_digest") != source.digest
175
+ or context.get("target_profile_digest") != target_profile_digest
176
+ or context.get("control_profile_digest") != control_profile_digest
177
+ or not isinstance(context.get("snapshot_digest"), str)
178
+ ):
179
+ raise ValueError("retained Entra source context differs from current inputs")
180
+ snapshot_digest = str(context["snapshot_digest"])
181
+ verify_source_snapshot(snapshot_directory, expected_digest=snapshot_digest)
182
+ return snapshot_directory, snapshot_digest
183
+ if snapshot_directory.exists() or snapshot_directory.is_symlink():
184
+ raise ValueError("unbound Entra source snapshot already exists")
185
+ snapshot_digest = materialize_source(source, snapshot_directory)
186
+ context = {
187
+ "schema_version": "fdai.entra-source-context.v1",
188
+ "source_digest": source.digest,
189
+ "snapshot_digest": snapshot_digest,
190
+ "target_profile_digest": target_profile_digest,
191
+ "control_profile_digest": control_profile_digest,
192
+ }
193
+ write_private_output(
194
+ context_path,
195
+ json.dumps(context, sort_keys=True, separators=(",", ":")) + "\n",
196
+ )
197
+ return snapshot_directory, snapshot_digest
198
+
199
+
200
+ def _ensure_private_directory(path: Path) -> None:
201
+ if path.is_symlink():
202
+ raise ValueError("Entra work directory MUST NOT be a symbolic link")
203
+ path.mkdir(parents=True, mode=0o700, exist_ok=True)
204
+ details = path.stat()
205
+ if (
206
+ not stat.S_ISDIR(details.st_mode)
207
+ or details.st_uid != os.geteuid()
208
+ or stat.S_IMODE(details.st_mode) != 0o700
209
+ ):
210
+ raise PermissionError("Entra work directory MUST be current-UID mode 0700")
211
+
212
+
213
+ def _run_identity_process(
214
+ command: list[str],
215
+ *,
216
+ cwd: Path,
217
+ environment: dict[str, str],
218
+ timeout_seconds: int,
219
+ pass_fds: tuple[int, ...],
220
+ ) -> int:
221
+ """Bound the identity coordinator and terminate its complete process group."""
222
+
223
+ process = subprocess.Popen( # noqa: S603 - fixed verified-snapshot bootstrap.
224
+ command,
225
+ cwd=cwd,
226
+ env=environment,
227
+ pass_fds=pass_fds,
228
+ start_new_session=True,
229
+ )
230
+ try:
231
+ return process.wait(timeout=timeout_seconds)
232
+ except BaseException:
233
+ for selected_signal, grace in ((signal.SIGTERM, 5), (signal.SIGKILL, 1)):
234
+ try:
235
+ os.killpg(process.pid, selected_signal)
236
+ except ProcessLookupError:
237
+ break
238
+ try:
239
+ process.wait(timeout=grace)
240
+ if selected_signal == signal.SIGKILL:
241
+ break
242
+ except subprocess.TimeoutExpired:
243
+ continue
244
+ raise