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,45 @@
|
|
|
1
|
+
"""The existing local configuration container, without trust or filesystem decisions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
|
|
7
|
+
from fdai_deployment_cli.contracts import canonical_bytes
|
|
8
|
+
from fdai_deployment_cli.lifecycle_configuration_package import SignedConfigurationPackage
|
|
9
|
+
from fdai_deployment_cli.lifecycle_configuration_sealing import read_object
|
|
10
|
+
|
|
11
|
+
CONFIGURATION_FILE_FORMAT = "fdai.lifecycle-configuration-file.v1"
|
|
12
|
+
MAX_CONFIGURATION_ARTIFACT_BYTES = 2 * 1024 * 1024
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def configuration_artifact_bytes(package: SignedConfigurationPackage) -> bytes:
|
|
16
|
+
return canonical_bytes(
|
|
17
|
+
{
|
|
18
|
+
"format": CONFIGURATION_FILE_FORMAT,
|
|
19
|
+
"payload": base64.b64encode(package.payload).decode("ascii"),
|
|
20
|
+
"signature": base64.b64encode(package.signature).decode("ascii"),
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def read_configuration_artifact(content: bytes) -> SignedConfigurationPackage:
|
|
26
|
+
if len(content) > MAX_CONFIGURATION_ARTIFACT_BYTES:
|
|
27
|
+
raise ValueError("configuration artifact exceeds its byte bound")
|
|
28
|
+
document = read_object(content)
|
|
29
|
+
if set(document) != {"format", "payload", "signature"}:
|
|
30
|
+
raise ValueError("configuration input fields are invalid")
|
|
31
|
+
if document["format"] != CONFIGURATION_FILE_FORMAT:
|
|
32
|
+
raise ValueError("unsupported configuration artifact format")
|
|
33
|
+
payload_text, signature_text = document["payload"], document["signature"]
|
|
34
|
+
if (
|
|
35
|
+
not isinstance(payload_text, str)
|
|
36
|
+
or not payload_text
|
|
37
|
+
or not isinstance(signature_text, str)
|
|
38
|
+
or not signature_text
|
|
39
|
+
):
|
|
40
|
+
raise ValueError("configuration field must be nonempty text")
|
|
41
|
+
payload = base64.b64decode(payload_text, validate=True)
|
|
42
|
+
signature = base64.b64decode(signature_text, validate=True)
|
|
43
|
+
if len(payload) > 1024 * 1024 or len(signature) != 64:
|
|
44
|
+
raise ValueError("configuration artifact payload or signature size is invalid")
|
|
45
|
+
return SignedConfigurationPackage(payload, signature)
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""Local-only configuration packaging; file inputs never establish operational trust."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from cryptography.exceptions import UnsupportedAlgorithm
|
|
11
|
+
from cryptography.hazmat.primitives import serialization
|
|
12
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
|
|
13
|
+
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey
|
|
14
|
+
|
|
15
|
+
from fdai_deployment_cli.cli_help import command, command_group
|
|
16
|
+
from fdai_deployment_cli.lifecycle_configuration_artifact import (
|
|
17
|
+
configuration_artifact_bytes as _encode,
|
|
18
|
+
read_configuration_artifact as _decode,
|
|
19
|
+
)
|
|
20
|
+
from fdai_deployment_cli.lifecycle_configuration_entities import (
|
|
21
|
+
EntityConfigurationInput,
|
|
22
|
+
InstallationConfigurationInput,
|
|
23
|
+
validate_entity_ids,
|
|
24
|
+
)
|
|
25
|
+
from fdai_deployment_cli.lifecycle_configuration_package import (
|
|
26
|
+
ConfigurationPackageInput,
|
|
27
|
+
SignedConfigurationPackage,
|
|
28
|
+
build_configuration_package,
|
|
29
|
+
build_installation_configuration_package,
|
|
30
|
+
verify_configuration_package,
|
|
31
|
+
verify_installation_configuration_package,
|
|
32
|
+
)
|
|
33
|
+
from fdai_deployment_cli.lifecycle_configuration_publication_cli import (
|
|
34
|
+
register_publication_commands,
|
|
35
|
+
)
|
|
36
|
+
from fdai_deployment_cli.lifecycle_configuration_sealing import encryption_key_digest, read_object
|
|
37
|
+
from fdai_deployment_cli.private_output import read_private_bytes, write_private_bytes
|
|
38
|
+
|
|
39
|
+
_MAX_INPUT = 1024 * 1024
|
|
40
|
+
_MAX_ARTIFACT = 2 * _MAX_INPUT
|
|
41
|
+
_MAX_HISTORY = 256
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def register_configuration_commands(
|
|
45
|
+
commands: argparse._SubParsersAction[argparse.ArgumentParser],
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Expose offline artifacts only, without enrollment, import, decryption or activation."""
|
|
48
|
+
group = command(
|
|
49
|
+
commands,
|
|
50
|
+
"configuration",
|
|
51
|
+
"Build, verify or publish local signed configuration packages",
|
|
52
|
+
epilog="Local files only. No trust registration, qualified Release or deployment is created.",
|
|
53
|
+
)
|
|
54
|
+
operations = command_group(group)
|
|
55
|
+
for operation in ("build", "verify"):
|
|
56
|
+
parser = command(
|
|
57
|
+
operations, operation, f"{operation.title()} one local configuration package"
|
|
58
|
+
)
|
|
59
|
+
for name, help_text in (
|
|
60
|
+
(
|
|
61
|
+
"release-input",
|
|
62
|
+
"Independent JSON Release/schema, sealed keys and optional opaque Entity selection",
|
|
63
|
+
),
|
|
64
|
+
("configuration-key", "Independently supplied Ed25519 verification key in PEM format"),
|
|
65
|
+
(
|
|
66
|
+
"installation-key",
|
|
67
|
+
"Explicitly supplied installation X25519 public key in PEM format",
|
|
68
|
+
),
|
|
69
|
+
):
|
|
70
|
+
parser.add_argument(
|
|
71
|
+
f"--{name}", type=Path, required=True, metavar="PRIVATE_FILE", help=help_text
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"--history",
|
|
75
|
+
type=Path,
|
|
76
|
+
action="append",
|
|
77
|
+
default=[],
|
|
78
|
+
metavar="ACCEPTED_PACKAGE",
|
|
79
|
+
help="Complete accepted chain, oldest first; repeat once per package (maximum 256)",
|
|
80
|
+
)
|
|
81
|
+
head = parser.add_mutually_exclusive_group(required=True)
|
|
82
|
+
head.add_argument(
|
|
83
|
+
"--initial-revision",
|
|
84
|
+
action="store_true",
|
|
85
|
+
help="Explicitly assert an empty accepted chain",
|
|
86
|
+
)
|
|
87
|
+
head.add_argument(
|
|
88
|
+
"--expected-predecessor", help="Independently retained accepted head SHA-256"
|
|
89
|
+
)
|
|
90
|
+
if operation == "build":
|
|
91
|
+
parser.add_argument(
|
|
92
|
+
"--input",
|
|
93
|
+
type=Path,
|
|
94
|
+
required=True,
|
|
95
|
+
metavar="PRIVATE_FILE",
|
|
96
|
+
help="Private JSON revision, environment, Entity override blocks and sealed identifiers",
|
|
97
|
+
)
|
|
98
|
+
parser.add_argument(
|
|
99
|
+
"--signing-key",
|
|
100
|
+
type=Path,
|
|
101
|
+
required=True,
|
|
102
|
+
metavar="PRIVATE_PEM_FILE",
|
|
103
|
+
help="Private unencrypted PKCS8 Ed25519 PEM file; never supply key bytes in argv",
|
|
104
|
+
)
|
|
105
|
+
parser.add_argument(
|
|
106
|
+
"--destination",
|
|
107
|
+
type=Path,
|
|
108
|
+
required=True,
|
|
109
|
+
metavar="NEW_PRIVATE_FILE",
|
|
110
|
+
help="New absolute mode-0600 file in an existing private mode-0700 directory",
|
|
111
|
+
)
|
|
112
|
+
else:
|
|
113
|
+
parser.add_argument(
|
|
114
|
+
"--package",
|
|
115
|
+
type=Path,
|
|
116
|
+
required=True,
|
|
117
|
+
metavar="PRIVATE_FILE",
|
|
118
|
+
help="Existing signed configuration file to verify; no import or activation",
|
|
119
|
+
)
|
|
120
|
+
parser.add_argument("--output", choices=("text", "json"), default="text")
|
|
121
|
+
parser.set_defaults(handler=_run, configuration_operation=operation)
|
|
122
|
+
register_publication_commands(operations, _execute)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _run(args: argparse.Namespace) -> int:
|
|
126
|
+
"""Suppress input-derived diagnostics; success follows independent reader verification."""
|
|
127
|
+
try:
|
|
128
|
+
package = _execute(args)
|
|
129
|
+
except (ValueError, TypeError, OSError, OverflowError, RecursionError, UnsupportedAlgorithm):
|
|
130
|
+
raise ValueError(
|
|
131
|
+
"configuration operation refused; check private file permissions, input shapes, "
|
|
132
|
+
"independent keys/Release inputs, complete history and a new destination"
|
|
133
|
+
) from None
|
|
134
|
+
result = {
|
|
135
|
+
"operation": args.configuration_operation,
|
|
136
|
+
"package_digest": package.digest,
|
|
137
|
+
"verified": True,
|
|
138
|
+
"local_only": True,
|
|
139
|
+
"trust_registered": False,
|
|
140
|
+
"deployment_activated": False,
|
|
141
|
+
"production_release_eligibility": "unverified",
|
|
142
|
+
}
|
|
143
|
+
print(
|
|
144
|
+
json.dumps(result, sort_keys=True)
|
|
145
|
+
if args.output == "json"
|
|
146
|
+
else f"verified local configuration package sha256={package.digest}; no deployment or trust change"
|
|
147
|
+
)
|
|
148
|
+
return 0
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _execute(
|
|
152
|
+
args: argparse.Namespace,
|
|
153
|
+
*,
|
|
154
|
+
retained_history: Sequence[SignedConfigurationPackage] | None = None,
|
|
155
|
+
retained_package: SignedConfigurationPackage | None = None,
|
|
156
|
+
persist_output: bool = True,
|
|
157
|
+
) -> SignedConfigurationPackage:
|
|
158
|
+
release = read_object(_read(args.release_input))
|
|
159
|
+
multi_entity = "entity_ids" in release
|
|
160
|
+
_fields(
|
|
161
|
+
release,
|
|
162
|
+
{"release_version", "configuration_schema", "sealed_keys"}
|
|
163
|
+
| ({"entity_ids", "release_digest"} if multi_entity else set()),
|
|
164
|
+
)
|
|
165
|
+
version = _text(release["release_version"])
|
|
166
|
+
schema = _object(release["configuration_schema"])
|
|
167
|
+
keys = release["sealed_keys"]
|
|
168
|
+
if not isinstance(keys, list) or any(not isinstance(key, str) or not key for key in keys):
|
|
169
|
+
raise ValueError("sealed keys must be nonempty strings")
|
|
170
|
+
sealed_keys = frozenset(_text(key) for key in keys)
|
|
171
|
+
if len(sealed_keys) != len(keys):
|
|
172
|
+
raise ValueError("sealed keys must be unique")
|
|
173
|
+
entity_ids = _entity_ids(release["entity_ids"]) if multi_entity else frozenset()
|
|
174
|
+
public_key = serialization.load_pem_public_key(_read(args.configuration_key, limit=16384))
|
|
175
|
+
recipient = serialization.load_pem_public_key(_read(args.installation_key, limit=16384))
|
|
176
|
+
if not isinstance(public_key, Ed25519PublicKey) or not isinstance(recipient, X25519PublicKey):
|
|
177
|
+
raise ValueError("configuration signing and installation encryption key types are invalid")
|
|
178
|
+
if len(args.history) > _MAX_HISTORY:
|
|
179
|
+
raise ValueError("configuration history exceeds its bound")
|
|
180
|
+
history = (
|
|
181
|
+
list(retained_history)
|
|
182
|
+
if retained_history is not None
|
|
183
|
+
else [_decode(_read(path, limit=_MAX_ARTIFACT)) for path in args.history]
|
|
184
|
+
)
|
|
185
|
+
if len(history) > _MAX_HISTORY:
|
|
186
|
+
raise ValueError("configuration history exceeds its bound")
|
|
187
|
+
head = history[-1].digest if history else None
|
|
188
|
+
if args.initial_revision:
|
|
189
|
+
if history:
|
|
190
|
+
raise ValueError("initial revision cannot have accepted history")
|
|
191
|
+
elif head is None or args.expected_predecessor != head:
|
|
192
|
+
raise ValueError("accepted history does not match the independently retained head")
|
|
193
|
+
if args.configuration_operation == "build":
|
|
194
|
+
private_key = serialization.load_pem_private_key(
|
|
195
|
+
_read(args.signing_key, limit=16384), password=None
|
|
196
|
+
)
|
|
197
|
+
if not isinstance(private_key, Ed25519PrivateKey):
|
|
198
|
+
raise ValueError("configuration signing key must be Ed25519")
|
|
199
|
+
if private_key.public_key().public_bytes_raw() != public_key.public_bytes_raw():
|
|
200
|
+
raise ValueError("signing key does not match the independent verification key")
|
|
201
|
+
source = read_object(_read(args.input))
|
|
202
|
+
if multi_entity:
|
|
203
|
+
_fields(source, {"revision_id", "environment", "entities", "sealed"})
|
|
204
|
+
package = build_installation_configuration_package(
|
|
205
|
+
InstallationConfigurationInput(
|
|
206
|
+
revision_id=_text(source["revision_id"]),
|
|
207
|
+
release_version=version,
|
|
208
|
+
release_digest=_text(release["release_digest"]),
|
|
209
|
+
configuration_schema=schema,
|
|
210
|
+
environment_config=_object(source["environment"]),
|
|
211
|
+
entities=_entities(source["entities"]),
|
|
212
|
+
sealed_values=_object(source["sealed"]),
|
|
213
|
+
sealed_keys=sealed_keys,
|
|
214
|
+
),
|
|
215
|
+
entity_ids=entity_ids,
|
|
216
|
+
signing_key=private_key,
|
|
217
|
+
installation_key=recipient,
|
|
218
|
+
history=history,
|
|
219
|
+
)
|
|
220
|
+
else:
|
|
221
|
+
_fields(source, {"revision_id", "environment", "entity_overrides", "sealed"})
|
|
222
|
+
package = build_configuration_package(
|
|
223
|
+
ConfigurationPackageInput(
|
|
224
|
+
revision_id=_text(source["revision_id"]),
|
|
225
|
+
release_version=version,
|
|
226
|
+
configuration_schema=schema,
|
|
227
|
+
environment_config=_object(source["environment"]),
|
|
228
|
+
entity_overrides=_overrides(source["entity_overrides"]),
|
|
229
|
+
sealed_values=_object(source["sealed"]),
|
|
230
|
+
sealed_keys=sealed_keys,
|
|
231
|
+
),
|
|
232
|
+
signing_key=private_key,
|
|
233
|
+
installation_key=recipient,
|
|
234
|
+
history=history,
|
|
235
|
+
)
|
|
236
|
+
else:
|
|
237
|
+
package = (
|
|
238
|
+
retained_package
|
|
239
|
+
if retained_package is not None
|
|
240
|
+
else _decode(_read(args.package, limit=_MAX_ARTIFACT))
|
|
241
|
+
)
|
|
242
|
+
if multi_entity:
|
|
243
|
+
verify_installation_configuration_package(
|
|
244
|
+
package,
|
|
245
|
+
configuration_key=public_key,
|
|
246
|
+
installation_key_digest=encryption_key_digest(recipient),
|
|
247
|
+
configuration_schema=schema,
|
|
248
|
+
sealed_keys=sealed_keys,
|
|
249
|
+
entity_ids=entity_ids,
|
|
250
|
+
release_version=version,
|
|
251
|
+
release_digest=_text(release["release_digest"]),
|
|
252
|
+
history=history,
|
|
253
|
+
)
|
|
254
|
+
else:
|
|
255
|
+
verify_configuration_package(
|
|
256
|
+
package,
|
|
257
|
+
configuration_key=public_key,
|
|
258
|
+
installation_key_digest=encryption_key_digest(recipient),
|
|
259
|
+
configuration_schema=schema,
|
|
260
|
+
sealed_keys=sealed_keys,
|
|
261
|
+
history=history,
|
|
262
|
+
)
|
|
263
|
+
if read_object(package.payload)["release_version"] != version:
|
|
264
|
+
raise ValueError("package does not match the expected Release")
|
|
265
|
+
if args.configuration_operation == "build" and persist_output:
|
|
266
|
+
content = _encode(package)
|
|
267
|
+
write_private_bytes(args.destination, content)
|
|
268
|
+
if read_private_bytes(args.destination, max_bytes=_MAX_ARTIFACT) != content:
|
|
269
|
+
raise ValueError("configuration artifact readback does not match")
|
|
270
|
+
return package
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _read(path: Path, *, limit: int = _MAX_INPUT) -> bytes:
|
|
274
|
+
return read_private_bytes(path, max_bytes=limit)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _entity_ids(value: object) -> frozenset[str]:
|
|
278
|
+
if not isinstance(value, list):
|
|
279
|
+
raise ValueError("selected Entities must be an array")
|
|
280
|
+
ids = frozenset(_text(item) for item in value)
|
|
281
|
+
if len(ids) != len(value):
|
|
282
|
+
raise ValueError("selected Entities must be unique")
|
|
283
|
+
validate_entity_ids(ids)
|
|
284
|
+
return ids
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _overrides(value: object) -> list[dict[str, object]]:
|
|
288
|
+
if not isinstance(value, list):
|
|
289
|
+
raise ValueError("entity overrides must be an array")
|
|
290
|
+
return [_object(block) for block in value]
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _entities(value: object) -> list[EntityConfigurationInput]:
|
|
294
|
+
if not isinstance(value, list):
|
|
295
|
+
raise ValueError("Entities must be an array")
|
|
296
|
+
result = []
|
|
297
|
+
for raw in value:
|
|
298
|
+
entry = _fields(_object(raw), {"entity_id", "entity_overrides", "sealed"})
|
|
299
|
+
result.append(
|
|
300
|
+
EntityConfigurationInput(
|
|
301
|
+
entity_id=_text(entry["entity_id"]),
|
|
302
|
+
entity_overrides=_overrides(entry["entity_overrides"]),
|
|
303
|
+
sealed_values=_object(entry["sealed"]),
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
return result
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _object(value: object) -> dict[str, object]:
|
|
310
|
+
if not isinstance(value, dict):
|
|
311
|
+
raise ValueError("configuration field must be an object")
|
|
312
|
+
return {str(key): item for key, item in value.items()}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _fields(document: dict[str, object], fields: set[str]) -> dict[str, object]:
|
|
316
|
+
if set(document) != fields:
|
|
317
|
+
raise ValueError("configuration input fields are invalid")
|
|
318
|
+
return document
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _text(value: object) -> str:
|
|
322
|
+
if not isinstance(value, str) or not value:
|
|
323
|
+
raise ValueError("configuration field must be nonempty text")
|
|
324
|
+
return value
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Shared complete-document validation for single and multi-Entity packages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
|
|
7
|
+
from fdai_deployment_cli.contracts import canonical_bytes
|
|
8
|
+
from fdai_deployment_cli.lifecycle_configuration import (
|
|
9
|
+
LayerResolution,
|
|
10
|
+
resolve_configuration_layers,
|
|
11
|
+
validate_configuration_package_for_signing,
|
|
12
|
+
validate_release_configuration_schema,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def configuration_object(value: object) -> dict[str, object]:
|
|
17
|
+
"""Require an object without coercing untrusted keys."""
|
|
18
|
+
if not isinstance(value, Mapping) or any(not isinstance(key, str) for key in value):
|
|
19
|
+
raise ValueError("configuration field must be an object")
|
|
20
|
+
return {str(key): item for key, item in value.items()}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def validate_configuration_document(
|
|
24
|
+
configuration: Mapping[str, object],
|
|
25
|
+
schema: Mapping[str, object],
|
|
26
|
+
release_version: str,
|
|
27
|
+
sealed_keys: set[str],
|
|
28
|
+
) -> LayerResolution:
|
|
29
|
+
"""Validate every layer, including unused overrides, before resolving one Entity."""
|
|
30
|
+
if set(configuration) != {"schema", "environment", "entity_overrides"}:
|
|
31
|
+
raise ValueError("configuration document fields are invalid")
|
|
32
|
+
if canonical_bytes(configuration["schema"]) != canonical_bytes(schema):
|
|
33
|
+
raise ValueError("configuration schema does not match the trusted Release schema")
|
|
34
|
+
validate_release_configuration_schema(schema)
|
|
35
|
+
defaults = {
|
|
36
|
+
key: entry["default"]
|
|
37
|
+
for key, raw_entry in schema.items()
|
|
38
|
+
if "default" in (entry := configuration_object(raw_entry))
|
|
39
|
+
}
|
|
40
|
+
validate_configuration_package_for_signing(
|
|
41
|
+
{
|
|
42
|
+
"defaults": defaults,
|
|
43
|
+
"environment": configuration["environment"],
|
|
44
|
+
"entity_overrides": configuration["entity_overrides"],
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
overrides = configuration["entity_overrides"]
|
|
48
|
+
if not isinstance(overrides, list | tuple):
|
|
49
|
+
raise ValueError("configuration overrides must be a sequence")
|
|
50
|
+
blocks = [configuration_object(item) for item in overrides]
|
|
51
|
+
for block in blocks:
|
|
52
|
+
if set(block) != {"versions", "values"}:
|
|
53
|
+
raise ValueError("configuration override fields are invalid")
|
|
54
|
+
_reject_public_identifiers(block["values"], sealed_keys)
|
|
55
|
+
_reject_public_identifiers(defaults, sealed_keys)
|
|
56
|
+
_reject_public_identifiers(configuration["environment"], sealed_keys)
|
|
57
|
+
return resolve_configuration_layers(
|
|
58
|
+
release_version=release_version,
|
|
59
|
+
configuration_schema=schema,
|
|
60
|
+
environment_config=configuration_object(configuration["environment"]),
|
|
61
|
+
entity_overrides=blocks,
|
|
62
|
+
sealed_keys=sealed_keys,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _reject_public_identifiers(value: object, sealed_keys: set[str]) -> None:
|
|
67
|
+
if isinstance(value, Mapping):
|
|
68
|
+
if sealed_keys.intersection(value):
|
|
69
|
+
raise ValueError("sealed identifier belongs in the sealed section")
|
|
70
|
+
for item in value.values():
|
|
71
|
+
_reject_public_identifiers(item, sealed_keys)
|
|
72
|
+
elif isinstance(value, list | tuple):
|
|
73
|
+
for item in value:
|
|
74
|
+
_reject_public_identifiers(item, sealed_keys)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Full-revision Entity binding and isolation, without ownership or trust enrollment."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Mapping, Sequence
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from fdai_deployment_cli.contracts import canonical_bytes
|
|
10
|
+
from fdai_deployment_cli.lifecycle_configuration import (
|
|
11
|
+
LayerResolution,
|
|
12
|
+
validate_configuration_package_for_signing,
|
|
13
|
+
)
|
|
14
|
+
from fdai_deployment_cli.lifecycle_configuration_document import (
|
|
15
|
+
configuration_object,
|
|
16
|
+
validate_configuration_document,
|
|
17
|
+
)
|
|
18
|
+
from fdai_deployment_cli.lifecycle_configuration_sealing import read_object
|
|
19
|
+
|
|
20
|
+
INSTALLATION_CONFIGURATION_FORMAT = "fdai.lifecycle-configuration-package.v2"
|
|
21
|
+
_ENTITY_ID = re.compile(r"[0-9a-f]{64}\Z", re.ASCII)
|
|
22
|
+
_MAX_ENTITIES = 256
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
26
|
+
class EntityConfigurationInput:
|
|
27
|
+
"""One opaque binding with its own public overrides and private identifiers."""
|
|
28
|
+
|
|
29
|
+
entity_id: str
|
|
30
|
+
entity_overrides: Sequence[Mapping[str, object]]
|
|
31
|
+
sealed_values: Mapping[str, object]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
35
|
+
class InstallationConfigurationInput:
|
|
36
|
+
"""One complete customer Git revision; caller-owned selection is checked separately."""
|
|
37
|
+
|
|
38
|
+
revision_id: str
|
|
39
|
+
release_version: str
|
|
40
|
+
release_digest: str
|
|
41
|
+
configuration_schema: Mapping[str, object]
|
|
42
|
+
environment_config: Mapping[str, object]
|
|
43
|
+
sealed_values: Mapping[str, object]
|
|
44
|
+
sealed_keys: frozenset[str]
|
|
45
|
+
entities: Sequence[EntityConfigurationInput]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, slots=True)
|
|
49
|
+
class InstallationLayerResolution:
|
|
50
|
+
"""Public configuration resolved independently for every selected Entity."""
|
|
51
|
+
|
|
52
|
+
release_version: str
|
|
53
|
+
entities: dict[str, LayerResolution]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def validate_entity_ids(entity_ids: frozenset[str]) -> None:
|
|
57
|
+
"""Require bounded opaque bindings, never derive identity or permission from them."""
|
|
58
|
+
if not 1 <= len(entity_ids) <= _MAX_ENTITIES or any(
|
|
59
|
+
not isinstance(entity_id, str) or not _ENTITY_ID.fullmatch(entity_id)
|
|
60
|
+
for entity_id in entity_ids
|
|
61
|
+
):
|
|
62
|
+
raise ValueError("selected Entity bindings must be bounded opaque identifiers")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def prepare_installation_configuration(
|
|
66
|
+
source: InstallationConfigurationInput, *, entity_ids: frozenset[str]
|
|
67
|
+
) -> tuple[dict[str, object], dict[str, object]]:
|
|
68
|
+
"""Normalize complete inputs before signing; never discard an unknown or duplicate Entity."""
|
|
69
|
+
validate_entity_ids(entity_ids)
|
|
70
|
+
if not 1 <= len(source.entities) <= _MAX_ENTITIES:
|
|
71
|
+
raise ValueError("configuration Entity count is invalid")
|
|
72
|
+
ids = [entity.entity_id for entity in source.entities]
|
|
73
|
+
if len(set(ids)) != len(ids) or frozenset(ids) != entity_ids:
|
|
74
|
+
raise ValueError("configuration does not cover the exact selected Entity set")
|
|
75
|
+
if not set(source.sealed_values) <= source.sealed_keys:
|
|
76
|
+
raise ValueError("common sealed identifier keys are invalid")
|
|
77
|
+
validate_configuration_package_for_signing(source.sealed_values)
|
|
78
|
+
sealed: dict[str, object] = {}
|
|
79
|
+
for entity in source.entities:
|
|
80
|
+
if not set(entity.sealed_values) <= source.sealed_keys:
|
|
81
|
+
raise ValueError("Entity sealed identifier keys are invalid")
|
|
82
|
+
validate_configuration_package_for_signing(entity.sealed_values)
|
|
83
|
+
values = {**source.sealed_values, **entity.sealed_values}
|
|
84
|
+
if set(values) != source.sealed_keys:
|
|
85
|
+
raise ValueError("Entity sealed identifier coverage is incomplete")
|
|
86
|
+
sealed[entity.entity_id] = values
|
|
87
|
+
configuration = read_object(
|
|
88
|
+
canonical_bytes(
|
|
89
|
+
{
|
|
90
|
+
"schema": source.configuration_schema,
|
|
91
|
+
"environment": source.environment_config,
|
|
92
|
+
"entities": [
|
|
93
|
+
{"entity_id": entity.entity_id, "entity_overrides": entity.entity_overrides}
|
|
94
|
+
for entity in sorted(source.entities, key=lambda item: item.entity_id)
|
|
95
|
+
],
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
resolve_installation_configuration(
|
|
100
|
+
configuration,
|
|
101
|
+
configuration_schema=source.configuration_schema,
|
|
102
|
+
release_version=source.release_version,
|
|
103
|
+
sealed_keys=source.sealed_keys,
|
|
104
|
+
entity_ids=entity_ids,
|
|
105
|
+
)
|
|
106
|
+
return configuration, read_object(canonical_bytes(sealed))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def resolve_installation_configuration(
|
|
110
|
+
configuration: Mapping[str, object],
|
|
111
|
+
*,
|
|
112
|
+
configuration_schema: Mapping[str, object],
|
|
113
|
+
release_version: str,
|
|
114
|
+
sealed_keys: frozenset[str],
|
|
115
|
+
entity_ids: frozenset[str],
|
|
116
|
+
) -> InstallationLayerResolution:
|
|
117
|
+
"""Require exact coverage and canonical ordering, then run the canonical layer validator."""
|
|
118
|
+
validate_entity_ids(entity_ids)
|
|
119
|
+
if set(configuration) != {"schema", "environment", "entities"}:
|
|
120
|
+
raise ValueError("installation configuration fields are invalid")
|
|
121
|
+
entries = configuration["entities"]
|
|
122
|
+
if not isinstance(entries, list) or not 1 <= len(entries) <= _MAX_ENTITIES:
|
|
123
|
+
raise ValueError("configuration Entities must be a bounded nonempty array")
|
|
124
|
+
entities = [configuration_object(entry) for entry in entries]
|
|
125
|
+
ids: list[str] = []
|
|
126
|
+
resolutions: dict[str, LayerResolution] = {}
|
|
127
|
+
for entity in entities:
|
|
128
|
+
if set(entity) != {"entity_id", "entity_overrides"}:
|
|
129
|
+
raise ValueError("Entity configuration fields are invalid")
|
|
130
|
+
entity_id = entity["entity_id"]
|
|
131
|
+
if not isinstance(entity_id, str) or entity_id not in entity_ids:
|
|
132
|
+
raise ValueError("configuration names an unselected Entity")
|
|
133
|
+
ids.append(entity_id)
|
|
134
|
+
resolutions[entity_id] = validate_configuration_document(
|
|
135
|
+
{
|
|
136
|
+
"schema": configuration["schema"],
|
|
137
|
+
"environment": configuration["environment"],
|
|
138
|
+
"entity_overrides": entity["entity_overrides"],
|
|
139
|
+
},
|
|
140
|
+
configuration_schema,
|
|
141
|
+
release_version,
|
|
142
|
+
set(sealed_keys),
|
|
143
|
+
)
|
|
144
|
+
if ids != sorted(entity_ids):
|
|
145
|
+
raise ValueError("configuration Entity coverage or ordering is invalid")
|
|
146
|
+
return InstallationLayerResolution(release_version, resolutions)
|