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,227 @@
|
|
|
1
|
+
"""Sanitized readiness contract for the standalone Entra identity operation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from fdai_deployment_cli.contracts import canonical_digest
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class IdentityProfileEvidence:
|
|
12
|
+
"""Value-free readback facts; ``None`` means the provider read was unavailable."""
|
|
13
|
+
|
|
14
|
+
premium_license_eligible: bool | None
|
|
15
|
+
role_groups_present: bool | None
|
|
16
|
+
role_group_profile_match: bool | None
|
|
17
|
+
conditional_access_policy_present: bool | None
|
|
18
|
+
access_review_present: bool | None
|
|
19
|
+
authentication_method_policy_present: bool | None
|
|
20
|
+
phishing_resistant_method_enabled: bool | None
|
|
21
|
+
azure_policy_assignment_present: bool | None
|
|
22
|
+
human_identity_present: bool | None
|
|
23
|
+
human_approver_authorized: bool | None
|
|
24
|
+
executor_identity_present: bool | None
|
|
25
|
+
executor_is_managed_identity: bool | None
|
|
26
|
+
executor_profile_match: bool | None
|
|
27
|
+
human_executor_separated: bool | None
|
|
28
|
+
|
|
29
|
+
def __post_init__(self) -> None:
|
|
30
|
+
if any(
|
|
31
|
+
value is not None and type(value) is not bool
|
|
32
|
+
for value in (
|
|
33
|
+
self.premium_license_eligible,
|
|
34
|
+
self.role_groups_present,
|
|
35
|
+
self.role_group_profile_match,
|
|
36
|
+
self.conditional_access_policy_present,
|
|
37
|
+
self.access_review_present,
|
|
38
|
+
self.authentication_method_policy_present,
|
|
39
|
+
self.phishing_resistant_method_enabled,
|
|
40
|
+
self.azure_policy_assignment_present,
|
|
41
|
+
self.human_identity_present,
|
|
42
|
+
self.human_approver_authorized,
|
|
43
|
+
self.executor_identity_present,
|
|
44
|
+
self.executor_is_managed_identity,
|
|
45
|
+
self.executor_profile_match,
|
|
46
|
+
self.human_executor_separated,
|
|
47
|
+
)
|
|
48
|
+
):
|
|
49
|
+
raise ValueError("identity profile evidence values MUST be booleans or unknown")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True, slots=True)
|
|
53
|
+
class IdentityProfileObservation:
|
|
54
|
+
"""Sanitized read-only identity posture with stable fail-closed blockers."""
|
|
55
|
+
|
|
56
|
+
evidence: IdentityProfileEvidence
|
|
57
|
+
blockers: tuple[str, ...]
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def from_evidence(cls, evidence: IdentityProfileEvidence) -> IdentityProfileObservation:
|
|
61
|
+
"""Classify every required readback without exposing provider values."""
|
|
62
|
+
|
|
63
|
+
blockers: list[str] = []
|
|
64
|
+
_classify(
|
|
65
|
+
blockers,
|
|
66
|
+
evidence.premium_license_eligible,
|
|
67
|
+
unavailable="premium_license_readback_unavailable",
|
|
68
|
+
absent="premium_license_ineligible",
|
|
69
|
+
)
|
|
70
|
+
_classify(
|
|
71
|
+
blockers,
|
|
72
|
+
evidence.role_groups_present,
|
|
73
|
+
unavailable="role_group_readback_unavailable",
|
|
74
|
+
absent="role_group_missing",
|
|
75
|
+
)
|
|
76
|
+
if evidence.role_groups_present is True:
|
|
77
|
+
_classify(
|
|
78
|
+
blockers,
|
|
79
|
+
evidence.role_group_profile_match,
|
|
80
|
+
unavailable="role_group_readback_unavailable",
|
|
81
|
+
absent="role_group_profile_mismatch",
|
|
82
|
+
)
|
|
83
|
+
_classify(
|
|
84
|
+
blockers,
|
|
85
|
+
evidence.conditional_access_policy_present,
|
|
86
|
+
unavailable="conditional_access_readback_unavailable",
|
|
87
|
+
absent="conditional_access_policy_missing",
|
|
88
|
+
)
|
|
89
|
+
_classify(
|
|
90
|
+
blockers,
|
|
91
|
+
evidence.access_review_present,
|
|
92
|
+
unavailable="access_review_readback_unavailable",
|
|
93
|
+
absent="access_review_missing",
|
|
94
|
+
)
|
|
95
|
+
_classify(
|
|
96
|
+
blockers,
|
|
97
|
+
evidence.authentication_method_policy_present,
|
|
98
|
+
unavailable="authentication_method_policy_readback_unavailable",
|
|
99
|
+
absent="authentication_method_policy_missing",
|
|
100
|
+
)
|
|
101
|
+
if evidence.authentication_method_policy_present is True:
|
|
102
|
+
_classify(
|
|
103
|
+
blockers,
|
|
104
|
+
evidence.phishing_resistant_method_enabled,
|
|
105
|
+
unavailable="authentication_method_policy_readback_unavailable",
|
|
106
|
+
absent="phishing_resistant_method_missing",
|
|
107
|
+
)
|
|
108
|
+
_classify(
|
|
109
|
+
blockers,
|
|
110
|
+
evidence.azure_policy_assignment_present,
|
|
111
|
+
unavailable="azure_policy_assignment_readback_unavailable",
|
|
112
|
+
absent="azure_policy_assignment_missing",
|
|
113
|
+
)
|
|
114
|
+
_classify(
|
|
115
|
+
blockers,
|
|
116
|
+
evidence.human_identity_present,
|
|
117
|
+
unavailable="human_identity_readback_unavailable",
|
|
118
|
+
absent="human_identity_missing",
|
|
119
|
+
)
|
|
120
|
+
if evidence.human_identity_present is True:
|
|
121
|
+
_classify(
|
|
122
|
+
blockers,
|
|
123
|
+
evidence.human_approver_authorized,
|
|
124
|
+
unavailable="human_approver_membership_readback_unavailable",
|
|
125
|
+
absent="human_approver_membership_missing",
|
|
126
|
+
)
|
|
127
|
+
_classify(
|
|
128
|
+
blockers,
|
|
129
|
+
evidence.executor_identity_present,
|
|
130
|
+
unavailable="executor_identity_readback_unavailable",
|
|
131
|
+
absent="executor_identity_missing",
|
|
132
|
+
)
|
|
133
|
+
if evidence.executor_identity_present is True:
|
|
134
|
+
_classify(
|
|
135
|
+
blockers,
|
|
136
|
+
evidence.executor_is_managed_identity,
|
|
137
|
+
unavailable="executor_identity_readback_unavailable",
|
|
138
|
+
absent="executor_is_not_managed_identity",
|
|
139
|
+
)
|
|
140
|
+
if evidence.executor_is_managed_identity is True:
|
|
141
|
+
_classify(
|
|
142
|
+
blockers,
|
|
143
|
+
evidence.executor_profile_match,
|
|
144
|
+
unavailable="executor_identity_readback_unavailable",
|
|
145
|
+
absent="executor_profile_mismatch",
|
|
146
|
+
)
|
|
147
|
+
if evidence.human_identity_present is True and evidence.executor_identity_present is True:
|
|
148
|
+
_classify(
|
|
149
|
+
blockers,
|
|
150
|
+
evidence.human_executor_separated,
|
|
151
|
+
unavailable="human_executor_separation_unavailable",
|
|
152
|
+
absent="human_executor_not_separated",
|
|
153
|
+
)
|
|
154
|
+
return cls(evidence=evidence, blockers=tuple(blockers))
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def ready(self) -> bool:
|
|
158
|
+
"""Return whether every required profile and identity readback passed."""
|
|
159
|
+
|
|
160
|
+
return not self.blockers
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def digest(self) -> str:
|
|
164
|
+
"""Bind the exact sanitized observation used by the mutation preflight."""
|
|
165
|
+
|
|
166
|
+
return canonical_digest(self._projection())
|
|
167
|
+
|
|
168
|
+
def to_mapping(self) -> dict[str, object]:
|
|
169
|
+
"""Return only generic booleans, stable blockers, and an opaque digest."""
|
|
170
|
+
|
|
171
|
+
projection = self._projection()
|
|
172
|
+
projection["profile_digest"] = canonical_digest(projection)
|
|
173
|
+
return projection
|
|
174
|
+
|
|
175
|
+
def _projection(self) -> dict[str, object]:
|
|
176
|
+
evidence = self.evidence
|
|
177
|
+
return {
|
|
178
|
+
"schema_version": "fdai.identity-profile-observation.v1",
|
|
179
|
+
"premium_license": _fact(evidence.premium_license_eligible, "eligible"),
|
|
180
|
+
"role_groups": {
|
|
181
|
+
"readback_available": evidence.role_groups_present is not None,
|
|
182
|
+
"all_present": evidence.role_groups_present is True,
|
|
183
|
+
"profile_match": evidence.role_group_profile_match is True,
|
|
184
|
+
},
|
|
185
|
+
"conditional_access": _fact(
|
|
186
|
+
evidence.conditional_access_policy_present, "policy_present"
|
|
187
|
+
),
|
|
188
|
+
"access_review": _fact(evidence.access_review_present, "definition_present"),
|
|
189
|
+
"authentication_method_policy": {
|
|
190
|
+
**_fact(evidence.authentication_method_policy_present, "policy_present"),
|
|
191
|
+
"phishing_resistant_method_enabled": bool(
|
|
192
|
+
evidence.phishing_resistant_method_enabled
|
|
193
|
+
),
|
|
194
|
+
},
|
|
195
|
+
"azure_policy": _fact(evidence.azure_policy_assignment_present, "assignment_present"),
|
|
196
|
+
"identity_separation": {
|
|
197
|
+
"human_readback_available": evidence.human_identity_present is not None,
|
|
198
|
+
"human_present": evidence.human_identity_present is True,
|
|
199
|
+
"human_approver_authorized": evidence.human_approver_authorized is True,
|
|
200
|
+
"executor_readback_available": evidence.executor_identity_present is not None,
|
|
201
|
+
"executor_present": evidence.executor_identity_present is True,
|
|
202
|
+
"executor_is_managed_identity": evidence.executor_is_managed_identity is True,
|
|
203
|
+
"executor_profile_match": evidence.executor_profile_match is True,
|
|
204
|
+
"human_executor_separated": evidence.human_executor_separated is True,
|
|
205
|
+
},
|
|
206
|
+
"blockers": list(self.blockers),
|
|
207
|
+
"ready": self.ready,
|
|
208
|
+
"mutation_performed": False,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _classify(
|
|
213
|
+
blockers: list[str],
|
|
214
|
+
value: bool | None,
|
|
215
|
+
*,
|
|
216
|
+
unavailable: str,
|
|
217
|
+
absent: str,
|
|
218
|
+
) -> None:
|
|
219
|
+
if value is None:
|
|
220
|
+
if unavailable not in blockers:
|
|
221
|
+
blockers.append(unavailable)
|
|
222
|
+
elif not value:
|
|
223
|
+
blockers.append(absent)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _fact(value: bool | None, field: str) -> dict[str, bool]:
|
|
227
|
+
return {"readback_available": value is not None, field: value is True}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Resolve changed repository paths to independently deployable service images."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from dataclasses import asdict, dataclass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class ImageTarget:
|
|
12
|
+
target: str
|
|
13
|
+
service: str
|
|
14
|
+
dockerfile: str
|
|
15
|
+
image: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
IMAGE_TARGETS = (
|
|
19
|
+
ImageTarget(
|
|
20
|
+
target="core-control-plane",
|
|
21
|
+
service="core-control-plane",
|
|
22
|
+
dockerfile="services/core-control-plane/docker/Dockerfile",
|
|
23
|
+
image="fdai-core-control-plane",
|
|
24
|
+
),
|
|
25
|
+
ImageTarget(
|
|
26
|
+
target="cost-governance",
|
|
27
|
+
service="core-control-plane",
|
|
28
|
+
dockerfile="extensions/cost-governance/docker/Dockerfile",
|
|
29
|
+
image="fdai-cost-governance",
|
|
30
|
+
),
|
|
31
|
+
ImageTarget(
|
|
32
|
+
target="operator-service",
|
|
33
|
+
service="operator-service",
|
|
34
|
+
dockerfile="services/operator-service/docker/Dockerfile",
|
|
35
|
+
image="fdai-operator-service",
|
|
36
|
+
),
|
|
37
|
+
ImageTarget(
|
|
38
|
+
target="document-ingestion-api",
|
|
39
|
+
service="document-ingestion-api",
|
|
40
|
+
dockerfile="services/document-ingestion-api/docker/Dockerfile",
|
|
41
|
+
image="fdai-document-ingestion-api",
|
|
42
|
+
),
|
|
43
|
+
ImageTarget(
|
|
44
|
+
target="document-processing-worker",
|
|
45
|
+
service="document-processing-worker",
|
|
46
|
+
dockerfile="services/document-processing-worker/docker/Dockerfile",
|
|
47
|
+
image="fdai-document-processing-worker",
|
|
48
|
+
),
|
|
49
|
+
ImageTarget(
|
|
50
|
+
target="isolated-executor",
|
|
51
|
+
service="isolated-executor",
|
|
52
|
+
dockerfile="services/isolated-executor/docker/Dockerfile",
|
|
53
|
+
image="fdai-isolated-executor",
|
|
54
|
+
),
|
|
55
|
+
ImageTarget(
|
|
56
|
+
target="system-knowledge-service",
|
|
57
|
+
service="system-knowledge-service",
|
|
58
|
+
dockerfile="services/system-knowledge-service/docker/Dockerfile",
|
|
59
|
+
image="fdai-system-knowledge-service",
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
_ALL_TARGET_EXACT_PATHS = {
|
|
64
|
+
".dockerignore",
|
|
65
|
+
".github/workflows/container-supply-chain.yml",
|
|
66
|
+
".trivyignore.yaml",
|
|
67
|
+
"config/service-image-build-override.json",
|
|
68
|
+
"LICENSE",
|
|
69
|
+
"README.md",
|
|
70
|
+
"pyproject.toml",
|
|
71
|
+
"scripts/deployment/service/apply_image_build_override.py",
|
|
72
|
+
"scripts/deployment/service/select_changed_images.py",
|
|
73
|
+
"uv.lock",
|
|
74
|
+
}
|
|
75
|
+
_ALL_TARGET_PREFIXES = (
|
|
76
|
+
"evaluation-sdk/",
|
|
77
|
+
"packages/service-contracts/",
|
|
78
|
+
"service-contracts/",
|
|
79
|
+
"src/",
|
|
80
|
+
)
|
|
81
|
+
_ALL_TARGET_METADATA_PATHS = {
|
|
82
|
+
"benchmarks/cybergym/pyproject.toml",
|
|
83
|
+
"benchmarks/sregym/pyproject.toml",
|
|
84
|
+
"extensions/code-assurance/pyproject.toml",
|
|
85
|
+
"extensions/cost-governance/pyproject.toml",
|
|
86
|
+
"packages/github-app-auth/pyproject.toml",
|
|
87
|
+
*(f"services/{target.service}/pyproject.toml" for target in IMAGE_TARGETS),
|
|
88
|
+
}
|
|
89
|
+
_CORE_EXACT_PATHS = {
|
|
90
|
+
"alembic.ini",
|
|
91
|
+
"docs/internals/sregym-absorption-ledger.json",
|
|
92
|
+
"scripts/deployment/local/materialize-authoritative-catalogs.py",
|
|
93
|
+
}
|
|
94
|
+
_CORE_PREFIXES = (
|
|
95
|
+
"alembic/",
|
|
96
|
+
"config/",
|
|
97
|
+
"policies/",
|
|
98
|
+
"provider-schema-catalog/",
|
|
99
|
+
"rule-catalog/",
|
|
100
|
+
"services/assets/",
|
|
101
|
+
"tests/scenarios/",
|
|
102
|
+
)
|
|
103
|
+
_COST_GOVERNANCE_PREFIX = "extensions/cost-governance/"
|
|
104
|
+
_GITHUB_APP_AUTH_PREFIX = "packages/github-app-auth/"
|
|
105
|
+
_RUNTIME_DIAGNOSTICS_PREFIX = "packages/runtime-diagnostics/"
|
|
106
|
+
_SOURCE_PACKAGE_PREFIXES = (
|
|
107
|
+
*(f"services/{target.service}/" for target in IMAGE_TARGETS),
|
|
108
|
+
_COST_GOVERNANCE_PREFIX,
|
|
109
|
+
_GITHUB_APP_AUTH_PREFIX,
|
|
110
|
+
_RUNTIME_DIAGNOSTICS_PREFIX,
|
|
111
|
+
"packages/service-contracts/",
|
|
112
|
+
"evaluation-sdk/",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def select_requested_images(selection: str) -> tuple[ImageTarget, ...]:
|
|
117
|
+
"""Resolve explicit target names without expanding optional image profiles."""
|
|
118
|
+
names = [name.strip() for name in selection.split(",")]
|
|
119
|
+
if names == ["all"]:
|
|
120
|
+
return IMAGE_TARGETS
|
|
121
|
+
by_name = {name: target for target in IMAGE_TARGETS for name in (target.target, target.image)}
|
|
122
|
+
selected: set[str] = set()
|
|
123
|
+
for name in names:
|
|
124
|
+
if name not in by_name:
|
|
125
|
+
raise ValueError("images must name known targets or image names, or only 'all'")
|
|
126
|
+
target = by_name[name]
|
|
127
|
+
if target.target in selected:
|
|
128
|
+
raise ValueError("images must not repeat a target or its image alias")
|
|
129
|
+
selected.add(target.target)
|
|
130
|
+
return tuple(target for target in IMAGE_TARGETS if target.target in selected)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _is_packaging_input(path: str) -> bool:
|
|
134
|
+
if path == "README.md":
|
|
135
|
+
return False
|
|
136
|
+
for prefix in _SOURCE_PACKAGE_PREFIXES:
|
|
137
|
+
if not path.startswith(prefix):
|
|
138
|
+
continue
|
|
139
|
+
relative = path.removeprefix(prefix)
|
|
140
|
+
if relative.startswith("tests/") and not (
|
|
141
|
+
prefix == "services/core-control-plane/" and relative.startswith("tests/scenarios/")
|
|
142
|
+
):
|
|
143
|
+
return False
|
|
144
|
+
if relative.startswith("docs/"):
|
|
145
|
+
return False
|
|
146
|
+
return not (relative.startswith("src/") and relative.endswith((".py", ".pyi")))
|
|
147
|
+
return not (path.startswith("src/") and path.endswith((".py", ".pyi")))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def select_image_targets(
|
|
151
|
+
changed_paths: Iterable[str], *, packaging_only: bool = False
|
|
152
|
+
) -> tuple[ImageTarget, ...]:
|
|
153
|
+
"""Select every image whose declared source or packaged asset changed."""
|
|
154
|
+
paths = {path.removeprefix("./") for path in changed_paths if path}
|
|
155
|
+
if packaging_only:
|
|
156
|
+
paths = {path for path in paths if _is_packaging_input(path)}
|
|
157
|
+
if any(
|
|
158
|
+
path in _ALL_TARGET_EXACT_PATHS
|
|
159
|
+
or path in _ALL_TARGET_METADATA_PATHS
|
|
160
|
+
or path.startswith(_ALL_TARGET_PREFIXES)
|
|
161
|
+
for path in paths
|
|
162
|
+
):
|
|
163
|
+
return IMAGE_TARGETS
|
|
164
|
+
|
|
165
|
+
selected: set[str] = set()
|
|
166
|
+
for path in paths:
|
|
167
|
+
if path.startswith(_RUNTIME_DIAGNOSTICS_PREFIX):
|
|
168
|
+
selected.update({"core-control-plane", "cost-governance", "operator-service"})
|
|
169
|
+
continue
|
|
170
|
+
if path.startswith(_GITHUB_APP_AUTH_PREFIX):
|
|
171
|
+
selected.update({"core-control-plane", "cost-governance", "document-ingestion-api"})
|
|
172
|
+
continue
|
|
173
|
+
if path.startswith(_COST_GOVERNANCE_PREFIX):
|
|
174
|
+
selected.add("cost-governance")
|
|
175
|
+
continue
|
|
176
|
+
if path in _CORE_EXACT_PATHS or path.startswith(_CORE_PREFIXES):
|
|
177
|
+
selected.update({"core-control-plane", "cost-governance"})
|
|
178
|
+
if path == "config/agent-stewardship.yaml":
|
|
179
|
+
selected.add("document-ingestion-api")
|
|
180
|
+
if not path.startswith("services/") or path.startswith("services/assets/"):
|
|
181
|
+
continue
|
|
182
|
+
matching = {
|
|
183
|
+
target.target
|
|
184
|
+
for target in IMAGE_TARGETS
|
|
185
|
+
if path.startswith(f"services/{target.service}/")
|
|
186
|
+
}
|
|
187
|
+
if not matching:
|
|
188
|
+
return IMAGE_TARGETS
|
|
189
|
+
selected.update(matching)
|
|
190
|
+
return tuple(target for target in IMAGE_TARGETS if target.target in selected)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def matrix_json(targets: Iterable[ImageTarget]) -> str:
|
|
194
|
+
return json.dumps(
|
|
195
|
+
{"include": [asdict(target) for target in targets]},
|
|
196
|
+
separators=(",", ":"),
|
|
197
|
+
)
|