Skip to content

Commit ae6fd3f

Browse files
Added support for additionalVolumes in VPCMachines (#2275)
1 parent ff2e9fa commit ae6fd3f

18 files changed

+567
-22
lines changed

api/v1beta1/zz_generated.conversion.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/v1beta2/ibmvpcmachine_types.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,14 @@ type IBMVPCMachineSpec struct {
7878
// SSHKeys is the SSH pub keys that will be used to access VM.
7979
// ID will take higher precedence over Name if both specified.
8080
SSHKeys []*IBMVPCResourceReference `json:"sshKeys,omitempty"`
81+
82+
// additionalVolumes is the list of additional volumes attached to the instance
83+
// There is a hard limit of 12 volume attachments per instance:
84+
// https://cloud.ibm.com/docs/vpc?topic=vpc-attaching-block-storage&interface=api#vol-attach-limits
85+
// +kubebuilder:validation:Optional
86+
// +kubebuilder:validation:MaxItems=12
87+
// +kubebuilder:validation:XValidation:rule="oldSelf.all(x, x in self)",message="Values may only be added"
88+
AdditionalVolumes []*VPCVolume `json:"additionalVolumes,omitempty"`
8189
}
8290

8391
// IBMVPCResourceReference is a reference to a specific VPC resource by ID or Name
@@ -95,7 +103,7 @@ type IBMVPCResourceReference struct {
95103
Name *string `json:"name,omitempty"`
96104
}
97105

98-
// VPCVolume defines the volume information for the instance.
106+
// VPCVolume defines the volume information.
99107
type VPCVolume struct {
100108
// DeleteVolumeOnInstanceDelete If set to true, when deleting the instance the volume will also be deleted.
101109
// Default is set as true
@@ -108,14 +116,15 @@ type VPCVolume struct {
108116
// +optional
109117
Name string `json:"name,omitempty"`
110118

111-
// SizeGiB is the size of the virtual server's boot disk in GiB.
119+
// SizeGiB is the size of the virtual server's disk in GiB.
112120
// Default to the size of the image's `minimum_provisioned_size`.
113121
// +optional
114122
SizeGiB int64 `json:"sizeGiB,omitempty"`
115123

116-
// Profile is the volume profile for the bootdisk, refer https://cloud.ibm.com/docs/vpc?topic=vpc-block-storage-profiles
124+
// Profile is the volume profile for the disk, refer https://cloud.ibm.com/docs/vpc?topic=vpc-block-storage-profiles
117125
// for more information.
118126
// Default to general-purpose
127+
// NOTE: If a profile other than custom is specified, the Iops and SizeGiB fields will be ignored
119128
// +kubebuilder:validation:Enum="general-purpose";"5iops-tier";"10iops-tier";"custom"
120129
// +kubebuilder:default=general-purpose
121130
// +optional

api/v1beta2/zz_generated.deepcopy.go

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cloud/scope/machine.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,3 +1155,63 @@ func (m *MachineScope) APIServerPort() int32 {
11551155
}
11561156
return infrav1beta2.DefaultAPIServerPort
11571157
}
1158+
1159+
// GetVolumeAttachments returns the volume attachments for the instance.
1160+
func (m *MachineScope) GetVolumeAttachments() ([]vpcv1.VolumeAttachment, error) {
1161+
options := vpcv1.ListInstanceVolumeAttachmentsOptions{
1162+
InstanceID: &m.IBMVPCMachine.Status.InstanceID,
1163+
}
1164+
result, _, err := m.IBMVPCClient.GetVolumeAttachments(&options)
1165+
if err != nil {
1166+
return nil, fmt.Errorf("error while getting volume attachments: %w", err)
1167+
}
1168+
return result.VolumeAttachments, nil
1169+
}
1170+
1171+
// CreateAndAttachVolume creates a new Volume and attaches it to the instance.
1172+
func (m *MachineScope) CreateAndAttachVolume(vpcVolume *infrav1beta2.VPCVolume) error {
1173+
volumeOptions := vpcv1.CreateVolumeOptions{}
1174+
// TODO: EncryptionKeyCRN is not supported for now, the field is omitted from the manifest
1175+
if vpcVolume.Profile != "custom" {
1176+
volumeOptions.VolumePrototype = &vpcv1.VolumePrototype{
1177+
Profile: &vpcv1.VolumeProfileIdentityByName{
1178+
Name: &vpcVolume.Profile,
1179+
},
1180+
Zone: &vpcv1.ZoneIdentity{
1181+
Name: &m.IBMVPCMachine.Spec.Zone,
1182+
},
1183+
Capacity: &vpcVolume.SizeGiB,
1184+
}
1185+
} else {
1186+
volumeOptions.VolumePrototype = &vpcv1.VolumePrototype{
1187+
Iops: &vpcVolume.Iops,
1188+
Profile: &vpcv1.VolumeProfileIdentityByName{
1189+
Name: &vpcVolume.Profile,
1190+
},
1191+
Zone: &vpcv1.ZoneIdentity{
1192+
Name: &m.IBMVPCMachine.Spec.Zone,
1193+
},
1194+
Capacity: &vpcVolume.SizeGiB,
1195+
}
1196+
}
1197+
1198+
volumeResult, _, err := m.IBMVPCClient.CreateVolume(&volumeOptions)
1199+
if err != nil {
1200+
return fmt.Errorf("error while creating volume: %w", err)
1201+
}
1202+
1203+
attachmentOptions := vpcv1.CreateInstanceVolumeAttachmentOptions{
1204+
InstanceID: &m.IBMVPCMachine.Status.InstanceID,
1205+
Volume: &vpcv1.VolumeAttachmentPrototypeVolume{
1206+
ID: volumeResult.ID,
1207+
},
1208+
DeleteVolumeOnInstanceDelete: &vpcVolume.DeleteVolumeOnInstanceDelete,
1209+
Name: &vpcVolume.Name,
1210+
}
1211+
1212+
_, _, err = m.IBMVPCClient.AttachVolumeToInstance(&attachmentOptions)
1213+
if err != nil {
1214+
err = fmt.Errorf("error while attaching volume to instance: %w", err)
1215+
}
1216+
return err
1217+
}

cloud/scope/machine_test.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,3 +1096,120 @@ func TestDeleteVPCLoadBalancerPoolMember(t *testing.T) {
10961096
})
10971097
})
10981098
}
1099+
1100+
func TestGetVolumeAttachments(t *testing.T) {
1101+
setup := func(t *testing.T) (*gomock.Controller, *mock.MockVpc) {
1102+
t.Helper()
1103+
return gomock.NewController(t), mock.NewMockVpc(gomock.NewController(t))
1104+
}
1105+
1106+
vpcMachine := infrav1beta2.IBMVPCMachine{
1107+
Status: infrav1beta2.IBMVPCMachineStatus{
1108+
InstanceID: "foo-instance-id",
1109+
},
1110+
}
1111+
volumeAttachmentName := "foo-volume-attachment"
1112+
volumeName := "foo-volume"
1113+
1114+
testVolumeAttachments := vpcv1.VolumeAttachmentCollection{
1115+
VolumeAttachments: []vpcv1.VolumeAttachment{{
1116+
Name: &volumeAttachmentName,
1117+
},
1118+
{
1119+
Name: &volumeName,
1120+
}},
1121+
}
1122+
1123+
t.Run("Return List of Volume Attachments for Machine", func(t *testing.T) {
1124+
g := NewWithT(t)
1125+
mockController, mockVPC := setup(t)
1126+
t.Cleanup(mockController.Finish)
1127+
scope := setupMachineScope(clusterName, machineName, mockVPC)
1128+
scope.IBMVPCMachine.Status = vpcMachine.Status
1129+
mockVPC.EXPECT().GetVolumeAttachments(gomock.AssignableToTypeOf(&vpcv1.ListInstanceVolumeAttachmentsOptions{})).Return(&testVolumeAttachments, nil, nil)
1130+
attachments, err := scope.GetVolumeAttachments()
1131+
g.Expect(attachments).To(Equal(testVolumeAttachments.VolumeAttachments))
1132+
g.Expect(err).Should(Succeed())
1133+
})
1134+
1135+
t.Run("Return Error when GetVolumeAttachments fails", func(t *testing.T) {
1136+
g := NewWithT(t)
1137+
mockController, mockVPC := setup(t)
1138+
t.Cleanup(mockController.Finish)
1139+
scope := setupMachineScope(clusterName, machineName, mockVPC)
1140+
scope.IBMVPCMachine.Status = vpcMachine.Status
1141+
mockVPC.EXPECT().GetVolumeAttachments(gomock.AssignableToTypeOf(&vpcv1.ListInstanceVolumeAttachmentsOptions{})).Return(nil, nil, errors.New("Error when getting volume attachments"))
1142+
attachments, err := scope.GetVolumeAttachments()
1143+
g.Expect(attachments).To(BeNil())
1144+
g.Expect(err).ShouldNot(Succeed())
1145+
})
1146+
}
1147+
1148+
func TestCreateAndAttachVolume(t *testing.T) {
1149+
setup := func(t *testing.T) (*gomock.Controller, *mock.MockVpc) {
1150+
t.Helper()
1151+
return gomock.NewController(t), mock.NewMockVpc(gomock.NewController(t))
1152+
}
1153+
1154+
volumeName := "foo-volume"
1155+
volumeID := "foo-volume-id"
1156+
1157+
vpcMachine := infrav1beta2.IBMVPCMachine{
1158+
Status: infrav1beta2.IBMVPCMachineStatus{
1159+
InstanceID: "foo-instance-id",
1160+
},
1161+
}
1162+
1163+
infraVolume := infrav1beta2.VPCVolume{
1164+
Name: volumeName,
1165+
}
1166+
1167+
vpcVolume := vpcv1.Volume{
1168+
Name: &volumeName,
1169+
ID: &volumeID,
1170+
}
1171+
1172+
volumeCreationError := errors.New("error while creating volume")
1173+
1174+
volumeAttachmentError := errors.New("error while attaching volume")
1175+
1176+
t.Run("Volume creation and attachment is successful", func(t *testing.T) {
1177+
g := NewWithT(t)
1178+
mockController, mockVPC := setup(t)
1179+
t.Cleanup(mockController.Finish)
1180+
scope := setupMachineScope(clusterName, machineName, mockVPC)
1181+
scope.IBMVPCMachine.Status = vpcMachine.Status
1182+
mockVPC.EXPECT().CreateVolume(gomock.AssignableToTypeOf(&vpcv1.CreateVolumeOptions{})).Return(&vpcVolume, nil, nil)
1183+
mockVPC.EXPECT().AttachVolumeToInstance(gomock.AssignableToTypeOf(&vpcv1.CreateInstanceVolumeAttachmentOptions{})).Return(nil, nil, nil)
1184+
1185+
err := scope.CreateAndAttachVolume(&infraVolume)
1186+
g.Expect(err).Should(Succeed())
1187+
})
1188+
1189+
t.Run("Volume Creation Fails", func(t *testing.T) {
1190+
g := NewWithT(t)
1191+
mockController, mockVPC := setup(t)
1192+
t.Cleanup(mockController.Finish)
1193+
scope := setupMachineScope(clusterName, machineName, mockVPC)
1194+
scope.IBMVPCMachine.Status = vpcMachine.Status
1195+
mockVPC.EXPECT().CreateVolume(gomock.AssignableToTypeOf(&vpcv1.CreateVolumeOptions{})).Return(nil, nil, volumeCreationError)
1196+
1197+
err := scope.CreateAndAttachVolume(&infraVolume)
1198+
g.Expect(err).ShouldNot(Succeed())
1199+
g.Expect(errors.Is(err, volumeCreationError)).To(BeTrue())
1200+
})
1201+
1202+
t.Run("Volume Attachment Fails", func(t *testing.T) {
1203+
g := NewWithT(t)
1204+
mockController, mockVPC := setup(t)
1205+
t.Cleanup(mockController.Finish)
1206+
scope := setupMachineScope(clusterName, machineName, mockVPC)
1207+
scope.IBMVPCMachine.Status = vpcMachine.Status
1208+
mockVPC.EXPECT().CreateVolume(gomock.AssignableToTypeOf(&vpcv1.CreateVolumeOptions{})).Return(&vpcVolume, nil, nil)
1209+
mockVPC.EXPECT().AttachVolumeToInstance(gomock.AssignableToTypeOf(&vpcv1.CreateInstanceVolumeAttachmentOptions{})).Return(nil, nil, volumeAttachmentError)
1210+
1211+
err := scope.CreateAndAttachVolume(&infraVolume)
1212+
g.Expect(err).ShouldNot(Succeed())
1213+
g.Expect(errors.Is(err, volumeAttachmentError)).To(BeTrue())
1214+
})
1215+
}

config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcmachines.yaml

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,65 @@ spec:
206206
spec:
207207
description: IBMVPCMachineSpec defines the desired state of IBMVPCMachine.
208208
properties:
209+
additionalVolumes:
210+
description: |-
211+
additionalVolumes is the list of additional volumes attached to the instance
212+
There is a hard limit of 12 volume attachments per instance:
213+
https://cloud.ibm.com/docs/vpc?topic=vpc-attaching-block-storage&interface=api#vol-attach-limits
214+
items:
215+
description: VPCVolume defines the volume information.
216+
properties:
217+
deleteVolumeOnInstanceDelete:
218+
default: true
219+
description: |-
220+
DeleteVolumeOnInstanceDelete If set to true, when deleting the instance the volume will also be deleted.
221+
Default is set as true
222+
type: boolean
223+
encryptionKeyCRN:
224+
description: |-
225+
EncryptionKey is the root key to use to wrap the data encryption key for the volume and this points to the CRN
226+
and possible values are as follows.
227+
The CRN of the [Key Protect Root
228+
Key](https://cloud.ibm.com/docs/key-protect?topic=key-protect-getting-started-tutorial) or [Hyper Protect Crypto
229+
Service Root Key](https://cloud.ibm.com/docs/hs-crypto?topic=hs-crypto-get-started) for this resource.
230+
If unspecified, the `encryption` type for the volume will be `provider_managed`.
231+
type: string
232+
iops:
233+
description: |-
234+
Iops is the maximum I/O operations per second (IOPS) to use for the volume. Applicable only to volumes using a profile
235+
family of `custom`.
236+
format: int64
237+
type: integer
238+
name:
239+
description: |-
240+
Name is the unique user-defined name for this volume.
241+
Default will be autogenerated
242+
type: string
243+
profile:
244+
default: general-purpose
245+
description: |-
246+
Profile is the volume profile for the disk, refer https://cloud.ibm.com/docs/vpc?topic=vpc-block-storage-profiles
247+
for more information.
248+
Default to general-purpose
249+
NOTE: If a profile other than custom is specified, the Iops and SizeGiB fields will be ignored
250+
enum:
251+
- general-purpose
252+
- 5iops-tier
253+
- 10iops-tier
254+
- custom
255+
type: string
256+
sizeGiB:
257+
description: |-
258+
SizeGiB is the size of the virtual server's disk in GiB.
259+
Default to the size of the image's `minimum_provisioned_size`.
260+
format: int64
261+
type: integer
262+
type: object
263+
maxItems: 12
264+
type: array
265+
x-kubernetes-validations:
266+
- message: Values may only be added
267+
rule: oldSelf.all(x, x in self)
209268
bootVolume:
210269
description: BootVolume contains machines's boot volume configurations
211270
like size, iops etc..
@@ -239,9 +298,10 @@ spec:
239298
profile:
240299
default: general-purpose
241300
description: |-
242-
Profile is the volume profile for the bootdisk, refer https://cloud.ibm.com/docs/vpc?topic=vpc-block-storage-profiles
301+
Profile is the volume profile for the disk, refer https://cloud.ibm.com/docs/vpc?topic=vpc-block-storage-profiles
243302
for more information.
244303
Default to general-purpose
304+
NOTE: If a profile other than custom is specified, the Iops and SizeGiB fields will be ignored
245305
enum:
246306
- general-purpose
247307
- 5iops-tier
@@ -250,7 +310,7 @@ spec:
250310
type: string
251311
sizeGiB:
252312
description: |-
253-
SizeGiB is the size of the virtual server's boot disk in GiB.
313+
SizeGiB is the size of the virtual server's disk in GiB.
254314
Default to the size of the image's `minimum_provisioned_size`.
255315
format: int64
256316
type: integer

0 commit comments

Comments
 (0)