Post-merge / detect-type (push) Successful in 1m3s
Post-merge / release (push) Successful in 1m8s
Post-merge / validate-commit-msg (push) Successful in 1m9s
Post-merge / vikunja (push) Successful in 1m13s
Post-merge / badges (push) Successful in 1m19s
Post-merge / sync-wiki (push) Successful in 1m44s
Post-merge / publish (push) Successful in 1m3s
Post-merge / configure-repo (push) Successful in 1m18s
106 lines
3.1 KiB
Python
106 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Prune stale runner images from a rootless Docker daemon.
|
|
|
|
Usage:
|
|
python3 prune_runner_images.py [--dry-run]
|
|
|
|
Removes all images matching the runner-images pattern from the local
|
|
Docker daemon so the runner pulls a fresh :latest on the next job.
|
|
|
|
Environment variables:
|
|
DOCKER_HOST — Docker daemon socket (set by caller)
|
|
XDG_RUNTIME_DIR — Runtime directory (set by caller)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import subprocess # nosec B404
|
|
import sys
|
|
from collections.abc import Sequence
|
|
|
|
#: Pattern for images we want to prune (repository:tag format).
|
|
IMAGE_PATTERN = re.compile(r"runner-images/(ci-base|ci-quality|ci-full)")
|
|
|
|
|
|
def list_docker_images() -> list[str]:
|
|
"""List all images in the local Docker daemon as repository:tag strings.
|
|
|
|
Returns:
|
|
List of ``repository:tag`` strings (excluding ``<none>`` entries).
|
|
"""
|
|
result = subprocess.run( # nosec B603
|
|
["docker", "images", "--format", "{{.Repository}}:{{.Tag}}"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
return [line.strip() for line in result.stdout.splitlines() if line.strip() and "<none>" not in line]
|
|
|
|
|
|
def filter_runner_images(images: Sequence[str]) -> list[str]:
|
|
"""Filter image list to only runner-images entries.
|
|
|
|
Args:
|
|
images: List of ``repository:tag`` strings.
|
|
|
|
Returns:
|
|
Subset matching the runner-images pattern.
|
|
"""
|
|
return [img for img in images if IMAGE_PATTERN.search(img)]
|
|
|
|
|
|
def remove_images(images: Sequence[str], dry_run: bool = False) -> list[str]:
|
|
"""Remove the given images from the local Docker daemon.
|
|
|
|
Args:
|
|
images: List of ``repository:tag`` strings to remove.
|
|
dry_run: If True, print what would be removed but don't execute.
|
|
|
|
Returns:
|
|
List of images that were removed (or would be removed in dry-run).
|
|
"""
|
|
removed: list[str] = []
|
|
for img in images:
|
|
if dry_run:
|
|
print(f"[dry-run] would remove: {img}")
|
|
removed.append(img)
|
|
continue
|
|
result = subprocess.run( # nosec B603
|
|
["docker", "rmi", "-f", img],
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
if result.returncode == 0:
|
|
print(f"removed: {img}")
|
|
removed.append(img)
|
|
else:
|
|
print(f"failed to remove {img}: {result.stderr.strip()}", file=sys.stderr)
|
|
return removed
|
|
|
|
|
|
def main(argv: Sequence[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description="Prune stale runner images.")
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Print what would be removed without executing.",
|
|
)
|
|
args = parser.parse_args(argv)
|
|
|
|
all_images = list_docker_images()
|
|
runner_images = filter_runner_images(all_images)
|
|
|
|
if not runner_images:
|
|
print("no runner images found to prune")
|
|
return 0
|
|
|
|
removed = remove_images(runner_images, dry_run=args.dry_run)
|
|
print(f"pruned {len(removed)} image(s)")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
sys.exit(main())
|