Retain inspected digest references in shared Docker caches

This commit is contained in:
twentyOne2x 2026-07-21 05:47:01 +02:00
parent e42d635523
commit c7b4abc347
3 changed files with 79 additions and 19 deletions

View file

@ -186,13 +186,15 @@ The Docker binary, config directory, and local `unix://` daemon endpoint are
explicit inputs; inherited `DOCKER_*` overrides are removed. The pull does not explicit inputs; inherited `DOCKER_*` overrides are removed. The pull does not
start a container, install a unit, restart a service, or contact Cloud SQL. start a container, install a unit, restart a service, or contact Cloud SQL.
Registry authentication and this pull require a separate live-read approval. Registry authentication and this pull require a separate live-read approval.
The exact repository-digest reference must be absent before inspection; use a While holding an advisory lock on the explicit Docker config directory, the
fresh isolated Docker context if it is already cached. While holding an command pulls and records the dynamically observed config ID. It intentionally
advisory lock on the explicit Docker config directory, the command pulls, retains the exact repository-digest reference: Docker's image cache belongs to
records the dynamically observed config ID, then removes the reference only if the daemon, so a config-directory lock cannot prove that another client did not
that ID is still unchanged and proves it absent before publishing the bundle. start relying on the same immutable reference after the precheck. Repeated runs
Content-addressable layers may remain. A failed pull, inspection, or cleanup re-pull and re-inspect the same digest without deleting shared cache state.
returns a bounded error without forwarding Docker stderr. Content-addressable layers and the verified digest reference may remain. A
failed pull or inspection returns a bounded error without forwarding Docker
stderr.
Tags, alternate repositories, alternate projects/instances/service accounts, Tags, alternate repositories, alternate projects/instances/service accounts,
extra descriptor fields, wrong-architecture images, missing repository-digest extra descriptor fields, wrong-architecture images, missing repository-digest
@ -202,7 +204,8 @@ values are rejected.
## Offline evidence and live gate ## Offline evidence and live gate
Offline tests prove descriptor and inspection strictness, explicit local Docker Offline tests prove descriptor and inspection strictness, explicit local Docker
authority, cache-reference rollback, atomic bundle publication, artifact and synthetic identity binding, authority, non-destructive cache reuse, atomic bundle publication, artifact and
synthetic identity binding,
digest-only images, pinned runtime versions, a secret-free context, the digest-only images, pinned runtime versions, a secret-free context, the
capability-drop contract, no published ports, no Docker-socket mount, no capability-drop contract, no published ports, no Docker-socket mount, no
environment file, and no production/VPS/Telegram target. CI also builds exactly environment file, and no production/VPS/Telegram target. CI also builds exactly
@ -231,7 +234,7 @@ virtual environment.
Publishing the draft branch and stacked PR is only a review handoff. The next Publishing the draft branch and stacked PR is only a review handoff. The next
deployment-evidence step must execute one authorized remote registry read plus deployment-evidence step must execute one authorized remote registry read plus
the reversible local exact-reference cache mutation against the staging digest. the retained local exact-reference cache mutation against the staging digest.
A later installer slice must prove installation and rollback consume the same A later installer slice must prove installation and rollback consume the same
receipt. Live installation, Artifact Registry publication, service restart, receipt. Live installation, Artifact Registry publication, service restart,
IAM/secret changes, Cloud SQL access, and proposal staging remain separate IAM/secret changes, Cloud SQL access, and proposal staging remain separate
@ -240,10 +243,10 @@ approvals.
## Rollback and sunset ## Rollback and sunset
This slice's rollback boundary is removal of the unpublished atomic output This slice's rollback boundary is removal of the unpublished atomic output
bundle plus removal of the exact repository-digest reference that this command bundle. The verified repository-digest reference and content-addressable layers
introduced into an initially clean, advisory-locked cache; content-addressable may remain in the local daemon cache so the finalizer never deletes shared cache
layers may remain. It installs no unit state it cannot exclusively own. It installs no unit and changes no running
and changes no running service. Restoring a prior descriptor, unit, and digest service. Restoring a prior descriptor, unit, and digest
belongs to the later installer slice, which must never restart or edit the belongs to the later installer slice, which must never restart or edit the
production Leo, Telegram, or VPS services. production Leo, Telegram, or VPS services.

View file

@ -1781,6 +1781,7 @@ def inspect_registry_image(
docker_binary: Path, docker_binary: Path,
docker_host: str, docker_host: str,
docker_config: Path, docker_config: Path,
cleanup_new_reference: bool = False,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Pull and inspect one immutable Artifact Registry image without starting it.""" """Pull and inspect one immutable Artifact Registry image without starting it."""
@ -1812,11 +1813,12 @@ def inspect_registry_image(
primary_error: BaseException | None = None primary_error: BaseException | None = None
with _DockerCacheLock(docker_config): with _DockerCacheLock(docker_config):
try: try:
_require( if cleanup_new_reference:
_local_image_reference_config_id(docker_binary, docker_host, docker_config, image_reference) is None, _require(
"exact registry image reference must be absent before inspection", _local_image_reference_config_id(docker_binary, docker_host, docker_config, image_reference) is None,
) "exact registry image reference must be absent before inspection cleanup",
prestate_observed = True )
prestate_observed = True
_run_docker( _run_docker(
docker_binary, docker_binary,
docker_host, docker_host,
@ -1852,7 +1854,7 @@ def inspect_registry_image(
primary_error = exc primary_error = exc
finally: finally:
try: try:
if prestate_observed: if cleanup_new_reference and prestate_observed:
if pulled_config_id is None: if pulled_config_id is None:
pulled_config_id = _local_image_reference_config_id( pulled_config_id = _local_image_reference_config_id(
docker_binary, docker_binary,
@ -2390,6 +2392,7 @@ def main(argv: list[str] | None = None) -> int:
docker_binary=args.docker_binary, docker_binary=args.docker_binary,
docker_host=args.docker_host, docker_host=args.docker_host,
docker_config=args.docker_config, docker_config=args.docker_config,
cleanup_new_reference=False,
) )
result = build_release_descriptor(image_input, inspection) result = build_release_descriptor(image_input, inspection)
publish_finalize_bundle(args.output_bundle, result) publish_finalize_bundle(args.output_bundle, result)

View file

@ -1125,6 +1125,7 @@ def test_registry_inspection_uses_explicit_local_authority_and_removes_new_refer
docker_binary=docker_binary, docker_binary=docker_binary,
docker_host=docker_host, docker_host=docker_host,
docker_config=docker_config, docker_config=docker_config,
cleanup_new_reference=True,
) )
assert inspection["status"] == "pass" assert inspection["status"] == "pass"
@ -1136,6 +1137,52 @@ def test_registry_inspection_uses_explicit_local_authority_and_removes_new_refer
assert [call[5:] for call in calls if call[5:7] == ["image", "rm"]] == [["image", "rm", reference]] assert [call[5:] for call in calls if call[5:7] == ["image", "rm"]] == [["image", "rm", reference]]
def test_registry_inspection_default_retains_preexisting_reference(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
image_input = image_input_fixture(tmp_path)
reference = f"{package.IMAGE_REPOSITORY}@sha256:{IMAGE_DIGEST}"
docker_binary, docker_host, docker_config = docker_authority(tmp_path)
payload = json.dumps([docker_inspection_fixture(image_input, reference)]).encode()
state = {"present": True, "pulls": 0, "removed": False}
def fake_run(arguments: list[str], **_kwargs):
operation = arguments[5:]
if operation[:2] == ["image", "ls"]:
return subprocess.CompletedProcess(
arguments,
0,
stdout=docker_inventory_fixture(reference, present=state["present"]),
stderr=b"",
)
if operation[:2] == ["image", "inspect"]:
return subprocess.CompletedProcess(arguments, 0, stdout=payload, stderr=b"")
if operation[0] == "pull":
state["pulls"] += 1
return subprocess.CompletedProcess(arguments, 0, stdout=b"pulled\n", stderr=b"")
if operation[:2] == ["image", "rm"]:
state["removed"] = True
return subprocess.CompletedProcess(arguments, 0, stdout=b"removed\n", stderr=b"")
raise AssertionError(operation)
monkeypatch.setattr(package.subprocess, "run", fake_run)
inspection = package.inspect_registry_image(
image_input,
reference,
expected_revision=image_input["runtime"]["teleo_git_head"],
expected_input_sha256=image_input["input_sha256"],
expected_config_digest="sha256:" + "3" * 64,
docker_binary=docker_binary,
docker_host=docker_host,
docker_config=docker_config,
)
assert inspection["status"] == "pass"
assert state == {"present": True, "pulls": 1, "removed": False}
def test_registry_inspection_rejects_reviewed_config_digest_mismatch_and_cleans_reference( def test_registry_inspection_rejects_reviewed_config_digest_mismatch_and_cleans_reference(
tmp_path: Path, tmp_path: Path,
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
@ -1177,6 +1224,7 @@ def test_registry_inspection_rejects_reviewed_config_digest_mismatch_and_cleans_
docker_binary=docker_binary, docker_binary=docker_binary,
docker_host=docker_host, docker_host=docker_host,
docker_config=docker_config, docker_config=docker_config,
cleanup_new_reference=True,
) )
assert "expected reviewed config digest" in str(raised.value.__cause__) assert "expected reviewed config digest" in str(raised.value.__cause__)
@ -1224,6 +1272,7 @@ def test_registry_inspection_failure_is_redacted_and_cleans_new_reference(
docker_binary=docker_binary, docker_binary=docker_binary,
docker_host=docker_host, docker_host=docker_host,
docker_config=docker_config, docker_config=docker_config,
cleanup_new_reference=True,
) )
assert state["present"] is False assert state["present"] is False
@ -1278,6 +1327,7 @@ def test_registry_inspection_validation_failure_removes_new_reference(
docker_binary=docker_binary, docker_binary=docker_binary,
docker_host=docker_host, docker_host=docker_host,
docker_config=docker_config, docker_config=docker_config,
cleanup_new_reference=True,
) )
assert state == {"present": False, "removed": True} assert state == {"present": False, "removed": True}
@ -1328,6 +1378,7 @@ def test_registry_inspection_cleanup_failure_prevents_success(
docker_binary=docker_binary, docker_binary=docker_binary,
docker_host=docker_host, docker_host=docker_host,
docker_config=docker_config, docker_config=docker_config,
cleanup_new_reference=True,
) )
@ -1383,6 +1434,7 @@ def test_registry_inspection_refuses_to_remove_changed_reference(
docker_binary=docker_binary, docker_binary=docker_binary,
docker_host=docker_host, docker_host=docker_host,
docker_config=docker_config, docker_config=docker_config,
cleanup_new_reference=True,
) )
assert state["present"] is True assert state["present"] is True
@ -1425,6 +1477,7 @@ def test_registry_inspection_rejects_preexisting_reference_without_mutation(
docker_binary=docker_binary, docker_binary=docker_binary,
docker_host=docker_host, docker_host=docker_host,
docker_config=docker_config, docker_config=docker_config,
cleanup_new_reference=True,
) )
assert "must be absent" in str(raised.value.__cause__) assert "must be absent" in str(raised.value.__cause__)
@ -1649,6 +1702,7 @@ def test_finalize_embeds_inspection_before_rendering_unit(
"docker_binary": docker_binary, "docker_binary": docker_binary,
"docker_host": docker_host, "docker_host": docker_host,
"docker_config": docker_config, "docker_config": docker_config,
"cleanup_new_reference": False,
} }
assert json.loads(capsys.readouterr().out) == {"schema": package.RELEASE_SCHEMA, "status": "pass"} assert json.loads(capsys.readouterr().out) == {"schema": package.RELEASE_SCHEMA, "status": "pass"}