v2.31.0-dev2 4b72742

volumes.go RawHistoryFolder

1package cli
2
3import (
4	"context"
5	"fmt"
6	"io"
7
8	"github.com/spf13/cobra"
9
10	"github.com/bnema/gordon/internal/adapters/dto"
11	"github.com/bnema/gordon/internal/adapters/in/cli/ui/components"
12)
13
14type volumesClient interface {
15	ListVolumes(ctx context.Context) ([]dto.Volume, error)
16	PruneVolumes(ctx context.Context, req dto.VolumePruneRequest) (*dto.VolumePruneResponse, error)
17}
18
19type volumesPruneOptions struct {
20	DryRun    bool
21	NoConfirm bool
22	Json      bool
23}
24
25func newVolumesCmd() *cobra.Command {
26	cmd := &cobra.Command{
27		Use:     "volumes",
28		Short:   "Manage volumes",
29		GroupID: groupManage,
30	}
31	cmd.AddCommand(newVolumesListCmd())
32	cmd.AddCommand(newVolumesPruneCmd())
33	return cmd
34}
35
36func newVolumesListCmd() *cobra.Command {
37	var jsonOut bool
38	cmd := &cobra.Command{
39		Use:   "list",
40		Short: "List all volumes",
41		RunE: func(cmd *cobra.Command, _ []string) error {
42			handle, err := resolveControlPlane(configPath)
43			if err != nil {
44				return err
45			}
46			defer handle.close()
47			return runVolumesList(cmd.Context(), handle.plane, cmd.OutOrStdout(), jsonOut)
48		},
49	}
50	cmd.Flags().BoolVar(&jsonOut, "json", false, "output as JSON")
51	return cmd
52}
53
54func newVolumesPruneCmd() *cobra.Command {
55	var opts volumesPruneOptions
56	cmd := &cobra.Command{
57		Use:   "prune",
58		Short: "Remove orphaned volumes",
59		RunE: func(cmd *cobra.Command, _ []string) error {
60			handle, err := resolveControlPlane(configPath)
61			if err != nil {
62				return err
63			}
64			defer handle.close()
65			return runVolumesPrune(cmd.Context(), handle.plane, opts, cmd.OutOrStdout())
66		},
67	}
68	cmd.Flags().BoolVar(&opts.DryRun, "dry-run", false, "show what would be removed")
69	cmd.Flags().BoolVar(&opts.NoConfirm, "no-confirm", false, "skip confirmation prompt")
70	cmd.Flags().BoolVar(&opts.Json, "json", false, "output as JSON")
71	return cmd
72}
73
74func runVolumesList(ctx context.Context, client volumesClient, out io.Writer, jsonOut bool) error {
75	volumes, err := client.ListVolumes(ctx)
76	if err != nil {
77		return err
78	}
79
80	if jsonOut {
81		return writeJSON(out, volumes)
82	}
83
84	if len(volumes) == 0 {
85		return cliWriteLine(out, "No volumes found.")
86	}
87
88	if err := cliWriteLine(out, cliRenderTitle("Volumes")); err != nil {
89		return err
90	}
91
92	for _, v := range volumes {
93		status := "orphaned"
94		if v.InUse {
95			status = "in-use"
96		}
97		containers := "-"
98		if len(v.Containers) > 0 {
99			containers = fmt.Sprintf("%v", v.Containers)
100		}
101		if err := cliWritef(out, "  %-30s %-10s %-10s %s\n", v.Name, status, formatImageSize(v.Size), containers); err != nil {
102			return err
103		}
104	}
105
106	var inUse, orphaned int
107	for _, v := range volumes {
108		if v.InUse {
109			inUse++
110		} else {
111			orphaned++
112		}
113	}
114	return cliWritef(out, "\n%d volumes (%d in-use, %d orphaned)\n", len(volumes), inUse, orphaned)
115}
116
117func renderPrunePreview(out io.Writer, resp *dto.VolumePruneResponse) error {
118	if err := cliWriteLine(out, cliRenderTitle("Volumes to remove")); err != nil {
119		return err
120	}
121	for _, v := range resp.Volumes {
122		if err := cliWritef(out, "  %s (%s)\n", v.Name, formatImageSize(v.Size)); err != nil {
123			return err
124		}
125	}
126	return cliWritef(out, "\n%d volumes, %s total\n", resp.VolumesRemoved, formatImageSize(resp.SpaceReclaimed))
127}
128
129func runVolumesPrune(ctx context.Context, client volumesClient, opts volumesPruneOptions, out io.Writer) error {
130	preview, err := client.PruneVolumes(ctx, dto.VolumePruneRequest{DryRun: true})
131	if err != nil {
132		return err
133	}
134
135	if preview.VolumesRemoved == 0 {
136		if opts.Json {
137			return writeJSON(out, preview)
138		}
139		return cliWriteLine(out, "No orphaned volumes to remove.")
140	}
141
142	if opts.DryRun {
143		if opts.Json {
144			return writeJSON(out, preview)
145		}
146		return renderPrunePreview(out, preview)
147	}
148
149	if !opts.Json {
150		if err := renderPrunePreview(out, preview); err != nil {
151			return err
152		}
153	}
154
155	if !opts.NoConfirm && !opts.Json {
156		confirmed, err := components.RunConfirm("Remove these volumes?")
157		if err != nil {
158			return err
159		}
160		if !confirmed {
161			return cliWriteLine(out, "Cancelled.")
162		}
163	}
164
165	result, err := client.PruneVolumes(ctx, dto.VolumePruneRequest{DryRun: false})
166	if err != nil {
167		return err
168	}
169
170	if opts.Json {
171		return writeJSON(out, result)
172	}
173
174	return cliWriteLine(out, cliRenderSuccess(fmt.Sprintf("Removed %d volumes, reclaimed %s", result.VolumesRemoved, formatImageSize(result.SpaceReclaimed))))
175}

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