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
start a container, install a unit, restart a service, or contact Cloud SQL.
Registry authentication and this pull require a separate live-read approval.
The exact repository-digest reference must be absent before inspection; use a
fresh isolated Docker context if it is already cached. While holding an
advisory lock on the explicit Docker config directory, the command pulls,
records the dynamically observed config ID, then removes the reference only if
that ID is still unchanged and proves it absent before publishing the bundle.
Content-addressable layers may remain. A failed pull, inspection, or cleanup
returns a bounded error without forwarding Docker stderr.
While holding an advisory lock on the explicit Docker config directory, the
command pulls and records the dynamically observed config ID. It intentionally
retains the exact repository-digest reference: Docker's image cache belongs to
the daemon, so a config-directory lock cannot prove that another client did not
start relying on the same immutable reference after the precheck. Repeated runs
re-pull and re-inspect the same digest without deleting shared cache state.
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,
extra descriptor fields, wrong-architecture images, missing repository-digest
@ -202,7 +204,8 @@ values are rejected.
## Offline evidence and live gate
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
capability-drop contract, no published ports, no Docker-socket mount, no
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
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
receipt. Live installation, Artifact Registry publication, service restart,
IAM/secret changes, Cloud SQL access, and proposal staging remain separate
@ -240,10 +243,10 @@ approvals.
## Rollback and sunset
This slice's rollback boundary is removal of the unpublished atomic output
bundle plus removal of the exact repository-digest reference that this command
introduced into an initially clean, advisory-locked cache; content-addressable
layers may remain. It installs no unit
and changes no running service. Restoring a prior descriptor, unit, and digest
bundle. The verified repository-digest reference and content-addressable layers
may remain in the local daemon cache so the finalizer never deletes shared cache
state it cannot exclusively own. It installs no unit and changes no running
service. Restoring a prior descriptor, unit, and digest
belongs to the later installer slice, which must never restart or edit the
production Leo, Telegram, or VPS services.

View file

@ -1781,6 +1781,7 @@ def inspect_registry_image(
docker_binary: Path,
docker_host: str,
docker_config: Path,
cleanup_new_reference: bool = False,
) -> dict[str, Any]:
"""Pull and inspect one immutable Artifact Registry image without starting it."""
@ -1812,11 +1813,12 @@ def inspect_registry_image(
primary_error: BaseException | None = None
with _DockerCacheLock(docker_config):
try:
_require(
_local_image_reference_config_id(docker_binary, docker_host, docker_config, image_reference) is None,
"exact registry image reference must be absent before inspection",
)
prestate_observed = True
if cleanup_new_reference:
_require(
_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
_run_docker(
docker_binary,
docker_host,
@ -1852,7 +1854,7 @@ def inspect_registry_image(
primary_error = exc
finally:
try:
if prestate_observed:
if cleanup_new_reference and prestate_observed:
if pulled_config_id is None:
pulled_config_id = _local_image_reference_config_id(
docker_binary,
@ -2390,6 +2392,7 @@ def main(argv: list[str] | None = None) -> int:
docker_binary=args.docker_binary,
docker_host=args.docker_host,
docker_config=args.docker_config,
cleanup_new_reference=False,
)
result = build_release_descriptor(image_input, inspection)
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_host=docker_host,
docker_config=docker_config,
cleanup_new_reference=True,
)
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]]
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(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
@ -1177,6 +1224,7 @@ def test_registry_inspection_rejects_reviewed_config_digest_mismatch_and_cleans_
docker_binary=docker_binary,
docker_host=docker_host,
docker_config=docker_config,
cleanup_new_reference=True,
)
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_host=docker_host,
docker_config=docker_config,
cleanup_new_reference=True,
)
assert state["present"] is False
@ -1278,6 +1327,7 @@ def test_registry_inspection_validation_failure_removes_new_reference(
docker_binary=docker_binary,
docker_host=docker_host,
docker_config=docker_config,
cleanup_new_reference=True,
)
assert state == {"present": False, "removed": True}
@ -1328,6 +1378,7 @@ def test_registry_inspection_cleanup_failure_prevents_success(
docker_binary=docker_binary,
docker_host=docker_host,
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_host=docker_host,
docker_config=docker_config,
cleanup_new_reference=True,
)
assert state["present"] is True
@ -1425,6 +1477,7 @@ def test_registry_inspection_rejects_preexisting_reference_without_mutation(
docker_binary=docker_binary,
docker_host=docker_host,
docker_config=docker_config,
cleanup_new_reference=True,
)
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_host": docker_host,
"docker_config": docker_config,
"cleanup_new_reference": False,
}
assert json.loads(capsys.readouterr().out) == {"schema": package.RELEASE_SCHEMA, "status": "pass"}