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