Retain inspected digest references in shared Docker caches
This commit is contained in:
parent
e42d635523
commit
c7b4abc347
3 changed files with 79 additions and 19 deletions
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,9 +1813,10 @@ def inspect_registry_image(
|
||||||
primary_error: BaseException | None = None
|
primary_error: BaseException | None = None
|
||||||
with _DockerCacheLock(docker_config):
|
with _DockerCacheLock(docker_config):
|
||||||
try:
|
try:
|
||||||
|
if cleanup_new_reference:
|
||||||
_require(
|
_require(
|
||||||
_local_image_reference_config_id(docker_binary, docker_host, docker_config, image_reference) is None,
|
_local_image_reference_config_id(docker_binary, docker_host, docker_config, image_reference) is None,
|
||||||
"exact registry image reference must be absent before inspection",
|
"exact registry image reference must be absent before inspection cleanup",
|
||||||
)
|
)
|
||||||
prestate_observed = True
|
prestate_observed = True
|
||||||
_run_docker(
|
_run_docker(
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue