Add failure-safe cluster image capture hook
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a06ecb-456a-71c2-b41e-0755d336e883
This commit is contained in:
codex 2026-09-05 10:04:38 +02:00
parent 05f315be84
commit da42f764c0
4 changed files with 60 additions and 3 deletions

View file

@ -106,8 +106,14 @@ Migration and rollout sequence:
other clusters' prior entries during outages. Run the hook on the durable other clusters' prior entries during outages. Run the hook on the durable
host, or transfer the complete export there before invoking it. host, or transfer the complete export there before invoking it.
The publisher is implemented here; host migration and the activity-core mount `tools/cmd/refresh-live-images` captures regular, init and ephemeral container
and rollout-hook adoption are tracked in `RPF-WP-0028` until verified live. images with a bounded kubectl call and publishes only after all requested
contexts succeed. `CONTEXTS` selects contexts; `EXTRA_LIVE_FILES` supplies saved
exports. Bare image names are accepted. Failed captures preserve the inventory.
The host installation exposes `~/.local/bin/railiance-live-images-refresh`;
invoke it after cluster image rollouts. Activity-core's `make refresh-live-images`
delegates to the same implementation. Live installation evidence is tracked in
`RPF-WP-0028`.
## Rollback procedure ## Rollback procedure

View file

@ -8,6 +8,7 @@ import hashlib
import json import json
import os import os
from pathlib import Path from pathlib import Path
import re
import tempfile import tempfile
@ -17,7 +18,7 @@ def read_images(path: Path) -> set[str]:
image = line.strip() image = line.strip()
if not image or image.startswith("#"): if not image or image.startswith("#"):
continue continue
if any(c.isspace() for c in image) or "/" not in image: if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:/@+-]*", image):
raise ValueError("invalid image export") raise ValueError("invalid image export")
images.add(image) images.add(image)
if not images: if not images:

View file

@ -1,5 +1,7 @@
import importlib.util import importlib.util
import os
from pathlib import Path from pathlib import Path
import subprocess
import tempfile import tempfile
import unittest import unittest
@ -11,6 +13,31 @@ SPEC.loader.exec_module(inventory)
class InventoryTests(unittest.TestCase): class InventoryTests(unittest.TestCase):
def test_bare_image_names_are_valid(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
source = root / "source"
source.write_text("nginx:stable\n")
self.assertEqual(inventory.refresh(root / "all", [source])["images"], 1)
def test_capture_failure_keeps_prior_inventory(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
output = root / "all"
output.write_text("forgejo.example/org/app:old\n")
kubectl = root / "kubectl"
kubectl.write_text('#!/bin/sh\necho forgejo.example/org/app:new\nexit 1\n')
kubectl.chmod(0o755)
env = {**os.environ, "PATH": str(root) + ":" + os.environ["PATH"], "OUT": str(output)}
command = Path(__file__).resolve().parents[1] / "tools/cmd/refresh-live-images"
failed = subprocess.run(["bash", str(command)], env=env, capture_output=True)
self.assertNotEqual(failed.returncode, 0)
self.assertEqual(output.read_text(), "forgejo.example/org/app:old\n")
kubectl.write_text('#!/bin/sh\necho forgejo.example/org/app:new\n')
succeeded = subprocess.run(["bash", str(command)], env=env, capture_output=True)
self.assertEqual(succeeded.returncode, 0, succeeded.stderr)
self.assertEqual(len(output.read_text().splitlines()), 2)
def test_refresh_preserves_other_clusters_and_previous_tags(self): def test_refresh_preserves_other_clusters_and_previous_tags(self):
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp) root = Path(tmp)

23
tools/cmd/refresh-live-images Executable file
View file

@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Capture complete cluster exports and atomically preserve the protection union.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
OUT="${OUT:-${HOME}/.local/state/railiance-platform/live-images/all.txt}"
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
sources=()
capture() {
local context="$1" file="$tmpdir/export-${#sources[@]}.txt"
local args=()
if [[ -n "$context" ]]; then args=(--context "$context"); fi
kubectl "${args[@]}" --request-timeout=30s get pods -A \
-o jsonpath='{range .items[*]}{range .spec.containers[*]}{.image}{"\n"}{end}{range .spec.initContainers[*]}{.image}{"\n"}{end}{range .spec.ephemeralContainers[*]}{.image}{"\n"}{end}{end}' > "$file"
sources+=(--source "$file")
}
if [[ -z "${CONTEXTS:-}" ]]; then
capture ""
else
for context in $CONTEXTS; do capture "$context"; done
fi
for file in ${EXTRA_LIVE_FILES:-}; do sources+=(--source "$file"); done
python3 "$ROOT/scripts/refresh_live_images.py" --output "$OUT" "${sources[@]}"