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