ac3132d172
- Add GET /api/projects/{id}/images endpoint returning local images matching the project
- Add ListImagesByRef with tag, size, and created timestamp to Docker client
- Display images table on project page with tag, ID (truncated), size (MB), and created date
- Only shown when Docker is available and images exist locally
126 lines
3.3 KiB
Go
126 lines
3.3 KiB
Go
package api
|
|
|
|
import (
|
|
"errors"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"github.com/alexei/docker-watcher/internal/store"
|
|
)
|
|
|
|
// listProjectImages handles GET /api/projects/{id}/images.
|
|
// Returns all local Docker images matching the project's image reference.
|
|
func (s *Server) listProjectImages(w http.ResponseWriter, r *http.Request) {
|
|
id := chi.URLParam(r, "id")
|
|
|
|
project, err := s.store.GetProjectByID(id)
|
|
if err != nil {
|
|
if errors.Is(err, store.ErrNotFound) {
|
|
respondNotFound(w, "project")
|
|
return
|
|
}
|
|
slog.Error("failed to get project", "error", err)
|
|
respondError(w, http.StatusInternalServerError, "internal server error")
|
|
return
|
|
}
|
|
|
|
if s.docker == nil || project.Image == "" {
|
|
respondJSON(w, http.StatusOK, []any{})
|
|
return
|
|
}
|
|
|
|
images, err := s.docker.ListImagesByRef(r.Context(), project.Image)
|
|
if err != nil {
|
|
slog.Warn("list project images", "project", project.Name, "error", err)
|
|
respondJSON(w, http.StatusOK, []any{})
|
|
return
|
|
}
|
|
|
|
respondJSON(w, http.StatusOK, images)
|
|
}
|
|
|
|
// pruneImages handles POST /api/docker/prune-images.
|
|
// Only removes images that belong to Docker Watcher projects (not all system images).
|
|
func (s *Server) pruneImages(w http.ResponseWriter, r *http.Request) {
|
|
if s.docker == nil {
|
|
respondError(w, http.StatusServiceUnavailable, "Docker is not available")
|
|
return
|
|
}
|
|
|
|
// Collect all image references from our projects.
|
|
projects, err := s.store.GetAllProjects()
|
|
if err != nil {
|
|
slog.Error("prune: failed to list projects", "error", err)
|
|
respondError(w, http.StatusInternalServerError, "internal server error")
|
|
return
|
|
}
|
|
|
|
// Build a set of image refs used by active instances.
|
|
activeImages := make(map[string]bool)
|
|
for _, p := range projects {
|
|
stages, _ := s.store.GetStagesByProjectID(p.ID)
|
|
for _, st := range stages {
|
|
instances, _ := s.store.GetInstancesByStageID(st.ID)
|
|
for _, inst := range instances {
|
|
if inst.ImageTag != "" {
|
|
activeImages[p.Image+":"+inst.ImageTag] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Collect all unique image bases from projects (without tags).
|
|
projectImages := make(map[string]bool)
|
|
for _, p := range projects {
|
|
if p.Image != "" {
|
|
projectImages[p.Image] = true
|
|
}
|
|
}
|
|
|
|
if len(projectImages) == 0 {
|
|
respondJSON(w, http.StatusOK, map[string]any{
|
|
"images_removed": 0,
|
|
"space_reclaimed_mb": 0,
|
|
"message": "No project images to clean up",
|
|
})
|
|
return
|
|
}
|
|
|
|
// List all local Docker images and find ones matching our projects but not actively used.
|
|
ctx := r.Context()
|
|
removed := 0
|
|
var reclaimedBytes int64
|
|
|
|
for imageBase := range projectImages {
|
|
// List all tags for this image.
|
|
images, err := s.docker.ListImagesByRef(ctx, imageBase)
|
|
if err != nil {
|
|
slog.Warn("prune: list images", "image", imageBase, "error", err)
|
|
continue
|
|
}
|
|
|
|
for _, img := range images {
|
|
// Skip images that are actively used by running instances.
|
|
if activeImages[img.Ref] {
|
|
continue
|
|
}
|
|
|
|
// Remove unused image.
|
|
if err := s.docker.RemoveImage(ctx, img.ID); err != nil {
|
|
slog.Warn("prune: remove image", "image", img.Ref, "error", err)
|
|
continue
|
|
}
|
|
removed++
|
|
reclaimedBytes += img.Size
|
|
slog.Info("prune: removed image", "ref", img.Ref, "size_mb", img.Size/(1024*1024))
|
|
}
|
|
}
|
|
|
|
respondJSON(w, http.StatusOK, map[string]any{
|
|
"images_removed": removed,
|
|
"space_reclaimed_mb": reclaimedBytes / (1024 * 1024),
|
|
})
|
|
}
|