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}