Compare commits
1 Commits
codex/host
...
codex/cicd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c43b3da2e |
@@ -133,10 +133,7 @@ jobs:
|
|||||||
# 2026-06-28 Codex: awoooi-non110-host maps to the dedicated
|
# 2026-06-28 Codex: awoooi-non110-host maps to the dedicated
|
||||||
# non-110 runner lane. Bootstrap tools defensively because host
|
# non-110 runner lane. Bootstrap tools defensively because host
|
||||||
# runners can start without the CI toolchain preinstalled.
|
# runners can start without the CI toolchain preinstalled.
|
||||||
run: |
|
run: bash scripts/ci/bootstrap-host-runner-tools.sh
|
||||||
if command -v apk >/dev/null 2>&1; then
|
|
||||||
apk add --no-cache nodejs npm git curl bash coreutils python3 openssh-client docker-cli docker-cli-buildx
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Checkout source from Gitea
|
- name: Checkout source from Gitea
|
||||||
env:
|
env:
|
||||||
@@ -1454,10 +1451,7 @@ jobs:
|
|||||||
- name: Bootstrap Host Runner Tools
|
- name: Bootstrap Host Runner Tools
|
||||||
# 2026-05-05 Codex: keep the host-mode runner self-healing before
|
# 2026-05-05 Codex: keep the host-mode runner self-healing before
|
||||||
# actions/checkout@v4 and Telegram failure notifications run.
|
# actions/checkout@v4 and Telegram failure notifications run.
|
||||||
run: |
|
run: bash scripts/ci/bootstrap-host-runner-tools.sh
|
||||||
if command -v apk >/dev/null 2>&1; then
|
|
||||||
apk add --no-cache nodejs npm git curl bash coreutils python3 openssh-client docker-cli docker-cli-buildx
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Checkout source from Gitea
|
- name: Checkout source from Gitea
|
||||||
env:
|
env:
|
||||||
@@ -5226,10 +5220,7 @@ jobs:
|
|||||||
- name: Bootstrap Host Runner Tools
|
- name: Bootstrap Host Runner Tools
|
||||||
# 2026-05-05 Codex: post-deploy also uses checkout and curl-based
|
# 2026-05-05 Codex: post-deploy also uses checkout and curl-based
|
||||||
# notifications, so it needs the same runner bootstrap as earlier jobs.
|
# notifications, so it needs the same runner bootstrap as earlier jobs.
|
||||||
run: |
|
run: bash scripts/ci/bootstrap-host-runner-tools.sh
|
||||||
if command -v apk >/dev/null 2>&1; then
|
|
||||||
apk add --no-cache nodejs npm git curl bash coreutils python3 openssh-client docker-cli docker-cli-buildx
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Checkout source from Gitea
|
- name: Checkout source from Gitea
|
||||||
env:
|
env:
|
||||||
|
|||||||
43
scripts/ci/bootstrap-host-runner-tools.sh
Executable file
43
scripts/ci/bootstrap-host-runner-tools.sh
Executable file
@@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
required_commands=(node npm git curl bash timeout python3 ssh docker)
|
||||||
|
missing_commands=()
|
||||||
|
|
||||||
|
for command_name in "${required_commands[@]}"; do
|
||||||
|
if ! command -v "${command_name}" >/dev/null 2>&1; then
|
||||||
|
missing_commands+=("${command_name}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if command -v docker >/dev/null 2>&1 && \
|
||||||
|
! docker buildx version >/dev/null 2>&1; then
|
||||||
|
missing_commands+=(docker-buildx)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#missing_commands[@]}" -eq 0 ]; then
|
||||||
|
echo "host_runner_tools_ready=1 bootstrap_install_performed=0"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'host_runner_tools_missing=%s\n' "$(IFS=,; echo "${missing_commands[*]}")"
|
||||||
|
if ! command -v apk >/dev/null 2>&1; then
|
||||||
|
echo "BLOCKER host_runner_tools_missing_and_apk_unavailable"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
apk add --no-cache \
|
||||||
|
nodejs npm git curl bash coreutils python3 openssh-client docker-cli docker-cli-buildx
|
||||||
|
|
||||||
|
for command_name in "${required_commands[@]}"; do
|
||||||
|
command -v "${command_name}" >/dev/null 2>&1 || {
|
||||||
|
echo "BLOCKER host_runner_tool_install_failed command=${command_name}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
docker buildx version >/dev/null 2>&1 || {
|
||||||
|
echo "BLOCKER host_runner_tool_install_failed command=docker-buildx"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "host_runner_tools_ready=1 bootstrap_install_performed=1"
|
||||||
96
scripts/ci/tests/test_bootstrap_host_runner_tools.py
Normal file
96
scripts/ci/tests/test_bootstrap_host_runner_tools.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[3]
|
||||||
|
SCRIPT = ROOT / "scripts/ci/bootstrap-host-runner-tools.sh"
|
||||||
|
REQUIRED = ("node", "npm", "git", "curl", "bash", "timeout", "python3", "ssh")
|
||||||
|
|
||||||
|
|
||||||
|
def _write_command(path: Path, body: str = "exit 0\n") -> None:
|
||||||
|
path.write_text("#!/bin/sh\n" + body, encoding="utf-8")
|
||||||
|
path.chmod(0o755)
|
||||||
|
|
||||||
|
|
||||||
|
def _fake_tool_path(directory: Path, *, include_npm: bool) -> None:
|
||||||
|
for command in REQUIRED:
|
||||||
|
if command != "npm" or include_npm:
|
||||||
|
_write_command(directory / command)
|
||||||
|
_write_command(directory / "docker")
|
||||||
|
|
||||||
|
|
||||||
|
def _run(directory: Path) -> subprocess.CompletedProcess[str]:
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["PATH"] = str(directory)
|
||||||
|
return subprocess.run(
|
||||||
|
["/bin/bash", str(SCRIPT)],
|
||||||
|
check=False,
|
||||||
|
text=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ready_runner_skips_package_install() -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temp:
|
||||||
|
directory = Path(temp)
|
||||||
|
_fake_tool_path(directory, include_npm=True)
|
||||||
|
|
||||||
|
completed = _run(directory)
|
||||||
|
|
||||||
|
assert completed.returncode == 0
|
||||||
|
assert "host_runner_tools_ready=1 bootstrap_install_performed=0" in completed.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_tool_without_apk_fails_closed() -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temp:
|
||||||
|
directory = Path(temp)
|
||||||
|
_fake_tool_path(directory, include_npm=False)
|
||||||
|
|
||||||
|
completed = _run(directory)
|
||||||
|
|
||||||
|
assert completed.returncode == 1
|
||||||
|
assert "host_runner_tools_missing=npm" in completed.stdout
|
||||||
|
assert "BLOCKER host_runner_tools_missing_and_apk_unavailable" in completed.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_tool_uses_apk_once_and_revalidates() -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temp:
|
||||||
|
directory = Path(temp)
|
||||||
|
_fake_tool_path(directory, include_npm=False)
|
||||||
|
apk_log = directory / "apk.log"
|
||||||
|
_write_command(
|
||||||
|
directory / "apk",
|
||||||
|
'printf "%s\\n" "$*" > "$APK_LOG"\n'
|
||||||
|
'printf "#!/bin/sh\\nexit 0\\n" > "$FAKE_BIN/npm"\n'
|
||||||
|
'/bin/chmod +x "$FAKE_BIN/npm"\n',
|
||||||
|
)
|
||||||
|
env = os.environ.copy()
|
||||||
|
env.update(
|
||||||
|
{
|
||||||
|
"PATH": str(directory),
|
||||||
|
"APK_LOG": str(apk_log),
|
||||||
|
"FAKE_BIN": str(directory),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
completed = subprocess.run(
|
||||||
|
["/bin/bash", str(SCRIPT)],
|
||||||
|
check=False,
|
||||||
|
text=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
|
||||||
|
install_args = apk_log.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert completed.returncode == 0
|
||||||
|
assert "bootstrap_install_performed=1" in completed.stdout
|
||||||
|
assert install_args.startswith("add --no-cache ")
|
||||||
|
assert "docker-cli-buildx" in install_args
|
||||||
@@ -19,9 +19,9 @@ from typing import Any, Sequence
|
|||||||
|
|
||||||
|
|
||||||
SCHEMA_VERSION = "awoooi_runtime_image_mirror_policy_v1"
|
SCHEMA_VERSION = "awoooi_runtime_image_mirror_policy_v1"
|
||||||
RECEIPT_SCHEMA_VERSION = "awoooi_runtime_image_mirror_receipt_v1"
|
RECEIPT_SCHEMA_VERSION = "awoooi_runtime_image_mirror_receipt_v2"
|
||||||
STAGING_SCHEMA_VERSION = "awoooi_runtime_image_mirror_staging_policy_v1"
|
STAGING_SCHEMA_VERSION = "awoooi_runtime_image_mirror_staging_policy_v1"
|
||||||
STAGING_RECEIPT_SCHEMA_VERSION = "awoooi_runtime_image_mirror_staging_receipt_v1"
|
STAGING_RECEIPT_SCHEMA_VERSION = "awoooi_runtime_image_mirror_staging_receipt_v2"
|
||||||
ALLOWED_KINDS = {"deployment", "daemonset", "statefulset"}
|
ALLOWED_KINDS = {"deployment", "daemonset", "statefulset"}
|
||||||
ALLOWED_CONTAINER_KINDS = {"container", "init_container"}
|
ALLOWED_CONTAINER_KINDS = {"container", "init_container"}
|
||||||
DIGEST_PATTERN = re.compile(r"^sha256:[0-9a-f]{64}$")
|
DIGEST_PATTERN = re.compile(r"^sha256:[0-9a-f]{64}$")
|
||||||
@@ -665,16 +665,25 @@ def mirror_images(
|
|||||||
known_hosts: Path,
|
known_hosts: Path,
|
||||||
apply: bool,
|
apply: bool,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
cache = RuntimeCache(policy, ssh_key, known_hosts)
|
cache: RuntimeCache | None = None
|
||||||
image_receipts: list[dict[str, Any]] = []
|
image_receipts: list[dict[str, Any]] = []
|
||||||
missing_count = 0
|
missing_count = 0
|
||||||
for image in policy.images:
|
for image in policy.images:
|
||||||
source_ref = cache.verify_source(image)
|
|
||||||
source_fingerprint = "sha256:" + hashlib.sha256(source_ref.encode()).hexdigest()
|
|
||||||
target_present = _target_digest_present(image)
|
target_present = _target_digest_present(image)
|
||||||
|
source_fingerprint = None
|
||||||
|
source_cache_verified = False
|
||||||
|
source_cache_verification_skipped_reason = "target_digest_already_verified"
|
||||||
execution = "already_present"
|
execution = "already_present"
|
||||||
if not target_present:
|
if not target_present:
|
||||||
missing_count += 1
|
missing_count += 1
|
||||||
|
if cache is None:
|
||||||
|
cache = RuntimeCache(policy, ssh_key, known_hosts)
|
||||||
|
source_ref = cache.verify_source(image)
|
||||||
|
source_fingerprint = (
|
||||||
|
"sha256:" + hashlib.sha256(source_ref.encode()).hexdigest()
|
||||||
|
)
|
||||||
|
source_cache_verified = True
|
||||||
|
source_cache_verification_skipped_reason = None
|
||||||
execution = "check_only_missing"
|
execution = "check_only_missing"
|
||||||
if apply:
|
if apply:
|
||||||
source_preexisting = _local_image_present(source_ref)
|
source_preexisting = _local_image_present(source_ref)
|
||||||
@@ -727,7 +736,10 @@ def mirror_images(
|
|||||||
"platform_digest": image.platform_digest,
|
"platform_digest": image.platform_digest,
|
||||||
"target_registry_digest": image.target_registry_digest,
|
"target_registry_digest": image.target_registry_digest,
|
||||||
"target_digest_ref": _target_digest_ref(image),
|
"target_digest_ref": _target_digest_ref(image),
|
||||||
"source_cache_verified": True,
|
"source_cache_verified": source_cache_verified,
|
||||||
|
"source_cache_verification_skipped_reason": (
|
||||||
|
source_cache_verification_skipped_reason
|
||||||
|
),
|
||||||
"target_digest_verified": target_present,
|
"target_digest_verified": target_present,
|
||||||
"execution": execution,
|
"execution": execution,
|
||||||
}
|
}
|
||||||
@@ -743,6 +755,7 @@ def mirror_images(
|
|||||||
"mode": "apply" if apply else "check",
|
"mode": "apply" if apply else "check",
|
||||||
"external_pull_allowed": False,
|
"external_pull_allowed": False,
|
||||||
"source_cache_only": True,
|
"source_cache_only": True,
|
||||||
|
"source_cache_accessed": cache is not None,
|
||||||
"candidate_count": len(image_receipts),
|
"candidate_count": len(image_receipts),
|
||||||
"missing_before_count": missing_count,
|
"missing_before_count": missing_count,
|
||||||
"verified_count": sum(
|
"verified_count": sum(
|
||||||
@@ -766,20 +779,30 @@ def stage_images(
|
|||||||
known_hosts: Path,
|
known_hosts: Path,
|
||||||
apply: bool,
|
apply: bool,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
cache = RuntimeCache(policy, ssh_key, known_hosts)
|
cache: RuntimeCache | None = None
|
||||||
image_receipts: list[dict[str, Any]] = []
|
image_receipts: list[dict[str, Any]] = []
|
||||||
missing_count = 0
|
missing_count = 0
|
||||||
for image in policy.images:
|
for image in policy.images:
|
||||||
source_ref = cache.verify_source(image)
|
|
||||||
source_fingerprint = "sha256:" + hashlib.sha256(source_ref.encode()).hexdigest()
|
|
||||||
source_config_digest = cache.source_config_digest(image.platform_digest)
|
|
||||||
if source_config_digest != image.source_config_digest:
|
|
||||||
raise ControllerError("runtime_cache_config_digest_mismatch")
|
|
||||||
|
|
||||||
descriptor = _registry_descriptor(image.push_ref)
|
descriptor = _registry_descriptor(image.push_ref)
|
||||||
|
source_fingerprint = None
|
||||||
|
source_cache_verified = False
|
||||||
|
source_cache_verification_skipped_reason = (
|
||||||
|
"internal_registry_provenance_already_verified"
|
||||||
|
)
|
||||||
execution = "already_present"
|
execution = "already_present"
|
||||||
if descriptor is None:
|
if descriptor is None:
|
||||||
missing_count += 1
|
missing_count += 1
|
||||||
|
if cache is None:
|
||||||
|
cache = RuntimeCache(policy, ssh_key, known_hosts)
|
||||||
|
source_ref = cache.verify_source(image)
|
||||||
|
source_fingerprint = (
|
||||||
|
"sha256:" + hashlib.sha256(source_ref.encode()).hexdigest()
|
||||||
|
)
|
||||||
|
source_config_digest = cache.source_config_digest(image.platform_digest)
|
||||||
|
if source_config_digest != image.source_config_digest:
|
||||||
|
raise ControllerError("runtime_cache_config_digest_mismatch")
|
||||||
|
source_cache_verified = True
|
||||||
|
source_cache_verification_skipped_reason = None
|
||||||
execution = "check_only_missing"
|
execution = "check_only_missing"
|
||||||
if apply:
|
if apply:
|
||||||
source_preexisting = _local_image_present(source_ref)
|
source_preexisting = _local_image_present(source_ref)
|
||||||
@@ -836,7 +859,10 @@ def stage_images(
|
|||||||
raise ControllerError("internal_registry_descriptor_missing")
|
raise ControllerError("internal_registry_descriptor_missing")
|
||||||
execution = "staged_from_runtime_cache"
|
execution = "staged_from_runtime_cache"
|
||||||
|
|
||||||
if descriptor is not None and descriptor.config_digest != source_config_digest:
|
if (
|
||||||
|
descriptor is not None
|
||||||
|
and descriptor.config_digest != image.source_config_digest
|
||||||
|
):
|
||||||
raise ControllerError("internal_registry_provenance_mismatch")
|
raise ControllerError("internal_registry_provenance_mismatch")
|
||||||
target_digest_ref = None
|
target_digest_ref = None
|
||||||
runtime_ref = None
|
runtime_ref = None
|
||||||
@@ -858,13 +884,16 @@ def stage_images(
|
|||||||
"source_ref_fingerprint": source_fingerprint,
|
"source_ref_fingerprint": source_fingerprint,
|
||||||
"source_index_digest": image.source_index_digest,
|
"source_index_digest": image.source_index_digest,
|
||||||
"platform_digest": image.platform_digest,
|
"platform_digest": image.platform_digest,
|
||||||
"source_config_digest": source_config_digest,
|
"source_config_digest": image.source_config_digest,
|
||||||
"target_registry_digest": (
|
"target_registry_digest": (
|
||||||
descriptor.digest if descriptor is not None else None
|
descriptor.digest if descriptor is not None else None
|
||||||
),
|
),
|
||||||
"target_digest_ref": target_digest_ref,
|
"target_digest_ref": target_digest_ref,
|
||||||
"runtime_ref": runtime_ref,
|
"runtime_ref": runtime_ref,
|
||||||
"source_cache_verified": True,
|
"source_cache_verified": source_cache_verified,
|
||||||
|
"source_cache_verification_skipped_reason": (
|
||||||
|
source_cache_verification_skipped_reason
|
||||||
|
),
|
||||||
"target_digest_verified": target_verified,
|
"target_digest_verified": target_verified,
|
||||||
"provenance_config_digest_match": descriptor is not None,
|
"provenance_config_digest_match": descriptor is not None,
|
||||||
"execution": execution,
|
"execution": execution,
|
||||||
@@ -889,6 +918,7 @@ def stage_images(
|
|||||||
"risk_level": policy.risk_level,
|
"risk_level": policy.risk_level,
|
||||||
"external_pull_allowed": False,
|
"external_pull_allowed": False,
|
||||||
"source_cache_only": True,
|
"source_cache_only": True,
|
||||||
|
"source_cache_accessed": cache is not None,
|
||||||
"candidate_count": len(image_receipts),
|
"candidate_count": len(image_receipts),
|
||||||
"missing_before_count": missing_count,
|
"missing_before_count": missing_count,
|
||||||
"verified_count": sum(
|
"verified_count": sum(
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ class RuntimeImageMirrorControllerTest(unittest.TestCase):
|
|||||||
policy = controller.load_staging_policy(STAGING_POLICY_PATH)
|
policy = controller.load_staging_policy(STAGING_POLICY_PATH)
|
||||||
return replace(policy, images=(policy.images[0],))
|
return replace(policy, images=(policy.images[0],))
|
||||||
|
|
||||||
|
def _single_image_policy(self):
|
||||||
|
policy = controller.load_policy(POLICY_PATH)
|
||||||
|
return replace(policy, images=(policy.images[0],))
|
||||||
|
|
||||||
def _image_with_availability_guard(self):
|
def _image_with_availability_guard(self):
|
||||||
image = controller.load_policy(POLICY_PATH).images[0]
|
image = controller.load_policy(POLICY_PATH).images[0]
|
||||||
return replace(
|
return replace(
|
||||||
@@ -904,12 +908,12 @@ class RuntimeImageMirrorControllerTest(unittest.TestCase):
|
|||||||
controller.RuntimeCache,
|
controller.RuntimeCache,
|
||||||
"verify_source",
|
"verify_source",
|
||||||
return_value="quay.io/argoproj/argocd:v3.3.6",
|
return_value="quay.io/argoproj/argocd:v3.3.6",
|
||||||
),
|
) as verify_source,
|
||||||
patch.object(
|
patch.object(
|
||||||
controller.RuntimeCache,
|
controller.RuntimeCache,
|
||||||
"source_config_digest",
|
"source_config_digest",
|
||||||
return_value=image.source_config_digest,
|
return_value=image.source_config_digest,
|
||||||
),
|
) as source_config_digest,
|
||||||
patch.object(controller.RuntimeCache, "export") as export,
|
patch.object(controller.RuntimeCache, "export") as export,
|
||||||
patch.object(
|
patch.object(
|
||||||
controller, "_registry_descriptor", return_value=descriptor
|
controller, "_registry_descriptor", return_value=descriptor
|
||||||
@@ -928,11 +932,85 @@ class RuntimeImageMirrorControllerTest(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(receipt["state"], "verified")
|
self.assertEqual(receipt["state"], "verified")
|
||||||
self.assertEqual(receipt["missing_before_count"], 0)
|
self.assertEqual(receipt["missing_before_count"], 0)
|
||||||
|
self.assertFalse(receipt["source_cache_accessed"])
|
||||||
self.assertEqual(receipt["images"][0]["execution"], "already_present")
|
self.assertEqual(receipt["images"][0]["execution"], "already_present")
|
||||||
self.assertTrue(receipt["images"][0]["provenance_config_digest_match"])
|
self.assertTrue(receipt["images"][0]["provenance_config_digest_match"])
|
||||||
|
self.assertFalse(receipt["images"][0]["source_cache_verified"])
|
||||||
|
self.assertEqual(
|
||||||
|
receipt["images"][0]["source_cache_verification_skipped_reason"],
|
||||||
|
"internal_registry_provenance_already_verified",
|
||||||
|
)
|
||||||
|
verify_source.assert_not_called()
|
||||||
|
source_config_digest.assert_not_called()
|
||||||
export.assert_not_called()
|
export.assert_not_called()
|
||||||
run.assert_not_called()
|
run.assert_not_called()
|
||||||
|
|
||||||
|
def test_mirror_reuses_verified_target_digest_without_runtime_cache(self) -> None:
|
||||||
|
policy = self._single_image_policy()
|
||||||
|
with tempfile.TemporaryDirectory() as temp:
|
||||||
|
receipt_path = Path(temp) / "receipt.json"
|
||||||
|
with (
|
||||||
|
patch.object(
|
||||||
|
controller.RuntimeCache,
|
||||||
|
"verify_source",
|
||||||
|
return_value="quay.io/example/image:v1",
|
||||||
|
) as verify_source,
|
||||||
|
patch.object(controller.RuntimeCache, "export") as export,
|
||||||
|
patch.object(controller, "_target_digest_present", return_value=True),
|
||||||
|
patch.object(controller, "_run") as run,
|
||||||
|
):
|
||||||
|
receipt = controller.mirror_images(
|
||||||
|
policy,
|
||||||
|
trace_id="aia-p0-006-02a-existing",
|
||||||
|
receipt_path=receipt_path,
|
||||||
|
ssh_key=Path("/unused/key"),
|
||||||
|
known_hosts=Path("/unused/known-hosts"),
|
||||||
|
apply=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(receipt["state"], "verified")
|
||||||
|
self.assertEqual(receipt["missing_before_count"], 0)
|
||||||
|
self.assertFalse(receipt["source_cache_accessed"])
|
||||||
|
self.assertEqual(receipt["images"][0]["execution"], "already_present")
|
||||||
|
self.assertFalse(receipt["images"][0]["source_cache_verified"])
|
||||||
|
self.assertEqual(
|
||||||
|
receipt["images"][0]["source_cache_verification_skipped_reason"],
|
||||||
|
"target_digest_already_verified",
|
||||||
|
)
|
||||||
|
verify_source.assert_not_called()
|
||||||
|
export.assert_not_called()
|
||||||
|
run.assert_not_called()
|
||||||
|
|
||||||
|
def test_mirror_missing_target_still_verifies_runtime_cache(self) -> None:
|
||||||
|
policy = self._single_image_policy()
|
||||||
|
with tempfile.TemporaryDirectory() as temp:
|
||||||
|
receipt_path = Path(temp) / "receipt.json"
|
||||||
|
with (
|
||||||
|
patch.object(
|
||||||
|
controller.RuntimeCache,
|
||||||
|
"verify_source",
|
||||||
|
return_value="quay.io/example/image:v1",
|
||||||
|
) as verify_source,
|
||||||
|
patch.object(controller.RuntimeCache, "export") as export,
|
||||||
|
patch.object(controller, "_target_digest_present", return_value=False),
|
||||||
|
):
|
||||||
|
receipt = controller.mirror_images(
|
||||||
|
policy,
|
||||||
|
trace_id="aia-p0-006-02a-missing-check",
|
||||||
|
receipt_path=receipt_path,
|
||||||
|
ssh_key=Path("/unused/key"),
|
||||||
|
known_hosts=Path("/unused/known-hosts"),
|
||||||
|
apply=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(receipt["state"], "blocked_with_safe_next_action")
|
||||||
|
self.assertEqual(receipt["missing_before_count"], 1)
|
||||||
|
self.assertTrue(receipt["source_cache_accessed"])
|
||||||
|
self.assertEqual(receipt["images"][0]["execution"], "check_only_missing")
|
||||||
|
self.assertTrue(receipt["images"][0]["source_cache_verified"])
|
||||||
|
verify_source.assert_called_once_with(policy.images[0])
|
||||||
|
export.assert_not_called()
|
||||||
|
|
||||||
def test_staging_exports_cached_source_and_verifies_remote_provenance(
|
def test_staging_exports_cached_source_and_verifies_remote_provenance(
|
||||||
self,
|
self,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -985,6 +1063,7 @@ class RuntimeImageMirrorControllerTest(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(receipt["state"], "verified")
|
self.assertEqual(receipt["state"], "verified")
|
||||||
self.assertEqual(receipt["missing_before_count"], 1)
|
self.assertEqual(receipt["missing_before_count"], 1)
|
||||||
|
self.assertTrue(receipt["source_cache_accessed"])
|
||||||
self.assertEqual(receipt["images"][0]["execution"], "staged_from_runtime_cache")
|
self.assertEqual(receipt["images"][0]["execution"], "staged_from_runtime_cache")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
receipt["images"][0]["target_registry_digest"], descriptor.digest
|
receipt["images"][0]["target_registry_digest"], descriptor.digest
|
||||||
|
|||||||
Reference in New Issue
Block a user