1package cli
2
3import (
4 "fmt"
5 "io"
6 "os"
7 "text/tabwriter"
8 "time"
9
10 "github.com/spf13/cobra"
11
12 "github.com/bnema/gordon/internal/adapters/dto"
13 "github.com/bnema/gordon/pkg/bytesize"
14)
15
16// newBackupCmd creates the backup command group.
17func newBackupCmd() *cobra.Command {
18 cmd := &cobra.Command{
19 Use: "backups",
20 Aliases: []string{"backup"},
21 Short: "Manage database backups",
22 Long: `Manage database backups.
23
24Runs locally via in-process services by default, or against a remote Gordon
25instance when --remote targeting is configured.`,
26 }
27
28 cmd.AddCommand(newBackupListCmd())
29 cmd.AddCommand(newBackupRunCmd())
30 cmd.AddCommand(newBackupDetectCmd())
31 cmd.AddCommand(newBackupStatusCmd())
32
33 return cmd
34}
35
36func newBackupListCmd() *cobra.Command {
37 var jsonOut bool
38
39 cmd := &cobra.Command{
40 Use: "list [domain]",
41 Short: "List backups",
42 Long: cliRenderMuted("List backups for all domains or a specific domain."),
43 Args: cobra.MaximumNArgs(1),
44 RunE: func(cmd *cobra.Command, args []string) error {
45 handle, err := resolveControlPlane(configPath)
46 if err != nil {
47 return err
48 }
49 defer handle.close()
50
51 domainName := ""
52 if len(args) == 1 {
53 domainName = args[0]
54 }
55
56 jobs, err := handle.plane.ListBackups(cmd.Context(), domainName)
57 if err != nil {
58 return fmt.Errorf("failed to list backups: %w", err)
59 }
60
61 return printBackupJobs(cmd.OutOrStdout(), jobs, jsonOut)
62 },
63 }
64
65 cmd.Flags().BoolVar(&jsonOut, "json", false, "Output as JSON")
66
67 return cmd
68}
69
70func printBackupJobs(out io.Writer, jobs []dto.BackupJob, jsonOut bool) error {
71 if len(jobs) == 0 {
72 if jsonOut {
73 return writeJSON(out, []dto.BackupJob{})
74 }
75 return cliWriteLine(out, cliRenderMuted("No backups found"))
76 }
77
78 if jsonOut {
79 return writeJSON(out, jobs)
80 }
81
82 if err := cliWriteLine(out, cliRenderTitle("Backups")); err != nil {
83 return err
84 }
85 w := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0)
86 if _, err := fmt.Fprintln(w, "DOMAIN\tDB\tSTATUS\tSTARTED_AT\tBACKUP_ID"); err != nil {
87 return err
88 }
89
90 for _, job := range jobs {
91 if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", job.Domain, job.DBName, job.Status, formatBackupTime(job.StartedAt), job.ID); err != nil {
92 return err
93 }
94 }
95 return w.Flush()
96}
97
98func newBackupRunCmd() *cobra.Command {
99 var dbName string
100
101 cmd := &cobra.Command{
102 Use: "run <domain>",
103 Short: "Run backup now",
104 Args: cobra.ExactArgs(1),
105 RunE: func(cmd *cobra.Command, args []string) error {
106 handle, err := resolveControlPlane(configPath)
107 if err != nil {
108 return err
109 }
110 defer handle.close()
111
112 result, err := handle.plane.RunBackup(cmd.Context(), args[0], dbName)
113 if err != nil {
114 return fmt.Errorf("failed to run backup: %w", err)
115 }
116
117 if result.Backup == nil {
118 return fmt.Errorf("backup run completed without backup payload")
119 }
120 if err := cliWriteLine(os.Stdout, cliRenderTitle("Backup Result")); err != nil {
121 return err
122 }
123
124 w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
125 if _, err := fmt.Fprintln(w, "DOMAIN\tDB\tSTATUS\tSTARTED_AT\tBACKUP_ID\tSIZE"); err != nil {
126 return err
127 }
128 if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", result.Backup.Domain, result.Backup.DBName, result.Backup.Status, formatBackupTime(result.Backup.StartedAt), result.Backup.ID, bytesize.Format(result.Backup.SizeBytes)); err != nil {
129 return err
130 }
131 if err := w.Flush(); err != nil {
132 return err
133 }
134 return nil
135 },
136 }
137
138 cmd.Flags().StringVar(&dbName, "db", "", "Database attachment name (optional)")
139 return cmd
140}
141
142func newBackupDetectCmd() *cobra.Command {
143 return &cobra.Command{
144 Use: "detect <domain>",
145 Short: "Detect databases for domain",
146 Args: cobra.ExactArgs(1),
147 RunE: func(cmd *cobra.Command, args []string) error {
148 handle, err := resolveControlPlane(configPath)
149 if err != nil {
150 return err
151 }
152 defer handle.close()
153
154 dbs, err := handle.plane.DetectDatabases(cmd.Context(), args[0])
155 if err != nil {
156 return fmt.Errorf("failed to detect databases: %w", err)
157 }
158
159 if len(dbs) == 0 {
160 if err := cliWriteLine(os.Stdout, cliRenderMuted("No supported databases detected")); err != nil {
161 return err
162 }
163 return nil
164 }
165 if err := cliWriteLine(os.Stdout, cliRenderTitle("Detected Databases")); err != nil {
166 return err
167 }
168 w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
169 if _, err := fmt.Fprintln(w, "NAME\tTYPE\tHOST\tPORT\tIMAGE"); err != nil {
170 return err
171 }
172
173 for _, db := range dbs {
174 if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%d\t%s\n", db.Name, db.Type, db.Host, db.Port, db.ImageName); err != nil {
175 return err
176 }
177 }
178 if err := w.Flush(); err != nil {
179 return err
180 }
181
182 return nil
183 },
184 }
185}
186
187func newBackupStatusCmd() *cobra.Command {
188 return &cobra.Command{
189 Use: "status",
190 Short: "Show backup status",
191 RunE: func(cmd *cobra.Command, args []string) error {
192 handle, err := resolveControlPlane(configPath)
193 if err != nil {
194 return err
195 }
196 defer handle.close()
197
198 jobs, err := handle.plane.BackupStatus(cmd.Context())
199 if err != nil {
200 return fmt.Errorf("failed to get backup status: %w", err)
201 }
202
203 if len(jobs) == 0 {
204 if err := cliWriteLine(os.Stdout, cliRenderMuted("No backup status available")); err != nil {
205 return err
206 }
207 return nil
208 }
209 if err := cliWriteLine(os.Stdout, cliRenderTitle("Backup Status")); err != nil {
210 return err
211 }
212 w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
213 if _, err := fmt.Fprintln(w, "DOMAIN\tDB\tSTATUS\tSTARTED_AT"); err != nil {
214 return err
215 }
216
217 for _, job := range jobs {
218 if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", job.Domain, job.DBName, job.Status, formatBackupTime(job.StartedAt)); err != nil {
219 return err
220 }
221 }
222 if err := w.Flush(); err != nil {
223 return err
224 }
225
226 return nil
227 },
228 }
229}
230
231func formatBackupTime(t *time.Time) string {
232 if t == nil {
233 return ""
234 }
235 return t.UTC().Format("2006-01-02T15:04:05Z")
236}