Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions cmd/dbaas/backup-schedules/backup-schedules.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package backupschedules

import (
"github.com/spf13/cobra"
)

const NoHeaderKey = "no-header"

// BackupSchedulesCmd represents the backup-schedules command
var BackupSchedulesCmd = &cobra.Command{
Use: "backup-schedules",
Aliases: []string{"backup-schedule", "schedules", "schedule"},
Short: "Manage database backup schedules",
Long: "Manage database backup schedules for database clusters",
}
148 changes: 148 additions & 0 deletions cmd/dbaas/backup-schedules/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package backupschedules

import (
"fmt"
"strings"

"github.com/spf13/cobra"

"github.com/thalassa-cloud/cli/internal/completion"
"github.com/thalassa-cloud/cli/internal/table"
"github.com/thalassa-cloud/cli/internal/thalassaclient"
"github.com/thalassa-cloud/client-go/dbaas"
tcclient "github.com/thalassa-cloud/client-go/pkg/client"
)

var (
backupScheduleCreateName string
backupScheduleCreateDescription string
backupScheduleCreateSchedule string
backupScheduleCreateRetentionPolicy string
backupScheduleCreateMethod string
backupScheduleCreateLabels []string
backupScheduleCreateAnnotations []string
)

// backupScheduleCreateCmd represents the backup-schedules create command
var backupScheduleCreateCmd = &cobra.Command{
Use: "create",
Short: "Create a database backup schedule",
Long: "Create a new backup schedule for a database cluster",
Args: cobra.ExactArgs(1),
ValidArgsFunction: completion.CompleteDbClusterID,
RunE: func(cmd *cobra.Command, args []string) error {
client, err := thalassaclient.GetThalassaClient()
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}

clusterIdentity := args[0]

if backupScheduleCreateName == "" {
return fmt.Errorf("name is required")
}
if backupScheduleCreateSchedule == "" {
return fmt.Errorf("schedule is required")
}
if backupScheduleCreateRetentionPolicy == "" {
return fmt.Errorf("retention-policy is required")
}
if backupScheduleCreateMethod == "" {
return fmt.Errorf("method is required")
}

// Parse labels from key=value format
labels := make(map[string]string)
for _, label := range backupScheduleCreateLabels {
parts := strings.SplitN(label, "=", 2)
if len(parts) == 2 {
labels[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
}
}

// Parse annotations from key=value format
annotations := make(map[string]string)
for _, annotation := range backupScheduleCreateAnnotations {
parts := strings.SplitN(annotation, "=", 2)
if len(parts) == 2 {
annotations[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
}
}

method := dbaas.DbClusterBackupScheduleMethod(backupScheduleCreateMethod)
if method != dbaas.DbClusterBackupScheduleMethodSnapshot && method != dbaas.DbClusterBackupScheduleMethodBarman {
return fmt.Errorf("method must be either 'snapshot' or 'barman'")
}

createReq := dbaas.CreateDbBackupScheduleRequest{
Name: backupScheduleCreateName,
Schedule: backupScheduleCreateSchedule,
RetentionPolicy: backupScheduleCreateRetentionPolicy,
Method: method,
Labels: labels,
Annotations: annotations,
}

if backupScheduleCreateDescription != "" {
createReq.Description = &backupScheduleCreateDescription
}

schedule, err := client.DBaaS().CreateDbBackupSchedule(cmd.Context(), clusterIdentity, createReq)
if err != nil {
if tcclient.IsNotFound(err) {
return fmt.Errorf("database cluster not found: %s", clusterIdentity)
}
return fmt.Errorf("failed to create backup schedule: %w", err)
}

// Output in table format
clusterName := ""
if schedule.DbCluster != nil {
clusterName = schedule.DbCluster.Name
}

nextBackup := "-"
if schedule.NextBackupAt != nil {
nextBackup = schedule.NextBackupAt.Format("2006-01-02 15:04:05")
}

body := [][]string{
{
schedule.Identity,
schedule.Name,
clusterName,
string(schedule.Method),
schedule.Schedule,
schedule.RetentionPolicy,
nextBackup,
string(schedule.Status),
},
}

backupScheduleCreateNoHeader, _ := cmd.Flags().GetBool(NoHeaderKey)
if backupScheduleCreateNoHeader {
table.Print(nil, body)
} else {
table.Print([]string{"ID", "Name", "Cluster", "Method", "Schedule", "Retention", "Next Backup", "Status"}, body)
}

return nil
},
}

func init() {
BackupSchedulesCmd.AddCommand(backupScheduleCreateCmd)

backupScheduleCreateCmd.Flags().Bool(NoHeaderKey, false, "Do not print the header")
backupScheduleCreateCmd.Flags().StringVarP(&backupScheduleCreateName, "name", "n", "", "Name of the backup schedule (required)")
backupScheduleCreateCmd.Flags().StringVar(&backupScheduleCreateDescription, "description", "", "Description of the backup schedule")
backupScheduleCreateCmd.Flags().StringVar(&backupScheduleCreateSchedule, "schedule", "", "Cron expression for the backup schedule (required)")
backupScheduleCreateCmd.Flags().StringVar(&backupScheduleCreateRetentionPolicy, "retention-policy", "", "Retention policy for the backup schedule (required)")
backupScheduleCreateCmd.Flags().StringVar(&backupScheduleCreateMethod, "method", "barman", "Backup method: 'barman' (default)")
backupScheduleCreateCmd.Flags().StringSliceVar(&backupScheduleCreateLabels, "labels", []string{}, "Labels in key=value format (can be specified multiple times)")
backupScheduleCreateCmd.Flags().StringSliceVar(&backupScheduleCreateAnnotations, "annotations", []string{}, "Annotations in key=value format (can be specified multiple times)")

_ = backupScheduleCreateCmd.MarkFlagRequired("name")
_ = backupScheduleCreateCmd.MarkFlagRequired("schedule")
_ = backupScheduleCreateCmd.MarkFlagRequired("retention-policy")
}
76 changes: 76 additions & 0 deletions cmd/dbaas/backup-schedules/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package backupschedules

import (
"fmt"

"github.com/spf13/cobra"

"github.com/thalassa-cloud/cli/internal/completion"
"github.com/thalassa-cloud/cli/internal/thalassaclient"
tcclient "github.com/thalassa-cloud/client-go/pkg/client"
)

var (
backupScheduleDeleteForce bool
)

// backupScheduleDeleteCmd represents the backup-schedules delete command
var backupScheduleDeleteCmd = &cobra.Command{
Use: "delete",
Short: "Delete a database backup schedule",
Long: "Delete a database backup schedule",
Aliases: []string{"d", "rm", "del", "remove"},
Args: cobra.ExactArgs(2),
ValidArgsFunction: completion.CompleteDbClusterID,
RunE: func(cmd *cobra.Command, args []string) error {
client, err := thalassaclient.GetThalassaClient()
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}

clusterIdentity := args[0]
scheduleIdentity := args[1]

// Get schedule to show name in confirmation
var scheduleName string
if !backupScheduleDeleteForce {
schedule, err := client.DBaaS().GetDbBackupSchedule(cmd.Context(), clusterIdentity, scheduleIdentity)
if err != nil {
if tcclient.IsNotFound(err) {
return fmt.Errorf("backup schedule not found: %s", scheduleIdentity)
}
return fmt.Errorf("failed to get backup schedule: %w", err)
}
scheduleName = schedule.Name
}

// Ask for confirmation unless --force is provided
if !backupScheduleDeleteForce {
fmt.Printf("Are you sure you want to delete backup schedule %s (%s)?\n", scheduleName, scheduleIdentity)
var confirm string
fmt.Printf("Enter 'yes' to confirm: ")
fmt.Scanln(&confirm)
if confirm != "yes" {
fmt.Println("Aborted")
return nil
}
}

err = client.DBaaS().DeleteDbBackupSchedule(cmd.Context(), clusterIdentity, scheduleIdentity)
if err != nil {
if tcclient.IsNotFound(err) {
return fmt.Errorf("backup schedule not found: %s", scheduleIdentity)
}
return fmt.Errorf("failed to delete backup schedule: %w", err)
}

fmt.Printf("Backup schedule %s deleted successfully\n", scheduleIdentity)
return nil
},
}

func init() {
BackupSchedulesCmd.AddCommand(backupScheduleDeleteCmd)

backupScheduleDeleteCmd.Flags().BoolVar(&backupScheduleDeleteForce, "force", false, "Force the deletion and skip the confirmation")
}
143 changes: 143 additions & 0 deletions cmd/dbaas/backup-schedules/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package backupschedules

import (
"fmt"
"sort"
"strings"

"github.com/spf13/cobra"

"github.com/thalassa-cloud/cli/internal/completion"
"github.com/thalassa-cloud/cli/internal/formattime"
"github.com/thalassa-cloud/cli/internal/table"
"github.com/thalassa-cloud/cli/internal/thalassaclient"
"github.com/thalassa-cloud/client-go/dbaas"
)

var (
backupScheduleListClusterFilter string
backupScheduleListNoHeader bool
backupScheduleListShowExactTime bool
backupScheduleListShowLabels bool
)

// backupScheduleListCmd represents the backup-schedules list command
var backupScheduleListCmd = &cobra.Command{
Use: "list",
Short: "List database backup schedules",
Long: "List database backup schedules for a specific cluster or all schedules in the organisation",
Aliases: []string{"ls", "get", "clusters", "cluster"},
Args: cobra.MaximumNArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return completion.CompleteDbClusterID(cmd, args, toComplete)
}
return nil, cobra.ShellCompDirectiveNoFileComp
},
RunE: func(cmd *cobra.Command, args []string) error {
client, err := thalassaclient.GetThalassaClient()
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}

var schedules []dbaas.DbClusterBackupSchedule

// If cluster identity is provided as argument, list schedules for that cluster
if len(args) > 0 {
clusterIdentity := args[0]
schedules, err = client.DBaaS().ListDbBackupSchedules(cmd.Context(), clusterIdentity)
if err != nil {
return fmt.Errorf("failed to list backup schedules for cluster: %w", err)
}
} else {
// Otherwise list all schedules for the organisation
schedules, err = client.DBaaS().ListDbBackupSchedulesForOrganisation(cmd.Context())
if err != nil {
return fmt.Errorf("failed to list backup schedules: %w", err)
}
}

if len(schedules) == 0 {
fmt.Println("No backup schedules found")
return nil
}

body := make([][]string, 0, len(schedules))
for _, schedule := range schedules {
clusterName := "-"
if schedule.DbCluster != nil {
clusterName = schedule.DbCluster.Name
}

status := string(schedule.Status)
if schedule.Suspended {
status = fmt.Sprintf("%s (suspended)", status)
}
if schedule.DeleteScheduledAt != nil {
status = fmt.Sprintf("%s (deletion scheduled)", status)
}

nextBackup := "-"
if schedule.NextBackupAt != nil {
nextBackup = formattime.FormatTime(schedule.NextBackupAt.Local(), backupScheduleListShowExactTime)
}

lastBackup := "-"
if schedule.LastBackupAt != nil {
lastBackup = formattime.FormatTime(schedule.LastBackupAt.Local(), backupScheduleListShowExactTime)
}

row := []string{
schedule.Identity,
schedule.Name,
clusterName,
string(schedule.Method),
schedule.Schedule,
schedule.RetentionPolicy,
fmt.Sprintf("%d", schedule.BackupCount),
nextBackup,
lastBackup,
status,
formattime.FormatTime(schedule.CreatedAt.Local(), backupScheduleListShowExactTime),
}

if backupScheduleListShowLabels {
labelStrs := []string{}
for k, v := range schedule.Labels {
labelStrs = append(labelStrs, k+"="+v)
}
sort.Strings(labelStrs)
if len(labelStrs) == 0 {
labelStrs = []string{"-"}
}
row = append(row, strings.Join(labelStrs, ","))
}

body = append(body, row)
}

if backupScheduleListNoHeader {
table.Print(nil, body)
} else {
headers := []string{"ID", "Name", "Cluster", "Method", "Schedule", "Retention", "Backups", "Next Backup", "Last Backup", "Status", "Created"}
if backupScheduleListShowLabels {
headers = append(headers, "Labels")
}
table.Print(headers, body)
}

return nil
},
}

func init() {
BackupSchedulesCmd.AddCommand(backupScheduleListCmd)

backupScheduleListCmd.Flags().BoolVar(&backupScheduleListNoHeader, NoHeaderKey, false, "Do not print the header")
backupScheduleListCmd.Flags().BoolVar(&backupScheduleListShowExactTime, "exact-time", false, "Show exact time instead of relative time")
backupScheduleListCmd.Flags().BoolVar(&backupScheduleListShowLabels, "show-labels", false, "Show labels")
backupScheduleListCmd.Flags().StringVar(&backupScheduleListClusterFilter, "cluster", "", "Filter by database cluster identity, slug, or name")

// Register completions
backupScheduleListCmd.RegisterFlagCompletionFunc("cluster", completion.CompleteDbClusterID)
}
Loading
Loading