104 lines
4.7 KiB
Python
104 lines
4.7 KiB
Python
"""Pure, fail-closed migration planning; this module cannot mutate Docker or files.
|
|
|
|
Accept only the sanitized inspect projection documented in README.md. A plan is
|
|
not deployment authorization, a snapshot, a receipt, or proof of successful backup.
|
|
"""
|
|
|
|
import re
|
|
from pathlib import PurePosixPath
|
|
|
|
|
|
TARGETS = {
|
|
"vacatureradar": ("VacatureRadar", "/mnt/pool/appdata/VacatureRadar"),
|
|
"lumaops": ("lumaops", "/mnt/pool/appdata/lumaops"),
|
|
}
|
|
SHA = re.compile(r"[0-9a-f]{40}")
|
|
ID = re.compile(r"[0-9a-f]{64}")
|
|
ATTEMPT = re.compile(r"[a-z0-9][a-z0-9-]{7,47}")
|
|
|
|
|
|
def _under(path, parent):
|
|
value = PurePosixPath(path)
|
|
return (
|
|
value.is_absolute()
|
|
and ".." not in value.parts
|
|
and value != PurePosixPath(parent)
|
|
and value.is_relative_to(parent)
|
|
)
|
|
|
|
|
|
def build_plan(slug, revision, attempt, observed):
|
|
"""Bind one target to retained runtime identity and isolated candidate data."""
|
|
if slug not in TARGETS or not SHA.fullmatch(revision) or not ATTEMPT.fullmatch(attempt):
|
|
raise ValueError("Invalid fixed target, full revision, or attempt identifier")
|
|
expected_name, appdata = TARGETS[slug]
|
|
keys = {"name", "containerId", "imageId", "running", "health", "composeProject", "mounts"}
|
|
if set(observed) != keys:
|
|
raise ValueError("Expected only the sanitized runtime projection")
|
|
if (
|
|
observed["name"] != expected_name
|
|
or observed["composeProject"] != slug
|
|
or not ID.fullmatch(observed["containerId"])
|
|
or not re.fullmatch(r"sha256:[0-9a-f]{64}", observed["imageId"])
|
|
or observed["running"] is not True
|
|
or observed["health"] != "healthy"
|
|
):
|
|
raise ValueError("Legacy runtime identity or health does not match")
|
|
mounts = observed["mounts"]
|
|
if not mounts or len(mounts) > 16:
|
|
raise ValueError("Expected bounded persistent mounts")
|
|
destinations = set()
|
|
sources = set()
|
|
for mount in mounts:
|
|
if set(mount) != {"type", "source", "destination", "rw"}:
|
|
raise ValueError("Unexpected mount fields")
|
|
destination = mount["destination"]
|
|
if not _under(destination, "/") or destination in destinations:
|
|
raise ValueError("Invalid or duplicate mount destination")
|
|
destinations.add(destination)
|
|
source = mount["source"]
|
|
if mount["type"] == "bind":
|
|
valid = _under(source, appdata)
|
|
elif mount["type"] == "volume":
|
|
valid = bool(re.fullmatch(r"/var/lib/docker/volumes/[0-9a-f]{64}/_data", source))
|
|
else:
|
|
valid = False
|
|
if not valid or mount["rw"] is not True:
|
|
raise ValueError("Unreviewed source, mount type, or access mode")
|
|
sources.add(source)
|
|
# Clone a parent only once; preserve nested bind destinations against the
|
|
# corresponding cloned subdirectory, not unrelated independent snapshots.
|
|
roots = sorted(source for source in sources if not any(
|
|
other != source and _under(source, other) for other in sources
|
|
))
|
|
candidate_root = f"/mnt/pool/appdata/gitea-deploy-runners/legacy-migrations/{slug}/{attempt}"
|
|
clones = {source: f"{candidate_root}/data/{index}" for index, source in enumerate(roots)}
|
|
candidate_mounts = []
|
|
for mount in mounts:
|
|
root = next(source for source in roots if mount["source"] == source or _under(mount["source"], source))
|
|
relative = PurePosixPath(mount["source"]).relative_to(root)
|
|
candidate_mounts.append({
|
|
"source": str(PurePosixPath(clones[root]) / relative),
|
|
"destination": mount["destination"],
|
|
"type": "bind", "rw": True,
|
|
})
|
|
return {
|
|
"schema": "legacy-migration-plan.v1", "executable": False,
|
|
"repository": f"jens/{slug}", "candidateRevision": revision,
|
|
"originalProvenance": "unknown-legacy", "originalRevision": None,
|
|
"originalContainerId": observed["containerId"], "originalImageId": observed["imageId"],
|
|
"originalName": expected_name, "retainedName": f"{expected_name}-legacy-{attempt}",
|
|
"candidateComposeProject": f"{slug}-managed",
|
|
"workspace": candidate_root,
|
|
"coldCopyRoots": [{"original": root, "candidate": clones[root]} for root in roots],
|
|
"candidateMounts": candidate_mounts,
|
|
"requiredGates": [
|
|
"exact-commit-and-tree-validated", "candidate-built-before-cutover",
|
|
"immutable-oci-provenance-verified", "project-and-global-locks-held",
|
|
"original-identity-revalidated", "legacy-stopped-before-data-copy",
|
|
"cold-copy-checksums-verified", "original-data-never-mounted-by-candidate",
|
|
"candidate-container-ids-recorded", "docker-and-http-health-verified",
|
|
"real-broker-receipt-verified", "broker-config-and-state-committed",
|
|
],
|
|
}
|