diff --git a/Makefile b/Makefile index 8b97cd0..67ac571 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,7 @@ generate: deps mockgen --destination ./internal/mocks/cloud_instances_service.go --package=mocks --source ./vendor/github.com/serverscom/serverscom-go-client/pkg/cloud_computing_instances.go mockgen --destination ./internal/mocks/cloud_computing_regions_service.go --package=mocks --source ./vendor/github.com/serverscom/serverscom-go-client/pkg/cloud_computing_regions.go mockgen --destination ./internal/mocks/cloud_block_storage_volumes_service.go --package=mocks --source ./vendor/github.com/serverscom/serverscom-go-client/pkg/cloud_block_storage_volumes.go + mockgen --destination ./internal/mocks/cloud_block_storage_backups_service.go --package=mocks --source ./vendor/github.com/serverscom/serverscom-go-client/pkg/cloud_block_storage_backups.go sed -i '' 's|github.com/serverscom/srvctl/vendor/github.com/serverscom/serverscom-go-client/pkg|github.com/serverscom/serverscom-go-client/pkg|g' \ ./internal/mocks/ssh_service.go \ ./internal/mocks/hosts_service.go \ @@ -37,6 +38,7 @@ generate: deps ./internal/mocks/kubernetes_clusters_service.go \ ./internal/mocks/l2_segment_service.go \ ./internal/mocks/network_pool_service.go \ - ./internal/mocks/cloud_block_storage_volumes_service.go \ ./internal/mocks/cloud_instances_service.go \ - ./internal/mocks/cloud_computing_regions_service.go + ./internal/mocks/cloud_computing_regions_service.go \ + ./internal/mocks/cloud_block_storage_volumes_service.go \ + ./internal/mocks/cloud_block_storage_backups_service.go diff --git a/cmd/entities/cloud-backups/add.go b/cmd/entities/cloud-backups/add.go new file mode 100644 index 0000000..f558579 --- /dev/null +++ b/cmd/entities/cloud-backups/add.go @@ -0,0 +1,83 @@ +package cloudbackups + +import ( + serverscom "github.com/serverscom/serverscom-go-client/pkg" + "github.com/serverscom/srvctl/cmd/base" + "github.com/spf13/cobra" +) + +type AddFlags struct { + VolumeID string + Name string + Incremental bool + Force bool + Labels []string +} + +func newAddCmd(cmdContext *base.CmdContext) *cobra.Command { + flags := &AddFlags{} + + cmd := &cobra.Command{ + Use: "add --volume-id --name ", + Short: "Add a cloud backup", + Long: "Create a new cloud block storage backup", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + manager := cmdContext.GetManager() + + ctx, cancel := base.SetupContext(cmd, manager) + defer cancel() + + base.SetupProxy(cmd, manager) + + input := &serverscom.CloudBlockStorageBackupCreateInput{} + + if err := flags.FillInput(cmd, input); err != nil { + return err + } + + scClient := cmdContext.GetClient().SetVerbose(manager.GetVerbose(cmd)).GetScClient() + backup, err := scClient.CloudBlockStorageBackups.Create(ctx, *input) + if err != nil { + return err + } + + if backup != nil { + formatter := cmdContext.GetOrCreateFormatter(cmd) + return formatter.Format(backup) + } + return nil + }, + } + + cmd.Flags().StringVarP(&flags.VolumeID, "volume-id", "", "", "ID of the volume to backup") + cmd.Flags().StringVarP(&flags.Name, "name", "n", "", "Name of the backup") + cmd.Flags().BoolVarP(&flags.Incremental, "incremental", "", false, "Create incremental backup") + cmd.Flags().BoolVarP(&flags.Force, "force", "", false, "Force backup creation") + cmd.Flags().StringArrayVarP(&flags.Labels, "label", "l", []string{}, "string in key=value format") + + _ = cmd.MarkFlagRequired("volume-id") + _ = cmd.MarkFlagRequired("name") + + return cmd +} + +func (f *AddFlags) FillInput(cmd *cobra.Command, input *serverscom.CloudBlockStorageBackupCreateInput) error { + input.VolumeID = f.VolumeID + input.Name = f.Name + if cmd.Flags().Changed("incremental") { + input.Incremental = f.Incremental + } + if cmd.Flags().Changed("force") { + input.Force = f.Force + } + if cmd.Flags().Changed("label") { + labelsMap, err := base.ParseLabels(f.Labels) + if err != nil { + return err + } + input.Labels = labelsMap + } + + return nil +} diff --git a/cmd/entities/cloud-backups/cloud_backups.go b/cmd/entities/cloud-backups/cloud_backups.go new file mode 100644 index 0000000..848afbb --- /dev/null +++ b/cmd/entities/cloud-backups/cloud_backups.go @@ -0,0 +1,42 @@ +package cloudbackups + +import ( + "log" + + serverscom "github.com/serverscom/serverscom-go-client/pkg" + "github.com/serverscom/srvctl/cmd/base" + "github.com/serverscom/srvctl/internal/output/entities" + "github.com/spf13/cobra" +) + +func NewCmd(cmdContext *base.CmdContext) *cobra.Command { + backupEntity, err := entities.Registry.GetEntityFromValue(serverscom.CloudBlockStorageBackup{}) + if err != nil { + log.Fatal(err) + } + entitiesMap := make(map[string]entities.EntityInterface) + entitiesMap["cloud-backups"] = backupEntity + cmd := &cobra.Command{ + Use: "cloud-backups", + Short: "Manage cloud backups", + PersistentPreRunE: base.CombinePreRunE( + base.CheckFormatterFlags(cmdContext, entitiesMap), + base.CheckEmptyContexts(cmdContext), + ), + Args: base.NoArgs, + Run: base.UsageRun, + } + + cmd.AddCommand( + newListCmd(cmdContext), + newAddCmd(cmdContext), + newGetCmd(cmdContext), + newUpdateCmd(cmdContext), + newDeleteCmd(cmdContext), + newRestoreCmd(cmdContext), + ) + + base.AddFormatFlags(cmd) + + return cmd +} diff --git a/cmd/entities/cloud-backups/cloud_backups_test.go b/cmd/entities/cloud-backups/cloud_backups_test.go new file mode 100644 index 0000000..433fe53 --- /dev/null +++ b/cmd/entities/cloud-backups/cloud_backups_test.go @@ -0,0 +1,570 @@ +package cloudbackups + +import ( + "errors" + "path/filepath" + "testing" + "time" + + . "github.com/onsi/gomega" + serverscom "github.com/serverscom/serverscom-go-client/pkg" + "github.com/serverscom/srvctl/cmd/testutils" + "github.com/serverscom/srvctl/internal/mocks" + "go.uber.org/mock/gomock" +) + +var ( + testBackupID = "backup-123" + testVolumeUUID = "vol-123-openstack" + testVolumeUUID2 = "vol-456-openstack" + fixtureBasePath = filepath.Join("..", "..", "..", "testdata", "entities", "cloud-backups") + fixedTime = time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC) + fixedTime2 = time.Date(2025, 1, 2, 12, 0, 0, 0, time.UTC) + testBackupOpenstackUUID *string + testBackup = serverscom.CloudBlockStorageBackup{ + ID: testBackupID, + OpenstackUUID: testBackupOpenstackUUID, + OpenstackVolumeUUID: testVolumeUUID, + RegionID: 1, + Size: 1073741824, + Status: "available", + Labels: map[string]string{"env": "test"}, + Created: &fixedTime, + Name: "test-backup", + } + testBackup2 = serverscom.CloudBlockStorageBackup{ + ID: "backup-456", + OpenstackUUID: testBackupOpenstackUUID, + OpenstackVolumeUUID: testVolumeUUID2, + RegionID: 1, + Size: 2147483648, + Status: "available", + Labels: map[string]string{"env": "prod"}, + Created: &fixedTime2, + Name: "test-backup-2", + } +) + +func TestAddCloudBackupsCmd(t *testing.T) { + testCases := []struct { + name string + output string + args []string + configureMock func(*mocks.MockCloudBlockStorageBackupsService) + expectedOutput []byte + expectError bool + }{ + { + name: "create cloud backup with flags", + output: "json", + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "get.json")), + args: []string{ + "--volume-id", testVolumeUUID, + "--name", "test-backup", + "--label", "env=test", + }, + configureMock: func(mock *mocks.MockCloudBlockStorageBackupsService) { + mock.EXPECT(). + Create(gomock.Any(), serverscom.CloudBlockStorageBackupCreateInput{ + VolumeID: testVolumeUUID, + Name: "test-backup", + Incremental: false, + Force: false, + Labels: map[string]string{"env": "test"}, + }). + Return(&testBackup, nil) + }, + }, + { + name: "create cloud backup with error", + expectError: true, + args: []string{"--volume-id", testVolumeUUID, "--name", "test-backup"}, + configureMock: func(mock *mocks.MockCloudBlockStorageBackupsService) { + mock.EXPECT(). + Create(gomock.Any(), gomock.Any()). + Return(nil, errors.New("some error")) + }, + }, + } + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + backupServiceHandler := mocks.NewMockCloudBlockStorageBackupsService(mockCtrl) + + scClient := serverscom.NewClientWithEndpoint("", "") + scClient.CloudBlockStorageBackups = backupServiceHandler + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + if tc.configureMock != nil { + tc.configureMock(backupServiceHandler) + } + + testCmdContext := testutils.NewTestCmdContext(scClient) + backupCmd := NewCmd(testCmdContext) + + args := []string{"cloud-backups", "add"} + if len(tc.args) > 0 { + args = append(args, tc.args...) + } + if tc.output != "" { + args = append(args, "--output", tc.output) + } + + builder := testutils.NewTestCommandBuilder(). + WithCommand(backupCmd). + WithArgs(args) + + cmd := builder.Build() + + err := cmd.Execute() + + if tc.expectError { + g.Expect(err).To(HaveOccurred()) + } else { + g.Expect(err).To(BeNil()) + g.Expect(builder.GetOutput()).To(BeEquivalentTo(string(tc.expectedOutput))) + } + }) + } +} + +func TestGetCloudBackupsCmd(t *testing.T) { + testCases := []struct { + name string + backupID string + output string + expectedOutput []byte + expectError bool + }{ + { + name: "get cloud backup in default format", + backupID: testBackupID, + output: "", + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "get.txt")), + }, + { + name: "get cloud backup in JSON format", + backupID: testBackupID, + output: "json", + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "get.json")), + }, + { + name: "get cloud backup in YAML format", + backupID: testBackupID, + output: "yaml", + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "get.yaml")), + }, + { + name: "get cloud backup with error", + backupID: testBackupID, + expectError: true, + }, + } + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + backupServiceHandler := mocks.NewMockCloudBlockStorageBackupsService(mockCtrl) + + scClient := serverscom.NewClientWithEndpoint("", "") + scClient.CloudBlockStorageBackups = backupServiceHandler + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + var err error + if tc.expectError { + err = errors.New("some error") + } + backupServiceHandler.EXPECT(). + Get(gomock.Any(), testBackupID). + Return(&testBackup, err) + + testCmdContext := testutils.NewTestCmdContext(scClient) + backupCmd := NewCmd(testCmdContext) + + args := []string{"cloud-backups", "get", tc.backupID} + if tc.output != "" { + args = append(args, "--output", tc.output) + } + + builder := testutils.NewTestCommandBuilder(). + WithCommand(backupCmd). + WithArgs(args) + + cmd := builder.Build() + + err = cmd.Execute() + + if tc.expectError { + g.Expect(err).To(HaveOccurred()) + } else { + g.Expect(err).To(BeNil()) + g.Expect(builder.GetOutput()).To(BeEquivalentTo(string(tc.expectedOutput))) + } + }) + } +} + +func TestListCloudBackupsCmd(t *testing.T) { + testCases := []struct { + name string + output string + args []string + expectedOutput []byte + expectError bool + configureMock func(*mocks.MockCollection[serverscom.CloudBlockStorageBackup]) + }{ + { + name: "list all cloud backups", + output: "json", + args: []string{"-A"}, + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "list_all.json")), + configureMock: func(mock *mocks.MockCollection[serverscom.CloudBlockStorageBackup]) { + mock.EXPECT(). + Collect(gomock.Any()). + Return([]serverscom.CloudBlockStorageBackup{ + testBackup, + testBackup2, + }, nil) + }, + }, + { + name: "list cloud backups", + output: "json", + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "list.json")), + configureMock: func(mock *mocks.MockCollection[serverscom.CloudBlockStorageBackup]) { + mock.EXPECT(). + List(gomock.Any()). + Return([]serverscom.CloudBlockStorageBackup{ + testBackup, + }, nil) + }, + }, + { + name: "list cloud backups with template", + args: []string{"--template", "{{range .}}ID: {{.ID}}, Name: {{.Name}}\n{{end}}"}, + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "list_template.txt")), + configureMock: func(mock *mocks.MockCollection[serverscom.CloudBlockStorageBackup]) { + mock.EXPECT(). + List(gomock.Any()). + Return([]serverscom.CloudBlockStorageBackup{ + testBackup, + testBackup2, + }, nil) + }, + }, + { + name: "list cloud backups with pageView", + args: []string{"--page-view"}, + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "list_pageview.txt")), + configureMock: func(mock *mocks.MockCollection[serverscom.CloudBlockStorageBackup]) { + mock.EXPECT(). + List(gomock.Any()). + Return([]serverscom.CloudBlockStorageBackup{ + testBackup, + testBackup2, + }, nil) + }, + }, + { + name: "list cloud backups with error", + expectError: true, + configureMock: func(mock *mocks.MockCollection[serverscom.CloudBlockStorageBackup]) { + mock.EXPECT(). + List(gomock.Any()). + Return(nil, errors.New("some error")) + }, + }, + } + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + backupServiceHandler := mocks.NewMockCloudBlockStorageBackupsService(mockCtrl) + collectionHandler := mocks.NewMockCollection[serverscom.CloudBlockStorageBackup](mockCtrl) + + backupServiceHandler.EXPECT(). + Collection(). + Return(collectionHandler). + AnyTimes() + + collectionHandler.EXPECT(). + SetParam(gomock.Any(), gomock.Any()). + Return(collectionHandler). + AnyTimes() + + scClient := serverscom.NewClientWithEndpoint("", "") + scClient.CloudBlockStorageBackups = backupServiceHandler + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + if tc.configureMock != nil { + tc.configureMock(collectionHandler) + } + + testCmdContext := testutils.NewTestCmdContext(scClient) + backupCmd := NewCmd(testCmdContext) + + args := []string{"cloud-backups", "list"} + if len(tc.args) > 0 { + args = append(args, tc.args...) + } + if tc.output != "" { + args = append(args, "--output", tc.output) + } + + builder := testutils.NewTestCommandBuilder(). + WithCommand(backupCmd). + WithArgs(args) + + cmd := builder.Build() + + err := cmd.Execute() + + if tc.expectError { + g.Expect(err).To(HaveOccurred()) + } else { + g.Expect(err).To(BeNil()) + g.Expect(builder.GetOutput()).To(BeEquivalentTo(string(tc.expectedOutput))) + } + }) + } +} + +func TestUpdateCloudBackupsCmd(t *testing.T) { + updatedBackup := testBackup + updatedBackup.Labels = map[string]string{"env": "updated"} + + testCases := []struct { + name string + backupID string + output string + args []string + configureMock func(*mocks.MockCloudBlockStorageBackupsService) + expectedOutput []byte + expectError bool + }{ + { + name: "update cloud backup", + backupID: testBackupID, + output: "json", + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "update.json")), + args: []string{"--label", "env=updated"}, + configureMock: func(mock *mocks.MockCloudBlockStorageBackupsService) { + mock.EXPECT(). + Update(gomock.Any(), testBackupID, serverscom.CloudBlockStorageBackupUpdateInput{ + Labels: map[string]string{"env": "updated"}, + }). + Return(&updatedBackup, nil) + }, + }, + { + name: "update cloud backup with error", + backupID: testBackupID, + expectError: true, + configureMock: func(mock *mocks.MockCloudBlockStorageBackupsService) { + mock.EXPECT(). + Update(gomock.Any(), testBackupID, serverscom.CloudBlockStorageBackupUpdateInput{ + Labels: make(map[string]string), + }). + Return(nil, errors.New("some error")) + }, + }, + } + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + backupServiceHandler := mocks.NewMockCloudBlockStorageBackupsService(mockCtrl) + + scClient := serverscom.NewClientWithEndpoint("", "") + scClient.CloudBlockStorageBackups = backupServiceHandler + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + if tc.configureMock != nil { + tc.configureMock(backupServiceHandler) + } + + testCmdContext := testutils.NewTestCmdContext(scClient) + backupCmd := NewCmd(testCmdContext) + + args := []string{"cloud-backups", "update", tc.backupID} + if len(tc.args) > 0 { + args = append(args, tc.args...) + } + if tc.output != "" { + args = append(args, "--output", tc.output) + } + + builder := testutils.NewTestCommandBuilder(). + WithCommand(backupCmd). + WithArgs(args) + + cmd := builder.Build() + + err := cmd.Execute() + + if tc.expectError { + g.Expect(err).To(HaveOccurred()) + } else { + g.Expect(err).To(BeNil()) + g.Expect(builder.GetOutput()).To(BeEquivalentTo(string(tc.expectedOutput))) + } + }) + } +} + +func TestDeleteCloudBackupsCmd(t *testing.T) { + testCases := []struct { + name string + backupID string + expectError bool + }{ + { + name: "delete cloud backup", + backupID: testBackupID, + }, + { + name: "delete cloud backup with error", + backupID: testBackupID, + expectError: true, + }, + } + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + backupServiceHandler := mocks.NewMockCloudBlockStorageBackupsService(mockCtrl) + + scClient := serverscom.NewClientWithEndpoint("", "") + scClient.CloudBlockStorageBackups = backupServiceHandler + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + var err error + if tc.expectError { + err = errors.New("some error") + } + backupServiceHandler.EXPECT(). + Delete(gomock.Any(), testBackupID). + Return(&testBackup, err) + + testCmdContext := testutils.NewTestCmdContext(scClient) + backupCmd := NewCmd(testCmdContext) + + args := []string{"cloud-backups", "delete", tc.backupID} + + builder := testutils.NewTestCommandBuilder(). + WithCommand(backupCmd). + WithArgs(args) + + cmd := builder.Build() + + err = cmd.Execute() + + if tc.expectError { + g.Expect(err).To(HaveOccurred()) + } else { + g.Expect(err).To(BeNil()) + } + }) + } +} + +func TestRestoreCloudBackupsCmd(t *testing.T) { + restoredBackup := testBackup + restoredBackup.OpenstackVolumeUUID = testVolumeUUID2 + + testCases := []struct { + name string + backupID string + output string + args []string + configureMock func(*mocks.MockCloudBlockStorageBackupsService) + expectedOutput []byte + expectError bool + }{ + { + name: "restore cloud backup", + backupID: testBackupID, + output: "json", + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "restore.json")), + args: []string{"--volume-id", testVolumeUUID2}, + configureMock: func(mock *mocks.MockCloudBlockStorageBackupsService) { + mock.EXPECT(). + Restore(gomock.Any(), testBackupID, serverscom.CloudBlockStorageBackupRestoreInput{ + VolumeID: testVolumeUUID2, + }). + Return(&restoredBackup, nil) + }, + }, + { + name: "restore cloud backup with error", + backupID: testBackupID, + expectError: true, + args: []string{"--volume-id", testVolumeUUID2}, + configureMock: func(mock *mocks.MockCloudBlockStorageBackupsService) { + mock.EXPECT(). + Restore(gomock.Any(), testBackupID, gomock.Any()). + Return(nil, errors.New("some error")) + }, + }, + } + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + backupServiceHandler := mocks.NewMockCloudBlockStorageBackupsService(mockCtrl) + + scClient := serverscom.NewClientWithEndpoint("", "") + scClient.CloudBlockStorageBackups = backupServiceHandler + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + if tc.configureMock != nil { + tc.configureMock(backupServiceHandler) + } + + testCmdContext := testutils.NewTestCmdContext(scClient) + backupCmd := NewCmd(testCmdContext) + + args := []string{"cloud-backups", "restore", tc.backupID} + if len(tc.args) > 0 { + args = append(args, tc.args...) + } + if tc.output != "" { + args = append(args, "--output", tc.output) + } + + builder := testutils.NewTestCommandBuilder(). + WithCommand(backupCmd). + WithArgs(args) + + cmd := builder.Build() + + err := cmd.Execute() + + if tc.expectError { + g.Expect(err).To(HaveOccurred()) + } else { + g.Expect(err).To(BeNil()) + g.Expect(builder.GetOutput()).To(BeEquivalentTo(string(tc.expectedOutput))) + } + }) + } +} diff --git a/cmd/entities/cloud-backups/delete.go b/cmd/entities/cloud-backups/delete.go new file mode 100644 index 0000000..5592b3b --- /dev/null +++ b/cmd/entities/cloud-backups/delete.go @@ -0,0 +1,31 @@ +package cloudbackups + +import ( + "github.com/serverscom/srvctl/cmd/base" + "github.com/spf13/cobra" +) + +func newDeleteCmd(cmdContext *base.CmdContext) *cobra.Command { + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a cloud backup", + Long: "Delete a cloud backup by ID", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + manager := cmdContext.GetManager() + + ctx, cancel := base.SetupContext(cmd, manager) + defer cancel() + + base.SetupProxy(cmd, manager) + + scClient := cmdContext.GetClient().SetVerbose(manager.GetVerbose(cmd)).GetScClient() + + id := args[0] + _, err := scClient.CloudBlockStorageBackups.Delete(ctx, id) + return err + }, + } + + return cmd +} diff --git a/cmd/entities/cloud-backups/get.go b/cmd/entities/cloud-backups/get.go new file mode 100644 index 0000000..61aad63 --- /dev/null +++ b/cmd/entities/cloud-backups/get.go @@ -0,0 +1,39 @@ +package cloudbackups + +import ( + "github.com/serverscom/srvctl/cmd/base" + "github.com/spf13/cobra" +) + +func newGetCmd(cmdContext *base.CmdContext) *cobra.Command { + cmd := &cobra.Command{ + Use: "get ", + Short: "Get a cloud backup", + Long: "Get a cloud backup by ID", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + manager := cmdContext.GetManager() + + ctx, cancel := base.SetupContext(cmd, manager) + defer cancel() + + base.SetupProxy(cmd, manager) + + scClient := cmdContext.GetClient().SetVerbose(manager.GetVerbose(cmd)).GetScClient() + + id := args[0] + backup, err := scClient.CloudBlockStorageBackups.Get(ctx, id) + if err != nil { + return err + } + + if backup != nil { + formatter := cmdContext.GetOrCreateFormatter(cmd) + return formatter.Format(backup) + } + return nil + }, + } + + return cmd +} diff --git a/cmd/entities/cloud-backups/list.go b/cmd/entities/cloud-backups/list.go new file mode 100644 index 0000000..af52023 --- /dev/null +++ b/cmd/entities/cloud-backups/list.go @@ -0,0 +1,22 @@ +package cloudbackups + +import ( + serverscom "github.com/serverscom/serverscom-go-client/pkg" + "github.com/serverscom/srvctl/cmd/base" + "github.com/spf13/cobra" +) + +func newListCmd(cmdContext *base.CmdContext) *cobra.Command { + factory := func(verbose bool, args ...string) serverscom.Collection[serverscom.CloudBlockStorageBackup] { + scClient := cmdContext.GetClient().SetVerbose(verbose).GetScClient() + return scClient.CloudBlockStorageBackups.Collection() + } + + opts := base.NewListOptions( + &base.BaseListOptions[serverscom.CloudBlockStorageBackup]{}, + &base.LabelSelectorOption[serverscom.CloudBlockStorageBackup]{}, + &base.RegionIDOption[serverscom.CloudBlockStorageBackup]{}, + ) + + return base.NewListCmd("list", "Cloud Backups", factory, cmdContext, opts...) +} diff --git a/cmd/entities/cloud-backups/restore.go b/cmd/entities/cloud-backups/restore.go new file mode 100644 index 0000000..18d434f --- /dev/null +++ b/cmd/entities/cloud-backups/restore.go @@ -0,0 +1,53 @@ +package cloudbackups + +import ( + serverscom "github.com/serverscom/serverscom-go-client/pkg" + "github.com/serverscom/srvctl/cmd/base" + "github.com/spf13/cobra" +) + +func newRestoreCmd(cmdContext *base.CmdContext) *cobra.Command { + var volumeID string + + cmd := &cobra.Command{ + Use: "restore --volume-id ", + Short: "Restore a cloud backup", + Long: "Restore a cloud backup to a volume", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + manager := cmdContext.GetManager() + + ctx, cancel := base.SetupContext(cmd, manager) + defer cancel() + + base.SetupProxy(cmd, manager) + + required := []string{"volume-id"} + if err := base.ValidateFlags(cmd, required); err != nil { + return err + } + + input := serverscom.CloudBlockStorageBackupRestoreInput{ + VolumeID: volumeID, + } + + scClient := cmdContext.GetClient().SetVerbose(manager.GetVerbose(cmd)).GetScClient() + + id := args[0] + backup, err := scClient.CloudBlockStorageBackups.Restore(ctx, id, input) + if err != nil { + return err + } + + if backup != nil { + formatter := cmdContext.GetOrCreateFormatter(cmd) + return formatter.Format(backup) + } + return nil + }, + } + + cmd.Flags().StringVarP(&volumeID, "volume-id", "", "", "ID of the volume to restore to") + + return cmd +} diff --git a/cmd/entities/cloud-backups/update.go b/cmd/entities/cloud-backups/update.go new file mode 100644 index 0000000..852ff7f --- /dev/null +++ b/cmd/entities/cloud-backups/update.go @@ -0,0 +1,54 @@ +package cloudbackups + +import ( + "log" + + serverscom "github.com/serverscom/serverscom-go-client/pkg" + "github.com/serverscom/srvctl/cmd/base" + "github.com/spf13/cobra" +) + +func newUpdateCmd(cmdContext *base.CmdContext) *cobra.Command { + var labels []string + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a cloud backup", + Long: "Update a cloud backup by ID", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + manager := cmdContext.GetManager() + + ctx, cancel := base.SetupContext(cmd, manager) + defer cancel() + + base.SetupProxy(cmd, manager) + + labelsMap, err := base.ParseLabels(labels) + if err != nil { + log.Fatal(err) + } + input := serverscom.CloudBlockStorageBackupUpdateInput{ + Labels: labelsMap, + } + + scClient := cmdContext.GetClient().SetVerbose(manager.GetVerbose(cmd)).GetScClient() + + id := args[0] + backup, err := scClient.CloudBlockStorageBackups.Update(ctx, id, input) + if err != nil { + return err + } + + if backup != nil { + formatter := cmdContext.GetOrCreateFormatter(cmd) + return formatter.Format(backup) + } + return nil + }, + } + + cmd.Flags().StringArrayVarP(&labels, "label", "l", []string{}, "string in key=value format") + + return cmd +} diff --git a/cmd/root.go b/cmd/root.go index 300a5c7..23d5df6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,6 +5,7 @@ import ( "github.com/serverscom/srvctl/cmd/config" "github.com/serverscom/srvctl/cmd/context" "github.com/serverscom/srvctl/cmd/entities/account" + cloudbackups "github.com/serverscom/srvctl/cmd/entities/cloud-backups" cloudinstances "github.com/serverscom/srvctl/cmd/entities/cloud-instances" cloudregions "github.com/serverscom/srvctl/cmd/entities/cloud-regions" cloudvolumes "github.com/serverscom/srvctl/cmd/entities/cloud-volumes" @@ -80,6 +81,7 @@ func NewRootCmd(version string) *cobra.Command { cmd.AddCommand(cloudinstances.NewCmd(cmdContext)) cmd.AddCommand(cloudregions.NewCmd(cmdContext)) cmd.AddCommand(cloudvolumes.NewCmd(cmdContext)) + cmd.AddCommand(cloudbackups.NewCmd(cmdContext)) return cmd } diff --git a/internal/mocks/cloud_block_storage_backups_service.go b/internal/mocks/cloud_block_storage_backups_service.go new file mode 100644 index 0000000..82b59da --- /dev/null +++ b/internal/mocks/cloud_block_storage_backups_service.go @@ -0,0 +1,131 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./vendor/github.com/serverscom/serverscom-go-client/pkg/cloud_block_storage_backups.go +// +// Generated by this command: +// +// mockgen --destination ./internal/mocks/cloud_block_storage_backups_service.go --package=mocks --source ./vendor/github.com/serverscom/serverscom-go-client/pkg/cloud_block_storage_backups.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + serverscom "github.com/serverscom/serverscom-go-client/pkg" + gomock "go.uber.org/mock/gomock" +) + +// MockCloudBlockStorageBackupsService is a mock of CloudBlockStorageBackupsService interface. +type MockCloudBlockStorageBackupsService struct { + ctrl *gomock.Controller + recorder *MockCloudBlockStorageBackupsServiceMockRecorder + isgomock struct{} +} + +// MockCloudBlockStorageBackupsServiceMockRecorder is the mock recorder for MockCloudBlockStorageBackupsService. +type MockCloudBlockStorageBackupsServiceMockRecorder struct { + mock *MockCloudBlockStorageBackupsService +} + +// NewMockCloudBlockStorageBackupsService creates a new mock instance. +func NewMockCloudBlockStorageBackupsService(ctrl *gomock.Controller) *MockCloudBlockStorageBackupsService { + mock := &MockCloudBlockStorageBackupsService{ctrl: ctrl} + mock.recorder = &MockCloudBlockStorageBackupsServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCloudBlockStorageBackupsService) EXPECT() *MockCloudBlockStorageBackupsServiceMockRecorder { + return m.recorder +} + +// Collection mocks base method. +func (m *MockCloudBlockStorageBackupsService) Collection() serverscom.Collection[serverscom.CloudBlockStorageBackup] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Collection") + ret0, _ := ret[0].(serverscom.Collection[serverscom.CloudBlockStorageBackup]) + return ret0 +} + +// Collection indicates an expected call of Collection. +func (mr *MockCloudBlockStorageBackupsServiceMockRecorder) Collection() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Collection", reflect.TypeOf((*MockCloudBlockStorageBackupsService)(nil).Collection)) +} + +// Create mocks base method. +func (m *MockCloudBlockStorageBackupsService) Create(ctx context.Context, input serverscom.CloudBlockStorageBackupCreateInput) (*serverscom.CloudBlockStorageBackup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", ctx, input) + ret0, _ := ret[0].(*serverscom.CloudBlockStorageBackup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +func (mr *MockCloudBlockStorageBackupsServiceMockRecorder) Create(ctx, input any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockCloudBlockStorageBackupsService)(nil).Create), ctx, input) +} + +// Delete mocks base method. +func (m *MockCloudBlockStorageBackupsService) Delete(ctx context.Context, id string) (*serverscom.CloudBlockStorageBackup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, id) + ret0, _ := ret[0].(*serverscom.CloudBlockStorageBackup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Delete indicates an expected call of Delete. +func (mr *MockCloudBlockStorageBackupsServiceMockRecorder) Delete(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockCloudBlockStorageBackupsService)(nil).Delete), ctx, id) +} + +// Get mocks base method. +func (m *MockCloudBlockStorageBackupsService) Get(ctx context.Context, id string) (*serverscom.CloudBlockStorageBackup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, id) + ret0, _ := ret[0].(*serverscom.CloudBlockStorageBackup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockCloudBlockStorageBackupsServiceMockRecorder) Get(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockCloudBlockStorageBackupsService)(nil).Get), ctx, id) +} + +// Restore mocks base method. +func (m *MockCloudBlockStorageBackupsService) Restore(ctx context.Context, id string, input serverscom.CloudBlockStorageBackupRestoreInput) (*serverscom.CloudBlockStorageBackup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Restore", ctx, id, input) + ret0, _ := ret[0].(*serverscom.CloudBlockStorageBackup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Restore indicates an expected call of Restore. +func (mr *MockCloudBlockStorageBackupsServiceMockRecorder) Restore(ctx, id, input any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Restore", reflect.TypeOf((*MockCloudBlockStorageBackupsService)(nil).Restore), ctx, id, input) +} + +// Update mocks base method. +func (m *MockCloudBlockStorageBackupsService) Update(ctx context.Context, id string, input serverscom.CloudBlockStorageBackupUpdateInput) (*serverscom.CloudBlockStorageBackup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, id, input) + ret0, _ := ret[0].(*serverscom.CloudBlockStorageBackup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockCloudBlockStorageBackupsServiceMockRecorder) Update(ctx, id, input any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockCloudBlockStorageBackupsService)(nil).Update), ctx, id, input) +} diff --git a/internal/output/entities/cloud_backups.go b/internal/output/entities/cloud_backups.go new file mode 100644 index 0000000..f748de4 --- /dev/null +++ b/internal/output/entities/cloud_backups.go @@ -0,0 +1,36 @@ +package entities + +import ( + "log" + "reflect" + + serverscom "github.com/serverscom/serverscom-go-client/pkg" +) + +var ( + CloudBlockStorageBackupType = reflect.TypeOf(serverscom.CloudBlockStorageBackup{}) + CloudBlockStorageBackupListDefaultFields = []string{"ID", "Name", "Status", "Size"} +) + +func RegisterCloudBackupDefinition() { + backupEntity := &Entity{ + fields: []Field{ + {ID: "ID", Name: "ID", Path: "ID", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "Name", Name: "Name", Path: "Name", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "Status", Name: "Status", Path: "Status", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "Size", Name: "Size", Path: "Size", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "RegionID", Name: "Region ID", Path: "RegionID", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler}, + {ID: "OpenstackUUID", Name: "Openstack UUID", Path: "OpenstackUUID", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler}, + {ID: "OpenstackVolumeUUID", Name: "Openstack Volume UUID", Path: "OpenstackVolumeUUID", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler}, + {ID: "Labels", Name: "Labels", Path: "Labels", PageViewHandlerFunc: mapPvHandler}, + {ID: "Created", Name: "Created", Path: "Created", ListHandlerFunc: timeHandler, PageViewHandlerFunc: timeHandler, Default: true}, + }, + cmdDefaultFields: map[string][]string{ + "list": CloudBlockStorageBackupListDefaultFields, + }, + eType: CloudBlockStorageBackupType, + } + if err := Registry.Register(backupEntity); err != nil { + log.Fatal(err) + } +} diff --git a/internal/output/entities/init.go b/internal/output/entities/init.go index a84a73c..2200f03 100644 --- a/internal/output/entities/init.go +++ b/internal/output/entities/init.go @@ -35,4 +35,5 @@ func init() { RegisterCloudComputingInstanceDefinition() RegisterCloudComputingRegionDefinitions() RegisterCloudVolumeDefinition() + RegisterCloudBackupDefinition() } diff --git a/testdata/entities/cloud-backups/create.json b/testdata/entities/cloud-backups/create.json new file mode 100644 index 0000000..e5b5369 --- /dev/null +++ b/testdata/entities/cloud-backups/create.json @@ -0,0 +1,9 @@ +{ + "volume_id": "vol-123-openstack", + "name": "test-backup", + "incremental": false, + "force": false, + "labels": { + "env": "test" + } +} \ No newline at end of file diff --git a/testdata/entities/cloud-backups/get.json b/testdata/entities/cloud-backups/get.json new file mode 100644 index 0000000..9f979bd --- /dev/null +++ b/testdata/entities/cloud-backups/get.json @@ -0,0 +1,13 @@ +{ + "id": "backup-123", + "openstack_uuid": null, + "openstack_volume_uuid": "vol-123-openstack", + "region_id": 1, + "size": 1073741824, + "status": "available", + "labels": { + "env": "test" + }, + "created_at": "2025-01-01T12:00:00Z", + "name": "test-backup" +} \ No newline at end of file diff --git a/testdata/entities/cloud-backups/get.txt b/testdata/entities/cloud-backups/get.txt new file mode 100644 index 0000000..f474ec8 --- /dev/null +++ b/testdata/entities/cloud-backups/get.txt @@ -0,0 +1,2 @@ +ID Name Status Size Created +backup-123 test-backup available 1073741824 2025-01-01T12:00:00Z diff --git a/testdata/entities/cloud-backups/get.yaml b/testdata/entities/cloud-backups/get.yaml new file mode 100644 index 0000000..d1b9be9 --- /dev/null +++ b/testdata/entities/cloud-backups/get.yaml @@ -0,0 +1,10 @@ +id: backup-123 +openstackuuid: null +openstackvolumeuuid: vol-123-openstack +regionid: 1 +size: 1073741824 +status: available +labels: + env: test +created: 2025-01-01T12:00:00Z +name: test-backup diff --git a/testdata/entities/cloud-backups/list.json b/testdata/entities/cloud-backups/list.json new file mode 100644 index 0000000..c78d7f4 --- /dev/null +++ b/testdata/entities/cloud-backups/list.json @@ -0,0 +1,15 @@ +[ + { + "id": "backup-123", + "openstack_uuid": null, + "openstack_volume_uuid": "vol-123-openstack", + "region_id": 1, + "size": 1073741824, + "status": "available", + "labels": { + "env": "test" + }, + "created_at": "2025-01-01T12:00:00Z", + "name": "test-backup" + } +] \ No newline at end of file diff --git a/testdata/entities/cloud-backups/list_all.json b/testdata/entities/cloud-backups/list_all.json new file mode 100644 index 0000000..e4f4b99 --- /dev/null +++ b/testdata/entities/cloud-backups/list_all.json @@ -0,0 +1,28 @@ +[ + { + "id": "backup-123", + "openstack_uuid": null, + "openstack_volume_uuid": "vol-123-openstack", + "region_id": 1, + "size": 1073741824, + "status": "available", + "labels": { + "env": "test" + }, + "created_at": "2025-01-01T12:00:00Z", + "name": "test-backup" + }, + { + "id": "backup-456", + "openstack_uuid": null, + "openstack_volume_uuid": "vol-456-openstack", + "region_id": 1, + "size": 2147483648, + "status": "available", + "labels": { + "env": "prod" + }, + "created_at": "2025-01-02T12:00:00Z", + "name": "test-backup-2" + } +] \ No newline at end of file diff --git a/testdata/entities/cloud-backups/list_pageview.txt b/testdata/entities/cloud-backups/list_pageview.txt new file mode 100644 index 0000000..6e545ce --- /dev/null +++ b/testdata/entities/cloud-backups/list_pageview.txt @@ -0,0 +1,19 @@ +ID: backup-123 +Name: test-backup +Status: available +Size: 1073741824 +Region ID: 1 +Openstack UUID: +Openstack Volume UUID: vol-123-openstack +Labels: env=test +Created: 2025-01-01T12:00:00Z +--- +ID: backup-456 +Name: test-backup-2 +Status: available +Size: 2147483648 +Region ID: 1 +Openstack UUID: +Openstack Volume UUID: vol-456-openstack +Labels: env=prod +Created: 2025-01-02T12:00:00Z diff --git a/testdata/entities/cloud-backups/list_template.txt b/testdata/entities/cloud-backups/list_template.txt new file mode 100644 index 0000000..9397332 --- /dev/null +++ b/testdata/entities/cloud-backups/list_template.txt @@ -0,0 +1,2 @@ +ID: backup-123, Name: test-backup +ID: backup-456, Name: test-backup-2 diff --git a/testdata/entities/cloud-backups/restore.json b/testdata/entities/cloud-backups/restore.json new file mode 100644 index 0000000..c6b22d9 --- /dev/null +++ b/testdata/entities/cloud-backups/restore.json @@ -0,0 +1,13 @@ +{ + "id": "backup-123", + "openstack_uuid": null, + "openstack_volume_uuid": "vol-456-openstack", + "region_id": 1, + "size": 1073741824, + "status": "available", + "labels": { + "env": "test" + }, + "created_at": "2025-01-01T12:00:00Z", + "name": "test-backup" +} \ No newline at end of file diff --git a/testdata/entities/cloud-backups/update.json b/testdata/entities/cloud-backups/update.json new file mode 100644 index 0000000..f16dd1a --- /dev/null +++ b/testdata/entities/cloud-backups/update.json @@ -0,0 +1,13 @@ +{ + "id": "backup-123", + "openstack_uuid": null, + "openstack_volume_uuid": "vol-123-openstack", + "region_id": 1, + "size": 1073741824, + "status": "available", + "labels": { + "env": "updated" + }, + "created_at": "2025-01-01T12:00:00Z", + "name": "test-backup" +} \ No newline at end of file