runtime_prune.go RawHistoryFolder

1package docker
2
3import (
4	"context"
5	"fmt"
6	"sort"
7	"strings"
8	"time"
9
10	"github.com/bnema/zerowrap"
11	"github.com/moby/moby/api/types/container"
12	"github.com/moby/moby/api/types/image"
13	"github.com/moby/moby/api/types/volume"
14	"github.com/moby/moby/client"
15
16	"github.com/bnema/gordon/internal/boundaries/out"
17	"github.com/bnema/gordon/internal/domain"
18)
19
20var _ out.PruneRuntime = (*Runtime)(nil)
21
22// InventoryRuntime implements out.PruneRuntime.
23//
24// It reads containers (running and stopped), images, and volumes in one
25// pass. A failed list is recorded as an inventory gap, never as
26// absence: the caller then fails closed per candidate instead of
27// planning against an empty runtime.
28func (r *Runtime) InventoryRuntime(ctx context.Context) (*domain.RuntimeInventory, error) {
29	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
30		zerowrap.FieldLayer:   "adapter",
31		zerowrap.FieldAdapter: "docker",
32		zerowrap.FieldAction:  "InventoryRuntime",
33	})
34	log := zerowrap.FromCtx(ctx)
35
36	inventory := &domain.RuntimeInventory{}
37
38	containerList, containerErr := r.client.ContainerList(ctx, client.ContainerListOptions{All: true})
39	if containerErr != nil {
40		log.Warn().Err(containerErr).Msg("failed to list containers for prune inventory")
41		inventory.Gaps = append(inventory.Gaps, domain.InventoryGap{
42			Source: domain.InventorySourceRuntimeContainers,
43			Reason: domain.PruneReasonUnknownContainerUse,
44			Detail: "list containers failed",
45		})
46	} else {
47		inventory.Containers = containerImageUses(containerList.Items)
48	}
49
50	imageList, imageErr := r.client.ImageList(ctx, client.ImageListOptions{All: true})
51	if imageErr != nil {
52		log.Warn().Err(imageErr).Msg("failed to list images for prune inventory")
53		inventory.Gaps = append(inventory.Gaps, domain.InventoryGap{
54			Source: domain.InventorySourceRuntimeImages,
55			Reason: domain.PruneReasonUnknownInventory,
56			Detail: "list images failed",
57		})
58	} else {
59		inventory.Images = runtimeImages(imageList.Items)
60	}
61
62	volumeList, volumeErr := r.client.VolumeList(ctx, client.VolumeListOptions{})
63	if volumeErr != nil {
64		log.Warn().Err(volumeErr).Msg("failed to list volumes for prune inventory")
65		inventory.Gaps = append(inventory.Gaps, domain.InventoryGap{
66			Source: domain.InventorySourceRuntimeVolumes,
67			Reason: domain.PruneReasonUnknownInventory,
68			Detail: "list volumes failed",
69		})
70	} else {
71		var mounts []volumeMount
72		if containerErr == nil {
73			mounts = volumeMounts(containerList.Items)
74		}
75		inventory.Volumes = volumeInfos(volumeList.Items, mounts)
76	}
77
78	return inventory, nil
79}
80
81// volumeMount is one volume mount observed on a container.
82type volumeMount struct {
83	volume    string
84	container string
85}
86
87func volumeMounts(containers []container.Summary) []volumeMount {
88	var mounts []volumeMount
89	for _, container := range containers {
90		name := ""
91		if len(container.Names) > 0 {
92			name = strings.TrimPrefix(container.Names[0], "/")
93		}
94		if name == "" {
95			name = container.ID
96		}
97		for _, mount := range container.Mounts {
98			if mount.Type != "volume" || mount.Name == "" {
99				continue
100			}
101			mounts = append(mounts, volumeMount{volume: mount.Name, container: name})
102		}
103	}
104	sort.Slice(mounts, func(i, j int) bool {
105		if mounts[i].volume != mounts[j].volume {
106			return mounts[i].volume < mounts[j].volume
107		}
108		return mounts[i].container < mounts[j].container
109	})
110	return mounts
111}
112
113// containerImageUses maps every container, running or not, to the image
114// identity it was created from. A container whose image identity cannot
115// be resolved marks ImageUnknown so no candidate can be proven unused.
116func containerImageUses(containers []container.Summary) []domain.RuntimeContainerUse {
117	uses := make([]domain.RuntimeContainerUse, 0, len(containers))
118	for _, container := range containers {
119		use := domain.RuntimeContainerUse{
120			ContainerID: container.ID,
121			ImageID:     container.ImageID,
122			ImageRef:    container.Image,
123			Running:     container.State == "running",
124		}
125		if use.ImageID == "" && use.ImageRef == "" {
126			use.ImageUnknown = true
127		}
128		uses = append(uses, use)
129	}
130	sort.Slice(uses, func(i, j int) bool { return uses[i].ContainerID < uses[j].ContainerID })
131	return uses
132}
133
134// runtimeImages maps the runtime image list to inventory entries.
135func runtimeImages(images []image.Summary) []domain.RuntimeImage {
136	out := make([]domain.RuntimeImage, 0, len(images))
137	for _, image := range images {
138		out = append(out, domain.RuntimeImage{
139			ID:          image.ID,
140			RepoTags:    append([]string(nil), image.RepoTags...),
141			RepoDigests: append([]string(nil), image.RepoDigests...),
142			Labels:      image.Labels,
143			Size:        image.Size,
144			Created:     time.Unix(image.Created, 0),
145		})
146	}
147	sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
148	return out
149}
150
151// volumeInfos maps volumes plus observed container mounts to inventory
152// entries.
153func volumeInfos(volumes []volume.Volume, mounts []volumeMount) []*domain.VolumeInfo {
154	users := make(map[string][]string)
155	for _, mount := range mounts {
156		users[mount.volume] = append(users[mount.volume], mount.container)
157	}
158
159	out := make([]*domain.VolumeInfo, 0, len(volumes))
160	for _, volume := range volumes {
161		var size int64
162		if volume.UsageData != nil {
163			size = volume.UsageData.Size
164		}
165		// A volume driver that cannot report creation time yields the
166		// zero time (for example some Podman responses). That is not a
167		// safety input: eligibility comes from ownership, not age.
168		created, _ := time.Parse(time.RFC3339, volume.CreatedAt)
169		containers := users[volume.Name]
170		out = append(out, &domain.VolumeInfo{
171			Name:       volume.Name,
172			Driver:     volume.Driver,
173			MountPoint: volume.Mountpoint,
174			Size:       size,
175			CreatedAt:  created,
176			InUse:      len(containers) > 0,
177			Containers: containers,
178			Labels:     volume.Labels,
179		})
180	}
181	sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
182	return out
183}
184
185// RemoveImageExact implements out.PruneRuntime. It removes one exact
186// runtime image ID and never forces: an image that is still referenced
187// fails loudly instead of being untagged behind the caller's back.
188func (r *Runtime) RemoveImageExact(ctx context.Context, ref domain.RuntimeImageRef) error {
189	if !ref.Valid() {
190		return fmt.Errorf("docker: refusing to remove image with invalid identity %q", ref.ID)
191	}
192	return r.RemoveImage(ctx, ref.ID, false)
193}
194
195// RemoveVolumeExact implements out.PruneRuntime. It removes one exact
196// runtime volume name and never forces.
197func (r *Runtime) RemoveVolumeExact(ctx context.Context, ref domain.RuntimeVolumeRef) error {
198	if !ref.Valid() {
199		return fmt.Errorf("docker: refusing to remove volume with invalid identity %q", ref.Name)
200	}
201	return r.RemoveVolume(ctx, ref.Name, false)
202}

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