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,177 @@
|
|
|
1
|
+
"""Install a verified capability token or installation entitlement through Key Vault.
|
|
2
|
+
|
|
3
|
+
The managed host reads the token from standard input, verifies it against the packaged
|
|
4
|
+
upstream key and the exact deployment bindings, writes it to the fixed Key Vault
|
|
5
|
+
secret, and verifies the stored content again before any runtime consumes it.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import hashlib
|
|
12
|
+
import re
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from fdai_deployment_cli import standalone_planned_outputs
|
|
20
|
+
from fdai_deployment_cli.license import (
|
|
21
|
+
INSTALLATION_ENTITLEMENT_SCHEMA,
|
|
22
|
+
inspect_installation_entitlement,
|
|
23
|
+
inspect_license,
|
|
24
|
+
signed_token_schema,
|
|
25
|
+
)
|
|
26
|
+
from fdai_deployment_cli.standalone_host_state import private_json, replace_private_json
|
|
27
|
+
from fdai_deployment_cli.standalone_host_values import vault_name
|
|
28
|
+
from fdai_deployment_cli.trust_roots import license_public_key_pem
|
|
29
|
+
|
|
30
|
+
LICENSE_SECRET_NAME = "fdai-capability-license"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def install_license(
|
|
34
|
+
args: argparse.Namespace,
|
|
35
|
+
work_dir: Path,
|
|
36
|
+
*,
|
|
37
|
+
login: Callable[[dict[str, object], Path], None],
|
|
38
|
+
) -> dict[str, object]:
|
|
39
|
+
"""Store one verified token and record the Container Apps license variables."""
|
|
40
|
+
|
|
41
|
+
context = private_json(work_dir / "context.json", "standalone host context")
|
|
42
|
+
login(context, work_dir)
|
|
43
|
+
token = sys.stdin.read(8193)
|
|
44
|
+
if len(token) > 8192 or token != token.strip():
|
|
45
|
+
raise ValueError("license token stdin is invalid")
|
|
46
|
+
_inspect(token, args)
|
|
47
|
+
token_digest = hashlib.sha256(token.encode("ascii")).hexdigest()
|
|
48
|
+
infra = Path(str(context["infra"]))
|
|
49
|
+
key_vault = vault_name(_output(infra, "key_vault_uri"))
|
|
50
|
+
result = subprocess.run(
|
|
51
|
+
(
|
|
52
|
+
"az",
|
|
53
|
+
"keyvault",
|
|
54
|
+
"secret",
|
|
55
|
+
"set",
|
|
56
|
+
"--vault-name",
|
|
57
|
+
key_vault,
|
|
58
|
+
"--name",
|
|
59
|
+
LICENSE_SECRET_NAME,
|
|
60
|
+
"--file",
|
|
61
|
+
"/dev/stdin",
|
|
62
|
+
"--encoding",
|
|
63
|
+
"utf-8",
|
|
64
|
+
"--query",
|
|
65
|
+
"id",
|
|
66
|
+
"--output",
|
|
67
|
+
"tsv",
|
|
68
|
+
"--only-show-errors",
|
|
69
|
+
),
|
|
70
|
+
input=token,
|
|
71
|
+
check=False,
|
|
72
|
+
capture_output=True,
|
|
73
|
+
text=True,
|
|
74
|
+
timeout=120,
|
|
75
|
+
)
|
|
76
|
+
token = ""
|
|
77
|
+
if result.returncode != 0:
|
|
78
|
+
raise ValueError("license token Key Vault installation failed")
|
|
79
|
+
versioned_secret_id = result.stdout.strip()
|
|
80
|
+
match = re.fullmatch(
|
|
81
|
+
rf"https://{re.escape(key_vault)}[.]vault[.]azure[.]net/secrets/"
|
|
82
|
+
rf"{LICENSE_SECRET_NAME}/([0-9a-f]{{32}})",
|
|
83
|
+
versioned_secret_id,
|
|
84
|
+
)
|
|
85
|
+
if match is None:
|
|
86
|
+
raise ValueError("license token Key Vault readback is invalid")
|
|
87
|
+
readback = subprocess.run(
|
|
88
|
+
(
|
|
89
|
+
"az",
|
|
90
|
+
"keyvault",
|
|
91
|
+
"secret",
|
|
92
|
+
"show",
|
|
93
|
+
"--id",
|
|
94
|
+
versioned_secret_id,
|
|
95
|
+
"--query",
|
|
96
|
+
"value",
|
|
97
|
+
"--output",
|
|
98
|
+
"tsv",
|
|
99
|
+
"--only-show-errors",
|
|
100
|
+
),
|
|
101
|
+
cwd=work_dir,
|
|
102
|
+
check=False,
|
|
103
|
+
capture_output=True,
|
|
104
|
+
text=True,
|
|
105
|
+
timeout=120,
|
|
106
|
+
)
|
|
107
|
+
if readback.returncode != 0:
|
|
108
|
+
raise ValueError("license token Key Vault content readback failed")
|
|
109
|
+
stored = readback.stdout.strip()
|
|
110
|
+
_inspect(stored, args)
|
|
111
|
+
if hashlib.sha256(stored.encode("ascii")).hexdigest() != token_digest:
|
|
112
|
+
raise ValueError("license token Key Vault content differs")
|
|
113
|
+
values = private_json(work_dir / "application.auto.tfvars.json", "application variables")
|
|
114
|
+
values["license"] = {
|
|
115
|
+
"token_secret_id": versioned_secret_id.rsplit("/", 1)[0],
|
|
116
|
+
"image_digest": args.image_digest,
|
|
117
|
+
"deployment_digest": args.deployment_binding,
|
|
118
|
+
"token_revision": hashlib.sha256(
|
|
119
|
+
f"{args.image_digest}:{args.deployment_binding}".encode()
|
|
120
|
+
).hexdigest(),
|
|
121
|
+
}
|
|
122
|
+
replace_private_json(work_dir / "application.auto.tfvars.json", values)
|
|
123
|
+
return {
|
|
124
|
+
"schema_version": "fdai.standalone-license-installation.v1",
|
|
125
|
+
"state": "installed",
|
|
126
|
+
"secret_metadata_verified": True,
|
|
127
|
+
"secret_content_verified": True,
|
|
128
|
+
"mutation_performed": True,
|
|
129
|
+
"subscription_ready": False,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def aks_license_environment(
|
|
134
|
+
application_values: dict[str, Any],
|
|
135
|
+
) -> tuple[dict[str, str], dict[str, str]]:
|
|
136
|
+
"""Return Core's plain and Key Vault-backed license environment once a token is installed."""
|
|
137
|
+
|
|
138
|
+
license_values = application_values.get("license")
|
|
139
|
+
if not isinstance(license_values, dict) or not license_values.get("token_secret_id"):
|
|
140
|
+
return {}, {}
|
|
141
|
+
return (
|
|
142
|
+
{
|
|
143
|
+
"FDAI_LICENSE_IMAGE_DIGEST": str(license_values["image_digest"]),
|
|
144
|
+
"FDAI_LICENSE_TOKEN_REVISION": str(license_values["token_revision"]),
|
|
145
|
+
},
|
|
146
|
+
{"FDAI_LICENSE_TOKEN": LICENSE_SECRET_NAME},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _inspect(token: str, args: argparse.Namespace) -> None:
|
|
151
|
+
if signed_token_schema(token) == INSTALLATION_ENTITLEMENT_SCHEMA:
|
|
152
|
+
installation_binding = getattr(args, "installation_binding", None)
|
|
153
|
+
if not installation_binding:
|
|
154
|
+
raise ValueError("an installation entitlement requires its installation binding")
|
|
155
|
+
inspect_installation_entitlement(
|
|
156
|
+
token,
|
|
157
|
+
public_key_pem=license_public_key_pem(),
|
|
158
|
+
expected_installation_binding=installation_binding,
|
|
159
|
+
expected_deployment_binding=args.deployment_binding,
|
|
160
|
+
)
|
|
161
|
+
return
|
|
162
|
+
inspect_license(
|
|
163
|
+
token,
|
|
164
|
+
public_key_pem=license_public_key_pem(),
|
|
165
|
+
expected_image_digest=args.image_digest,
|
|
166
|
+
expected_tenant_binding=args.deployment_binding,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _output(infra: Path, name: str) -> str:
|
|
171
|
+
value = standalone_planned_outputs.read_output(
|
|
172
|
+
infra, name, raw=True, reason="Terraform output readback failed"
|
|
173
|
+
)
|
|
174
|
+
return str(value)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
__all__ = ["LICENSE_SECRET_NAME", "aks_license_environment", "install_license"]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Derive the AKS API server authorized range from the managed host's egress.
|
|
2
|
+
|
|
3
|
+
The AKS baseline keeps a public API server with authorized IP ranges. Only the managed deployment
|
|
4
|
+
host runs Terraform, ``kubectl``, and readback against it, and that host leaves the operations
|
|
5
|
+
network through its NAT gateway. The range is therefore the NAT gateway's single public address,
|
|
6
|
+
read back from Azure Resource Manager rather than from any external address-echo service.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import ipaddress
|
|
12
|
+
import json
|
|
13
|
+
import subprocess
|
|
14
|
+
from collections.abc import Mapping
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def management_egress_cidrs(context: Mapping[str, object], *, cwd: Path) -> list[str]:
|
|
19
|
+
"""Return the one ``/32`` range of the operations NAT gateway, or fail closed."""
|
|
20
|
+
|
|
21
|
+
ops_resource_group = str(context["state_resource_group"])
|
|
22
|
+
subscription_id = str(context["subscription_id"])
|
|
23
|
+
listed = _az(
|
|
24
|
+
(
|
|
25
|
+
"network",
|
|
26
|
+
"nat",
|
|
27
|
+
"gateway",
|
|
28
|
+
"list",
|
|
29
|
+
"--resource-group",
|
|
30
|
+
ops_resource_group,
|
|
31
|
+
"--subscription",
|
|
32
|
+
subscription_id,
|
|
33
|
+
"--query",
|
|
34
|
+
"[].publicIpAddresses[].id",
|
|
35
|
+
"--output",
|
|
36
|
+
"json",
|
|
37
|
+
),
|
|
38
|
+
cwd,
|
|
39
|
+
)
|
|
40
|
+
try:
|
|
41
|
+
addresses = json.loads(listed)
|
|
42
|
+
except json.JSONDecodeError as exc:
|
|
43
|
+
raise ValueError("management NAT gateway readback is invalid") from exc
|
|
44
|
+
if not isinstance(addresses, list) or len(addresses) != 1 or not isinstance(addresses[0], str):
|
|
45
|
+
raise ValueError("management egress requires exactly one NAT gateway public IP")
|
|
46
|
+
raw = _az(
|
|
47
|
+
(
|
|
48
|
+
"network",
|
|
49
|
+
"public-ip",
|
|
50
|
+
"show",
|
|
51
|
+
"--ids",
|
|
52
|
+
addresses[0],
|
|
53
|
+
"--query",
|
|
54
|
+
"ipAddress",
|
|
55
|
+
"--output",
|
|
56
|
+
"tsv",
|
|
57
|
+
),
|
|
58
|
+
cwd,
|
|
59
|
+
).strip()
|
|
60
|
+
try:
|
|
61
|
+
address = ipaddress.IPv4Address(raw)
|
|
62
|
+
except ValueError as exc:
|
|
63
|
+
raise ValueError("management NAT gateway public IP is invalid") from exc
|
|
64
|
+
if not address.is_global:
|
|
65
|
+
raise ValueError("management NAT gateway public IP is not globally routable")
|
|
66
|
+
return [f"{address}/32"]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _az(arguments: tuple[str, ...], cwd: Path) -> str:
|
|
70
|
+
result = subprocess.run(
|
|
71
|
+
("az", *arguments, "--only-show-errors"),
|
|
72
|
+
cwd=cwd,
|
|
73
|
+
check=False,
|
|
74
|
+
capture_output=True,
|
|
75
|
+
text=True,
|
|
76
|
+
timeout=60,
|
|
77
|
+
)
|
|
78
|
+
if result.returncode != 0:
|
|
79
|
+
raise ValueError("management egress readback failed")
|
|
80
|
+
return result.stdout
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
__all__ = ["management_egress_cidrs"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Check the adoption evidence one service migration bootstrap may write."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from fdai_deployment_cli.standalone_host_state import private_json
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def verify_service_evidence(evidence: Path, service: str) -> bool:
|
|
11
|
+
"""Verify fresh adoption evidence; return False when the lineage was already adopted.
|
|
12
|
+
|
|
13
|
+
`bootstrap` writes adoption evidence only when it adopts a legacy baseline. When an earlier
|
|
14
|
+
run already adopted the service lineage, bootstrap only advances it to head and writes
|
|
15
|
+
nothing, so absent evidence is a verified resume rather than a failure. A partial or
|
|
16
|
+
inconsistent pair still fails closed.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
migration_path = evidence / f"{service}.json"
|
|
20
|
+
schema_path = evidence / f"{service}-schema.json"
|
|
21
|
+
if not migration_path.exists() and not schema_path.exists():
|
|
22
|
+
return False
|
|
23
|
+
for path in (migration_path, schema_path):
|
|
24
|
+
path.chmod(0o600)
|
|
25
|
+
migration = private_json(migration_path, f"{service} migration evidence")
|
|
26
|
+
schema = private_json(schema_path, f"{service} migration schema")
|
|
27
|
+
if (
|
|
28
|
+
migration.get("service_id") != service
|
|
29
|
+
or not isinstance(migration.get("observed_schema_fingerprint"), str)
|
|
30
|
+
or schema.get("schema_version") != 1
|
|
31
|
+
or schema.get("service_id") != service
|
|
32
|
+
or schema.get("observed_schema_fingerprint") != migration.get("observed_schema_fingerprint")
|
|
33
|
+
):
|
|
34
|
+
raise ValueError("service migration evidence is incomplete")
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__all__ = ["verify_service_evidence"]
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""AKS workload rendering for the independent operational evidence verifier."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
_GUID = re.compile(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}")
|
|
11
|
+
_OPERATIONAL_EVIDENCE_PORT = 8791
|
|
12
|
+
_OPERATIONAL_EVIDENCE_READINESS_PATH = "/v1/operational-evidence/readiness"
|
|
13
|
+
_SERVICE_DATABASE_ROLES = {
|
|
14
|
+
"core": "fdai_core",
|
|
15
|
+
"core-control-plane": "fdai_core",
|
|
16
|
+
"operator": "fdai_operator",
|
|
17
|
+
"operator-service": "fdai_operator",
|
|
18
|
+
"executor": "fdai_executor",
|
|
19
|
+
"isolated-executor": "fdai_executor",
|
|
20
|
+
"ingestion": "fdai_ingestion_api",
|
|
21
|
+
"document-ingestion-api": "fdai_ingestion_api",
|
|
22
|
+
"worker": "fdai_ingestion_worker",
|
|
23
|
+
"document-processing-worker": "fdai_ingestion_worker",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def bind_service_database_role(service: str, environment: Mapping[str, object]) -> dict[str, str]:
|
|
28
|
+
"""Return one workload environment with its service-owned PostgreSQL role."""
|
|
29
|
+
|
|
30
|
+
runtime_environment = {name: str(value) for name, value in environment.items()}
|
|
31
|
+
database_role = _SERVICE_DATABASE_ROLES.get(service)
|
|
32
|
+
if database_role is not None:
|
|
33
|
+
runtime_environment["FDAI_DATABASE_ROLE"] = database_role
|
|
34
|
+
runtime_environment["PGOPTIONS"] = f"-c role={database_role}"
|
|
35
|
+
return runtime_environment
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def aks_workload(
|
|
39
|
+
component: str,
|
|
40
|
+
refs: dict[str, Any],
|
|
41
|
+
identity: dict[str, Any],
|
|
42
|
+
environment: Mapping[str, object],
|
|
43
|
+
secret_environment: Mapping[str, str],
|
|
44
|
+
readiness_path: str,
|
|
45
|
+
liveness_path: str,
|
|
46
|
+
*,
|
|
47
|
+
external: bool = False,
|
|
48
|
+
service_port: int | None = None,
|
|
49
|
+
fs_group: int | None = None,
|
|
50
|
+
additional_identities: dict[str, dict[str, Any]] | None = None,
|
|
51
|
+
sidecars: dict[str, dict[str, object]] | None = None,
|
|
52
|
+
) -> dict[str, object]:
|
|
53
|
+
"""Build one runtime-neutral AKS long-running workload specification."""
|
|
54
|
+
|
|
55
|
+
image_names = {
|
|
56
|
+
"core": "core-control-plane",
|
|
57
|
+
"operator": "operator-service",
|
|
58
|
+
"executor": "isolated-executor",
|
|
59
|
+
"ingestion": "document-ingestion-api",
|
|
60
|
+
"worker": "document-processing-worker",
|
|
61
|
+
}
|
|
62
|
+
image_name = image_names.get(component)
|
|
63
|
+
if image_name is None or not isinstance(refs.get(image_name), str):
|
|
64
|
+
raise ValueError(f"AKS {component} workload image is unavailable")
|
|
65
|
+
cpu, memory = {
|
|
66
|
+
"core": ("1000m", "2Gi"),
|
|
67
|
+
"operator": ("500m", "1Gi"),
|
|
68
|
+
"executor": ("500m", "1Gi"),
|
|
69
|
+
"ingestion": ("500m", "1Gi"),
|
|
70
|
+
"worker": ("500m", "1Gi"),
|
|
71
|
+
}[component]
|
|
72
|
+
runtime_environment = bind_service_database_role(image_name, environment)
|
|
73
|
+
runtime_environment["FDAI_EXECUTION_VENUE"] = "deployed"
|
|
74
|
+
return {
|
|
75
|
+
"component": component,
|
|
76
|
+
"image": refs[image_name],
|
|
77
|
+
"identity_resource_id": identity["resource_id"],
|
|
78
|
+
"identity_client_id": identity["client_id"],
|
|
79
|
+
"additional_identities": additional_identities or {},
|
|
80
|
+
"command": [],
|
|
81
|
+
"args": [],
|
|
82
|
+
"replicas": 2,
|
|
83
|
+
"max_replicas": 4,
|
|
84
|
+
"cpu": cpu,
|
|
85
|
+
"memory": memory,
|
|
86
|
+
"port": 8000,
|
|
87
|
+
"service_port": service_port,
|
|
88
|
+
"external": external,
|
|
89
|
+
"readiness_path": readiness_path,
|
|
90
|
+
"liveness_path": liveness_path,
|
|
91
|
+
"fs_group": fs_group,
|
|
92
|
+
"environment": runtime_environment,
|
|
93
|
+
"secret_environment": secret_environment,
|
|
94
|
+
"sidecars": sidecars or {},
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def aks_operational_evidence_verifier_workload(
|
|
99
|
+
*,
|
|
100
|
+
refs: dict[str, Any],
|
|
101
|
+
verifier_identity: object,
|
|
102
|
+
core_identity: dict[str, Any],
|
|
103
|
+
executor_identity: object,
|
|
104
|
+
deploy_runner_principal: str,
|
|
105
|
+
application_values: dict[str, Any],
|
|
106
|
+
postgres_fqdn: str,
|
|
107
|
+
postgres_database: str,
|
|
108
|
+
) -> dict[str, object] | None:
|
|
109
|
+
"""Render the optional non-agent verifier workload from deployment-owned bindings."""
|
|
110
|
+
|
|
111
|
+
binding = application_values.get("operational_evidence_verifier")
|
|
112
|
+
if binding is None:
|
|
113
|
+
return None
|
|
114
|
+
if not isinstance(binding, dict) or binding.get("enabled") is not True:
|
|
115
|
+
raise ValueError("operational evidence verifier binding is invalid")
|
|
116
|
+
if not isinstance(verifier_identity, dict):
|
|
117
|
+
raise TypeError("operational evidence verifier identity is unavailable")
|
|
118
|
+
|
|
119
|
+
verifier = _identity_binding(verifier_identity, "operational evidence verifier identity")
|
|
120
|
+
core = _identity_binding(core_identity, "Core runtime identity")
|
|
121
|
+
if isinstance(executor_identity, dict):
|
|
122
|
+
executor_principal = _identity_binding(executor_identity, "isolated executor identity")[
|
|
123
|
+
"principal_id"
|
|
124
|
+
]
|
|
125
|
+
else:
|
|
126
|
+
executor_principal = _anchor_principal(
|
|
127
|
+
binding.get("anchors_json"), "anchor:isolated-executor"
|
|
128
|
+
)
|
|
129
|
+
if verifier["principal_id"] in {
|
|
130
|
+
core["principal_id"],
|
|
131
|
+
executor_principal,
|
|
132
|
+
deploy_runner_principal,
|
|
133
|
+
}:
|
|
134
|
+
raise ValueError("operational evidence verifier identity overlaps an independent anchor")
|
|
135
|
+
|
|
136
|
+
json_fields = {
|
|
137
|
+
"anchors_json": "FDAI_OPERATIONAL_EVIDENCE_ANCHORS_JSON",
|
|
138
|
+
"caller_token_jwks_json": "FDAI_OPERATIONAL_EVIDENCE_CALLER_TOKEN_JWKS_JSON",
|
|
139
|
+
"role_readback_scopes_json": "FDAI_OPERATIONAL_EVIDENCE_ROLE_READBACK_SCOPES_JSON",
|
|
140
|
+
"allowed_role_scopes_json": "FDAI_OPERATIONAL_EVIDENCE_ALLOWED_ROLE_SCOPES_JSON",
|
|
141
|
+
"vertical_executor_principals_json": (
|
|
142
|
+
"FDAI_OPERATIONAL_EVIDENCE_VERTICAL_EXECUTOR_PRINCIPALS_JSON"
|
|
143
|
+
),
|
|
144
|
+
"writer_members_json": "FDAI_OPERATIONAL_EVIDENCE_WRITER_MEMBERS_JSON",
|
|
145
|
+
}
|
|
146
|
+
optional_fields = {
|
|
147
|
+
"grant_registry_json": "FDAI_OPERATIONAL_EVIDENCE_GRANT_REGISTRY_JSON",
|
|
148
|
+
"observation_metric_workspace_id": (
|
|
149
|
+
"FDAI_OPERATIONAL_EVIDENCE_OBSERVATION_METRIC_WORKSPACE_ID"
|
|
150
|
+
),
|
|
151
|
+
"observation_metric_queries_json": (
|
|
152
|
+
"FDAI_OPERATIONAL_EVIDENCE_OBSERVATION_METRIC_QUERIES_JSON"
|
|
153
|
+
),
|
|
154
|
+
"observation_scope_rows_json": "FDAI_OPERATIONAL_EVIDENCE_OBSERVATION_SCOPE_ROWS_JSON",
|
|
155
|
+
"forecast_history_sources_json": "FDAI_FORECAST_HISTORY_SOURCES_JSON",
|
|
156
|
+
}
|
|
157
|
+
environment: dict[str, object] = {
|
|
158
|
+
"AZURE_CLIENT_ID": verifier["client_id"],
|
|
159
|
+
"FDAI_MI_CLIENT_ID": verifier["client_id"],
|
|
160
|
+
"POSTGRES_HOST": postgres_fqdn,
|
|
161
|
+
"POSTGRES_DATABASE": postgres_database,
|
|
162
|
+
"FDAI_DATABASE_ROLE": "fdai_operational_evidence_verifier",
|
|
163
|
+
"PGOPTIONS": "-c role=fdai_operational_evidence_verifier",
|
|
164
|
+
"RUNTIME_ENV": application_values["env"],
|
|
165
|
+
"FDAI_OPERATIONAL_EVIDENCE_ENABLED": "1",
|
|
166
|
+
"FDAI_OPERATIONAL_EVIDENCE_TRUST_REGISTRY_PATH": _optional_text(
|
|
167
|
+
binding,
|
|
168
|
+
"trust_registry_path",
|
|
169
|
+
default="config/operational-evidence-trust-registry.json",
|
|
170
|
+
),
|
|
171
|
+
"FDAI_OPERATIONAL_EVIDENCE_TRUST_REGISTRY_PIN": _required_text(
|
|
172
|
+
binding, "trust_registry_pin"
|
|
173
|
+
),
|
|
174
|
+
"FDAI_OPERATIONAL_EVIDENCE_GRANT_REGISTRY_PATH": _required_text(
|
|
175
|
+
binding, "grant_registry_path"
|
|
176
|
+
),
|
|
177
|
+
"FDAI_OPERATIONAL_EVIDENCE_GRANT_REGISTRY_PIN": _required_text(
|
|
178
|
+
binding, "grant_registry_pin"
|
|
179
|
+
),
|
|
180
|
+
"FDAI_OPERATIONAL_EVIDENCE_VERIFIER_URL": (
|
|
181
|
+
f"http://127.0.0.1:{_OPERATIONAL_EVIDENCE_PORT}"
|
|
182
|
+
),
|
|
183
|
+
"FDAI_OPERATIONAL_EVIDENCE_READER_ROLE": "fdai_core",
|
|
184
|
+
"FDAI_OPERATIONAL_EVIDENCE_CORE_EXECUTOR_PRINCIPAL_ID": core["principal_id"],
|
|
185
|
+
"FDAI_OPERATIONAL_EVIDENCE_ISOLATED_EXECUTOR_PRINCIPAL_ID": executor_principal,
|
|
186
|
+
"FDAI_OPERATIONAL_EVIDENCE_DEV_GATEWAY_EXECUTOR_PRINCIPAL_ID": _required_text(
|
|
187
|
+
binding, "dev_gateway_executor_principal_id"
|
|
188
|
+
),
|
|
189
|
+
"FDAI_OPERATIONAL_EVIDENCE_DEPLOY_RUNNER_PRINCIPAL_ID": deploy_runner_principal,
|
|
190
|
+
"FDAI_OPERATIONAL_EVIDENCE_CALLER_TOKEN_ISSUER": _required_text(
|
|
191
|
+
binding, "caller_token_issuer"
|
|
192
|
+
),
|
|
193
|
+
"FDAI_OPERATIONAL_EVIDENCE_CALLER_TOKEN_AUDIENCE": _required_text(
|
|
194
|
+
binding, "caller_token_audience"
|
|
195
|
+
),
|
|
196
|
+
}
|
|
197
|
+
for key, env_name in json_fields.items():
|
|
198
|
+
environment[env_name] = _required_json_text(binding, key)
|
|
199
|
+
for key, env_name in optional_fields.items():
|
|
200
|
+
if key in binding:
|
|
201
|
+
environment[env_name] = (
|
|
202
|
+
_required_text(binding, key)
|
|
203
|
+
if key == "observation_metric_workspace_id"
|
|
204
|
+
else _required_json_text(binding, key)
|
|
205
|
+
)
|
|
206
|
+
vertical_principals = json.loads(
|
|
207
|
+
str(environment["FDAI_OPERATIONAL_EVIDENCE_VERTICAL_EXECUTOR_PRINCIPALS_JSON"])
|
|
208
|
+
)
|
|
209
|
+
if not isinstance(vertical_principals, list) or not all(
|
|
210
|
+
isinstance(principal, str) and _GUID.fullmatch(principal) is not None
|
|
211
|
+
for principal in vertical_principals
|
|
212
|
+
):
|
|
213
|
+
raise ValueError("vertical_executor_principals_json must be a JSON list of GUIDs")
|
|
214
|
+
environment["FDAI_OPERATIONAL_EVIDENCE_EXECUTOR_PRINCIPALS_JSON"] = json.dumps(
|
|
215
|
+
sorted(
|
|
216
|
+
{
|
|
217
|
+
str(environment["FDAI_OPERATIONAL_EVIDENCE_CORE_EXECUTOR_PRINCIPAL_ID"]),
|
|
218
|
+
str(environment["FDAI_OPERATIONAL_EVIDENCE_ISOLATED_EXECUTOR_PRINCIPAL_ID"]),
|
|
219
|
+
str(environment["FDAI_OPERATIONAL_EVIDENCE_DEV_GATEWAY_EXECUTOR_PRINCIPAL_ID"]),
|
|
220
|
+
deploy_runner_principal,
|
|
221
|
+
*vertical_principals,
|
|
222
|
+
}
|
|
223
|
+
),
|
|
224
|
+
separators=(",", ":"),
|
|
225
|
+
)
|
|
226
|
+
image = refs.get("core-control-plane")
|
|
227
|
+
if not isinstance(image, str):
|
|
228
|
+
raise TypeError("AKS operational evidence verifier image is unavailable")
|
|
229
|
+
return {
|
|
230
|
+
"component": "operational-evidence-verifier",
|
|
231
|
+
"image": image,
|
|
232
|
+
"identity_resource_id": verifier["resource_id"],
|
|
233
|
+
"identity_client_id": verifier["client_id"],
|
|
234
|
+
"additional_identities": {},
|
|
235
|
+
"command": ["python", "-m", "fdai.delivery.operational_evidence_server"],
|
|
236
|
+
"args": ["--host", "0.0.0.0", "--port", str(_OPERATIONAL_EVIDENCE_PORT)],
|
|
237
|
+
"replicas": 1,
|
|
238
|
+
"max_replicas": 1,
|
|
239
|
+
"cpu": "500m",
|
|
240
|
+
"memory": "1Gi",
|
|
241
|
+
"port": _OPERATIONAL_EVIDENCE_PORT,
|
|
242
|
+
"service_port": None,
|
|
243
|
+
"external": False,
|
|
244
|
+
"readiness_path": _OPERATIONAL_EVIDENCE_READINESS_PATH,
|
|
245
|
+
"liveness_path": _OPERATIONAL_EVIDENCE_READINESS_PATH,
|
|
246
|
+
"fs_group": None,
|
|
247
|
+
"environment": {
|
|
248
|
+
name: str(value)
|
|
249
|
+
for name, value in {**environment, "FDAI_EXECUTION_VENUE": "deployed"}.items()
|
|
250
|
+
},
|
|
251
|
+
"secret_environment": {"FDAI_OPERATIONAL_EVIDENCE_VERIFIER_DSN": "fdai-state-store-dsn"},
|
|
252
|
+
"sidecars": {},
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def add_aks_operational_evidence_verifier_workload(
|
|
257
|
+
workloads: dict[str, dict[str, object]],
|
|
258
|
+
*,
|
|
259
|
+
refs: dict[str, Any],
|
|
260
|
+
identities: dict[str, Any],
|
|
261
|
+
substrate_outputs: Mapping[str, object],
|
|
262
|
+
context: Mapping[str, object],
|
|
263
|
+
application_values: dict[str, Any],
|
|
264
|
+
) -> None:
|
|
265
|
+
"""Add the optional verifier workload to ``workloads`` when its binding is present."""
|
|
266
|
+
|
|
267
|
+
if application_values.get("operational_evidence_verifier") is None:
|
|
268
|
+
return
|
|
269
|
+
verifier_workload = aks_operational_evidence_verifier_workload(
|
|
270
|
+
refs=refs,
|
|
271
|
+
verifier_identity=substrate_outputs["operational_evidence_verifier_identity"],
|
|
272
|
+
core_identity=_mapping(identities.get("core"), "Core runtime identity"),
|
|
273
|
+
executor_identity=identities.get("executor"),
|
|
274
|
+
deploy_runner_principal=_required_text(context, "principal_id"),
|
|
275
|
+
application_values=application_values,
|
|
276
|
+
postgres_fqdn=_required_text(substrate_outputs, "postgres_fqdn"),
|
|
277
|
+
postgres_database=_required_text(substrate_outputs, "postgres_database"),
|
|
278
|
+
)
|
|
279
|
+
if verifier_workload is not None:
|
|
280
|
+
workloads["operational-evidence-verifier"] = verifier_workload
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def runtime_principal_ids_with_operational_evidence_verifier(
|
|
284
|
+
identities: Mapping[str, Any], names: tuple[str, ...]
|
|
285
|
+
) -> set[str]:
|
|
286
|
+
"""Return runtime principal ids, including the optional verifier identity."""
|
|
287
|
+
|
|
288
|
+
principals = {
|
|
289
|
+
str(_mapping(identities.get(name), f"{name} runtime identity")["principal_id"])
|
|
290
|
+
for name in names
|
|
291
|
+
}
|
|
292
|
+
verifier_identity = identities.get("operational_evidence_verifier")
|
|
293
|
+
if verifier_identity is not None:
|
|
294
|
+
principals.add(
|
|
295
|
+
str(
|
|
296
|
+
_mapping(verifier_identity, "operational evidence verifier runtime identity")[
|
|
297
|
+
"principal_id"
|
|
298
|
+
]
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
return principals
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _mapping(value: object, label: str) -> dict[str, Any]:
|
|
305
|
+
if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
|
|
306
|
+
raise ValueError(f"{label} is invalid")
|
|
307
|
+
return {str(key): item for key, item in value.items()}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _identity_binding(value: dict[str, Any], label: str) -> dict[str, str]:
|
|
311
|
+
result = {
|
|
312
|
+
"resource_id": _required_text(value, "resource_id"),
|
|
313
|
+
"client_id": _required_text(value, "client_id"),
|
|
314
|
+
"principal_id": _required_text(value, "principal_id"),
|
|
315
|
+
}
|
|
316
|
+
if any(_GUID.fullmatch(result[key]) is None for key in ("client_id", "principal_id")):
|
|
317
|
+
raise ValueError(f"{label} is invalid")
|
|
318
|
+
return result
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _required_text(value: Mapping[str, Any], key: str) -> str:
|
|
322
|
+
item = value.get(key)
|
|
323
|
+
if not isinstance(item, str) or not item.strip():
|
|
324
|
+
raise ValueError(f"{key} is required")
|
|
325
|
+
return item.strip()
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _optional_text(value: Mapping[str, Any], key: str, *, default: str) -> str:
|
|
329
|
+
item = value.get(key, default)
|
|
330
|
+
if not isinstance(item, str) or not item.strip():
|
|
331
|
+
raise ValueError(f"{key} is required")
|
|
332
|
+
return item.strip()
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _required_json_text(value: Mapping[str, Any], key: str) -> str:
|
|
336
|
+
item = _required_text(value, key)
|
|
337
|
+
try:
|
|
338
|
+
json.loads(item)
|
|
339
|
+
except json.JSONDecodeError as exc:
|
|
340
|
+
raise ValueError(f"{key} must be valid JSON") from exc
|
|
341
|
+
return item
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _anchor_principal(anchors_json: object, anchor_id: str) -> str:
|
|
345
|
+
if isinstance(anchors_json, str) and anchors_json.strip():
|
|
346
|
+
try:
|
|
347
|
+
parsed = json.loads(anchors_json)
|
|
348
|
+
except json.JSONDecodeError:
|
|
349
|
+
parsed = None
|
|
350
|
+
if isinstance(parsed, dict) and isinstance(parsed.get("anchors"), list):
|
|
351
|
+
for entry in parsed["anchors"]:
|
|
352
|
+
if isinstance(entry, dict) and entry.get("anchor_id") == anchor_id:
|
|
353
|
+
principal = entry.get("principal_id")
|
|
354
|
+
if isinstance(principal, str) and _GUID.fullmatch(principal) is not None:
|
|
355
|
+
return principal
|
|
356
|
+
raise TypeError("operational evidence verifier requires the isolated executor anchor")
|