v3.4.0 593c1c3

service.go RawHistoryFolder

1// Package volumes implements volume management operations.
2package volumes
3
4import (
5	"context"
6	"fmt"
7
8	"github.com/bnema/zerowrap"
9
10	"github.com/bnema/gordon/internal/boundaries/out"
11	"github.com/bnema/gordon/internal/domain"
12	"github.com/bnema/gordon/internal/usecase/pruneguard"
13)
14
15// Service implements the VolumeService interface.
16type Service struct {
17	runtime out.ContainerRuntime
18	// protection, pruneRuntime, and barrier are the prune ports. All
19	// three are required for PruneVolumes; a missing port fails closed.
20	protection   out.PruneProtectionStore
21	pruneRuntime out.PruneRuntime
22	barrier      out.GCBarrier
23}
24
25// NewService creates a new volume service.
26func NewService(runtime out.ContainerRuntime) *Service {
27	return &Service{runtime: runtime}
28}
29
30// WithPrunePorts wires the prune ports: the coherent protection store,
31// the runtime inventory/deletion port, and the GC barrier.
32func (s *Service) WithPrunePorts(protection out.PruneProtectionStore, pruneRuntime out.PruneRuntime, barrier out.GCBarrier) *Service {
33	s.protection = protection
34	s.pruneRuntime = pruneRuntime
35	s.barrier = barrier
36	return s
37}
38
39// ListVolumes returns all volumes with usage status.
40func (s *Service) ListVolumes(ctx context.Context) ([]*domain.VolumeInfo, error) {
41	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
42		zerowrap.FieldLayer:   "usecase",
43		zerowrap.FieldUseCase: "ListVolumes",
44	})
45
46	vols, err := s.runtime.ListVolumes(ctx)
47	if err != nil {
48		return nil, fmt.Errorf("failed to list volumes: %w", err)
49	}
50	return vols, nil
51}
52
53// PruneVolumes plans (and unless dryRun executes) one volume prune run.
54//
55// A volume is removed only when a durable ownership record explicitly
56// records it as released, the runtime labels agree with that record, and
57// no container uses it. Every other volume is protected or unknown.
58// A valid plan with zero eligible volumes succeeds: with current
59// metadata a zero-deletion volume prune is a normal outcome.
60func (s *Service) PruneVolumes(ctx context.Context, dryRun bool) (*domain.VolumePruneReport, []*domain.VolumeInfo, error) {
61	ctx = zerowrap.CtxWithFields(ctx, map[string]any{
62		zerowrap.FieldLayer:   "usecase",
63		zerowrap.FieldUseCase: "PruneVolumes",
64		"dry_run":             dryRun,
65	})
66	log := zerowrap.FromCtx(ctx)
67
68	lease, err := s.acquireExclusive(ctx)
69	if err != nil {
70		return nil, nil, fmt.Errorf("volumes: acquire prune lease: %w", err)
71	}
72	defer lease.Release()
73
74	if s.protection == nil || s.pruneRuntime == nil {
75		return nil, nil, fmt.Errorf("volumes: prune requires a protection store and a runtime inventory port: %w", domain.ErrPruneDisabled)
76	}
77
78	snapshot, err := s.protection.ProtectionSnapshot(ctx)
79	if err != nil {
80		return nil, nil, fmt.Errorf("volumes: read protection snapshot: %w", err)
81	}
82
83	inventory, err := s.pruneRuntime.InventoryRuntime(ctx)
84	if err != nil {
85		return nil, nil, fmt.Errorf("volumes: read runtime inventory: %w", err)
86	}
87
88	plan := &domain.PrunePlan{
89		Gaps: append(append([]domain.InventoryGap(nil), snapshot.Gaps...), inventory.Gaps...),
90		Volumes: pruneguard.PlanVolumes(pruneguard.VolumePlanInput{
91			Volumes:  inventory.Volumes,
92			Snapshot: snapshot,
93			Complete: inventory.Complete(),
94		}),
95	}
96	if err := plan.Validate(); err != nil {
97		return nil, nil, fmt.Errorf("volumes: refusing to execute an invalid prune plan: %w", err)
98	}
99
100	report := &domain.VolumePruneReport{Plan: *domain.NewPruneReport(plan, !dryRun)}
101	if dryRun {
102		return report, nil, nil
103	}
104
105	byName := make(map[string]*domain.VolumeInfo, len(inventory.Volumes))
106	for _, volume := range inventory.Volumes {
107		if volume != nil {
108			byName[volume.Name] = volume
109		}
110	}
111
112	var removed []*domain.VolumeInfo
113	for _, ref := range plan.EligibleVolumes() {
114		if err := s.pruneRuntime.RemoveVolumeExact(ctx, ref); err != nil {
115			log.Warn().Err(err).Str("volume", ref.Name).Msg("failed to remove volume, skipping")
116			report.Plan.Failures = append(report.Plan.Failures, domain.PruneFailure{
117				Kind: domain.PruneResourceVolume, Ref: ref.Name, Err: err.Error(),
118			})
119			continue
120		}
121		report.Plan.Deleted = append(report.Plan.Deleted, domain.PruneCandidateReport{
122			Kind: domain.PruneResourceVolume, Ref: ref.Name, Verdict: domain.PruneVerdictEligible,
123		})
124		report.VolumesRemoved++
125		if volume, ok := byName[ref.Name]; ok {
126			report.SpaceReclaimed += volume.Size
127			report.Plan.ReclaimedBytes += volume.Size
128			// The driver reported this size for the volume it removed.
129			report.Plan.ReclaimedKnown = true
130			removed = append(removed, volume)
131			continue
132		}
133		removed = append(removed, &domain.VolumeInfo{Name: ref.Name})
134	}
135
136	return report, removed, nil
137}
138
139// acquireExclusive takes the GC exclusive lease for the whole prune run,
140// or a no-op lease when no barrier is wired.
141func (s *Service) acquireExclusive(ctx context.Context) (out.GCLease, error) {
142	if s.barrier == nil {
143		return noopGCLease{}, nil
144	}
145	return s.barrier.AcquireExclusive(ctx)
146}
147
148// noopGCLease is the lease used when no barrier is wired.
149type noopGCLease struct{}
150
151func (noopGCLease) Release() {}

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