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,47 @@
|
|
|
1
|
+
"""Public CLI input selection for one source service update."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from fdai_deployment_cli.development_authority_cli import (
|
|
10
|
+
selected_development_authority,
|
|
11
|
+
)
|
|
12
|
+
from fdai_deployment_cli.development_authority_profile import (
|
|
13
|
+
DevelopmentAuthorityDeploymentProfile,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def source_service_update_inputs(
|
|
18
|
+
args: argparse.Namespace,
|
|
19
|
+
) -> tuple[
|
|
20
|
+
Path,
|
|
21
|
+
Path,
|
|
22
|
+
Path,
|
|
23
|
+
DevelopmentAuthorityDeploymentProfile | None,
|
|
24
|
+
bool,
|
|
25
|
+
]:
|
|
26
|
+
"""Resolve paths and the Core-only development-authority operation."""
|
|
27
|
+
|
|
28
|
+
source = args.source.absolute()
|
|
29
|
+
application = args.application_work_dir.absolute()
|
|
30
|
+
selected = args.work_dir
|
|
31
|
+
if selected is None:
|
|
32
|
+
commit = subprocess.run(
|
|
33
|
+
("git", "rev-parse", "HEAD"),
|
|
34
|
+
cwd=source,
|
|
35
|
+
check=True,
|
|
36
|
+
capture_output=True,
|
|
37
|
+
text=True,
|
|
38
|
+
timeout=30,
|
|
39
|
+
).stdout.strip()
|
|
40
|
+
selected = Path.home() / ".local/state/fdai/source-service-updates" / commit / args.service
|
|
41
|
+
profile, clear = selected_development_authority(args, cwd=Path.cwd())
|
|
42
|
+
if (profile is not None or clear) and args.service != "core-control-plane":
|
|
43
|
+
raise ValueError("development authority changes require a Core service update")
|
|
44
|
+
return source, application, selected, profile, clear
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
__all__ = ["source_service_update_inputs"]
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Materialize only pinned Git files for connected builds and private-host transfer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import os
|
|
7
|
+
import stat
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from fdai_deployment_cli.contracts import canonical_bytes, canonical_digest, load_json_object
|
|
12
|
+
from fdai_deployment_cli.private_output import read_private_bytes, write_private_bytes
|
|
13
|
+
from fdai_deployment_cli.source_input import SourceDeploymentInput, _git, _read_tracked
|
|
14
|
+
|
|
15
|
+
_MANIFEST = "source-input.json"
|
|
16
|
+
_FULL_SCHEMA = "fdai.source-snapshot.v1"
|
|
17
|
+
_PROJECTED_SCHEMA = "fdai.source-snapshot.v2"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def materialize_source(
|
|
21
|
+
source: SourceDeploymentInput,
|
|
22
|
+
destination: Path,
|
|
23
|
+
*,
|
|
24
|
+
selected_paths: tuple[str, ...] | None = None,
|
|
25
|
+
purpose: str | None = None,
|
|
26
|
+
) -> str:
|
|
27
|
+
"""Create a fresh private execution copy, excluding every ignored or untracked input.
|
|
28
|
+
|
|
29
|
+
Return the manifest digest for independent retention. Failure preserves the partial
|
|
30
|
+
destination for inspection; it never repairs, reuses or deletes prior deployment state.
|
|
31
|
+
"""
|
|
32
|
+
source.reverify()
|
|
33
|
+
destination.mkdir(mode=0o700)
|
|
34
|
+
tree = destination / "tree"
|
|
35
|
+
tree.mkdir(mode=0o700)
|
|
36
|
+
selected = set(selected_paths) if selected_paths is not None else None
|
|
37
|
+
if selected is not None and (
|
|
38
|
+
not selected
|
|
39
|
+
or purpose is None
|
|
40
|
+
or not purpose.strip()
|
|
41
|
+
or len(purpose) > 128
|
|
42
|
+
or any(character not in "abcdefghijklmnopqrstuvwxyz0123456789-._:" for character in purpose)
|
|
43
|
+
):
|
|
44
|
+
raise ValueError("projected source snapshot requires a bounded purpose and file set")
|
|
45
|
+
records: list[dict[str, str]] = []
|
|
46
|
+
for entry in _git(source.root, "ls-tree", "-rz", "--full-tree", source.commit).split(b"\0"):
|
|
47
|
+
if not entry:
|
|
48
|
+
continue
|
|
49
|
+
metadata, raw_path = entry.split(b"\t", 1)
|
|
50
|
+
mode, kind, _blob = metadata.decode("ascii").split()
|
|
51
|
+
relative = raw_path.decode("utf-8")
|
|
52
|
+
if selected is not None and relative not in selected:
|
|
53
|
+
continue
|
|
54
|
+
if kind != "blob" or mode not in {"100644", "100755", "120000"}:
|
|
55
|
+
raise ValueError("source snapshot contains an unsupported input")
|
|
56
|
+
content = _read_tracked(source.root, relative, mode=mode)
|
|
57
|
+
output = tree / relative
|
|
58
|
+
for parent in reversed(output.parents):
|
|
59
|
+
if parent == tree or not parent.is_relative_to(tree):
|
|
60
|
+
continue
|
|
61
|
+
parent.mkdir(mode=0o700, exist_ok=True)
|
|
62
|
+
if mode == "120000":
|
|
63
|
+
output.symlink_to(os.fsdecode(content))
|
|
64
|
+
else:
|
|
65
|
+
write_private_bytes(output, content)
|
|
66
|
+
if mode == "100755":
|
|
67
|
+
output.chmod(0o700)
|
|
68
|
+
records.append(
|
|
69
|
+
{"path": relative, "mode": mode, "sha256": hashlib.sha256(content).hexdigest()}
|
|
70
|
+
)
|
|
71
|
+
source.reverify()
|
|
72
|
+
content_digest = canonical_digest({"files": records})
|
|
73
|
+
if selected is None and content_digest != source.content_digest:
|
|
74
|
+
raise ValueError("source snapshot differs from the selected input")
|
|
75
|
+
if selected is not None and {record["path"] for record in records} != selected:
|
|
76
|
+
raise ValueError("projected source snapshot contains unavailable inputs")
|
|
77
|
+
manifest: dict[str, object] = {
|
|
78
|
+
"schema_version": _FULL_SCHEMA if selected is None else _PROJECTED_SCHEMA,
|
|
79
|
+
"source": source.to_mapping(),
|
|
80
|
+
"files": records,
|
|
81
|
+
}
|
|
82
|
+
if selected is not None:
|
|
83
|
+
manifest["projection"] = {
|
|
84
|
+
"purpose": purpose,
|
|
85
|
+
"file_count": len(records),
|
|
86
|
+
"content_digest": content_digest,
|
|
87
|
+
}
|
|
88
|
+
digest = canonical_digest(manifest)
|
|
89
|
+
write_private_bytes(destination / _MANIFEST, canonical_bytes(manifest))
|
|
90
|
+
verify_source_snapshot(destination, expected_digest=digest)
|
|
91
|
+
return digest
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def verify_source_snapshot(directory: Path, *, expected_digest: str) -> dict[str, Any]:
|
|
95
|
+
"""Recheck an execution snapshot against an independently retained manifest digest."""
|
|
96
|
+
raw = read_private_bytes(directory / _MANIFEST, max_bytes=16 * 1024 * 1024)
|
|
97
|
+
manifest = load_json_object(raw, label="source snapshot", max_bytes=16 * 1024 * 1024)
|
|
98
|
+
if canonical_digest(manifest) != expected_digest or raw != canonical_bytes(manifest):
|
|
99
|
+
raise ValueError("source snapshot manifest differs from the retained digest")
|
|
100
|
+
schema = manifest.get("schema_version")
|
|
101
|
+
expected_keys = (
|
|
102
|
+
{"schema_version", "source", "files"}
|
|
103
|
+
if schema == _FULL_SCHEMA
|
|
104
|
+
else {"schema_version", "source", "projection", "files"}
|
|
105
|
+
if schema == _PROJECTED_SCHEMA
|
|
106
|
+
else set()
|
|
107
|
+
)
|
|
108
|
+
if set(manifest) != expected_keys:
|
|
109
|
+
raise ValueError("source snapshot schema is invalid")
|
|
110
|
+
source = manifest["source"]
|
|
111
|
+
records = manifest["files"]
|
|
112
|
+
if (
|
|
113
|
+
not isinstance(source, dict)
|
|
114
|
+
or source.get("provenance") != "operator-selected-source"
|
|
115
|
+
or source.get("release_signature_verified") is not False
|
|
116
|
+
):
|
|
117
|
+
raise ValueError("source snapshot cannot claim release trust")
|
|
118
|
+
projection = manifest.get("projection")
|
|
119
|
+
expected_count = (
|
|
120
|
+
source.get("file_count")
|
|
121
|
+
if schema == _FULL_SCHEMA
|
|
122
|
+
else projection.get("file_count")
|
|
123
|
+
if isinstance(projection, dict)
|
|
124
|
+
else None
|
|
125
|
+
)
|
|
126
|
+
expected_content_digest = (
|
|
127
|
+
source.get("content_digest")
|
|
128
|
+
if schema == _FULL_SCHEMA
|
|
129
|
+
else projection.get("content_digest")
|
|
130
|
+
if isinstance(projection, dict)
|
|
131
|
+
else None
|
|
132
|
+
)
|
|
133
|
+
if (
|
|
134
|
+
not isinstance(records, list)
|
|
135
|
+
or len(records) != expected_count
|
|
136
|
+
or (
|
|
137
|
+
schema == _PROJECTED_SCHEMA
|
|
138
|
+
and (
|
|
139
|
+
not isinstance(projection, dict)
|
|
140
|
+
or set(projection) != {"purpose", "file_count", "content_digest"}
|
|
141
|
+
or not isinstance(projection.get("purpose"), str)
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
):
|
|
145
|
+
raise ValueError("source snapshot inventory is invalid")
|
|
146
|
+
if canonical_digest({"files": records}) != expected_content_digest:
|
|
147
|
+
raise ValueError("source snapshot content binding is invalid")
|
|
148
|
+
tree = directory / "tree"
|
|
149
|
+
if tree.is_symlink() or not tree.is_dir():
|
|
150
|
+
raise ValueError("source snapshot root is invalid")
|
|
151
|
+
paths = set()
|
|
152
|
+
for record in records:
|
|
153
|
+
relative, mode = record["path"], record["mode"]
|
|
154
|
+
if relative in paths:
|
|
155
|
+
raise ValueError("source snapshot repeats a file")
|
|
156
|
+
paths.add(relative)
|
|
157
|
+
content = _read_tracked(tree, relative, mode=mode)
|
|
158
|
+
if hashlib.sha256(content).hexdigest() != record["sha256"]:
|
|
159
|
+
raise ValueError("source snapshot content changed")
|
|
160
|
+
if mode == "120000":
|
|
161
|
+
target = (tree / relative).resolve(strict=True)
|
|
162
|
+
if not target.is_relative_to(tree.resolve()) or target.relative_to(
|
|
163
|
+
tree
|
|
164
|
+
).as_posix() not in {item["path"] for item in records}:
|
|
165
|
+
raise ValueError("source snapshot link leaves the tracked file set")
|
|
166
|
+
actual = set()
|
|
167
|
+
for root, directories, files in os.walk(tree, followlinks=False):
|
|
168
|
+
for name in directories:
|
|
169
|
+
if (Path(root) / name).is_symlink():
|
|
170
|
+
raise ValueError("source snapshot cannot contain linked directories")
|
|
171
|
+
for name in files:
|
|
172
|
+
path = Path(root) / name
|
|
173
|
+
if not stat.S_ISREG(path.lstat().st_mode) and not path.is_symlink():
|
|
174
|
+
raise ValueError("source snapshot contains a special file")
|
|
175
|
+
actual.add(path.relative_to(tree).as_posix())
|
|
176
|
+
if actual != paths:
|
|
177
|
+
raise ValueError("source snapshot file set changed")
|
|
178
|
+
return source
|