Phase 18: image prune (dangling/unused), local + agent (0.24.0)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
menzelj
2026-06-09 11:30:10 +00:00
co-authored by Claude Opus 4.8
parent d46a6c3576
commit 34c5fffa85
6 changed files with 128 additions and 7 deletions
+5
View File
@@ -499,6 +499,11 @@ async def image_check() -> dict:
return await update_service.check_all()
@app.post("/agent/images/prune", dependencies=[Depends(verify_token)])
def image_prune(all_unused: bool = Query(False, alias="all")) -> dict:
return image_service.prune_images(all_unused)
# --------------------------------------------------------------------------- #
# Volumes
# --------------------------------------------------------------------------- #
+20
View File
@@ -655,6 +655,26 @@ async def agent_image_check(
return result
@router.post("/{agent_id}/images/prune")
async def agent_image_prune(
agent_id: int,
request: Request,
all_unused: bool = Query(False, alias="all"),
session: Session = Depends(get_session),
user: User = Depends(require_admin),
) -> dict:
agent = _get_or_404(session, agent_id)
result = await _proxy(
session, agent, "POST", "/agent/images/prune", params={"all": all_unused}
)
audit_service.record(
session, user=user.username, action="agent.image.prune", target=agent.name,
detail=f"all={all_unused} reclaimed={(result or {}).get('SpaceReclaimed')}",
ip=_ip(request),
)
return result
# --------------------------------------------------------------------------- #
# Volumes (proxied)
# --------------------------------------------------------------------------- #
+24 -2
View File
@@ -1,15 +1,21 @@
"""Image listing + update-check endpoints."""
from __future__ import annotations
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, Query, Request
from sqlmodel import Session
from auth import get_current_user, require_admin
from database import get_session
from models.user import User
from services import image_service, update_service
from services import audit_service, image_service, update_service
router = APIRouter(prefix="/api/images", tags=["images"])
def _ip(request: Request) -> str:
return request.client.host if request.client else "unknown"
@router.get("")
def list_images(_user: User = Depends(get_current_user)) -> list[dict]:
return image_service.list_images()
@@ -23,3 +29,19 @@ def updates(_user: User = Depends(get_current_user)) -> dict:
@router.post("/check")
async def check(_user: User = Depends(require_admin)) -> dict:
return await update_service.check_all()
@router.post("/prune")
def prune(
request: Request,
all_unused: bool = Query(False, alias="all"),
session: Session = Depends(get_session),
user: User = Depends(require_admin),
) -> dict:
result = image_service.prune_images(all_unused)
audit_service.record(
session, user=user.username, action="image.prune", target="*",
detail=f"all={all_unused} reclaimed={result.get('SpaceReclaimed')}",
ip=_ip(request),
)
return result
+14
View File
@@ -45,3 +45,17 @@ def list_images() -> list[dict]:
)
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),
}