v2.31.0-dev18 7b9923f

backup.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close