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 domainName := ""
46 if len(args) == 1 {
47 domainName = args[0]
48 }
49
50 var (
51 handle *controlPlaneHandle
52 err error
53 )
54 if domainName != "" {
55 handle, err = resolveControlPlaneForRouteDomain(cmd.Context(), domainName)
56 } else {
57 handle, err = resolveControlPlane(configPath)
58 }
59 if err != nil {
60 return err
61 }
62 defer handle.close()
63
64 jobs, err := handle.plane.ListBackups(cmd.Context(), domainName)
65 if err != nil {
66 return fmt.Errorf("failed to list backups: %w", err)
67 }
68
69 return printBackupJobs(cmd.OutOrStdout(), jobs, jsonOut)
70 },
71 }
72
73 cmd.Flags().BoolVar(&jsonOut, "json", false, "Output as JSON")
74
75 return cmd
76}
77
78func printBackupJobs(out io.Writer, jobs []dto.BackupJob, jsonOut bool) error {
79 if len(jobs) == 0 {
80 if jsonOut {
81 return writeJSON(out, []dto.BackupJob{})
82 }
83 return cliWriteLine(out, cliRenderMuted("No backups found"))
84 }
85
86 if jsonOut {
87 return writeJSON(out, jobs)
88 }
89
90 if err := cliWriteLine(out, cliRenderTitle("Backups")); err != nil {
91 return err
92 }
93 w := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0)
94 if _, err := fmt.Fprintln(w, "DOMAIN\tDB\tSTATUS\tSTARTED_AT\tBACKUP_ID"); err != nil {
95 return err
96 }
97
98 for _, job := range jobs {
99 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 {
100 return err
101 }
102 }
103 return w.Flush()
104}
105
106func newBackupRunCmd() *cobra.Command {
107 var dbName string
108
109 cmd := &cobra.Command{
110 Use: "run <domain>",
111 Short: "Run backup now",
112 Args: cobra.ExactArgs(1),
113 RunE: func(cmd *cobra.Command, args []string) error {
114 handle, err := resolveControlPlaneForRouteDomain(cmd.Context(), args[0])
115 if err != nil {
116 return err
117 }
118 defer handle.close()
119
120 result, err := handle.plane.RunBackup(cmd.Context(), args[0], dbName)
121 if err != nil {
122 return fmt.Errorf("failed to run backup: %w", err)
123 }
124
125 if result.Backup == nil {
126 return fmt.Errorf("backup run completed without backup payload")
127 }
128 if err := cliWriteLine(os.Stdout, cliRenderTitle("Backup Result")); err != nil {
129 return err
130 }
131
132 w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
133 if _, err := fmt.Fprintln(w, "DOMAIN\tDB\tSTATUS\tSTARTED_AT\tBACKUP_ID\tSIZE"); err != nil {
134 return err
135 }
136 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 {
137 return err
138 }
139 if err := w.Flush(); err != nil {
140 return err
141 }
142 return nil
143 },
144 }
145
146 cmd.Flags().StringVar(&dbName, "db", "", "Database attachment name (optional)")
147 return cmd
148}
149
150func newBackupDetectCmd() *cobra.Command {
151 return &cobra.Command{
152 Use: "detect <domain>",
153 Short: "Detect databases for domain",
154 Args: cobra.ExactArgs(1),
155 RunE: func(cmd *cobra.Command, args []string) error {
156 handle, err := resolveControlPlaneForRouteDomain(cmd.Context(), args[0])
157 if err != nil {
158 return err
159 }
160 defer handle.close()
161
162 dbs, err := handle.plane.DetectDatabases(cmd.Context(), args[0])
163 if err != nil {
164 return fmt.Errorf("failed to detect databases: %w", err)
165 }
166
167 if len(dbs) == 0 {
168 if err := cliWriteLine(os.Stdout, cliRenderMuted("No supported databases detected")); err != nil {
169 return err
170 }
171 return nil
172 }
173 if err := cliWriteLine(os.Stdout, cliRenderTitle("Detected Databases")); err != nil {
174 return err
175 }
176 w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
177 if _, err := fmt.Fprintln(w, "NAME\tTYPE\tHOST\tPORT\tIMAGE"); err != nil {
178 return err
179 }
180
181 for _, db := range dbs {
182 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 {
183 return err
184 }
185 }
186 if err := w.Flush(); err != nil {
187 return err
188 }
189
190 return nil
191 },
192 }
193}
194
195func newBackupStatusCmd() *cobra.Command {
196 return &cobra.Command{
197 Use: "status",
198 Short: "Show backup status",
199 RunE: func(cmd *cobra.Command, args []string) error {
200 handle, err := resolveControlPlane(configPath)
201 if err != nil {
202 return err
203 }
204 defer handle.close()
205
206 jobs, err := handle.plane.BackupStatus(cmd.Context())
207 if err != nil {
208 return fmt.Errorf("failed to get backup status: %w", err)
209 }
210
211 if len(jobs) == 0 {
212 if err := cliWriteLine(os.Stdout, cliRenderMuted("No backup status available")); err != nil {
213 return err
214 }
215 return nil
216 }
217 if err := cliWriteLine(os.Stdout, cliRenderTitle("Backup Status")); err != nil {
218 return err
219 }
220 w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
221 if _, err := fmt.Fprintln(w, "DOMAIN\tDB\tSTATUS\tSTARTED_AT"); err != nil {
222 return err
223 }
224
225 for _, job := range jobs {
226 if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", job.Domain, job.DBName, job.Status, formatBackupTime(job.StartedAt)); err != nil {
227 return err
228 }
229 }
230 if err := w.Flush(); err != nil {
231 return err
232 }
233
234 return nil
235 },
236 }
237}
238
239func formatBackupTime(t *time.Time) string {
240 if t == nil {
241 return ""
242 }
243 return t.UTC().Format("2006-01-02T15:04:05Z")
244}