Skip to content

Commit cda0fc9

Browse files
committed
feat(dbaas): add backup and backup schedule commands
Signed-off-by: Thomas Kooi <thomas@thalassa.cloud>
1 parent 237e372 commit cda0fc9

17 files changed

Lines changed: 1484 additions & 21 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package backupschedules
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
)
6+
7+
const NoHeaderKey = "no-header"
8+
9+
// BackupSchedulesCmd represents the backup-schedules command
10+
var BackupSchedulesCmd = &cobra.Command{
11+
Use: "backup-schedules",
12+
Aliases: []string{"backup-schedule", "schedules", "schedule"},
13+
Short: "Manage database backup schedules",
14+
Long: "Manage database backup schedules for database clusters",
15+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package backupschedules
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/spf13/cobra"
8+
9+
"github.com/thalassa-cloud/cli/internal/completion"
10+
"github.com/thalassa-cloud/cli/internal/table"
11+
"github.com/thalassa-cloud/cli/internal/thalassaclient"
12+
"github.com/thalassa-cloud/client-go/dbaas"
13+
tcclient "github.com/thalassa-cloud/client-go/pkg/client"
14+
)
15+
16+
var (
17+
backupScheduleCreateName string
18+
backupScheduleCreateDescription string
19+
backupScheduleCreateSchedule string
20+
backupScheduleCreateRetentionPolicy string
21+
backupScheduleCreateMethod string
22+
backupScheduleCreateLabels []string
23+
backupScheduleCreateAnnotations []string
24+
)
25+
26+
// backupScheduleCreateCmd represents the backup-schedules create command
27+
var backupScheduleCreateCmd = &cobra.Command{
28+
Use: "create",
29+
Short: "Create a database backup schedule",
30+
Long: "Create a new backup schedule for a database cluster",
31+
Args: cobra.ExactArgs(1),
32+
ValidArgsFunction: completion.CompleteDbClusterID,
33+
RunE: func(cmd *cobra.Command, args []string) error {
34+
client, err := thalassaclient.GetThalassaClient()
35+
if err != nil {
36+
return fmt.Errorf("failed to create client: %w", err)
37+
}
38+
39+
clusterIdentity := args[0]
40+
41+
if backupScheduleCreateName == "" {
42+
return fmt.Errorf("name is required")
43+
}
44+
if backupScheduleCreateSchedule == "" {
45+
return fmt.Errorf("schedule is required")
46+
}
47+
if backupScheduleCreateRetentionPolicy == "" {
48+
return fmt.Errorf("retention-policy is required")
49+
}
50+
if backupScheduleCreateMethod == "" {
51+
return fmt.Errorf("method is required")
52+
}
53+
54+
// Parse labels from key=value format
55+
labels := make(map[string]string)
56+
for _, label := range backupScheduleCreateLabels {
57+
parts := strings.SplitN(label, "=", 2)
58+
if len(parts) == 2 {
59+
labels[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
60+
}
61+
}
62+
63+
// Parse annotations from key=value format
64+
annotations := make(map[string]string)
65+
for _, annotation := range backupScheduleCreateAnnotations {
66+
parts := strings.SplitN(annotation, "=", 2)
67+
if len(parts) == 2 {
68+
annotations[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
69+
}
70+
}
71+
72+
method := dbaas.DbClusterBackupScheduleMethod(backupScheduleCreateMethod)
73+
if method != dbaas.DbClusterBackupScheduleMethodSnapshot && method != dbaas.DbClusterBackupScheduleMethodBarman {
74+
return fmt.Errorf("method must be either 'snapshot' or 'barman'")
75+
}
76+
77+
createReq := dbaas.CreateDbBackupScheduleRequest{
78+
Name: backupScheduleCreateName,
79+
Schedule: backupScheduleCreateSchedule,
80+
RetentionPolicy: backupScheduleCreateRetentionPolicy,
81+
Method: method,
82+
Labels: labels,
83+
Annotations: annotations,
84+
}
85+
86+
if backupScheduleCreateDescription != "" {
87+
createReq.Description = &backupScheduleCreateDescription
88+
}
89+
90+
schedule, err := client.DBaaS().CreateDbBackupSchedule(cmd.Context(), clusterIdentity, createReq)
91+
if err != nil {
92+
if tcclient.IsNotFound(err) {
93+
return fmt.Errorf("database cluster not found: %s", clusterIdentity)
94+
}
95+
return fmt.Errorf("failed to create backup schedule: %w", err)
96+
}
97+
98+
// Output in table format
99+
clusterName := ""
100+
if schedule.DbCluster != nil {
101+
clusterName = schedule.DbCluster.Name
102+
}
103+
104+
nextBackup := "-"
105+
if schedule.NextBackupAt != nil {
106+
nextBackup = schedule.NextBackupAt.Format("2006-01-02 15:04:05")
107+
}
108+
109+
body := [][]string{
110+
{
111+
schedule.Identity,
112+
schedule.Name,
113+
clusterName,
114+
string(schedule.Method),
115+
schedule.Schedule,
116+
schedule.RetentionPolicy,
117+
nextBackup,
118+
string(schedule.Status),
119+
},
120+
}
121+
122+
backupScheduleCreateNoHeader, _ := cmd.Flags().GetBool(NoHeaderKey)
123+
if backupScheduleCreateNoHeader {
124+
table.Print(nil, body)
125+
} else {
126+
table.Print([]string{"ID", "Name", "Cluster", "Method", "Schedule", "Retention", "Next Backup", "Status"}, body)
127+
}
128+
129+
return nil
130+
},
131+
}
132+
133+
func init() {
134+
BackupSchedulesCmd.AddCommand(backupScheduleCreateCmd)
135+
136+
backupScheduleCreateCmd.Flags().Bool(NoHeaderKey, false, "Do not print the header")
137+
backupScheduleCreateCmd.Flags().StringVarP(&backupScheduleCreateName, "name", "n", "", "Name of the backup schedule (required)")
138+
backupScheduleCreateCmd.Flags().StringVar(&backupScheduleCreateDescription, "description", "", "Description of the backup schedule")
139+
backupScheduleCreateCmd.Flags().StringVar(&backupScheduleCreateSchedule, "schedule", "", "Cron expression for the backup schedule (required)")
140+
backupScheduleCreateCmd.Flags().StringVar(&backupScheduleCreateRetentionPolicy, "retention-policy", "", "Retention policy for the backup schedule (required)")
141+
backupScheduleCreateCmd.Flags().StringVar(&backupScheduleCreateMethod, "method", "barman", "Backup method: 'barman' (default)")
142+
backupScheduleCreateCmd.Flags().StringSliceVar(&backupScheduleCreateLabels, "labels", []string{}, "Labels in key=value format (can be specified multiple times)")
143+
backupScheduleCreateCmd.Flags().StringSliceVar(&backupScheduleCreateAnnotations, "annotations", []string{}, "Annotations in key=value format (can be specified multiple times)")
144+
145+
_ = backupScheduleCreateCmd.MarkFlagRequired("name")
146+
_ = backupScheduleCreateCmd.MarkFlagRequired("schedule")
147+
_ = backupScheduleCreateCmd.MarkFlagRequired("retention-policy")
148+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package backupschedules
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
8+
"github.com/thalassa-cloud/cli/internal/completion"
9+
"github.com/thalassa-cloud/cli/internal/thalassaclient"
10+
tcclient "github.com/thalassa-cloud/client-go/pkg/client"
11+
)
12+
13+
var (
14+
backupScheduleDeleteForce bool
15+
)
16+
17+
// backupScheduleDeleteCmd represents the backup-schedules delete command
18+
var backupScheduleDeleteCmd = &cobra.Command{
19+
Use: "delete",
20+
Short: "Delete a database backup schedule",
21+
Long: "Delete a database backup schedule",
22+
Aliases: []string{"d", "rm", "del", "remove"},
23+
Args: cobra.ExactArgs(2),
24+
ValidArgsFunction: completion.CompleteDbClusterID,
25+
RunE: func(cmd *cobra.Command, args []string) error {
26+
client, err := thalassaclient.GetThalassaClient()
27+
if err != nil {
28+
return fmt.Errorf("failed to create client: %w", err)
29+
}
30+
31+
clusterIdentity := args[0]
32+
scheduleIdentity := args[1]
33+
34+
// Get schedule to show name in confirmation
35+
var scheduleName string
36+
if !backupScheduleDeleteForce {
37+
schedule, err := client.DBaaS().GetDbBackupSchedule(cmd.Context(), clusterIdentity, scheduleIdentity)
38+
if err != nil {
39+
if tcclient.IsNotFound(err) {
40+
return fmt.Errorf("backup schedule not found: %s", scheduleIdentity)
41+
}
42+
return fmt.Errorf("failed to get backup schedule: %w", err)
43+
}
44+
scheduleName = schedule.Name
45+
}
46+
47+
// Ask for confirmation unless --force is provided
48+
if !backupScheduleDeleteForce {
49+
fmt.Printf("Are you sure you want to delete backup schedule %s (%s)?\n", scheduleName, scheduleIdentity)
50+
var confirm string
51+
fmt.Printf("Enter 'yes' to confirm: ")
52+
fmt.Scanln(&confirm)
53+
if confirm != "yes" {
54+
fmt.Println("Aborted")
55+
return nil
56+
}
57+
}
58+
59+
err = client.DBaaS().DeleteDbBackupSchedule(cmd.Context(), clusterIdentity, scheduleIdentity)
60+
if err != nil {
61+
if tcclient.IsNotFound(err) {
62+
return fmt.Errorf("backup schedule not found: %s", scheduleIdentity)
63+
}
64+
return fmt.Errorf("failed to delete backup schedule: %w", err)
65+
}
66+
67+
fmt.Printf("Backup schedule %s deleted successfully\n", scheduleIdentity)
68+
return nil
69+
},
70+
}
71+
72+
func init() {
73+
BackupSchedulesCmd.AddCommand(backupScheduleDeleteCmd)
74+
75+
backupScheduleDeleteCmd.Flags().BoolVar(&backupScheduleDeleteForce, "force", false, "Force the deletion and skip the confirmation")
76+
}

‎cmd/dbaas/backup-schedules/list.go‎

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package backupschedules
2+
3+
import (
4+
"fmt"
5+
"sort"
6+
"strings"
7+
8+
"github.com/spf13/cobra"
9+
10+
"github.com/thalassa-cloud/cli/internal/completion"
11+
"github.com/thalassa-cloud/cli/internal/formattime"
12+
"github.com/thalassa-cloud/cli/internal/table"
13+
"github.com/thalassa-cloud/cli/internal/thalassaclient"
14+
"github.com/thalassa-cloud/client-go/dbaas"
15+
)
16+
17+
var (
18+
backupScheduleListClusterFilter string
19+
backupScheduleListNoHeader bool
20+
backupScheduleListShowExactTime bool
21+
backupScheduleListShowLabels bool
22+
)
23+
24+
// backupScheduleListCmd represents the backup-schedules list command
25+
var backupScheduleListCmd = &cobra.Command{
26+
Use: "list",
27+
Short: "List database backup schedules",
28+
Long: "List database backup schedules for a specific cluster or all schedules in the organisation",
29+
Aliases: []string{"ls", "get", "clusters", "cluster"},
30+
Args: cobra.MaximumNArgs(1),
31+
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
32+
if len(args) == 0 {
33+
return completion.CompleteDbClusterID(cmd, args, toComplete)
34+
}
35+
return nil, cobra.ShellCompDirectiveNoFileComp
36+
},
37+
RunE: func(cmd *cobra.Command, args []string) error {
38+
client, err := thalassaclient.GetThalassaClient()
39+
if err != nil {
40+
return fmt.Errorf("failed to create client: %w", err)
41+
}
42+
43+
var schedules []dbaas.DbClusterBackupSchedule
44+
45+
// If cluster identity is provided as argument, list schedules for that cluster
46+
if len(args) > 0 {
47+
clusterIdentity := args[0]
48+
schedules, err = client.DBaaS().ListDbBackupSchedules(cmd.Context(), clusterIdentity)
49+
if err != nil {
50+
return fmt.Errorf("failed to list backup schedules for cluster: %w", err)
51+
}
52+
} else {
53+
// Otherwise list all schedules for the organisation
54+
schedules, err = client.DBaaS().ListDbBackupSchedulesForOrganisation(cmd.Context())
55+
if err != nil {
56+
return fmt.Errorf("failed to list backup schedules: %w", err)
57+
}
58+
}
59+
60+
if len(schedules) == 0 {
61+
fmt.Println("No backup schedules found")
62+
return nil
63+
}
64+
65+
body := make([][]string, 0, len(schedules))
66+
for _, schedule := range schedules {
67+
clusterName := "-"
68+
if schedule.DbCluster != nil {
69+
clusterName = schedule.DbCluster.Name
70+
}
71+
72+
status := string(schedule.Status)
73+
if schedule.Suspended {
74+
status = fmt.Sprintf("%s (suspended)", status)
75+
}
76+
if schedule.DeleteScheduledAt != nil {
77+
status = fmt.Sprintf("%s (deletion scheduled)", status)
78+
}
79+
80+
nextBackup := "-"
81+
if schedule.NextBackupAt != nil {
82+
nextBackup = formattime.FormatTime(schedule.NextBackupAt.Local(), backupScheduleListShowExactTime)
83+
}
84+
85+
lastBackup := "-"
86+
if schedule.LastBackupAt != nil {
87+
lastBackup = formattime.FormatTime(schedule.LastBackupAt.Local(), backupScheduleListShowExactTime)
88+
}
89+
90+
row := []string{
91+
schedule.Identity,
92+
schedule.Name,
93+
clusterName,
94+
string(schedule.Method),
95+
schedule.Schedule,
96+
schedule.RetentionPolicy,
97+
fmt.Sprintf("%d", schedule.BackupCount),
98+
nextBackup,
99+
lastBackup,
100+
status,
101+
formattime.FormatTime(schedule.CreatedAt.Local(), backupScheduleListShowExactTime),
102+
}
103+
104+
if backupScheduleListShowLabels {
105+
labelStrs := []string{}
106+
for k, v := range schedule.Labels {
107+
labelStrs = append(labelStrs, k+"="+v)
108+
}
109+
sort.Strings(labelStrs)
110+
if len(labelStrs) == 0 {
111+
labelStrs = []string{"-"}
112+
}
113+
row = append(row, strings.Join(labelStrs, ","))
114+
}
115+
116+
body = append(body, row)
117+
}
118+
119+
if backupScheduleListNoHeader {
120+
table.Print(nil, body)
121+
} else {
122+
headers := []string{"ID", "Name", "Cluster", "Method", "Schedule", "Retention", "Backups", "Next Backup", "Last Backup", "Status", "Created"}
123+
if backupScheduleListShowLabels {
124+
headers = append(headers, "Labels")
125+
}
126+
table.Print(headers, body)
127+
}
128+
129+
return nil
130+
},
131+
}
132+
133+
func init() {
134+
BackupSchedulesCmd.AddCommand(backupScheduleListCmd)
135+
136+
backupScheduleListCmd.Flags().BoolVar(&backupScheduleListNoHeader, NoHeaderKey, false, "Do not print the header")
137+
backupScheduleListCmd.Flags().BoolVar(&backupScheduleListShowExactTime, "exact-time", false, "Show exact time instead of relative time")
138+
backupScheduleListCmd.Flags().BoolVar(&backupScheduleListShowLabels, "show-labels", false, "Show labels")
139+
backupScheduleListCmd.Flags().StringVar(&backupScheduleListClusterFilter, "cluster", "", "Filter by database cluster identity, slug, or name")
140+
141+
// Register completions
142+
backupScheduleListCmd.RegisterFlagCompletionFunc("cluster", completion.CompleteDbClusterID)
143+
}

0 commit comments

Comments
 (0)