Files
stackpilot/backend/services/image_service.py
T

62 lines
2.1 KiB
Python

"""Image listing — shared by the central images router and the agent."""
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),
}