v2.31.0-dev12 6a38a35

images.go RawHistoryFolder

1package admin
2
3import (
4	"encoding/json"
5	"errors"
6	"io"
7	"net/http"
8
9	"github.com/bnema/zerowrap"
10
11	"github.com/bnema/gordon/internal/adapters/dto"
12	"github.com/bnema/gordon/internal/domain"
13)
14
15func toImageResponse(image domain.ImageInfo) dto.Image {
16	return dto.Image{
17		Repository: image.Repository,
18		Tag:        image.Tag,
19		Size:       image.Size,
20		Created:    image.Created,
21		ID:         image.ID,
22		Dangling:   image.Dangling,
23	}
24}
25
26func toImagePruneResponse(report domain.ImagePruneReport) dto.ImagePruneResponse {
27	return dto.ImagePruneResponse{
28		Runtime: dto.RuntimePruneResult{
29			DeletedCount:   report.Runtime.DeletedCount,
30			SpaceReclaimed: report.Runtime.SpaceReclaimed,
31		},
32		Registry: dto.RegistryPruneResult{
33			TagsRemoved:    report.Registry.TagsRemoved,
34			BlobsRemoved:   report.Registry.BlobsRemoved,
35			SpaceReclaimed: report.Registry.SpaceReclaimed,
36		},
37	}
38}
39
40func (h *Handler) handleImages(w http.ResponseWriter, r *http.Request, path string) {
41	if path != "/images" && path != "/images/prune" {
42		h.sendError(w, http.StatusNotFound, "route not found")
43		return
44	}
45
46	if h.imageSvc == nil {
47		h.sendError(w, http.StatusServiceUnavailable, "image service not available")
48		return
49	}
50
51	switch {
52	case path == "/images" && r.Method == http.MethodGet:
53		h.handleImagesGet(w, r)
54	case path == "/images/prune" && r.Method == http.MethodPost:
55		h.handleImagesPrune(w, r)
56	default:
57		h.sendError(w, http.StatusMethodNotAllowed, "method not allowed")
58	}
59}
60
61func (h *Handler) handleImagesGet(w http.ResponseWriter, r *http.Request) {
62	ctx := r.Context()
63
64	if !HasAccess(ctx, domain.AdminResourceStatus, domain.AdminActionRead) {
65		h.sendError(w, http.StatusForbidden, "insufficient permissions for status:read")
66		return
67	}
68
69	images, err := h.imageSvc.ListImages(ctx)
70	if err != nil {
71		h.sendError(w, http.StatusInternalServerError, "failed to list images")
72		return
73	}
74
75	response := make([]dto.Image, 0, len(images))
76	for _, image := range images {
77		response = append(response, toImageResponse(image))
78	}
79
80	h.sendJSON(w, http.StatusOK, dto.ImagesResponse{Images: response})
81}
82
83func (h *Handler) handleImagesPrune(w http.ResponseWriter, r *http.Request) {
84	ctx := r.Context()
85	log := zerowrap.FromCtx(ctx)
86
87	if !HasAccess(ctx, domain.AdminResourceConfig, domain.AdminActionWrite) {
88		h.sendError(w, http.StatusForbidden, "insufficient permissions for config:write")
89		return
90	}
91
92	r.Body = http.MaxBytesReader(w, r.Body, maxAdminRequestSize)
93	var req dto.ImagePruneRequest
94	if err := json.NewDecoder(r.Body).Decode(&req); err != nil && !errors.Is(err, io.EOF) {
95		log.Warn().Err(err).Msg("invalid image prune JSON")
96		h.sendError(w, http.StatusBadRequest, "invalid JSON")
97		return
98	}
99
100	opts := domain.DefaultImagePruneOptions()
101	if req.KeepLast != nil {
102		if *req.KeepLast < 0 {
103			h.sendError(w, http.StatusBadRequest, "keep_last must be >= 0")
104			return
105		}
106		opts.KeepLast = *req.KeepLast
107	}
108	if req.PruneDangling != nil {
109		opts.PruneDangling = *req.PruneDangling
110	}
111	if req.PruneRegistry != nil {
112		opts.PruneRegistry = *req.PruneRegistry
113	}
114	if !opts.PruneDangling && !opts.PruneRegistry {
115		h.sendError(w, http.StatusBadRequest, "at least one prune scope must be enabled")
116		return
117	}
118
119	report, err := h.imageSvc.Prune(ctx, opts)
120	if err != nil {
121		log.Error().Err(err).Int("keep_last", opts.KeepLast).Msg("image prune failed")
122		h.sendError(w, http.StatusInternalServerError, "failed to prune images")
123		return
124	}
125
126	h.sendJSON(w, http.StatusOK, toImagePruneResponse(report))
127}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close