"""Image listing for the images router.""" from __future__ import annotations from docker_client import DockerError, get_client, safe_call from services import update_service COMPOSE_PROJECT_LABEL = "com.docker.compose.project" def list_images() -> list[dict]: """Return one row per image tag, annotated with using-stacks + update status.""" try: client = get_client() images = safe_call(client.images.list) containers = safe_call(client.containers.list, all=True) except DockerError: return [] # Map image ref -> stacks using it. usage: dict[str, set[str]] = {} for c in containers: ref = c.attrs.get("Config", {}).get("Image") stack = c.labels.get(COMPOSE_PROJECT_LABEL) if ref: usage.setdefault(ref, set()) if stack: usage[ref].add(stack) cache = update_service.get_cache() result = [] for img in images: tags = img.tags or [] if not tags: continue for tag in tags: result.append( { "id": img.short_id, "tag": tag, "size": img.attrs.get("Size", 0), "created": img.attrs.get("Created"), "stacks": sorted(usage.get(tag, set())), "update": cache.get(tag), } ) result.sort(key=lambda r: r["tag"]) return result def prune_images(all_unused: bool = False) -> dict: """Remove unused images. By default only dangling (untagged) images are removed; ``all_unused=True`` removes every image not referenced by a container (``docker image prune -a``).""" client = get_client() # dangling=false tells the engine to also consider tagged-but-unused images. filters = {"dangling": False} if all_unused else {"dangling": True} result = safe_call(client.images.prune, filters=filters) return { "ImagesDeleted": result.get("ImagesDeleted") or [], "SpaceReclaimed": result.get("SpaceReclaimed", 0), }