Add failure-safe cluster image capture hook
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a06ecb-456a-71c2-b41e-0755d336e883
This commit is contained in:
parent
05f315be84
commit
da42f764c0
4 changed files with 60 additions and 3 deletions
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
23
tools/cmd/refresh-live-images
Executable 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[@]}"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue