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