From b339b24d97d9fc0958e2741e076d00b34451fe7e Mon Sep 17 00:00:00 2001 From: Oleg Isakov Date: Fri, 23 Jan 2026 16:13:43 +0200 Subject: [PATCH] add cloud-volumes cmd; add tests --- Makefile | 4 +- cmd/base/list_options.go | 28 + cmd/entities/cloud-volumes/add.go | 117 ++++ cmd/entities/cloud-volumes/cloud_volumes.go | 43 ++ .../cloud-volumes/cloud_volumes_test.go | 585 ++++++++++++++++++ cmd/entities/cloud-volumes/delete.go | 32 + cmd/entities/cloud-volumes/get.go | 39 ++ cmd/entities/cloud-volumes/list.go | 23 + cmd/entities/cloud-volumes/update.go | 66 ++ cmd/entities/cloud-volumes/volume_attach.go | 53 ++ cmd/entities/cloud-volumes/volume_detach.go | 53 ++ cmd/root.go | 2 + go.mod | 2 +- go.sum | 4 +- .../cloud_block_storage_volumes_service.go | 146 +++++ internal/output/entities/cloud_volumes.go | 34 + internal/output/entities/init.go | 1 + testdata/entities/cloud-volumes/create.json | 9 + testdata/entities/cloud-volumes/get.json | 15 + testdata/entities/cloud-volumes/get.txt | 2 + testdata/entities/cloud-volumes/get.yaml | 12 + testdata/entities/cloud-volumes/list.json | 17 + testdata/entities/cloud-volumes/list.txt | 2 + testdata/entities/cloud-volumes/list_all.json | 17 + testdata/entities/cloud-volumes/list_all.txt | 2 + .../entities/cloud-volumes/list_pageview.txt | 7 + .../entities/cloud-volumes/list_template.txt | 1 + testdata/entities/cloud-volumes/update.json | 7 + 28 files changed, 1319 insertions(+), 4 deletions(-) create mode 100644 cmd/entities/cloud-volumes/add.go create mode 100644 cmd/entities/cloud-volumes/cloud_volumes.go create mode 100644 cmd/entities/cloud-volumes/cloud_volumes_test.go create mode 100644 cmd/entities/cloud-volumes/delete.go create mode 100644 cmd/entities/cloud-volumes/get.go create mode 100644 cmd/entities/cloud-volumes/list.go create mode 100644 cmd/entities/cloud-volumes/update.go create mode 100644 cmd/entities/cloud-volumes/volume_attach.go create mode 100644 cmd/entities/cloud-volumes/volume_detach.go create mode 100644 internal/mocks/cloud_block_storage_volumes_service.go create mode 100644 internal/output/entities/cloud_volumes.go create mode 100644 testdata/entities/cloud-volumes/create.json create mode 100644 testdata/entities/cloud-volumes/get.json create mode 100644 testdata/entities/cloud-volumes/get.txt create mode 100644 testdata/entities/cloud-volumes/get.yaml create mode 100644 testdata/entities/cloud-volumes/list.json create mode 100644 testdata/entities/cloud-volumes/list.txt create mode 100644 testdata/entities/cloud-volumes/list_all.json create mode 100644 testdata/entities/cloud-volumes/list_all.txt create mode 100644 testdata/entities/cloud-volumes/list_pageview.txt create mode 100644 testdata/entities/cloud-volumes/list_template.txt create mode 100644 testdata/entities/cloud-volumes/update.json diff --git a/Makefile b/Makefile index aa98965..3c2494a 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,7 @@ generate: deps mockgen --destination ./internal/mocks/kubernetes_clusters_service.go --package=mocks --source ./vendor/github.com/serverscom/serverscom-go-client/pkg/kubernetes_clusters.go mockgen --destination ./internal/mocks/l2_segment_service.go --package=mocks --source ./vendor/github.com/serverscom/serverscom-go-client/pkg/l2_segments.go mockgen --destination ./internal/mocks/network_pool_service.go --package=mocks --source ./vendor/github.com/serverscom/serverscom-go-client/pkg/network_pools.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 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 \ @@ -33,4 +34,5 @@ generate: deps ./internal/mocks/locations_service.go \ ./internal/mocks/kubernetes_clusters_service.go \ ./internal/mocks/l2_segment_service.go \ - ./internal/mocks/network_pool_service.go \ No newline at end of file + ./internal/mocks/network_pool_service.go \ + ./internal/mocks/cloud_block_storage_volumes_service.go \ No newline at end of file diff --git a/cmd/base/list_options.go b/cmd/base/list_options.go index 6602048..3a3e5e9 100644 --- a/cmd/base/list_options.go +++ b/cmd/base/list_options.go @@ -459,3 +459,31 @@ func (o *RackIDOption[T]) ApplyToCollection(collection serverscom.Collection[T]) collection.SetParam("rack_id", o.rackID) } } + +type RegionIDOption[T any] struct { + regionID int +} + +func (o *RegionIDOption[T]) AddFlags(cmd *cobra.Command) { + cmd.Flags().IntVar(&o.regionID, "region-id", 0, "Filter results by region ID") +} + +func (o *RegionIDOption[T]) ApplyToCollection(collection serverscom.Collection[T]) { + if o.regionID != 0 { + collection.SetParam("region_id", fmt.Sprintf("%d", o.regionID)) + } +} + +type InstanceIDOption[T any] struct { + instanceID string +} + +func (o *InstanceIDOption[T]) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.instanceID, "instance-id", "", "Filter results by instance ID") +} + +func (o *InstanceIDOption[T]) ApplyToCollection(collection serverscom.Collection[T]) { + if o.instanceID != "" { + collection.SetParam("instance_id", o.instanceID) + } +} diff --git a/cmd/entities/cloud-volumes/add.go b/cmd/entities/cloud-volumes/add.go new file mode 100644 index 0000000..c8e0512 --- /dev/null +++ b/cmd/entities/cloud-volumes/add.go @@ -0,0 +1,117 @@ +package cloudvolumes + +import ( + serverscom "github.com/serverscom/serverscom-go-client/pkg" + "github.com/serverscom/srvctl/cmd/base" + "github.com/spf13/cobra" +) + +type AddFlags struct { + InputPath string + Name string + RegionID int + Size int + Description string + ImageID string + SnapshotID string + AttachInstanceID string + BackupID string + Labels []string +} + +func newAddCmd(cmdContext *base.CmdContext) *cobra.Command { + flags := &AddFlags{} + + cmd := &cobra.Command{ + Use: "add --input ", + Short: "Add a cloud volume", + Long: "Add a new cloud volume to a Cloud region", + 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.CloudBlockStorageVolumeCreateInput{} + + if flags.InputPath != "" { + if err := base.ReadInputJSON(flags.InputPath, cmd.InOrStdin(), input); err != nil { + return err + } + } else { + required := []string{"name", "region-id"} + if err := base.ValidateFlags(cmd, required); err != nil { + return err + } + } + + if err := flags.FillInput(cmd, input); err != nil { + return err + } + + scClient := cmdContext.GetClient().SetVerbose(manager.GetVerbose(cmd)).GetScClient() + volume, err := scClient.CloudBlockStorageVolumes.Create(ctx, *input) + if err != nil { + return err + } + + if volume != nil { + formatter := cmdContext.GetOrCreateFormatter(cmd) + return formatter.Format(volume) + } + return nil + }, + } + + cmd.Flags().StringVarP(&flags.InputPath, "input", "i", "", "path to input file or '-' to read from stdin") + cmd.Flags().StringVarP(&flags.Name, "name", "n", "", "A name of the cloud volume") + cmd.Flags().IntVar(&flags.RegionID, "region-id", 0, "ID of the cloud region") + cmd.Flags().IntVar(&flags.Size, "size", 0, "Size of the volume in GB") + cmd.Flags().StringVar(&flags.Description, "description", "", "Description of the volume") + cmd.Flags().StringVar(&flags.ImageID, "image-id", "", "ID of the image to create volume from") + cmd.Flags().StringVar(&flags.SnapshotID, "snapshot-id", "", "ID of the snapshot to create volume from") + cmd.Flags().StringVar(&flags.AttachInstanceID, "attach-instance-id", "", "ID of the instance to attach volume to") + cmd.Flags().StringVar(&flags.BackupID, "backup-id", "", "ID of the backup to create volume from") + cmd.Flags().StringArrayVarP(&flags.Labels, "label", "l", []string{}, "string in key=value format") + + return cmd +} + +func (f *AddFlags) FillInput(cmd *cobra.Command, input *serverscom.CloudBlockStorageVolumeCreateInput) error { + if cmd.Flags().Changed("name") { + input.Name = f.Name + } + if cmd.Flags().Changed("region-id") { + input.RegionID = f.RegionID + } + if cmd.Flags().Changed("size") { + input.Size = f.Size + } + if cmd.Flags().Changed("description") { + input.Description = f.Description + } + if cmd.Flags().Changed("image-id") { + input.ImageID = f.ImageID + } + if cmd.Flags().Changed("snapshot-id") { + input.SnapshotID = f.SnapshotID + } + if cmd.Flags().Changed("attach-instance-id") { + input.AttachInstanceID = f.AttachInstanceID + } + if cmd.Flags().Changed("backup-id") { + input.BackupID = f.BackupID + } + 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-volumes/cloud_volumes.go b/cmd/entities/cloud-volumes/cloud_volumes.go new file mode 100644 index 0000000..9ff01fb --- /dev/null +++ b/cmd/entities/cloud-volumes/cloud_volumes.go @@ -0,0 +1,43 @@ +package cloudvolumes + +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 { + cloudVolumeEntity, err := entities.Registry.GetEntityFromValue(serverscom.CloudBlockStorageVolume{}) + if err != nil { + log.Fatal(err) + } + entitiesMap := make(map[string]entities.EntityInterface) + entitiesMap["cloud-volumes"] = cloudVolumeEntity + cmd := &cobra.Command{ + Use: "cloud-volumes", + Short: "Manage cloud volumes", + 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), + newVolumeAttachCmd(cmdContext), + newVolumeDetachCmd(cmdContext), + ) + + base.AddFormatFlags(cmd) + + return cmd +} diff --git a/cmd/entities/cloud-volumes/cloud_volumes_test.go b/cmd/entities/cloud-volumes/cloud_volumes_test.go new file mode 100644 index 0000000..93cb079 --- /dev/null +++ b/cmd/entities/cloud-volumes/cloud_volumes_test.go @@ -0,0 +1,585 @@ +package cloudvolumes + +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 ( + testVolumeID = "vol-12345" + testInstanceID = "instance-123" + fixtureBasePath = filepath.Join("..", "..", "..", "testdata", "entities", "cloud-volumes") + fixedTime = time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC) + testVolume = serverscom.CloudBlockStorageVolume{ + ID: testVolumeID, + Name: "test-volume", + RegionID: 1, + Size: 100, + Description: testutils.PtrString("Test volume"), + Labels: map[string]string{"foo": "bar"}, + Created: &fixedTime, + } +) + +func TestGetCloudVolumesCmd(t *testing.T) { + testCases := []struct { + name string + output string + args []string + configureMock func(*mocks.MockCloudBlockStorageVolumesService) + expectedOutput []byte + expectError bool + }{ + { + name: "get volume text", + output: "text", + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "get.txt")), + args: []string{testVolumeID}, + configureMock: func(mock *mocks.MockCloudBlockStorageVolumesService) { + mock.EXPECT().Get(gomock.Any(), testVolumeID).Return(&testVolume, nil) + }, + }, + { + name: "get volume json", + output: "json", + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "get.json")), + args: []string{testVolumeID, "--output", "json"}, + configureMock: func(mock *mocks.MockCloudBlockStorageVolumesService) { + mock.EXPECT().Get(gomock.Any(), testVolumeID).Return(&testVolume, nil) + }, + }, + { + name: "get volume yaml", + output: "yaml", + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "get.yaml")), + args: []string{testVolumeID, "--output", "yaml"}, + configureMock: func(mock *mocks.MockCloudBlockStorageVolumesService) { + mock.EXPECT().Get(gomock.Any(), testVolumeID).Return(&testVolume, nil) + }, + }, + { + name: "get volume error", + expectError: true, + args: []string{testVolumeID}, + configureMock: func(mock *mocks.MockCloudBlockStorageVolumesService) { + mock.EXPECT().Get(gomock.Any(), testVolumeID).Return(nil, errors.New("not found")) + }, + }, + } + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + volumesServiceHandler := mocks.NewMockCloudBlockStorageVolumesService(mockCtrl) + + scClient := serverscom.NewClientWithEndpoint("", "") + scClient.CloudBlockStorageVolumes = volumesServiceHandler + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + if tc.configureMock != nil { + tc.configureMock(volumesServiceHandler) + } + + testCmdContext := testutils.NewTestCmdContext(scClient) + volumesCmd := NewCmd(testCmdContext) + + args := []string{"cloud-volumes", "get"} + if len(tc.args) > 0 { + args = append(args, tc.args...) + } + + builder := testutils.NewTestCommandBuilder().WithCommand(volumesCmd).WithArgs(args) + err := builder.Build().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 TestListCloudVolumesCmd(t *testing.T) { + testCases := []struct { + name string + args []string + configureMock func(*mocks.MockCloudBlockStorageVolumesService, *mocks.MockCollection[serverscom.CloudBlockStorageVolume]) + expectedOutput []byte + expectError bool + }{ + { + name: "list all", + args: []string{"-A", "--output", "json"}, + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "list_all.json")), + configureMock: func(serviceMock *mocks.MockCloudBlockStorageVolumesService, collectionMock *mocks.MockCollection[serverscom.CloudBlockStorageVolume]) { + serviceMock.EXPECT().Collection().Return(collectionMock).AnyTimes() + collectionMock.EXPECT().SetParam(gomock.Any(), gomock.Any()).Return(collectionMock).AnyTimes() + collectionMock.EXPECT().Collect(gomock.Any()).Return([]serverscom.CloudBlockStorageVolume{testVolume}, nil) + }, + }, + { + name: "list", + args: []string{"--output", "json"}, + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "list.json")), + configureMock: func(serviceMock *mocks.MockCloudBlockStorageVolumesService, collectionMock *mocks.MockCollection[serverscom.CloudBlockStorageVolume]) { + serviceMock.EXPECT().Collection().Return(collectionMock).AnyTimes() + collectionMock.EXPECT().SetParam(gomock.Any(), gomock.Any()).Return(collectionMock).AnyTimes() + collectionMock.EXPECT().List(gomock.Any()).Return([]serverscom.CloudBlockStorageVolume{testVolume}, nil) + }, + }, + { + name: "template", + args: []string{"--template", "{{range .}}{{.ID}}{{end}}"}, + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "list_template.txt")), + configureMock: func(serviceMock *mocks.MockCloudBlockStorageVolumesService, collectionMock *mocks.MockCollection[serverscom.CloudBlockStorageVolume]) { + serviceMock.EXPECT().Collection().Return(collectionMock).AnyTimes() + collectionMock.EXPECT().SetParam(gomock.Any(), gomock.Any()).Return(collectionMock).AnyTimes() + collectionMock.EXPECT().List(gomock.Any()).Return([]serverscom.CloudBlockStorageVolume{testVolume}, nil) + }, + }, + { + name: "page-view", + args: []string{"--page-view"}, + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "list_pageview.txt")), + configureMock: func(serviceMock *mocks.MockCloudBlockStorageVolumesService, collectionMock *mocks.MockCollection[serverscom.CloudBlockStorageVolume]) { + serviceMock.EXPECT().Collection().Return(collectionMock).AnyTimes() + collectionMock.EXPECT().SetParam(gomock.Any(), gomock.Any()).Return(collectionMock).AnyTimes() + collectionMock.EXPECT().List(gomock.Any()).Return([]serverscom.CloudBlockStorageVolume{testVolume}, nil) + }, + }, + { + name: "error", + expectError: true, + configureMock: func(serviceMock *mocks.MockCloudBlockStorageVolumesService, collectionMock *mocks.MockCollection[serverscom.CloudBlockStorageVolume]) { + serviceMock.EXPECT().Collection().Return(collectionMock).AnyTimes() + collectionMock.EXPECT().SetParam(gomock.Any(), gomock.Any()).Return(collectionMock).AnyTimes() + collectionMock.EXPECT().List(gomock.Any()).Return(nil, errors.New("error")) + }, + }, + } + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + volumesServiceHandler := mocks.NewMockCloudBlockStorageVolumesService(mockCtrl) + collectionHandler := mocks.NewMockCollection[serverscom.CloudBlockStorageVolume](mockCtrl) + + scClient := serverscom.NewClientWithEndpoint("", "") + scClient.CloudBlockStorageVolumes = volumesServiceHandler + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + if tc.configureMock != nil { + tc.configureMock(volumesServiceHandler, collectionHandler) + } + + testCmdContext := testutils.NewTestCmdContext(scClient) + volumesCmd := NewCmd(testCmdContext) + + args := []string{"cloud-volumes", "list"} + if len(tc.args) > 0 { + args = append(args, tc.args...) + } + + builder := testutils.NewTestCommandBuilder().WithCommand(volumesCmd).WithArgs(args) + err := builder.Build().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 TestAddCloudVolumesCmd(t *testing.T) { + testCases := []struct { + name string + output string + args []string + configureMock func(*mocks.MockCloudBlockStorageVolumesService) + expectedOutput []byte + expectError bool + }{ + { + name: "create volume with input", + output: "json", + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "get.json")), + args: []string{"--input", filepath.Join(fixtureBasePath, "create.json"), "--output", "json"}, + configureMock: func(mock *mocks.MockCloudBlockStorageVolumesService) { + mock.EXPECT(). + Create(gomock.Any(), serverscom.CloudBlockStorageVolumeCreateInput{ + Name: "test-volume", + RegionID: 1, + Size: 100, + Description: "Test volume", + Labels: map[string]string{"foo": "bar"}, + }). + Return(&testVolume, nil) + }, + }, + { + name: "create volume", + output: "json", + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "get.json")), + args: []string{ + "--name", "test-volume", + "--region-id", "1", + "--size", "100", + "--description", "Test volume", + "--label", "foo=bar", + "--output", "json", + }, + configureMock: func(mock *mocks.MockCloudBlockStorageVolumesService) { + mock.EXPECT(). + Create(gomock.Any(), serverscom.CloudBlockStorageVolumeCreateInput{ + Name: "test-volume", + RegionID: 1, + Size: 100, + Description: "Test volume", + Labels: map[string]string{"foo": "bar"}, + }). + Return(&testVolume, nil) + }, + }, + { + name: "create volume with error", + expectError: true, + args: []string{"--name", "test-volume", "--region-id", "1"}, + configureMock: func(mock *mocks.MockCloudBlockStorageVolumesService) { + mock.EXPECT(). + Create(gomock.Any(), gomock.Any()). + Return(nil, errors.New("create error")) + }, + }, + } + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + volumesServiceHandler := mocks.NewMockCloudBlockStorageVolumesService(mockCtrl) + + scClient := serverscom.NewClientWithEndpoint("", "") + scClient.CloudBlockStorageVolumes = volumesServiceHandler + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + if tc.configureMock != nil { + tc.configureMock(volumesServiceHandler) + } + + testCmdContext := testutils.NewTestCmdContext(scClient) + volumesCmd := NewCmd(testCmdContext) + + args := []string{"cloud-volumes", "add"} + if len(tc.args) > 0 { + args = append(args, tc.args...) + } + + builder := testutils.NewTestCommandBuilder().WithCommand(volumesCmd).WithArgs(args) + err := builder.Build().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 TestUpdateCloudVolumesCmd(t *testing.T) { + testCases := []struct { + name string + output string + args []string + configureMock func(*mocks.MockCloudBlockStorageVolumesService) + expectedOutput []byte + expectError bool + }{ + { + name: "update volume", + output: "json", + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "get.json")), + args: []string{ + testVolumeID, + "--name", "updated-volume", + "--description", "Updated volume", + "--label", "new=label", + "--output", "json", + }, + configureMock: func(mock *mocks.MockCloudBlockStorageVolumesService) { + mock.EXPECT(). + Update(gomock.Any(), testVolumeID, serverscom.CloudBlockStorageVolumeUpdateInput{ + Name: "updated-volume", + Description: "Updated volume", + Labels: map[string]string{"new": "label"}, + }). + Return(&testVolume, nil) + }, + }, + { + name: "update volume with error", + expectError: true, + args: []string{testVolumeID, "--name", "updated-volume"}, + configureMock: func(mock *mocks.MockCloudBlockStorageVolumesService) { + mock.EXPECT(). + Update(gomock.Any(), testVolumeID, gomock.Any()). + Return(nil, errors.New("update error")) + }, + }, + } + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + volumesServiceHandler := mocks.NewMockCloudBlockStorageVolumesService(mockCtrl) + + scClient := serverscom.NewClientWithEndpoint("", "") + scClient.CloudBlockStorageVolumes = volumesServiceHandler + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + if tc.configureMock != nil { + tc.configureMock(volumesServiceHandler) + } + + testCmdContext := testutils.NewTestCmdContext(scClient) + volumesCmd := NewCmd(testCmdContext) + + args := []string{"cloud-volumes", "update"} + if len(tc.args) > 0 { + args = append(args, tc.args...) + } + + builder := testutils.NewTestCommandBuilder().WithCommand(volumesCmd).WithArgs(args) + err := builder.Build().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 TestDeleteCloudVolumesCmd(t *testing.T) { + testCases := []struct { + name string + volumeID string + expectError bool + }{ + { + name: "delete volume", + volumeID: testVolumeID, + }, + { + name: "delete volume with error", + volumeID: testVolumeID, + expectError: true, + }, + } + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + volumesServiceHandler := mocks.NewMockCloudBlockStorageVolumesService(mockCtrl) + + scClient := serverscom.NewClientWithEndpoint("", "") + scClient.CloudBlockStorageVolumes = volumesServiceHandler + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + var err error + if tc.expectError { + err = errors.New("delete error") + } + volumesServiceHandler.EXPECT(). + Delete(gomock.Any(), tc.volumeID). + Return(nil, err) + + testCmdContext := testutils.NewTestCmdContext(scClient) + volumesCmd := NewCmd(testCmdContext) + + args := []string{"cloud-volumes", "delete", tc.volumeID} + + builder := testutils.NewTestCommandBuilder(). + WithCommand(volumesCmd). + WithArgs(args) + + cmd := builder.Build() + + err = cmd.Execute() + + if tc.expectError { + g.Expect(err).To(HaveOccurred()) + } else { + g.Expect(err).To(BeNil()) + } + }) + } +} + +func TestAttachCloudVolumesCmd(t *testing.T) { + testCases := []struct { + name string + args []string + configureMock func(*mocks.MockCloudBlockStorageVolumesService) + expectedOutput []byte + expectError bool + }{ + { + name: "attach volume", + args: []string{testVolumeID, "--instance-id", testInstanceID, "--output", "json"}, + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "get.json")), + configureMock: func(mock *mocks.MockCloudBlockStorageVolumesService) { + mock.EXPECT(). + Attach(gomock.Any(), testVolumeID, serverscom.CloudBlockStorageVolumeAttachInput{ + InstanceID: testInstanceID, + }). + Return(&testVolume, nil) + }, + }, + { + name: "attach volume with error", + expectError: true, + args: []string{testVolumeID, "--instance-id", testInstanceID}, + configureMock: func(mock *mocks.MockCloudBlockStorageVolumesService) { + mock.EXPECT(). + Attach(gomock.Any(), testVolumeID, gomock.Any()). + Return(nil, errors.New("attach error")) + }, + }, + } + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + volumesServiceHandler := mocks.NewMockCloudBlockStorageVolumesService(mockCtrl) + + scClient := serverscom.NewClientWithEndpoint("", "") + scClient.CloudBlockStorageVolumes = volumesServiceHandler + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + if tc.configureMock != nil { + tc.configureMock(volumesServiceHandler) + } + + testCmdContext := testutils.NewTestCmdContext(scClient) + volumesCmd := NewCmd(testCmdContext) + + args := []string{"cloud-volumes", "volume-attach"} + if len(tc.args) > 0 { + args = append(args, tc.args...) + } + + builder := testutils.NewTestCommandBuilder().WithCommand(volumesCmd).WithArgs(args) + err := builder.Build().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 TestDetachCloudVolumesCmd(t *testing.T) { + testCases := []struct { + name string + args []string + configureMock func(*mocks.MockCloudBlockStorageVolumesService) + expectedOutput []byte + expectError bool + }{ + { + name: "detach volume", + args: []string{testVolumeID, "--instance-id", testInstanceID, "--output", "json"}, + expectedOutput: testutils.ReadFixture(filepath.Join(fixtureBasePath, "get.json")), + configureMock: func(mock *mocks.MockCloudBlockStorageVolumesService) { + mock.EXPECT(). + Detach(gomock.Any(), testVolumeID, serverscom.CloudBlockStorageVolumeDetachInput{ + InstanceID: testInstanceID, + }). + Return(&testVolume, nil) + }, + }, + { + name: "detach volume with error", + expectError: true, + args: []string{testVolumeID, "--instance-id", testInstanceID}, + configureMock: func(mock *mocks.MockCloudBlockStorageVolumesService) { + mock.EXPECT(). + Detach(gomock.Any(), testVolumeID, gomock.Any()). + Return(nil, errors.New("detach error")) + }, + }, + } + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + volumesServiceHandler := mocks.NewMockCloudBlockStorageVolumesService(mockCtrl) + + scClient := serverscom.NewClientWithEndpoint("", "") + scClient.CloudBlockStorageVolumes = volumesServiceHandler + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + if tc.configureMock != nil { + tc.configureMock(volumesServiceHandler) + } + + testCmdContext := testutils.NewTestCmdContext(scClient) + volumesCmd := NewCmd(testCmdContext) + + args := []string{"cloud-volumes", "volume-detach"} + if len(tc.args) > 0 { + args = append(args, tc.args...) + } + + builder := testutils.NewTestCommandBuilder().WithCommand(volumesCmd).WithArgs(args) + err := builder.Build().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-volumes/delete.go b/cmd/entities/cloud-volumes/delete.go new file mode 100644 index 0000000..f67d9e7 --- /dev/null +++ b/cmd/entities/cloud-volumes/delete.go @@ -0,0 +1,32 @@ +package cloudvolumes + +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 volume", + Long: "Delete a cloud volume by volume 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() + + volumeID := args[0] + _, err := scClient.CloudBlockStorageVolumes.Delete(ctx, volumeID) + + return err + }, + } + + return cmd +} diff --git a/cmd/entities/cloud-volumes/get.go b/cmd/entities/cloud-volumes/get.go new file mode 100644 index 0000000..c5f357e --- /dev/null +++ b/cmd/entities/cloud-volumes/get.go @@ -0,0 +1,39 @@ +package cloudvolumes + +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 volume", + Long: "Get a cloud volume by volume 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() + + volumeID := args[0] + volume, err := scClient.CloudBlockStorageVolumes.Get(ctx, volumeID) + if err != nil { + return err + } + + if volume != nil { + formatter := cmdContext.GetOrCreateFormatter(cmd) + return formatter.Format(volume) + } + return nil + }, + } + + return cmd +} diff --git a/cmd/entities/cloud-volumes/list.go b/cmd/entities/cloud-volumes/list.go new file mode 100644 index 0000000..14265f2 --- /dev/null +++ b/cmd/entities/cloud-volumes/list.go @@ -0,0 +1,23 @@ +package cloudvolumes + +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.CloudBlockStorageVolume] { + scClient := cmdContext.GetClient().SetVerbose(verbose).GetScClient() + return scClient.CloudBlockStorageVolumes.Collection() + } + + opts := base.NewListOptions( + &base.BaseListOptions[serverscom.CloudBlockStorageVolume]{}, + &base.LabelSelectorOption[serverscom.CloudBlockStorageVolume]{}, + &base.RegionIDOption[serverscom.CloudBlockStorageVolume]{}, + &base.InstanceIDOption[serverscom.CloudBlockStorageVolume]{}, + ) + + return base.NewListCmd("list", "Cloud volumes", factory, cmdContext, opts...) +} diff --git a/cmd/entities/cloud-volumes/update.go b/cmd/entities/cloud-volumes/update.go new file mode 100644 index 0000000..a6fd0ba --- /dev/null +++ b/cmd/entities/cloud-volumes/update.go @@ -0,0 +1,66 @@ +package cloudvolumes + +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 name string + var description string + var imageID string + var snapshotID string + var labels []string + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a cloud volume", + Long: "Update a cloud volume by volume 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.CloudBlockStorageVolumeUpdateInput{ + Name: name, + Description: description, + ImageID: imageID, + SnapshotID: snapshotID, + Labels: labelsMap, + } + + scClient := cmdContext.GetClient().SetVerbose(manager.GetVerbose(cmd)).GetScClient() + + volumeID := args[0] + volume, err := scClient.CloudBlockStorageVolumes.Update(ctx, volumeID, input) + if err != nil { + return err + } + + if volume != nil { + formatter := cmdContext.GetOrCreateFormatter(cmd) + return formatter.Format(volume) + } + return nil + }, + } + + cmd.Flags().StringVarP(&name, "name", "n", "", "A name of the cloud volume") + cmd.Flags().StringVar(&description, "description", "", "Description of the volume") + cmd.Flags().StringVar(&imageID, "image-id", "", "ID of the image") + cmd.Flags().StringVar(&snapshotID, "snapshot-id", "", "ID of the snapshot") + cmd.Flags().StringArrayVarP(&labels, "label", "l", []string{}, "string in key=value format") + + return cmd +} diff --git a/cmd/entities/cloud-volumes/volume_attach.go b/cmd/entities/cloud-volumes/volume_attach.go new file mode 100644 index 0000000..939f4e6 --- /dev/null +++ b/cmd/entities/cloud-volumes/volume_attach.go @@ -0,0 +1,53 @@ +package cloudvolumes + +import ( + serverscom "github.com/serverscom/serverscom-go-client/pkg" + "github.com/serverscom/srvctl/cmd/base" + "github.com/spf13/cobra" +) + +func newVolumeAttachCmd(cmdContext *base.CmdContext) *cobra.Command { + var instanceID string + + cmd := &cobra.Command{ + Use: "volume-attach ", + Short: "Attach cloud volume to cloud instance", + Long: "Attach a cloud volume to a cloud instance", + 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{"instance-id"} + if err := base.ValidateFlags(cmd, required); err != nil { + return err + } + + input := serverscom.CloudBlockStorageVolumeAttachInput{ + InstanceID: instanceID, + } + + scClient := cmdContext.GetClient().SetVerbose(manager.GetVerbose(cmd)).GetScClient() + + volumeID := args[0] + volume, err := scClient.CloudBlockStorageVolumes.Attach(ctx, volumeID, input) + if err != nil { + return err + } + + if volume != nil { + formatter := cmdContext.GetOrCreateFormatter(cmd) + return formatter.Format(volume) + } + return nil + }, + } + + cmd.Flags().StringVar(&instanceID, "instance-id", "", "ID of the cloud instance") + + return cmd +} diff --git a/cmd/entities/cloud-volumes/volume_detach.go b/cmd/entities/cloud-volumes/volume_detach.go new file mode 100644 index 0000000..2e89fc4 --- /dev/null +++ b/cmd/entities/cloud-volumes/volume_detach.go @@ -0,0 +1,53 @@ +package cloudvolumes + +import ( + serverscom "github.com/serverscom/serverscom-go-client/pkg" + "github.com/serverscom/srvctl/cmd/base" + "github.com/spf13/cobra" +) + +func newVolumeDetachCmd(cmdContext *base.CmdContext) *cobra.Command { + var instanceID string + + cmd := &cobra.Command{ + Use: "volume-detach ", + Short: "Detach cloud volume from cloud instance", + Long: "Detach a cloud volume from a cloud instance", + 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{"instance-id"} + if err := base.ValidateFlags(cmd, required); err != nil { + return err + } + + input := serverscom.CloudBlockStorageVolumeDetachInput{ + InstanceID: instanceID, + } + + scClient := cmdContext.GetClient().SetVerbose(manager.GetVerbose(cmd)).GetScClient() + + volumeID := args[0] + volume, err := scClient.CloudBlockStorageVolumes.Detach(ctx, volumeID, input) + if err != nil { + return err + } + + if volume != nil { + formatter := cmdContext.GetOrCreateFormatter(cmd) + return formatter.Format(volume) + } + return nil + }, + } + + cmd.Flags().StringVar(&instanceID, "instance-id", "", "ID of the cloud instance") + + return cmd +} diff --git a/cmd/root.go b/cmd/root.go index 7c7c08f..624d922 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" + "github.com/serverscom/srvctl/cmd/entities/cloud-volumes" "github.com/serverscom/srvctl/cmd/entities/drivemodels" "github.com/serverscom/srvctl/cmd/entities/hosts" "github.com/serverscom/srvctl/cmd/entities/invoices" @@ -74,6 +75,7 @@ func NewRootCmd(version string) *cobra.Command { cmd.AddCommand(sbmmodels.NewCmd(cmdContext)) cmd.AddCommand(l2segments.NewCmd(cmdContext)) cmd.AddCommand(networkpools.NewCmd(cmdContext)) + cmd.AddCommand(cloudvolumes.NewCmd(cmdContext)) return cmd } diff --git a/go.mod b/go.mod index 89cf130..58c1033 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/creack/pty v1.1.24 github.com/jmespath/go-jmespath v0.4.0 github.com/onsi/gomega v1.38.0 - github.com/serverscom/serverscom-go-client v1.0.25 + github.com/serverscom/serverscom-go-client v1.0.28 github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.7 github.com/spf13/viper v1.20.1 diff --git a/go.sum b/go.sum index c0e3b48..64a8f63 100644 --- a/go.sum +++ b/go.sum @@ -43,8 +43,8 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.10.0 h1:FM8Cv6j2KqIhM2ZK7HZjm4mpj9NBktLgowT1aN9q5Cc= github.com/sagikazarmark/locafero v0.10.0/go.mod h1:Ieo3EUsjifvQu4NZwV5sPd4dwvu0OCgEQV7vjc9yDjw= -github.com/serverscom/serverscom-go-client v1.0.25 h1:W0onVxNRAFDTZNrJyQx/kMNx+ZeICouLWgkdaE3FMnk= -github.com/serverscom/serverscom-go-client v1.0.25/go.mod h1:/Nf+XygKOxm19Sl2gvMzT55O4X+tWDkj/UM4mjzfKgM= +github.com/serverscom/serverscom-go-client v1.0.28 h1:gmUsdIzPlqCEZKc0qwC4SFgpHmsTLymcXGzQ3zNRS5M= +github.com/serverscom/serverscom-go-client v1.0.28/go.mod h1:/Nf+XygKOxm19Sl2gvMzT55O4X+tWDkj/UM4mjzfKgM= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= diff --git a/internal/mocks/cloud_block_storage_volumes_service.go b/internal/mocks/cloud_block_storage_volumes_service.go new file mode 100644 index 0000000..5d911ef --- /dev/null +++ b/internal/mocks/cloud_block_storage_volumes_service.go @@ -0,0 +1,146 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./vendor/github.com/serverscom/serverscom-go-client/pkg/cloud_block_storage_volumes.go +// +// Generated by this command: +// +// 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 +// + +// 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" +) + +// MockCloudBlockStorageVolumesService is a mock of CloudBlockStorageVolumesService interface. +type MockCloudBlockStorageVolumesService struct { + ctrl *gomock.Controller + recorder *MockCloudBlockStorageVolumesServiceMockRecorder + isgomock struct{} +} + +// MockCloudBlockStorageVolumesServiceMockRecorder is the mock recorder for MockCloudBlockStorageVolumesService. +type MockCloudBlockStorageVolumesServiceMockRecorder struct { + mock *MockCloudBlockStorageVolumesService +} + +// NewMockCloudBlockStorageVolumesService creates a new mock instance. +func NewMockCloudBlockStorageVolumesService(ctrl *gomock.Controller) *MockCloudBlockStorageVolumesService { + mock := &MockCloudBlockStorageVolumesService{ctrl: ctrl} + mock.recorder = &MockCloudBlockStorageVolumesServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCloudBlockStorageVolumesService) EXPECT() *MockCloudBlockStorageVolumesServiceMockRecorder { + return m.recorder +} + +// Attach mocks base method. +func (m *MockCloudBlockStorageVolumesService) Attach(ctx context.Context, id string, input serverscom.CloudBlockStorageVolumeAttachInput) (*serverscom.CloudBlockStorageVolume, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Attach", ctx, id, input) + ret0, _ := ret[0].(*serverscom.CloudBlockStorageVolume) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Attach indicates an expected call of Attach. +func (mr *MockCloudBlockStorageVolumesServiceMockRecorder) Attach(ctx, id, input any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Attach", reflect.TypeOf((*MockCloudBlockStorageVolumesService)(nil).Attach), ctx, id, input) +} + +// Collection mocks base method. +func (m *MockCloudBlockStorageVolumesService) Collection() serverscom.Collection[serverscom.CloudBlockStorageVolume] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Collection") + ret0, _ := ret[0].(serverscom.Collection[serverscom.CloudBlockStorageVolume]) + return ret0 +} + +// Collection indicates an expected call of Collection. +func (mr *MockCloudBlockStorageVolumesServiceMockRecorder) Collection() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Collection", reflect.TypeOf((*MockCloudBlockStorageVolumesService)(nil).Collection)) +} + +// Create mocks base method. +func (m *MockCloudBlockStorageVolumesService) Create(ctx context.Context, input serverscom.CloudBlockStorageVolumeCreateInput) (*serverscom.CloudBlockStorageVolume, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", ctx, input) + ret0, _ := ret[0].(*serverscom.CloudBlockStorageVolume) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +func (mr *MockCloudBlockStorageVolumesServiceMockRecorder) Create(ctx, input any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockCloudBlockStorageVolumesService)(nil).Create), ctx, input) +} + +// Delete mocks base method. +func (m *MockCloudBlockStorageVolumesService) Delete(ctx context.Context, id string) (*serverscom.CloudBlockStorageVolume, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, id) + ret0, _ := ret[0].(*serverscom.CloudBlockStorageVolume) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Delete indicates an expected call of Delete. +func (mr *MockCloudBlockStorageVolumesServiceMockRecorder) Delete(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockCloudBlockStorageVolumesService)(nil).Delete), ctx, id) +} + +// Detach mocks base method. +func (m *MockCloudBlockStorageVolumesService) Detach(ctx context.Context, id string, input serverscom.CloudBlockStorageVolumeDetachInput) (*serverscom.CloudBlockStorageVolume, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Detach", ctx, id, input) + ret0, _ := ret[0].(*serverscom.CloudBlockStorageVolume) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Detach indicates an expected call of Detach. +func (mr *MockCloudBlockStorageVolumesServiceMockRecorder) Detach(ctx, id, input any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Detach", reflect.TypeOf((*MockCloudBlockStorageVolumesService)(nil).Detach), ctx, id, input) +} + +// Get mocks base method. +func (m *MockCloudBlockStorageVolumesService) Get(ctx context.Context, id string) (*serverscom.CloudBlockStorageVolume, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, id) + ret0, _ := ret[0].(*serverscom.CloudBlockStorageVolume) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockCloudBlockStorageVolumesServiceMockRecorder) Get(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockCloudBlockStorageVolumesService)(nil).Get), ctx, id) +} + +// Update mocks base method. +func (m *MockCloudBlockStorageVolumesService) Update(ctx context.Context, id string, input serverscom.CloudBlockStorageVolumeUpdateInput) (*serverscom.CloudBlockStorageVolume, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, id, input) + ret0, _ := ret[0].(*serverscom.CloudBlockStorageVolume) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockCloudBlockStorageVolumesServiceMockRecorder) Update(ctx, id, input any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockCloudBlockStorageVolumesService)(nil).Update), ctx, id, input) +} diff --git a/internal/output/entities/cloud_volumes.go b/internal/output/entities/cloud_volumes.go new file mode 100644 index 0000000..a7a25f5 --- /dev/null +++ b/internal/output/entities/cloud_volumes.go @@ -0,0 +1,34 @@ +package entities + +import ( + "log" + "reflect" + + serverscom "github.com/serverscom/serverscom-go-client/pkg" +) + +var ( + CloudVolumeType = reflect.TypeOf(serverscom.CloudBlockStorageVolume{}) + CloudVolumeListDefaultFields = []string{"ID", "Name", "RegionID", "Size", "Description"} +) + +func RegisterCloudVolumeDefinition() { + cloudVolumeEntity := &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: "RegionID", Name: "Region ID", Path: "RegionID", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "Size", Name: "Size", Path: "Size", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {ID: "Description", Name: "Description", Path: "Description", ListHandlerFunc: stringHandler, PageViewHandlerFunc: stringHandler, Default: true}, + {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": CloudVolumeListDefaultFields, + }, + eType: CloudVolumeType, + } + if err := Registry.Register(cloudVolumeEntity); err != nil { + log.Fatal(err) + } +} diff --git a/internal/output/entities/init.go b/internal/output/entities/init.go index a5244a8..6184e0e 100644 --- a/internal/output/entities/init.go +++ b/internal/output/entities/init.go @@ -32,4 +32,5 @@ func init() { RegisterRAMOptionDefinition() RegisterL2SegmentDefinitions() RegisterNetworkPoolDefinitions() + RegisterCloudVolumeDefinition() } diff --git a/testdata/entities/cloud-volumes/create.json b/testdata/entities/cloud-volumes/create.json new file mode 100644 index 0000000..074bf1c --- /dev/null +++ b/testdata/entities/cloud-volumes/create.json @@ -0,0 +1,9 @@ +{ + "name": "test-volume", + "region_id": 1, + "size": 100, + "description": "Test volume", + "labels": { + "foo": "bar" + } +} \ No newline at end of file diff --git a/testdata/entities/cloud-volumes/get.json b/testdata/entities/cloud-volumes/get.json new file mode 100644 index 0000000..373a1ad --- /dev/null +++ b/testdata/entities/cloud-volumes/get.json @@ -0,0 +1,15 @@ +{ + "id": "vol-12345", + "openstack_uuid": null, + "region_id": 1, + "size": 100, + "status": "", + "bootable": false, + "labels": { + "foo": "bar" + }, + "created_at": "2025-01-01T12:00:00Z", + "description": "Test volume", + "name": "test-volume", + "attachments": null +} \ No newline at end of file diff --git a/testdata/entities/cloud-volumes/get.txt b/testdata/entities/cloud-volumes/get.txt new file mode 100644 index 0000000..1999612 --- /dev/null +++ b/testdata/entities/cloud-volumes/get.txt @@ -0,0 +1,2 @@ +ID Name Region ID Size Description Created +vol-12345 test-volume 1 100 Test volume 2025-01-01T12:00:00Z diff --git a/testdata/entities/cloud-volumes/get.yaml b/testdata/entities/cloud-volumes/get.yaml new file mode 100644 index 0000000..f575669 --- /dev/null +++ b/testdata/entities/cloud-volumes/get.yaml @@ -0,0 +1,12 @@ +id: vol-12345 +openstackuuid: null +regionid: 1 +size: 100 +status: "" +bootable: false +labels: + foo: bar +created: 2025-01-01T12:00:00Z +description: Test volume +name: test-volume +attachments: [] diff --git a/testdata/entities/cloud-volumes/list.json b/testdata/entities/cloud-volumes/list.json new file mode 100644 index 0000000..c1f7277 --- /dev/null +++ b/testdata/entities/cloud-volumes/list.json @@ -0,0 +1,17 @@ +[ + { + "id": "vol-12345", + "openstack_uuid": null, + "region_id": 1, + "size": 100, + "status": "", + "bootable": false, + "labels": { + "foo": "bar" + }, + "created_at": "2025-01-01T12:00:00Z", + "description": "Test volume", + "name": "test-volume", + "attachments": null + } +] \ No newline at end of file diff --git a/testdata/entities/cloud-volumes/list.txt b/testdata/entities/cloud-volumes/list.txt new file mode 100644 index 0000000..94ac51c --- /dev/null +++ b/testdata/entities/cloud-volumes/list.txt @@ -0,0 +1,2 @@ +ID Name Region ID Size Description +vol-12345 test-volume 1 100 Test volume \ No newline at end of file diff --git a/testdata/entities/cloud-volumes/list_all.json b/testdata/entities/cloud-volumes/list_all.json new file mode 100644 index 0000000..c1f7277 --- /dev/null +++ b/testdata/entities/cloud-volumes/list_all.json @@ -0,0 +1,17 @@ +[ + { + "id": "vol-12345", + "openstack_uuid": null, + "region_id": 1, + "size": 100, + "status": "", + "bootable": false, + "labels": { + "foo": "bar" + }, + "created_at": "2025-01-01T12:00:00Z", + "description": "Test volume", + "name": "test-volume", + "attachments": null + } +] \ No newline at end of file diff --git a/testdata/entities/cloud-volumes/list_all.txt b/testdata/entities/cloud-volumes/list_all.txt new file mode 100644 index 0000000..94ac51c --- /dev/null +++ b/testdata/entities/cloud-volumes/list_all.txt @@ -0,0 +1,2 @@ +ID Name Region ID Size Description +vol-12345 test-volume 1 100 Test volume \ No newline at end of file diff --git a/testdata/entities/cloud-volumes/list_pageview.txt b/testdata/entities/cloud-volumes/list_pageview.txt new file mode 100644 index 0000000..bc83d69 --- /dev/null +++ b/testdata/entities/cloud-volumes/list_pageview.txt @@ -0,0 +1,7 @@ +ID: vol-12345 +Name: test-volume +Region ID: 1 +Size: 100 +Description: Test volume +Labels: foo=bar +Created: 2025-01-01T12:00:00Z diff --git a/testdata/entities/cloud-volumes/list_template.txt b/testdata/entities/cloud-volumes/list_template.txt new file mode 100644 index 0000000..ea57026 --- /dev/null +++ b/testdata/entities/cloud-volumes/list_template.txt @@ -0,0 +1 @@ +vol-12345 \ No newline at end of file diff --git a/testdata/entities/cloud-volumes/update.json b/testdata/entities/cloud-volumes/update.json new file mode 100644 index 0000000..869d84b --- /dev/null +++ b/testdata/entities/cloud-volumes/update.json @@ -0,0 +1,7 @@ +{ + "name": "updated-volume", + "description": "Updated volume", + "labels": { + "new": "label" + } +} \ No newline at end of file