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
43 changes: 42 additions & 1 deletion cmd/dbaas/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/thalassa-cloud/cli/internal/thalassaclient"
"github.com/thalassa-cloud/client-go/dbaas"
"github.com/thalassa-cloud/client-go/iaas"
tcclient "github.com/thalassa-cloud/client-go/pkg/client"
)

var (
Expand All @@ -35,14 +36,17 @@ var (
createClusterWaitTimeout time.Duration
createClusterProvisionDbBackupObjectStorageBucket bool
createClusterDbBackupObjectStorageId string
createClusterRestoreFromBackup string
createClusterRestoreTargetTime string
createClusterRestoreTargetLSN string
)

// createCmd represents the create command
var createCmd = &cobra.Command{
Use: "create",
Aliases: []string{"create-cluster", "cluster-create"},
Short: "Create a database cluster",
Long: "Create a new database cluster in the Thalassa Cloud Platform.",
Long: "Create a new database cluster in the Thalassa Cloud Platform. Use --restore-from-backup to create a cluster from an existing backup.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
client, err := thalassaclient.GetThalassaClient()
Expand All @@ -68,6 +72,12 @@ var createCmd = &cobra.Command{
if createClusterReplicas < 0 {
return fmt.Errorf("replicas must be 0 or greater")
}
if createClusterRestoreTargetTime != "" && createClusterRestoreTargetLSN != "" {
return fmt.Errorf("--restore-target-time and --restore-target-lsn are mutually exclusive")
}
if (createClusterRestoreTargetTime != "" || createClusterRestoreTargetLSN != "") && createClusterRestoreFromBackup == "" {
return fmt.Errorf("--restore-from-backup is required when using a restore recovery target")
}

// Resolve volume type
volumeTypes, err := client.IaaS().ListVolumeTypes(cmd.Context(), &iaas.ListVolumeTypesRequest{})
Expand Down Expand Up @@ -147,6 +157,33 @@ var createCmd = &cobra.Command{
req.Replicas = createClusterReplicas
}

if createClusterRestoreFromBackup != "" {
_, err := client.DBaaS().GetDbBackup(cmd.Context(), createClusterRestoreFromBackup)
if err != nil {
if tcclient.IsNotFound(err) {
return fmt.Errorf("backup not found: %s", createClusterRestoreFromBackup)
}
return fmt.Errorf("failed to get backup: %w", err)
}
req.RestoreFromBackupIdentity = &createClusterRestoreFromBackup

if createClusterRestoreTargetTime != "" || createClusterRestoreTargetLSN != "" {
recoveryTarget := &dbaas.RestoreRecoveryTarget{}
if createClusterRestoreTargetTime != "" {
targetTime, err := parseBarmanRestoreTargetTime(createClusterRestoreTargetTime)
if err != nil {
return fmt.Errorf("invalid restore target time: %w", err)
}
recoveryTarget.TargetTime = &targetTime
}
if createClusterRestoreTargetLSN != "" {
lsn := strings.TrimSpace(createClusterRestoreTargetLSN)
recoveryTarget.TargetLSN = &lsn
}
req.RestoreRecoveryTarget = recoveryTarget
}
}

cluster, err := client.DBaaS().CreateDbCluster(cmd.Context(), req)
if err != nil {
return fmt.Errorf("failed to create database cluster: %w", err)
Expand Down Expand Up @@ -239,11 +276,15 @@ func init() {
createCmd.Flags().DurationVar(&createClusterWaitTimeout, "wait-timeout", 20*time.Minute, "Maximum time to wait for the database cluster to be available")
createCmd.Flags().BoolVar(&createClusterProvisionDbBackupObjectStorageBucket, "with-backup-bucket", false, "Provision a backup object storage bucket for the database cluster")
createCmd.Flags().StringVar(&createClusterDbBackupObjectStorageId, "backup-object-storage-id", "", "Backup object storage ID (enables backup storage, requires --with-backup-bucket=false)")
createCmd.Flags().StringVar(&createClusterRestoreFromBackup, "restore-from-backup", "", "Backup identity to restore the cluster from")
createCmd.Flags().StringVar(&createClusterRestoreTargetTime, "restore-target-time", "", barmanTargetTimeDescription+" (requires --restore-from-backup)")
createCmd.Flags().StringVar(&createClusterRestoreTargetLSN, "restore-target-lsn", "", "Point-in-time recovery target LSN (requires --restore-from-backup)")

// Register completions
_ = createCmd.RegisterFlagCompletionFunc("vpc", completion.CompleteVPCID)
_ = createCmd.RegisterFlagCompletionFunc("subnet", completion.CompleteSubnetEnhanced)
_ = createCmd.RegisterFlagCompletionFunc("engine-version", completion.CompleteDbEngineVersion)
_ = createCmd.RegisterFlagCompletionFunc("restore-from-backup", completion.CompleteDbBackupID)

_ = createCmd.MarkFlagRequired("name")
_ = createCmd.MarkFlagRequired("engine")
Expand Down
27 changes: 27 additions & 0 deletions cmd/dbaas/restore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package dbaas

import (
"fmt"
"strings"
"time"
)

const (
barmanTargetTimeExample = "2023-08-11 11:14:21.00000+02"
barmanTargetTimeDescription = "Timestamp to restore to in barman format (YYYY-MM-DD HH:MM:SS.00000±TZ). Example: '" + barmanTargetTimeExample + "'"
barmanTargetTimeLayout = "2006-01-02 15:04:05.00000-07"
)

func parseBarmanRestoreTargetTime(value string) (string, error) {
value = strings.TrimSpace(value)
if value == "" {
return "", fmt.Errorf("restore target time is empty")
}

targetTime, err := time.Parse(barmanTargetTimeLayout, value)
if err != nil {
return "", fmt.Errorf("parse barman target time: %w", err)
}

return targetTime.Format(barmanTargetTimeLayout), nil
}
56 changes: 56 additions & 0 deletions cmd/dbaas/restore_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package dbaas

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestParseBarmanRestoreTargetTime(t *testing.T) {
tests := []struct {
name string
input string
want string
wantErr bool
}{
{
name: "valid positive offset",
input: "2023-08-11 11:14:21.00000+02",
want: "2023-08-11 11:14:21.00000+02",
},
{
name: "valid negative offset",
input: "2023-08-11 11:14:21.00000-05",
want: "2023-08-11 11:14:21.00000-05",
},
{
name: "trims whitespace",
input: " 2023-08-11 11:14:21.00000+02 ",
want: "2023-08-11 11:14:21.00000+02",
},
{
name: "invalid format",
input: "2026-07-15T10:00:00Z",
wantErr: true,
},
{
name: "empty",
input: " ",
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseBarmanRestoreTargetTime(tt.input)
if tt.wantErr {
require.Error(t, err)
return
}

require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
13 changes: 13 additions & 0 deletions cmd/dbaas/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ var (
updateClusterAnnotations []string
updateClusterDeleteProtection bool
updateClusterAutoUpgradePolicy string
updateClusterSecurityGroups []string
)

// updateCmd represents the update command
Expand Down Expand Up @@ -64,6 +65,11 @@ var updateCmd = &cobra.Command{
if current.Replicas > 0 {
req.Replicas = current.Replicas
}
if len(current.SecurityGroups) > 0 {
for _, sg := range current.SecurityGroups {
req.SecurityGroupAttachments = append(req.SecurityGroupAttachments, sg.Identity)
}
}

// Update name if provided
if cmd.Flags().Changed("name") {
Expand Down Expand Up @@ -119,6 +125,11 @@ var updateCmd = &cobra.Command{
req.DeleteProtection = updateClusterDeleteProtection
}

// Update security groups if provided
if cmd.Flags().Changed("security-groups") {
req.SecurityGroupAttachments = updateClusterSecurityGroups
}

// Update auto-upgrade policy if provided
if cmd.Flags().Changed("auto-upgrade-policy") {
policy := dbaas.DbClusterAutoUpgradePolicy(updateClusterAutoUpgradePolicy)
Expand Down Expand Up @@ -188,9 +199,11 @@ func init() {
updateCmd.Flags().StringSliceVar(&updateClusterAnnotations, "annotations", []string{}, "Annotations in key=value format (can be specified multiple times)")
updateCmd.Flags().BoolVar(&updateClusterDeleteProtection, "delete-protection", false, "Enable or disable delete protection")
updateCmd.Flags().StringVar(&updateClusterAutoUpgradePolicy, "auto-upgrade-policy", "", "Auto-upgrade policy: none, latest-version, latest-stable, latest-patch, latest-minor, latest-major")
updateCmd.Flags().StringSliceVar(&updateClusterSecurityGroups, "security-groups", []string{}, "Security group identities to attach")

// Register completions
_ = updateCmd.RegisterFlagCompletionFunc("instance-type", completion.CompleteDbInstanceType)
_ = updateCmd.RegisterFlagCompletionFunc("security-groups", completion.CompleteSecurityGroupID)
_ = updateCmd.RegisterFlagCompletionFunc("auto-upgrade-policy", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{
string(dbaas.DbClusterAutoUpgradePolicyNone),
Expand Down
Loading