diff --git a/api/v1beta1/zz_generated.conversion.go b/api/v1beta1/zz_generated.conversion.go index 6649d3fd9..2f0055868 100644 --- a/api/v1beta1/zz_generated.conversion.go +++ b/api/v1beta1/zz_generated.conversion.go @@ -1418,6 +1418,7 @@ func autoConvert_v1beta2_IBMVPCMachineSpec_To_v1beta1_IBMVPCMachineSpec(in *v1be if err := Convert_Slice_Pointer_v1beta2_IBMVPCResourceReference_To_Slice_Pointer_string(&in.SSHKeys, &out.SSHKeys, s); err != nil { return err } + // WARNING: in.AdditionalVolumes requires manual conversion: does not exist in peer-type return nil } @@ -1443,6 +1444,7 @@ func autoConvert_v1beta2_IBMVPCMachineStatus_To_v1beta1_IBMVPCMachineStatus(in * // WARNING: in.FailureMessage requires manual conversion: does not exist in peer-type out.InstanceStatus = in.InstanceStatus // WARNING: in.LoadBalancerPoolMembers requires manual conversion: does not exist in peer-type + // WARNING: in.AdditionalVolumeIDs requires manual conversion: does not exist in peer-type return nil } diff --git a/api/v1beta2/ibmvpcmachine_types.go b/api/v1beta2/ibmvpcmachine_types.go index d726128fe..7fc43d93c 100644 --- a/api/v1beta2/ibmvpcmachine_types.go +++ b/api/v1beta2/ibmvpcmachine_types.go @@ -78,6 +78,14 @@ type IBMVPCMachineSpec struct { // SSHKeys is the SSH pub keys that will be used to access VM. // ID will take higher precedence over Name if both specified. SSHKeys []*IBMVPCResourceReference `json:"sshKeys,omitempty"` + + // additionalVolumes is the list of additional volumes attached to the instance + // There is a hard limit of 12 volume attachments per instance: + // https://cloud.ibm.com/docs/vpc?topic=vpc-attaching-block-storage&interface=api#vol-attach-limits + // +kubebuilder:validation:Optional + // +kubebuilder:validation:MaxItems=12 + // +kubebuilder:validation:XValidation:rule="oldSelf.all(x, x in self)",message="Values may only be added" + AdditionalVolumes []*VPCVolume `json:"additionalVolumes,omitempty"` } // IBMVPCResourceReference is a reference to a specific VPC resource by ID or Name @@ -95,7 +103,7 @@ type IBMVPCResourceReference struct { Name *string `json:"name,omitempty"` } -// VPCVolume defines the volume information for the instance. +// VPCVolume defines the volume information. type VPCVolume struct { // DeleteVolumeOnInstanceDelete If set to true, when deleting the instance the volume will also be deleted. // Default is set as true @@ -108,14 +116,15 @@ type VPCVolume struct { // +optional Name string `json:"name,omitempty"` - // SizeGiB is the size of the virtual server's boot disk in GiB. + // SizeGiB is the size of the virtual server's disk in GiB. // Default to the size of the image's `minimum_provisioned_size`. // +optional SizeGiB int64 `json:"sizeGiB,omitempty"` - // Profile is the volume profile for the bootdisk, refer https://cloud.ibm.com/docs/vpc?topic=vpc-block-storage-profiles + // Profile is the volume profile for the disk, refer https://cloud.ibm.com/docs/vpc?topic=vpc-block-storage-profiles // for more information. // Default to general-purpose + // NOTE: If a profile other than custom is specified, the Iops and SizeGiB fields will be ignored // +kubebuilder:validation:Enum="general-purpose";"5iops-tier";"10iops-tier";"custom" // +kubebuilder:default=general-purpose // +optional @@ -175,6 +184,10 @@ type IBMVPCMachineStatus struct { // LoadBalancerPoolMembers is the status of IBM Cloud VPC Load Balancer Backend Pools the machine is a member. // +optional LoadBalancerPoolMembers []VPCLoadBalancerBackendPoolMember `json:"loadBalancerPoolMembers,omitempty"` + + // AdditionalVolumeIDs is a list of Volume ID as per IBMCloud + // +optional + AdditionalVolumeIDs []string `json:"additionalVolumeIDs,omitempty"` } // +kubebuilder:object:root=true diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index 1454db67b..aae344884 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -1247,6 +1247,17 @@ func (in *IBMVPCMachineSpec) DeepCopyInto(out *IBMVPCMachineSpec) { } } } + if in.AdditionalVolumes != nil { + in, out := &in.AdditionalVolumes, &out.AdditionalVolumes + *out = make([]*VPCVolume, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(VPCVolume) + **out = **in + } + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IBMVPCMachineSpec. @@ -1291,6 +1302,11 @@ func (in *IBMVPCMachineStatus) DeepCopyInto(out *IBMVPCMachineStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.AdditionalVolumeIDs != nil { + in, out := &in.AdditionalVolumeIDs, &out.AdditionalVolumeIDs + *out = make([]string, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IBMVPCMachineStatus. diff --git a/cloud/scope/machine.go b/cloud/scope/machine.go index 4844ff08b..2f1dbfae4 100644 --- a/cloud/scope/machine.go +++ b/cloud/scope/machine.go @@ -1155,3 +1155,92 @@ func (m *MachineScope) APIServerPort() int32 { } return infrav1.DefaultAPIServerPort } + +// GetVolumeAttachments returns the volume attachments for the instance. +func (m *MachineScope) GetVolumeAttachments() ([]vpcv1.VolumeAttachment, error) { + options := vpcv1.ListInstanceVolumeAttachmentsOptions{ + InstanceID: &m.IBMVPCMachine.Status.InstanceID, + } + result, _, err := m.IBMVPCClient.GetVolumeAttachments(&options) + if err != nil { + return nil, fmt.Errorf("error while getting volume attachments: %w", err) + } + return result.VolumeAttachments, nil +} + +// GetVolumeState returns the volume's state. +func (m *MachineScope) GetVolumeState(volumeID string) (string, error) { + options := vpcv1.GetVolumeOptions{ + ID: &volumeID, + } + result, _, err := m.IBMVPCClient.GetVolume(&options) + if err != nil { + return "", fmt.Errorf("could not fetch volume status: %w", err) + } + return *result.Status, err +} + +// CreateVolume creates a new Volume and attaches it to the instance. +func (m *MachineScope) CreateVolume(vpcVolume *infrav1.VPCVolume) (string, error) { + volumeOptions := vpcv1.CreateVolumeOptions{} + var resourceGroupID string + if m.IBMVPCCluster.Status.ResourceGroup != nil { + resourceGroupID = m.IBMVPCCluster.Status.ResourceGroup.ID + } else { + resourceGroupID = m.IBMVPCCluster.Spec.ResourceGroup + } + // TODO: EncryptionKeyCRN is not supported for now, the field is omitted from the manifest + if vpcVolume.Profile != "custom" { + volumeOptions.VolumePrototype = &vpcv1.VolumePrototype{ + ResourceGroup: &vpcv1.ResourceGroupIdentityByID{ + ID: &resourceGroupID, + }, + Profile: &vpcv1.VolumeProfileIdentityByName{ + Name: &vpcVolume.Profile, + }, + Zone: &vpcv1.ZoneIdentity{ + Name: &m.IBMVPCMachine.Spec.Zone, + }, + Capacity: &vpcVolume.SizeGiB, + } + } else { + volumeOptions.VolumePrototype = &vpcv1.VolumePrototype{ + ResourceGroup: &vpcv1.ResourceGroupIdentityByID{ + ID: &resourceGroupID, + }, + Iops: &vpcVolume.Iops, + Profile: &vpcv1.VolumeProfileIdentityByName{ + Name: &vpcVolume.Profile, + }, + Zone: &vpcv1.ZoneIdentity{ + Name: &m.IBMVPCMachine.Spec.Zone, + }, + Capacity: &vpcVolume.SizeGiB, + } + } + + volumeResult, _, err := m.IBMVPCClient.CreateVolume(&volumeOptions) + if err != nil { + return "", fmt.Errorf("error while creating volume: %w", err) + } + + return *volumeResult.ID, nil +} + +// AttachVolume attaches the given volume to the instance. +func (m *MachineScope) AttachVolume(deleteOnInstanceDelete bool, volumeID, volumeName string) error { + attachmentOptions := vpcv1.CreateInstanceVolumeAttachmentOptions{ + InstanceID: &m.IBMVPCMachine.Status.InstanceID, + Volume: &vpcv1.VolumeAttachmentPrototypeVolume{ + ID: &volumeID, + }, + DeleteVolumeOnInstanceDelete: &deleteOnInstanceDelete, + Name: &volumeName, + } + + _, _, err := m.IBMVPCClient.AttachVolumeToInstance(&attachmentOptions) + if err != nil { + err = fmt.Errorf("error while attaching volume to instance: %w", err) + } + return err +} diff --git a/cloud/scope/machine_test.go b/cloud/scope/machine_test.go index adf022e62..878f35b61 100644 --- a/cloud/scope/machine_test.go +++ b/cloud/scope/machine_test.go @@ -43,6 +43,11 @@ import ( . "github.com/onsi/gomega" ) +var ( + volumeName = "foo-volume" + volumeID = "foo-volume-id" +) + func newVPCMachine(clusterName, machineName string) *infrav1.IBMVPCMachine { return &infrav1.IBMVPCMachine{ ObjectMeta: metav1.ObjectMeta{ @@ -1096,3 +1101,183 @@ func TestDeleteVPCLoadBalancerPoolMember(t *testing.T) { }) }) } + +func TestGetVolumeAttachments(t *testing.T) { + setup := func(t *testing.T) (*gomock.Controller, *mock.MockVpc) { + t.Helper() + return gomock.NewController(t), mock.NewMockVpc(gomock.NewController(t)) + } + + vpcMachine := infrav1.IBMVPCMachine{ + Status: infrav1.IBMVPCMachineStatus{ + InstanceID: "foo-instance-id", + }, + } + volumeAttachmentName := "foo-volume-attachment" + + testVolumeAttachments := vpcv1.VolumeAttachmentCollection{ + VolumeAttachments: []vpcv1.VolumeAttachment{{ + Name: &volumeAttachmentName, + }, + { + Name: &volumeName, + }}, + } + + t.Run("Return List of Volume Attachments for Machine", func(t *testing.T) { + g := NewWithT(t) + mockController, mockVPC := setup(t) + t.Cleanup(mockController.Finish) + scope := setupMachineScope(clusterName, machineName, mockVPC) + scope.IBMVPCMachine.Status = vpcMachine.Status + mockVPC.EXPECT().GetVolumeAttachments(gomock.AssignableToTypeOf(&vpcv1.ListInstanceVolumeAttachmentsOptions{})).Return(&testVolumeAttachments, nil, nil) + attachments, err := scope.GetVolumeAttachments() + g.Expect(attachments).To(Equal(testVolumeAttachments.VolumeAttachments)) + g.Expect(err).Should(Succeed()) + }) + + t.Run("Return Error when GetVolumeAttachments fails", func(t *testing.T) { + g := NewWithT(t) + mockController, mockVPC := setup(t) + t.Cleanup(mockController.Finish) + scope := setupMachineScope(clusterName, machineName, mockVPC) + scope.IBMVPCMachine.Status = vpcMachine.Status + mockVPC.EXPECT().GetVolumeAttachments(gomock.AssignableToTypeOf(&vpcv1.ListInstanceVolumeAttachmentsOptions{})).Return(nil, nil, errors.New("Error when getting volume attachments")) + attachments, err := scope.GetVolumeAttachments() + g.Expect(attachments).To(BeNil()) + g.Expect(err).ShouldNot(Succeed()) + }) +} + +func TestGetVolumeState(t *testing.T) { + setup := func(t *testing.T) (*gomock.Controller, *mock.MockVpc) { + t.Helper() + return gomock.NewController(t), mock.NewMockVpc(gomock.NewController(t)) + } + + volumeStatus := vpcv1.VolumeStatusPendingConst + + vpcMachine := infrav1.IBMVPCMachine{ + Status: infrav1.IBMVPCMachineStatus{ + InstanceID: "foo-instance-id", + }, + } + + vpcVolume := vpcv1.Volume{ + Name: &volumeName, + ID: &volumeID, + Status: &volumeStatus, + } + volumeFetchError := errors.New("error while fetching volume") + + t.Run("Return correct volume state", func(t *testing.T) { + g := NewWithT(t) + mockController, mockVPC := setup(t) + t.Cleanup(mockController.Finish) + scope := setupMachineScope(clusterName, machineName, mockVPC) + scope.IBMVPCMachine.Status = vpcMachine.Status + mockVPC.EXPECT().GetVolume(gomock.AssignableToTypeOf(&vpcv1.GetVolumeOptions{})).Return(&vpcVolume, nil, nil) + state, err := scope.GetVolumeState(volumeID) + g.Expect(err).To(BeNil()) + g.Expect(state).To(Equal(volumeStatus)) + }) + + t.Run("Return error when GetVolumeState returns error", func(t *testing.T) { + g := NewWithT(t) + mockController, mockVPC := setup(t) + t.Cleanup(mockController.Finish) + scope := setupMachineScope(clusterName, machineName, mockVPC) + scope.IBMVPCMachine.Status = vpcMachine.Status + mockVPC.EXPECT().GetVolume(gomock.AssignableToTypeOf(&vpcv1.GetVolumeOptions{})).Return(nil, nil, volumeFetchError) + state, err := scope.GetVolumeState(volumeID) + g.Expect(state).To(BeZero()) + g.Expect(errors.Is(err, volumeFetchError)).To(BeTrue()) + }) +} + +func TestCreateVolume(t *testing.T) { + setup := func(t *testing.T) (*gomock.Controller, *mock.MockVpc) { + t.Helper() + return gomock.NewController(t), mock.NewMockVpc(gomock.NewController(t)) + } + + vpcMachine := infrav1.IBMVPCMachine{ + Status: infrav1.IBMVPCMachineStatus{ + InstanceID: "foo-instance-id", + }, + } + + infraVolume := infrav1.VPCVolume{ + Name: volumeName, + Profile: "custom", + Iops: 100, + SizeGiB: 50, + } + pendingState := vpcv1.VolumeStatusPendingConst + + vpcVolume := vpcv1.Volume{ + Name: &volumeName, + ID: &volumeID, + Status: &pendingState, + } + + volumeCreationError := errors.New("error while creating volume") + t.Run("Volume creation is successful", func(t *testing.T) { + g := NewWithT(t) + mockController, mockVPC := setup(t) + t.Cleanup(mockController.Finish) + scope := setupMachineScope(clusterName, machineName, mockVPC) + mockVPC.EXPECT().CreateVolume(gomock.AssignableToTypeOf(&vpcv1.CreateVolumeOptions{})).Return(&vpcVolume, nil, nil) + id, err := scope.CreateVolume(&infraVolume) + g.Expect(err).Should(Succeed()) + g.Expect(id).Should(Equal(volumeID)) + }) + t.Run("Volume creation fails", func(t *testing.T) { + g := NewWithT(t) + mockController, mockVPC := setup(t) + t.Cleanup(mockController.Finish) + scope := setupMachineScope(clusterName, machineName, mockVPC) + scope.IBMVPCMachine.Status = vpcMachine.Status + mockVPC.EXPECT().CreateVolume(gomock.AssignableToTypeOf(&vpcv1.CreateVolumeOptions{})).Return(nil, nil, volumeCreationError) + id, err := scope.CreateVolume(&infraVolume) + g.Expect(err).ShouldNot(Succeed()) + g.Expect(errors.Is(err, volumeCreationError)).To(BeTrue()) + g.Expect(id).To(BeZero()) + }) +} + +func TestAttachVolume(t *testing.T) { + setup := func(t *testing.T) (*gomock.Controller, *mock.MockVpc) { + t.Helper() + return gomock.NewController(t), mock.NewMockVpc(gomock.NewController(t)) + } + + deleteOnInstanceDelete := true + vpcMachine := infrav1.IBMVPCMachine{ + Status: infrav1.IBMVPCMachineStatus{ + InstanceID: "foo-instance-id", + }, + } + volumeAttachmentError := errors.New("error while attaching volume") + t.Run("Volume attachment is successful", func(t *testing.T) { + g := NewWithT(t) + mockController, mockVPC := setup(t) + t.Cleanup(mockController.Finish) + scope := setupMachineScope(clusterName, machineName, mockVPC) + scope.IBMVPCMachine.Status = vpcMachine.Status + mockVPC.EXPECT().AttachVolumeToInstance(gomock.AssignableToTypeOf(&vpcv1.CreateInstanceVolumeAttachmentOptions{})).Return(nil, nil, nil) + err := scope.AttachVolume(deleteOnInstanceDelete, volumeID, volumeName) + g.Expect(err).Should(Succeed()) + }) + t.Run("Volume attachment fails", func(t *testing.T) { + g := NewWithT(t) + mockController, mockVPC := setup(t) + t.Cleanup(mockController.Finish) + scope := setupMachineScope(clusterName, machineName, mockVPC) + scope.IBMVPCMachine.Status = vpcMachine.Status + mockVPC.EXPECT().AttachVolumeToInstance(gomock.AssignableToTypeOf(&vpcv1.CreateInstanceVolumeAttachmentOptions{})).Return(nil, nil, volumeAttachmentError) + err := scope.AttachVolume(deleteOnInstanceDelete, volumeID, volumeName) + g.Expect(err).ShouldNot(Succeed()) + g.Expect(errors.Is(err, volumeAttachmentError)).To(BeTrue()) + }) +} diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcmachines.yaml index 50e8ebdb5..c474d07a3 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcmachines.yaml @@ -206,6 +206,65 @@ spec: spec: description: IBMVPCMachineSpec defines the desired state of IBMVPCMachine. properties: + additionalVolumes: + description: |- + additionalVolumes is the list of additional volumes attached to the instance + There is a hard limit of 12 volume attachments per instance: + https://cloud.ibm.com/docs/vpc?topic=vpc-attaching-block-storage&interface=api#vol-attach-limits + items: + description: VPCVolume defines the volume information. + properties: + deleteVolumeOnInstanceDelete: + default: true + description: |- + DeleteVolumeOnInstanceDelete If set to true, when deleting the instance the volume will also be deleted. + Default is set as true + type: boolean + encryptionKeyCRN: + description: |- + EncryptionKey is the root key to use to wrap the data encryption key for the volume and this points to the CRN + and possible values are as follows. + The CRN of the [Key Protect Root + Key](https://cloud.ibm.com/docs/key-protect?topic=key-protect-getting-started-tutorial) or [Hyper Protect Crypto + Service Root Key](https://cloud.ibm.com/docs/hs-crypto?topic=hs-crypto-get-started) for this resource. + If unspecified, the `encryption` type for the volume will be `provider_managed`. + type: string + iops: + description: |- + Iops is the maximum I/O operations per second (IOPS) to use for the volume. Applicable only to volumes using a profile + family of `custom`. + format: int64 + type: integer + name: + description: |- + Name is the unique user-defined name for this volume. + Default will be autogenerated + type: string + profile: + default: general-purpose + description: |- + Profile is the volume profile for the disk, refer https://cloud.ibm.com/docs/vpc?topic=vpc-block-storage-profiles + for more information. + Default to general-purpose + NOTE: If a profile other than custom is specified, the Iops and SizeGiB fields will be ignored + enum: + - general-purpose + - 5iops-tier + - 10iops-tier + - custom + type: string + sizeGiB: + description: |- + SizeGiB is the size of the virtual server's disk in GiB. + Default to the size of the image's `minimum_provisioned_size`. + format: int64 + type: integer + type: object + maxItems: 12 + type: array + x-kubernetes-validations: + - message: Values may only be added + rule: oldSelf.all(x, x in self) bootVolume: description: BootVolume contains machines's boot volume configurations like size, iops etc.. @@ -239,9 +298,10 @@ spec: profile: default: general-purpose description: |- - Profile is the volume profile for the bootdisk, refer https://cloud.ibm.com/docs/vpc?topic=vpc-block-storage-profiles + Profile is the volume profile for the disk, refer https://cloud.ibm.com/docs/vpc?topic=vpc-block-storage-profiles for more information. Default to general-purpose + NOTE: If a profile other than custom is specified, the Iops and SizeGiB fields will be ignored enum: - general-purpose - 5iops-tier @@ -250,7 +310,7 @@ spec: type: string sizeGiB: description: |- - SizeGiB is the size of the virtual server's boot disk in GiB. + SizeGiB is the size of the virtual server's disk in GiB. Default to the size of the image's `minimum_provisioned_size`. format: int64 type: integer @@ -479,6 +539,11 @@ spec: status: description: IBMVPCMachineStatus defines the observed state of IBMVPCMachine. properties: + additionalVolumeIDs: + description: AdditionalVolumeIDs is a list of Volume ID as per IBMCloud + items: + type: string + type: array addresses: description: Addresses contains the IBM Cloud instance associated addresses. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcmachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcmachinetemplates.yaml index 2663fc4d5..7a4cc475c 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcmachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcmachinetemplates.yaml @@ -193,6 +193,65 @@ spec: description: Spec is the specification of the desired behavior of the machine. properties: + additionalVolumes: + description: |- + additionalVolumes is the list of additional volumes attached to the instance + There is a hard limit of 12 volume attachments per instance: + https://cloud.ibm.com/docs/vpc?topic=vpc-attaching-block-storage&interface=api#vol-attach-limits + items: + description: VPCVolume defines the volume information. + properties: + deleteVolumeOnInstanceDelete: + default: true + description: |- + DeleteVolumeOnInstanceDelete If set to true, when deleting the instance the volume will also be deleted. + Default is set as true + type: boolean + encryptionKeyCRN: + description: |- + EncryptionKey is the root key to use to wrap the data encryption key for the volume and this points to the CRN + and possible values are as follows. + The CRN of the [Key Protect Root + Key](https://cloud.ibm.com/docs/key-protect?topic=key-protect-getting-started-tutorial) or [Hyper Protect Crypto + Service Root Key](https://cloud.ibm.com/docs/hs-crypto?topic=hs-crypto-get-started) for this resource. + If unspecified, the `encryption` type for the volume will be `provider_managed`. + type: string + iops: + description: |- + Iops is the maximum I/O operations per second (IOPS) to use for the volume. Applicable only to volumes using a profile + family of `custom`. + format: int64 + type: integer + name: + description: |- + Name is the unique user-defined name for this volume. + Default will be autogenerated + type: string + profile: + default: general-purpose + description: |- + Profile is the volume profile for the disk, refer https://cloud.ibm.com/docs/vpc?topic=vpc-block-storage-profiles + for more information. + Default to general-purpose + NOTE: If a profile other than custom is specified, the Iops and SizeGiB fields will be ignored + enum: + - general-purpose + - 5iops-tier + - 10iops-tier + - custom + type: string + sizeGiB: + description: |- + SizeGiB is the size of the virtual server's disk in GiB. + Default to the size of the image's `minimum_provisioned_size`. + format: int64 + type: integer + type: object + maxItems: 12 + type: array + x-kubernetes-validations: + - message: Values may only be added + rule: oldSelf.all(x, x in self) bootVolume: description: BootVolume contains machines's boot volume configurations like size, iops etc.. @@ -226,9 +285,10 @@ spec: profile: default: general-purpose description: |- - Profile is the volume profile for the bootdisk, refer https://cloud.ibm.com/docs/vpc?topic=vpc-block-storage-profiles + Profile is the volume profile for the disk, refer https://cloud.ibm.com/docs/vpc?topic=vpc-block-storage-profiles for more information. Default to general-purpose + NOTE: If a profile other than custom is specified, the Iops and SizeGiB fields will be ignored enum: - general-purpose - 5iops-tier @@ -237,7 +297,7 @@ spec: type: string sizeGiB: description: |- - SizeGiB is the size of the virtual server's boot disk in GiB. + SizeGiB is the size of the virtual server's disk in GiB. Default to the size of the image's `minimum_provisioned_size`. format: int64 type: integer diff --git a/controllers/ibmvpcmachine_controller.go b/controllers/ibmvpcmachine_controller.go index 8d14aa522..fc047b9ff 100644 --- a/controllers/ibmvpcmachine_controller.go +++ b/controllers/ibmvpcmachine_controller.go @@ -18,6 +18,7 @@ package controllers import ( "context" + "errors" "fmt" "time" @@ -27,6 +28,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" @@ -256,10 +258,17 @@ func (r *IBMVPCMachineReconciler) reconcileNormal(machineScope *scope.MachineSco } } + // Handle Additional Volumes + var result ctrl.Result + result, err = r.reconcileAdditionalVolumes(machineScope) + if err != nil { + return ctrl.Result{}, fmt.Errorf("error reconciling additional volumes: %w", err) + } + // With a running machine and all Load Balancer Pool Members reconciled, mark machine as ready. machineScope.SetReady() v1beta1conditions.MarkTrue(machineScope.IBMVPCMachine, infrav1.InstanceReadyCondition) - return ctrl.Result{}, nil + return result, nil } func (r *IBMVPCMachineReconciler) getOrCreate(scope *scope.MachineScope) (*vpcv1.Instance, error) { @@ -290,3 +299,57 @@ func (r *IBMVPCMachineReconciler) reconcileDelete(scope *scope.MachineScope) (_ return ctrl.Result{}, nil } + +func (r *IBMVPCMachineReconciler) reconcileAdditionalVolumes(machineScope *scope.MachineScope) (ctrl.Result, error) { + if machineScope.IBMVPCMachine.Status.AdditionalVolumeIDs == nil { + machineScope.IBMVPCMachine.Status.AdditionalVolumeIDs = make([]string, len(machineScope.IBMVPCMachine.Spec.AdditionalVolumes)) + } + machineVolumes := machineScope.IBMVPCMachine.Spec.AdditionalVolumes + result := ctrl.Result{} + if len(machineVolumes) == 0 { + return result, nil + } + volumeAttachmentList, err := machineScope.GetVolumeAttachments() + if err != nil { + return result, err + } + volumeAttachmentNames := sets.New[string]() + for i := range volumeAttachmentList { + sets.Insert(volumeAttachmentNames, *volumeAttachmentList[i].Name) + } + errList := []error{} + // Read through the list, checking if volume exists and create volume if it does not + for v := range machineVolumes { + if volumeAttachmentNames.Has(machineVolumes[v].Name) { + // volume attachment has been created so volume is already attached + continue + } + if machineScope.IBMVPCMachine.Status.AdditionalVolumeIDs[v] != "" { + // volume was already created, fetch volume status and attach if possible + state, err := machineScope.GetVolumeState(machineScope.IBMVPCMachine.Status.AdditionalVolumeIDs[v]) + if err != nil { + errList = append(errList, err) + } + switch state { + case vpcv1.VolumeStatusPendingConst, vpcv1.VolumeStatusUpdatingConst: + result = ctrl.Result{RequeueAfter: 10 * time.Second} + case vpcv1.VolumeStatusFailedConst, vpcv1.VolumeStatusUnusableConst: + errList = append(errList, fmt.Errorf("volume in unexpected state: %s", state)) + case vpcv1.VolumeStatusAvailableConst: + err = machineScope.AttachVolume(machineVolumes[v].DeleteVolumeOnInstanceDelete, machineScope.IBMVPCMachine.Status.AdditionalVolumeIDs[v], machineVolumes[v].Name) + if err != nil { + errList = append(errList, err) + } + } + } else { + // volume does not exist, create it and requeue so that it becomes available + volumeID, err := machineScope.CreateVolume(machineVolumes[v]) + machineScope.IBMVPCMachine.Status.AdditionalVolumeIDs[v] = volumeID + if err != nil { + errList = append(errList, err) + } + result = ctrl.Result{RequeueAfter: 10 * time.Second} + } + } + return result, errors.Join(errList...) +} diff --git a/controllers/ibmvpcmachine_controller_test.go b/controllers/ibmvpcmachine_controller_test.go index 499addb0e..4ed01f006 100644 --- a/controllers/ibmvpcmachine_controller_test.go +++ b/controllers/ibmvpcmachine_controller_test.go @@ -703,3 +703,176 @@ func TestIBMVPCMachineLBReconciler_Delete(t *testing.T) { }) }) } + +func TestReconcileAdditionalVolumes(t *testing.T) { + volumeName := "foo-volume" + volumeID := "foo-volume-id" + volumeGeneratedName := "foo-generated-name" + clusterResourceGroup := "foo-resource-group" + + setup := func(t *testing.T) (*gomock.Controller, *vpcmock.MockVpc, *scope.MachineScope, IBMVPCMachineReconciler) { + t.Helper() + mockvpc := vpcmock.NewMockVpc(gomock.NewController(t)) + reconciler := IBMVPCMachineReconciler{ + Client: testEnv.Client, + Log: klog.Background(), + } + machineScope := &scope.MachineScope{ + Logger: klog.Background(), + IBMVPCCluster: &infrav1.IBMVPCCluster{ + Spec: infrav1.IBMVPCClusterSpec{ + ResourceGroup: clusterResourceGroup, + }, + }, + IBMVPCMachine: &infrav1.IBMVPCMachine{ + ObjectMeta: metav1.ObjectMeta{ + Name: "capi-machine", + Labels: map[string]string{ + clusterv1.MachineControlPlaneNameLabel: "capi-control-plane-machine", + }, + }, + }, + IBMVPCClient: mockvpc, + } + return gomock.NewController(t), mockvpc, machineScope, reconciler + } + + additionalVolumes := []*infrav1.VPCVolume{ + { + Name: volumeName, + SizeGiB: 15, + Profile: "custom", + Iops: 150, + }, + } + + vpcVolume := vpcv1.Volume{ + Name: &volumeName, + ID: &volumeID, + } + + testVolumeAttachments := vpcv1.VolumeAttachmentCollection{ + VolumeAttachments: []vpcv1.VolumeAttachment{{ + Name: &volumeName, + }, + { + Name: &volumeGeneratedName, + }}, + } + + testMachineStatus := infrav1.IBMVPCMachineStatus{ + AdditionalVolumeIDs: []string{volumeID}, + } + + emptyVolumeAttachments := vpcv1.VolumeAttachmentCollection{} + volumeAvailableState := vpcv1.VolumeStatusAvailableConst + volumePendingState := vpcv1.VolumeStatusPendingConst + volumeUpdatingState := vpcv1.VolumeStatusUpdatingConst + volumeFailedState := vpcv1.VolumeStatusFailedConst + volumeUnusableState := vpcv1.VolumeStatusUnusableConst + + t.Run("Should successfully return when no additional volumes are present in spec", func(t *testing.T) { + g := NewWithT(t) + mockController, _, machineScope, reconciler := setup(t) + t.Cleanup(mockController.Finish) + result, err := reconciler.reconcileAdditionalVolumes(machineScope) + g.Expect(err).Should(BeNil()) + g.Expect(result).To(Equal(ctrl.Result{})) + }) + + t.Run("Should successfully attach volume when volume id is defined and volume is in available state", func(t *testing.T) { + g := NewWithT(t) + mockController, mockvpc, machineScope, reconciler := setup(t) + t.Cleanup(mockController.Finish) + mockvpc.EXPECT().GetVolumeAttachments(gomock.AssignableToTypeOf(&vpcv1.ListInstanceVolumeAttachmentsOptions{})).Return(&emptyVolumeAttachments, nil, nil) + machineScope.IBMVPCMachine.Spec.AdditionalVolumes = additionalVolumes + machineScope.IBMVPCMachine.Status = testMachineStatus + mockvpc.EXPECT().AttachVolumeToInstance(gomock.AssignableToTypeOf(&vpcv1.CreateInstanceVolumeAttachmentOptions{})).Return(nil, nil, nil) + vpcVolume.Status = &volumeAvailableState + mockvpc.EXPECT().GetVolume(gomock.AssignableToTypeOf(&vpcv1.GetVolumeOptions{})).Return(&vpcVolume, nil, nil) + result, err := reconciler.reconcileAdditionalVolumes(machineScope) + g.Expect(err).Should(BeNil()) + g.Expect(result).To(Equal(ctrl.Result{})) + }) + + t.Run("Should requeue when volume is successfully created but in pending state", func(t *testing.T) { + g := NewWithT(t) + mockController, mockvpc, machineScope, reconciler := setup(t) + t.Cleanup(mockController.Finish) + mockvpc.EXPECT().GetVolumeAttachments(gomock.AssignableToTypeOf(&vpcv1.ListInstanceVolumeAttachmentsOptions{})).Return(&emptyVolumeAttachments, nil, nil) + machineScope.IBMVPCMachine.Spec.AdditionalVolumes = additionalVolumes + machineScope.IBMVPCMachine.Status = testMachineStatus + vpcVolume.Status = &volumePendingState + mockvpc.EXPECT().GetVolume(gomock.AssignableToTypeOf(&vpcv1.GetVolumeOptions{})).Return(&vpcVolume, nil, nil) + result, err := reconciler.reconcileAdditionalVolumes(machineScope) + g.Expect(err).Should(BeNil()) + g.Expect(result.RequeueAfter).ToNot(BeZero()) + }) + + t.Run("Should requeue when volume is successfully created but in updating state", func(t *testing.T) { + g := NewWithT(t) + mockController, mockvpc, machineScope, reconciler := setup(t) + t.Cleanup(mockController.Finish) + mockvpc.EXPECT().GetVolumeAttachments(gomock.AssignableToTypeOf(&vpcv1.ListInstanceVolumeAttachmentsOptions{})).Return(&emptyVolumeAttachments, nil, nil) + machineScope.IBMVPCMachine.Spec.AdditionalVolumes = additionalVolumes + machineScope.IBMVPCMachine.Status = testMachineStatus + vpcVolume.Status = &volumeUpdatingState + mockvpc.EXPECT().GetVolume(gomock.AssignableToTypeOf(&vpcv1.GetVolumeOptions{})).Return(&vpcVolume, nil, nil) + result, err := reconciler.reconcileAdditionalVolumes(machineScope) + g.Expect(err).Should(BeNil()) + g.Expect(result.RequeueAfter).ToNot(BeZero()) + }) + + t.Run("Should requeue when volume is in updating state", func(t *testing.T) { + g := NewWithT(t) + mockController, mockvpc, machineScope, reconciler := setup(t) + t.Cleanup(mockController.Finish) + mockvpc.EXPECT().GetVolumeAttachments(gomock.AssignableToTypeOf(&vpcv1.ListInstanceVolumeAttachmentsOptions{})).Return(&emptyVolumeAttachments, nil, nil) + machineScope.IBMVPCMachine.Spec.AdditionalVolumes = additionalVolumes + machineScope.IBMVPCMachine.Status = testMachineStatus + vpcVolume.Status = &volumeUpdatingState + mockvpc.EXPECT().GetVolume(gomock.AssignableToTypeOf(&vpcv1.GetVolumeOptions{})).Return(&vpcVolume, nil, nil) + result, err := reconciler.reconcileAdditionalVolumes(machineScope) + g.Expect(err).Should(BeNil()) + g.Expect(result.RequeueAfter).ToNot(BeZero()) + }) + + t.Run("Should return error when volume is in failed state", func(t *testing.T) { + g := NewWithT(t) + mockController, mockvpc, machineScope, reconciler := setup(t) + t.Cleanup(mockController.Finish) + mockvpc.EXPECT().GetVolumeAttachments(gomock.AssignableToTypeOf(&vpcv1.ListInstanceVolumeAttachmentsOptions{})).Return(&emptyVolumeAttachments, nil, nil) + machineScope.IBMVPCMachine.Spec.AdditionalVolumes = additionalVolumes + machineScope.IBMVPCMachine.Status = testMachineStatus + vpcVolume.Status = &volumeFailedState + mockvpc.EXPECT().GetVolume(gomock.AssignableToTypeOf(&vpcv1.GetVolumeOptions{})).Return(&vpcVolume, nil, nil) + result, err := reconciler.reconcileAdditionalVolumes(machineScope) + g.Expect(err).ShouldNot(BeNil()) + g.Expect(result).To(BeZero()) + }) + + t.Run("Should return error when volume is in unusable state", func(t *testing.T) { + g := NewWithT(t) + mockController, mockvpc, machineScope, reconciler := setup(t) + t.Cleanup(mockController.Finish) + mockvpc.EXPECT().GetVolumeAttachments(gomock.AssignableToTypeOf(&vpcv1.ListInstanceVolumeAttachmentsOptions{})).Return(&emptyVolumeAttachments, nil, nil) + machineScope.IBMVPCMachine.Spec.AdditionalVolumes = additionalVolumes + machineScope.IBMVPCMachine.Status = testMachineStatus + vpcVolume.Status = &volumeUnusableState + mockvpc.EXPECT().GetVolume(gomock.AssignableToTypeOf(&vpcv1.GetVolumeOptions{})).Return(&vpcVolume, nil, nil) + result, err := reconciler.reconcileAdditionalVolumes(machineScope) + g.Expect(err).ShouldNot(BeNil()) + g.Expect(result).To(BeZero()) + }) + + t.Run("Should not create new volume if it already exists", func(t *testing.T) { + g := NewWithT(t) + mockController, mockvpc, machineScope, reconciler := setup(t) + t.Cleanup(mockController.Finish) + machineScope.IBMVPCMachine.Spec.AdditionalVolumes = additionalVolumes + mockvpc.EXPECT().GetVolumeAttachments(gomock.AssignableToTypeOf(&vpcv1.ListInstanceVolumeAttachmentsOptions{})).Return(&testVolumeAttachments, nil, nil) + result, err := reconciler.reconcileAdditionalVolumes(machineScope) + g.Expect(err).Should(BeNil()) + g.Expect(result).To(Equal(ctrl.Result{})) + }) +} diff --git a/internal/webhooks/common.go b/internal/webhooks/common.go index 841c350d0..d82a05e18 100644 --- a/internal/webhooks/common.go +++ b/internal/webhooks/common.go @@ -17,6 +17,7 @@ limitations under the License. package webhooks import ( + "fmt" "strconv" "k8s.io/apimachinery/pkg/util/intstr" @@ -82,8 +83,23 @@ func defaultIBMVPCMachineSpec(spec *infrav1.IBMVPCMachineSpec) { } } -func validateBootVolume(spec infrav1.IBMVPCMachineSpec) field.ErrorList { +func validateVolumes(spec infrav1.IBMVPCMachineSpec) field.ErrorList { var allErrs field.ErrorList + const customProfile = "custom" + + for i := range spec.AdditionalVolumes { + if spec.AdditionalVolumes[i].Profile == customProfile { + if spec.AdditionalVolumes[i].Iops == 0 { + allErrs = append(allErrs, field.Invalid(field.NewPath(fmt.Sprintf("spec.AdditionalVolumes[%d]", i)), spec, "iops has to be specified when profile is set to `custom` ")) + } + if spec.AdditionalVolumes[i].SizeGiB == 0 { + allErrs = append(allErrs, field.Invalid(field.NewPath(fmt.Sprintf("spec.AdditionalVolumes[%d]", i)), spec, "sizeGiB has to be specified when profile is set to `custom` ")) + } + } + if spec.AdditionalVolumes[i].Iops != 0 && spec.AdditionalVolumes[i].Profile != customProfile { + allErrs = append(allErrs, field.Invalid(field.NewPath(fmt.Sprintf("spec.AdditionalVolumes[%d]", i)), spec, "iops applicable only to volumes using a profile of type `custom`")) + } + } if spec.BootVolume == nil { return allErrs @@ -93,7 +109,7 @@ func validateBootVolume(spec infrav1.IBMVPCMachineSpec) field.ErrorList { allErrs = append(allErrs, field.Invalid(field.NewPath("spec.bootVolume.sizeGiB"), spec, "valid Boot VPCVolume size is 10 - 250 GB")) } - if spec.BootVolume.Iops != 0 && spec.BootVolume.Profile != "custom" { + if spec.BootVolume.Iops != 0 && spec.BootVolume.Profile != customProfile { allErrs = append(allErrs, field.Invalid(field.NewPath("spec.bootVolume.iops"), spec, "iops applicable only to volumes using a profile of type `custom`")) } diff --git a/internal/webhooks/common_test.go b/internal/webhooks/common_test.go index 31728deb5..96c74b2ef 100644 --- a/internal/webhooks/common_test.go +++ b/internal/webhooks/common_test.go @@ -112,58 +112,58 @@ func TestValidateIBMPowerVSProcessorValues(t *testing.T) { } } -func Test_validateBootVolume(t *testing.T) { +func Test_validateVolumes(t *testing.T) { tests := []struct { name string spec infrav1.IBMVPCMachineSpec wantError bool }{ { - name: "Nil bootvolume", + name: "Nil bootvolume for Boot Volume", spec: infrav1.IBMVPCMachineSpec{ BootVolume: nil, }, wantError: false, }, { - name: "valid sizeGiB", + name: "valid sizeGiB for Boot Volume", spec: infrav1.IBMVPCMachineSpec{ BootVolume: &infrav1.VPCVolume{SizeGiB: 20}, }, wantError: false, }, { - name: "Invalid sizeGiB", + name: "Invalid sizeGiB for Boot Volume", spec: infrav1.IBMVPCMachineSpec{ BootVolume: &infrav1.VPCVolume{SizeGiB: 1}, }, wantError: true, }, { - name: "Valid Iops", + name: "Missing Iops for custom profile for Additional Volume", spec: infrav1.IBMVPCMachineSpec{ - BootVolume: &infrav1.VPCVolume{Iops: 1000, Profile: "custom"}, + AdditionalVolumes: []*infrav1.VPCVolume{{Profile: "custom", SizeGiB: 20}}, }, wantError: true, }, { - name: "Invalid Iops", + name: "Missing SizeGiB for custom profile for Additional Volume", spec: infrav1.IBMVPCMachineSpec{ - BootVolume: &infrav1.VPCVolume{Iops: 1234, Profile: "general-purpose"}, + AdditionalVolumes: []*infrav1.VPCVolume{{Profile: "custom", Iops: 4000}}, }, wantError: true, }, { - name: "Missing Iops for custom profile", + name: "Valid iops and sizeGiB for custom profile", spec: infrav1.IBMVPCMachineSpec{ - BootVolume: &infrav1.VPCVolume{Profile: "general-purpose"}, + AdditionalVolumes: []*infrav1.VPCVolume{{Profile: "custom", SizeGiB: 25, Iops: 4000}}, }, - wantError: true, + wantError: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := validateBootVolume(tt.spec); (err != nil) != tt.wantError { + if err := validateVolumes(tt.spec); (err != nil) != tt.wantError { t.Errorf("validateBootVolume() = %v, wantError %v", err, tt.wantError) } }) diff --git a/internal/webhooks/ibmvpcmachine.go b/internal/webhooks/ibmvpcmachine.go index 6d124c45d..956ef290d 100644 --- a/internal/webhooks/ibmvpcmachine.go +++ b/internal/webhooks/ibmvpcmachine.go @@ -80,5 +80,5 @@ func (r *IBMVPCMachine) ValidateDelete(_ context.Context, _ runtime.Object) (adm } func validateIBMVPCMachineBootVolume(spec infrav1.IBMVPCMachineSpec) field.ErrorList { - return validateBootVolume(spec) + return validateVolumes(spec) } diff --git a/pkg/cloud/services/vpc/mock/vpc_generated.go b/pkg/cloud/services/vpc/mock/vpc_generated.go index 5d8308830..372ffd6ac 100644 --- a/pkg/cloud/services/vpc/mock/vpc_generated.go +++ b/pkg/cloud/services/vpc/mock/vpc_generated.go @@ -56,6 +56,22 @@ func (m *MockVpc) EXPECT() *MockVpcMockRecorder { return m.recorder } +// AttachVolumeToInstance mocks base method. +func (m *MockVpc) AttachVolumeToInstance(options *vpcv1.CreateInstanceVolumeAttachmentOptions) (*vpcv1.VolumeAttachment, *core.DetailedResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AttachVolumeToInstance", options) + ret0, _ := ret[0].(*vpcv1.VolumeAttachment) + ret1, _ := ret[1].(*core.DetailedResponse) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// AttachVolumeToInstance indicates an expected call of AttachVolumeToInstance. +func (mr *MockVpcMockRecorder) AttachVolumeToInstance(options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AttachVolumeToInstance", reflect.TypeOf((*MockVpc)(nil).AttachVolumeToInstance), options) +} + // CreateImage mocks base method. func (m *MockVpc) CreateImage(options *vpcv1.CreateImageOptions) (*vpcv1.Image, *core.DetailedResponse, error) { m.ctrl.T.Helper() @@ -200,6 +216,22 @@ func (mr *MockVpcMockRecorder) CreateVPC(options any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateVPC", reflect.TypeOf((*MockVpc)(nil).CreateVPC), options) } +// CreateVolume mocks base method. +func (m *MockVpc) CreateVolume(options *vpcv1.CreateVolumeOptions) (*vpcv1.Volume, *core.DetailedResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateVolume", options) + ret0, _ := ret[0].(*vpcv1.Volume) + ret1, _ := ret[1].(*core.DetailedResponse) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// CreateVolume indicates an expected call of CreateVolume. +func (mr *MockVpcMockRecorder) CreateVolume(options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateVolume", reflect.TypeOf((*MockVpc)(nil).CreateVolume), options) +} + // DeleteInstance mocks base method. func (m *MockVpc) DeleteInstance(options *vpcv1.DeleteInstanceOptions) (*core.DetailedResponse, error) { m.ctrl.T.Helper() @@ -600,6 +632,38 @@ func (mr *MockVpcMockRecorder) GetVPCZonesByRegion(region any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVPCZonesByRegion", reflect.TypeOf((*MockVpc)(nil).GetVPCZonesByRegion), region) } +// GetVolume mocks base method. +func (m *MockVpc) GetVolume(options *vpcv1.GetVolumeOptions) (*vpcv1.Volume, *core.DetailedResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetVolume", options) + ret0, _ := ret[0].(*vpcv1.Volume) + ret1, _ := ret[1].(*core.DetailedResponse) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetVolume indicates an expected call of GetVolume. +func (mr *MockVpcMockRecorder) GetVolume(options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVolume", reflect.TypeOf((*MockVpc)(nil).GetVolume), options) +} + +// GetVolumeAttachments mocks base method. +func (m *MockVpc) GetVolumeAttachments(options *vpcv1.ListInstanceVolumeAttachmentsOptions) (*vpcv1.VolumeAttachmentCollection, *core.DetailedResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetVolumeAttachments", options) + ret0, _ := ret[0].(*vpcv1.VolumeAttachmentCollection) + ret1, _ := ret[1].(*core.DetailedResponse) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetVolumeAttachments indicates an expected call of GetVolumeAttachments. +func (mr *MockVpcMockRecorder) GetVolumeAttachments(options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVolumeAttachments", reflect.TypeOf((*MockVpc)(nil).GetVolumeAttachments), options) +} + // ListImages mocks base method. func (m *MockVpc) ListImages(options *vpcv1.ListImagesOptions) (*vpcv1.ImageCollection, *core.DetailedResponse, error) { m.ctrl.T.Helper() diff --git a/pkg/cloud/services/vpc/service.go b/pkg/cloud/services/vpc/service.go index f980c6612..330b4d157 100644 --- a/pkg/cloud/services/vpc/service.go +++ b/pkg/cloud/services/vpc/service.go @@ -526,6 +526,26 @@ func (s *Service) GetVPCZonesByRegion(region string) ([]string, error) { return zones, nil } +// GetVolumeAttachments returns the volumeattachments for the instance. +func (s *Service) GetVolumeAttachments(options *vpcv1.ListInstanceVolumeAttachmentsOptions) (*vpcv1.VolumeAttachmentCollection, *core.DetailedResponse, error) { + return s.vpcService.ListInstanceVolumeAttachments(options) +} + +// CreateVolume creates a volume. +func (s *Service) CreateVolume(options *vpcv1.CreateVolumeOptions) (*vpcv1.Volume, *core.DetailedResponse, error) { + return s.vpcService.CreateVolume(options) +} + +// AttachVolumeToInstance attaches the given volume to the instance. +func (s *Service) AttachVolumeToInstance(options *vpcv1.CreateInstanceVolumeAttachmentOptions) (*vpcv1.VolumeAttachment, *core.DetailedResponse, error) { + return s.vpcService.CreateInstanceVolumeAttachment(options) +} + +// GetVolume fetches the given volume's status. +func (s *Service) GetVolume(options *vpcv1.GetVolumeOptions) (result *vpcv1.Volume, response *core.DetailedResponse, err error) { + return s.vpcService.GetVolume(options) +} + // NewService returns a new VPC Service. func NewService(svcEndpoint string) (Vpc, error) { service := &Service{} diff --git a/pkg/cloud/services/vpc/vpc.go b/pkg/cloud/services/vpc/vpc.go index 5c3d0fcd9..68ebfc75a 100644 --- a/pkg/cloud/services/vpc/vpc.go +++ b/pkg/cloud/services/vpc/vpc.go @@ -73,4 +73,8 @@ type Vpc interface { GetSecurityGroupRule(options *vpcv1.GetSecurityGroupRuleOptions) (vpcv1.SecurityGroupRuleIntf, *core.DetailedResponse, error) ListSecurityGroupRules(options *vpcv1.ListSecurityGroupRulesOptions) (*vpcv1.SecurityGroupRuleCollection, *core.DetailedResponse, error) GetVPCZonesByRegion(region string) ([]string, error) + CreateVolume(options *vpcv1.CreateVolumeOptions) (*vpcv1.Volume, *core.DetailedResponse, error) + AttachVolumeToInstance(options *vpcv1.CreateInstanceVolumeAttachmentOptions) (*vpcv1.VolumeAttachment, *core.DetailedResponse, error) + GetVolumeAttachments(options *vpcv1.ListInstanceVolumeAttachmentsOptions) (result *vpcv1.VolumeAttachmentCollection, response *core.DetailedResponse, err error) + GetVolume(options *vpcv1.GetVolumeOptions) (result *vpcv1.Volume, response *core.DetailedResponse, err error) }