v2.31.0-dev5 4162c86

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

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 8

Tags 106

↑↓ moveenter openesc close