"""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", ], }