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,147 @@
1
+ """Read shared-root Terraform outputs that targeted applies do not persist.
2
+
3
+ Terraform writes a root output during ``apply -target`` only when that output depends on a
4
+ targeted resource. Outputs derived only from variables and locals, such as logical topic names,
5
+ stay absent from state even though their values are fully determined by the verified
6
+ configuration. When state lacks such an output, this module evaluates one refresh-free,
7
+ lock-free, non-targeted plan of the same root and accepts only outputs whose planned values are
8
+ known. Unknown values mean the backing resource was never applied, so readback fails closed.
9
+ The plan is read-only, private, cached per process, and deleted after evaluation.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import os
16
+ import shutil
17
+ import subprocess
18
+ import tempfile
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+
22
+ _MISSING = "not found"
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class _Source:
27
+ variables: Path
28
+ data_dir: Path
29
+
30
+
31
+ _SOURCES: dict[Path, _Source] = {}
32
+ _CACHE: dict[Path, dict[str, object]] = {}
33
+
34
+
35
+ def bind(infra: Path, *, variables: Path, data_dir: Path) -> None:
36
+ """Allow planned-output evaluation for one shared root with its exact inputs."""
37
+
38
+ _SOURCES[infra.resolve()] = _Source(variables=variables, data_dir=data_dir)
39
+
40
+
41
+ def read_output(infra: Path, name: str, *, raw: bool, reason: str) -> object:
42
+ """Return a state output, or a known planned value when state never recorded it."""
43
+
44
+ command = ("terraform", "output", "-raw" if raw else "-json", name)
45
+ result = subprocess.run(
46
+ command, cwd=infra, check=False, capture_output=True, text=True, timeout=120
47
+ )
48
+ if result.returncode == 0:
49
+ return result.stdout.strip() if raw else _json(result.stdout)
50
+ source = _SOURCES.get(infra.resolve())
51
+ if source is None or _MISSING not in result.stderr:
52
+ raise ValueError(reason)
53
+ outputs = _CACHE.get(infra.resolve())
54
+ if outputs is None:
55
+ outputs = _planned_outputs(infra, source)
56
+ _CACHE[infra.resolve()] = outputs
57
+ if name not in outputs:
58
+ raise ValueError(f"{reason}: output is unknown until its resource is applied")
59
+ value = outputs[name]
60
+ if not raw:
61
+ return value
62
+ if isinstance(value, bool):
63
+ return "true" if value else "false"
64
+ if isinstance(value, str | int | float):
65
+ return str(value)
66
+ raise ValueError(f"{reason}: output is not a primitive value")
67
+
68
+
69
+ def _planned_outputs(infra: Path, source: _Source) -> dict[str, object]:
70
+ environment = {**os.environ, "TF_DATA_DIR": str(source.data_dir)}
71
+ scratch = Path(tempfile.mkdtemp(prefix="planned-outputs-", dir=source.variables.parent))
72
+ plan = scratch / "outputs.tfplan"
73
+ try:
74
+ planned = subprocess.run(
75
+ (
76
+ "terraform",
77
+ "plan",
78
+ "-refresh=false",
79
+ "-lock=false",
80
+ "-input=false",
81
+ "-no-color",
82
+ f"-var-file={source.variables}",
83
+ f"-out={plan}",
84
+ ),
85
+ cwd=infra,
86
+ env=environment,
87
+ check=False,
88
+ capture_output=True,
89
+ text=True,
90
+ timeout=1800,
91
+ )
92
+ if planned.returncode != 0:
93
+ raise ValueError("planned Terraform output evaluation failed")
94
+ shown = subprocess.run(
95
+ ("terraform", "show", "-json", str(plan)),
96
+ cwd=infra,
97
+ env=environment,
98
+ check=False,
99
+ capture_output=True,
100
+ text=True,
101
+ timeout=300,
102
+ )
103
+ if shown.returncode != 0:
104
+ raise ValueError("planned Terraform output evaluation failed")
105
+ document = _json(shown.stdout)
106
+ finally:
107
+ shutil.rmtree(scratch, ignore_errors=True)
108
+ if not isinstance(document, dict):
109
+ raise ValueError("planned Terraform output document is invalid")
110
+ return known_planned_outputs(document)
111
+
112
+
113
+ def known_planned_outputs(document: dict[str, object]) -> dict[str, object]:
114
+ """Select planned root outputs whose values are fully known."""
115
+
116
+ planned = document.get("planned_values")
117
+ changes = document.get("output_changes")
118
+ outputs = planned.get("outputs") if isinstance(planned, dict) else None
119
+ if not isinstance(outputs, dict) or not isinstance(changes, dict):
120
+ raise ValueError("planned Terraform output document is invalid")
121
+ known: dict[str, object] = {}
122
+ for name, output in outputs.items():
123
+ change = changes.get(name)
124
+ if not isinstance(output, dict) or "value" not in output:
125
+ continue
126
+ if isinstance(change, dict) and _has_unknown(change.get("after_unknown")):
127
+ continue
128
+ known[str(name)] = output["value"]
129
+ return known
130
+
131
+
132
+ def _has_unknown(marker: object) -> bool:
133
+ if isinstance(marker, dict):
134
+ return any(_has_unknown(item) for item in marker.values())
135
+ if isinstance(marker, list):
136
+ return any(_has_unknown(item) for item in marker)
137
+ return marker is True
138
+
139
+
140
+ def _json(raw: str) -> object:
141
+ try:
142
+ return json.loads(raw)
143
+ except json.JSONDecodeError as exc:
144
+ raise ValueError("Terraform JSON output is invalid") from exc
145
+
146
+
147
+ __all__ = ["bind", "known_planned_outputs", "read_output"]
@@ -0,0 +1,100 @@
1
+ """Wait for the managed host's selected private application data planes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import subprocess
7
+ import time
8
+ from collections.abc import Callable
9
+ from pathlib import Path
10
+
11
+ _RESOURCE_NAME = re.compile(r"[a-z0-9-]{3,63}")
12
+
13
+
14
+ def wait_for_private_data_planes(
15
+ *,
16
+ context: dict[str, object],
17
+ work_dir: Path,
18
+ timeout_seconds: float = 600,
19
+ interval_seconds: float = 15,
20
+ clock: Callable[[], float] = time.monotonic,
21
+ sleep: Callable[[float], None] = time.sleep,
22
+ run: Callable[..., subprocess.CompletedProcess[str]] = subprocess.run,
23
+ ) -> dict[str, object]:
24
+ """Require Key Vault and Data Lake access before a residual can consume authority."""
25
+
26
+ vault = _name(context, "key_vault_name")
27
+ storage = _name(context, "document_storage_account_name")
28
+ if (
29
+ context.get("key_vault_private_access") is not True
30
+ or context.get("document_storage_private_access") is not True
31
+ or timeout_seconds <= 0
32
+ or interval_seconds <= 0
33
+ ):
34
+ raise ValueError("private application data-plane probe context is invalid")
35
+ commands = (
36
+ (
37
+ "az",
38
+ "keyvault",
39
+ "secret",
40
+ "list",
41
+ "--vault-name",
42
+ vault,
43
+ "--query",
44
+ "length(@)",
45
+ "--output",
46
+ "tsv",
47
+ "--only-show-errors",
48
+ ),
49
+ (
50
+ "az",
51
+ "storage",
52
+ "fs",
53
+ "list",
54
+ "--account-name",
55
+ storage,
56
+ "--auth-mode",
57
+ "login",
58
+ "--query",
59
+ "length(@)",
60
+ "--output",
61
+ "tsv",
62
+ "--only-show-errors",
63
+ ),
64
+ )
65
+ deadline = clock() + timeout_seconds
66
+ attempts = 0
67
+ while True:
68
+ attempts += 1
69
+ results = [
70
+ run(
71
+ command,
72
+ cwd=work_dir,
73
+ check=False,
74
+ capture_output=True,
75
+ text=True,
76
+ timeout=60,
77
+ )
78
+ for command in commands
79
+ ]
80
+ if all(result.returncode == 0 and result.stdout.strip().isdigit() for result in results):
81
+ return {
82
+ "schema_version": "fdai.private-data-plane-probe.v1",
83
+ "state": "ready",
84
+ "key_vault_verified": True,
85
+ "document_storage_verified": True,
86
+ "attempts": attempts,
87
+ "mutation_performed": False,
88
+ "subscription_ready": False,
89
+ }
90
+ remaining = deadline - clock()
91
+ if remaining <= 0:
92
+ raise ValueError("private application data planes did not become ready")
93
+ sleep(min(interval_seconds, remaining))
94
+
95
+
96
+ def _name(context: dict[str, object], field: str) -> str:
97
+ value = context.get(field)
98
+ if not isinstance(value, str) or _RESOURCE_NAME.fullmatch(value) is None:
99
+ raise ValueError("private application data-plane resource name is invalid")
100
+ return value
@@ -0,0 +1,110 @@
1
+ """Compile one product profile into authority-neutral Terraform values."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections.abc import Callable
7
+ from typing import Any
8
+
9
+ from fdai_service_contracts.product_profile import (
10
+ AzureObservationRole,
11
+ ProductAddOn,
12
+ ProductProfile,
13
+ )
14
+
15
+ from fdai_deployment_cli.runtime_profile import RuntimeDeploymentProfile
16
+
17
+ _STEWARD_AGENT_NAMES = (
18
+ "Odin",
19
+ "Thor",
20
+ "Forseti",
21
+ "Huginn",
22
+ "Heimdall",
23
+ "Vidar",
24
+ "Var",
25
+ "Bragi",
26
+ "Saga",
27
+ "Mimir",
28
+ "Muninn",
29
+ "Norns",
30
+ "Njord",
31
+ "Freyr",
32
+ )
33
+
34
+
35
+ def product_terraform_values(
36
+ runtime_profile: RuntimeDeploymentProfile,
37
+ entra: dict[str, Any],
38
+ *,
39
+ require_guid: Callable[[dict[str, Any], str], str],
40
+ ) -> dict[str, object]:
41
+ """Return only bindings selected by the immutable product profile."""
42
+
43
+ profile = runtime_profile.product_profile
44
+ console = profile.selects(ProductAddOn.READ_ONLY_CONSOLE)
45
+ governed_execution = profile.selects(ProductAddOn.GOVERNED_EXECUTION)
46
+ notifications = profile.selects(ProductAddOn.NOTIFICATIONS)
47
+ enterprise_identity = profile.selects(ProductAddOn.ENTERPRISE_IDENTITY_GOVERNANCE)
48
+ permissions = profile.observation_permissions
49
+ values: dict[str, object] = {
50
+ "enable_console": console,
51
+ "enable_operator_api": console or enterprise_identity,
52
+ "enable_isolated_executor": governed_execution,
53
+ "enable_governed_execution": governed_execution,
54
+ "enable_email_notifications": notifications,
55
+ "enable_document_ingestion": console and runtime_profile.runtime_platform.value == "aks",
56
+ "enable_inventory_monitoring_reader": permissions.selects(
57
+ AzureObservationRole.MONITORING_READER
58
+ ),
59
+ "enable_inventory_log_analytics_reader": permissions.selects(
60
+ AzureObservationRole.LOG_ANALYTICS_READER
61
+ ),
62
+ "enable_inventory_cost_management_reader": permissions.selects(
63
+ AzureObservationRole.COST_MANAGEMENT_READER
64
+ ),
65
+ "enable_inventory_aks_reader": permissions.selects(AzureObservationRole.AKS_READ_ONLY),
66
+ "inventory_kubernetes_subscription_discovery_enabled": permissions.selects(
67
+ AzureObservationRole.AKS_READ_ONLY
68
+ ),
69
+ "enable_inventory_evidence_store_reader": permissions.selects(
70
+ AzureObservationRole.EVIDENCE_STORE_DATA_READER
71
+ ),
72
+ "product_profile_json": json.dumps(
73
+ profile.model_dump(mode="json"),
74
+ sort_keys=True,
75
+ separators=(",", ":"),
76
+ ),
77
+ }
78
+ if runtime_profile.database_sku is not None:
79
+ values["postgres_sku_name"] = runtime_profile.database_sku
80
+ if not enterprise_identity:
81
+ return values
82
+ operator_id = require_guid(entra, "CURRENT_OPERATOR_OBJECT_ID")
83
+ values.update(
84
+ operator_api_audience=str(entra["OPERATOR_API_AUDIENCE"]),
85
+ rbac_readers_group_id=str(entra["RBAC_READERS_GROUP_ID"]),
86
+ rbac_contributors_group_id=str(entra["RBAC_CONTRIBUTORS_GROUP_ID"]),
87
+ rbac_approvers_group_id=str(entra["RBAC_APPROVERS_GROUP_ID"]),
88
+ rbac_owners_group_id=str(entra["RBAC_OWNERS_GROUP_ID"]),
89
+ rbac_break_glass_group_id=str(entra["RBAC_BREAK_GLASS_GROUP_ID"]),
90
+ stewardship_maintainers=operator_id,
91
+ stewardship_agent_bindings={name: f"user:{operator_id}" for name in _STEWARD_AGENT_NAMES},
92
+ )
93
+ return values
94
+
95
+
96
+ def context_selects_add_on(context: dict[str, Any], add_on: ProductAddOn) -> bool:
97
+ """Report whether the recorded deployment context explicitly selected one add-on."""
98
+
99
+ runtime = context.get("runtime_profile")
100
+ if not isinstance(runtime, dict):
101
+ return False
102
+ product = runtime.get("product_profile")
103
+ if product is None:
104
+ return False
105
+ if not isinstance(product, dict):
106
+ raise ValueError("product profile is invalid")
107
+ return ProductProfile.model_validate(product).selects(add_on)
108
+
109
+
110
+ __all__ = ["context_selects_add_on", "product_terraform_values"]