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