Skip to content

Commit d1f5d05

Browse files
Readded support for additional-volumes, fixed race condition in additional-vol reconciliation
This reverts commit 4f7eb65.
1 parent 02f0946 commit d1f5d05

15 files changed

+540
-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
@@ -1162,3 +1162,63 @@ func (m *MachineScope) APIServerPort() int32 {
11621162
}
11631163
return infrav1.DefaultAPIServerPort
11641164
}
1165+
1166+
// GetVolumeAttachments returns the volume attachments for the instance.
1167+
func (m *MachineScope) GetVolumeAttachments() ([]vpcv1.VolumeAttachment, error) {
1168+
options := vpcv1.ListInstanceVolumeAttachmentsOptions{
1169+
InstanceID: &m.IBMVPCMachine.Status.InstanceID,
1170+
}
1171+
result, _, err := m.IBMVPCClient.GetVolumeAttachments(&options)
1172+
if err != nil {
1173+
return nil, fmt.Errorf("error while getting volume attachments: %w", err)
1174+
}
1175+
return result.VolumeAttachments, nil
1176+
}
1177+
1178+
// CreateAndAttachVolume creates a new Volume and attaches it to the instance.
1179+
func (m *MachineScope) CreateAndAttachVolume(vpcVolume *infrav1.VPCVolume) error {
1180+
volumeOptions := vpcv1.CreateVolumeOptions{}
1181+
// TODO: EncryptionKeyCRN is not supported for now, the field is omitted from the manifest
1182+
if vpcVolume.Profile != "custom" {
1183+
volumeOptions.VolumePrototype = &vpcv1.VolumePrototype{
1184+
Profile: &vpcv1.VolumeProfileIdentityByName{
1185+
Name: &vpcVolume.Profile,
1186+
},
1187+
Zone: &vpcv1.ZoneIdentity{
1188+
Name: &m.IBMVPCMachine.Spec.Zone,
1189+
},
1190+
Capacity: &vpcVolume.SizeGiB,
1191+
}
1192+
} else {
1193+
volumeOptions.VolumePrototype = &vpcv1.VolumePrototype{
1194+
Iops: &vpcVolume.Iops,
1195+
Profile: &vpcv1.VolumeProfileIdentityByName{
1196+
Name: &vpcVolume.Profile,
1197+
},
1198+
Zone: &vpcv1.ZoneIdentity{
1199+
Name: &m.IBMVPCMachine.Spec.Zone,
1200+
},
1201+
Capacity: &vpcVolume.SizeGiB,
1202+
}
1203+
}
1204+
1205+
volumeResult, _, err := m.IBMVPCClient.CreateVolume(&volumeOptions)
1206+
if err != nil {
1207+
return fmt.Errorf("error while creating volume: %w", err)
1208+
}
1209+
1210+
attachmentOptions := vpcv1.CreateInstanceVolumeAttachmentOptions{
1211+
InstanceID: &m.IBMVPCMachine.Status.InstanceID,
1212+
Volume: &vpcv1.VolumeAttachmentPrototypeVolume{
1213+
ID: volumeResult.ID,
1214+
},
1215+
DeleteVolumeOnInstanceDelete: &vpcVolume.DeleteVolumeOnInstanceDelete,
1216+
Name: &vpcVolume.Name,
1217+
}
1218+
1219+
_, _, err = m.IBMVPCClient.AttachVolumeToInstance(&attachmentOptions)
1220+
if err != nil {
1221+
err = fmt.Errorf("error while attaching volume to instance: %w", err)
1222+
}
1223+
return err
1224+
}

cloud/scope/machine_test.go

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

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)