6.7 KiB
@{ Tooling.DockerBundle.Contract [C:4] [TYPE ADR]
@BRIEF Full enterprise-v2 offline image import with bound deployment metadata, validated before Docker operations. @RATIONALE A checksum does not establish image tags, image identity, publisher authenticity or source provenance. @REJECTED Unversioned/light manifests and shell xz pipelines are not accepted compatibility paths.
Package both scripts/import_docker_bundle.py and scripts/docker_bundle_contract.py together in the bundle root. The importer uses Python's standard library plus installed xz and Docker CLI.
python3 import_docker_bundle.py --bundle-dir . \
--manifest manifest.v1.0.0.json --expected-release-tag v1.0.0 --verify-only
python3 import_docker_bundle.py --bundle-dir . \
--manifest manifest.v1.0.0.json --expected-release-tag v1.0.0
The manifest must contain exactly manifest_version, bundle_kind, release_tag, generated_at_utc, images, compose_file, compose_sha256, env_template, env_template_sha256, env_bootstrap_fields, and checksums_file. Version is integer2, kind enterprise, release equals the explicitly requested Docker tag, timestamp is UTC YYYY-MM-DDTHH:MM:SSZ. The deployment fields are exactly docker-compose.enterprise-clean.yml, .env.example, the ordered INITIAL_ADMIN_CREATE/USERNAME/PASSWORD/EMAIL names, and sha256sums.<tag>.txt.
The producer generates both deployment files before publishing the manifest. Each required metadata digest is lowercase64hex SHA256 of the exact corresponding file bytes. The importer reads each file through its regular no-follow descriptor with a 1MiB bound and verifies both hashes before any Docker query or load. Replacing the shared Compose with another release or editing the template makes the earlier manifest refuse; preserve each release as a complete separate directory. The operator's editable .env is not part of this verification. Version1 has no fallback. These two hashes remain separate from the exact three archive checksum lines and do not establish authenticity or parse Compose semantics.
Images contain exactly three unique roles postgres, backend, frontend. Each record has exactly role, image, image_id, repo_digest, archive: image is superset-tools-<role>:<tag>, ID is lowercase sha256:<64hex>, archive is superset-tools-<role>.<tag>.tar.xz. Repository digests are informational explicit digest references or unavailable. The checksum file has exactly three SHA256 lines, one per expected archive, without duplicates or extras. Unversioned, single-image and light bundles require another explicit schema/kind and are refused here.
All named files are opened relative to a no-follow directory descriptor, nonblocking, then checked as regular files on that same descriptor. Archive descriptors stay open across validation and load; filenames are never reopened for the load pipeline. Size/mtime/ctime/inode changes are checked. This closes pathname/symlink replacement gaps; it does not promise atomic exclusion of in-place writers. A change detected after a load is an explicit partial-failure state, with no rollback or global image deletion.
Before any image load, every archive passes compressed SHA256, real xz --test, full streaming tar/xz integrity and internal image validation. Nothing is extracted onto the filesystem. Outer tar links/devices/FIFOs/sparse/PAX members, unsafe paths, duplicates, undeclared images/files, foreign tags and nonzero trailing tar data are refused. Layer payloads are streamed, not extracted or subject to an arbitrary small size cap. Metadata bounds are manifest/index1MiB, per config16MiB, aggregate retained metadata64MiB, JSON depth64 and 100000 archive members.
Supported shapes are Docker-save classic single-image metadata and the observed Docker-save OCI-layout hybrid with manifest.json, optional LayerSources, index.json, oci-layout, blobs and backward layer metadata/repositories. The config filename and actual SHA256 must equal the expected image ID; layer bytes must match config rootfs diff IDs. Both Docker-compatible RepoTags and OCI references are checked. Root OCI annotations are exactly the image-name/ref-name pair; config/layer descriptors cannot add annotations/platform. LayerSources keys bind the exact corresponding layer digest/path. Multi-image/multi-platform indexes, compressed OCI layers and other undeclared shapes remain visibly unsupported; the importer never guesses an alternative layout.
The actual owned Docker-save probe artifacts/.orchestrator/docker-save-format-probe.tar was inspected read-only: OCI-layout hybrid, one image, 28 regular members, 11 layers, LayerSources and backward metadata. Docker/OCI manifest media types have explicit allowlists. Reference semantics: Docker save, Docker load, Moby image archive specification, containerd exporter.
After complete offline validation, all three target tags are checked before the first load. A different existing ID refuses the whole import; matching IDs are idempotent. Read-only Docker queries have a 16KiB output cap and ten-second deadline; only an owned query client is reaped on timeout/output refusal. A load uses held-FD stdin → xz --decompress --stdout → docker image load --quiet, argv lists without shell. Both child RCs are checked, and each resulting tag plus the complete final tag set is inspected for exact IDs. No Compose startup, credential read, container operation, automatic pull, rollback or cleanup occurs.
CLI stdout is JSON (except --help): exit0 means verified, imported or already_present; exit2 is preflight refusal with no load attempt; exit3 is load/post-inspect/input-change failure after a role attempt. verified_roles, loaded_roles, existing_roles, attempted_roles, fixed code, and partial_load_possible distinguish completed work from possible partial Docker mutation. Pipeline RCs are retained when available. --verify-only performs no Docker query/mutation and reports target_state: not_checked.
Checksums provide integrity, not authenticity. Output reports authenticity: not_established_by_checksums and source_identity: {status: unavailable, reason: manifest_v2_has_no_source_identity}. Tags, local Git and plausible metadata never substitute for absent source provenance. A failed import can leave already-loaded images; inspect that explicit state before a separately chosen retry. Never infer a successful deployment from image import alone.