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