v2.30.1 1983e1b

backup.go RawHistoryFolder

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}

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