prune: merge exported live-image files into protection (multi-cluster)
ACTIVITY-WP-0020-T07: the kubectl scan only sees the prune host's own cluster. New repeatable --live-images-file merges image refs exported from other production clusters; missing file surfaces as WARN (reduced coverage), forgejo-registry tags protected, other registries ignored. 5/5 tests. Evidence gathered 2026-07-18: activity-core on railiance01 runs a locally-imported image (activity-core:railiance01-prod), not a forgejo registry tag — the largest would_delete set has no live registry consumer. Live forgejo tags: state-hub:main-1cf949b (railiance01), issue-core:0.2.1 + state-hub:f2e042a + vergabe-teilnahme:064d295 (coulombcore cluster). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
d4c95f78ff
commit
ca4e1526bd
2 changed files with 72 additions and 1 deletions
|
|
@ -109,6 +109,35 @@ def collect_protected_versions(apps_root: Path) -> set[tuple[str, str, str]]:
|
||||||
return protected
|
return protected
|
||||||
|
|
||||||
|
|
||||||
|
def collect_live_images_from_files(
|
||||||
|
paths: list[Path],
|
||||||
|
) -> tuple[set[tuple[str, str, str]], list[str]]:
|
||||||
|
"""Protect image tags listed in exported live-image files.
|
||||||
|
|
||||||
|
Each file holds one image ref per line (`kubectl get pods ... jsonpath`
|
||||||
|
output from another cluster). This closes the multi-cluster gap
|
||||||
|
(ACTIVITY-WP-0020-T07): the prune host's kubectl only sees its own
|
||||||
|
cluster, so every other production cluster exports its live images to a
|
||||||
|
file that is merged here. A missing file is a WARN, not a failure —
|
||||||
|
but it means reduced protection coverage, so the note must surface.
|
||||||
|
"""
|
||||||
|
protected: set[tuple[str, str, str]] = set()
|
||||||
|
notes: list[str] = []
|
||||||
|
for raw_path in paths:
|
||||||
|
path = raw_path.expanduser()
|
||||||
|
if not path.is_file():
|
||||||
|
notes.append(f"live-images file missing: {path}")
|
||||||
|
continue
|
||||||
|
for line in path.read_text(encoding="utf-8").splitlines():
|
||||||
|
image = line.strip()
|
||||||
|
if not image or image.startswith("#"):
|
||||||
|
continue
|
||||||
|
match = FORGEJO_IMAGE_RE.match(image)
|
||||||
|
if match and match.group("tag"):
|
||||||
|
protected.add(("container", match.group("name"), match.group("tag")))
|
||||||
|
return protected, notes
|
||||||
|
|
||||||
|
|
||||||
def collect_live_cluster_versions(
|
def collect_live_cluster_versions(
|
||||||
*, kubectl: str = "kubectl", timeout: float = 60.0
|
*, kubectl: str = "kubectl", timeout: float = 60.0
|
||||||
) -> tuple[set[tuple[str, str, str]], list[str]]:
|
) -> tuple[set[tuple[str, str, str]], list[str]]:
|
||||||
|
|
@ -388,6 +417,18 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
help="Skip protecting image tags currently running in the cluster (kubectl)",
|
help="Skip protecting image tags currently running in the cluster (kubectl)",
|
||||||
)
|
)
|
||||||
parser.set_defaults(protect_live=True)
|
parser.set_defaults(protect_live=True)
|
||||||
|
parser.add_argument(
|
||||||
|
"--live-images-file",
|
||||||
|
dest="live_images_files",
|
||||||
|
type=Path,
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
|
help=(
|
||||||
|
"File with one image ref per line, exported from another "
|
||||||
|
"production cluster; repeatable. Tags matching the Forgejo "
|
||||||
|
"registry are protected in addition to the local kubectl scan."
|
||||||
|
),
|
||||||
|
)
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
apply = bool(args.apply)
|
apply = bool(args.apply)
|
||||||
|
|
@ -402,6 +443,13 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
print(f"Protected live cluster tags: {len(live)}", file=sys.stderr)
|
print(f"Protected live cluster tags: {len(live)}", file=sys.stderr)
|
||||||
for note in protect_notes:
|
for note in protect_notes:
|
||||||
print(f" WARN: {note}", file=sys.stderr)
|
print(f" WARN: {note}", file=sys.stderr)
|
||||||
|
if args.live_images_files:
|
||||||
|
file_live, file_notes = collect_live_images_from_files(args.live_images_files)
|
||||||
|
protected |= file_live
|
||||||
|
protect_notes.extend(file_notes)
|
||||||
|
print(f"Protected exported live tags: {len(file_live)}", file=sys.stderr)
|
||||||
|
for note in file_notes:
|
||||||
|
print(f" WARN: {note}", file=sys.stderr)
|
||||||
|
|
||||||
print(f"Forgejo package prune — owner={args.owner} keep={args.max_versions}", file=sys.stderr)
|
print(f"Forgejo package prune — owner={args.owner} keep={args.max_versions}", file=sys.stderr)
|
||||||
print(f"Protected production tags: {len(protected)}", file=sys.stderr)
|
print(f"Protected production tags: {len(protected)}", file=sys.stderr)
|
||||||
|
|
|
||||||
|
|
@ -95,4 +95,27 @@ image:
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
||||||
|
def test_collect_live_images_from_files(tmp_path):
|
||||||
|
from scripts.forgejo_package_prune import collect_live_images_from_files
|
||||||
|
|
||||||
|
export = tmp_path / "live-coulombcore.txt"
|
||||||
|
export.write_text(
|
||||||
|
"# exported 2026-07-18\n"
|
||||||
|
"forgejo.coulomb.social/coulomb/issue-core:0.2.1\n"
|
||||||
|
"forgejo.coulomb.social/coulomb/state-hub:f2e042a\n"
|
||||||
|
"gitea.coulomb.social/coulomb/core-hub:3ed8531\n"
|
||||||
|
"nats:2.10-alpine\n"
|
||||||
|
"\n"
|
||||||
|
)
|
||||||
|
protected, notes = collect_live_images_from_files(
|
||||||
|
[export, tmp_path / "missing.txt"]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert ("container", "issue-core", "0.2.1") in protected
|
||||||
|
assert ("container", "state-hub", "f2e042a") in protected
|
||||||
|
# non-forgejo registries and bare images are ignored
|
||||||
|
assert all(name != "core-hub" for (_, name, _) in protected)
|
||||||
|
assert len(protected) == 2
|
||||||
|
assert any("missing.txt" in n for n in notes)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue