Skip to content

Commit 3bdb3d7

Browse files
Readded support for additional disks (#2448)
* Readded support for additional-volumes, fixed race condition in additional-vol reconciliation This reverts commit 4f7eb65. * Fixed race condition in additional volumes reconcile
1 parent 059dd2f commit 3bdb3d7

19 files changed

+935
-30
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: 15 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
@@ -190,6 +199,9 @@ type IBMVPCMachineV1Beta2Status struct {
190199
// +listMapKey=type
191200
// +kubebuilder:validation:MaxItems=32
192201
Conditions []metav1.Condition `json:"conditions,omitempty"`
202+
// AdditionalVolumeIDs is a list of Volume IDs as per IBMCloud
203+
// +optional
204+
AdditionalVolumeIDs []string `json:"additionalVolumeIDs,omitempty"`
193205
}
194206

195207
// +kubebuilder:object:root=true

api/v1beta2/zz_generated.deepcopy.go

Lines changed: 16 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: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,3 +1162,80 @@ 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+
// GetVolumeState returns the volume's state.
1179+
func (m *MachineScope) GetVolumeState(volumeID string) (string, error) {
1180+
options := vpcv1.GetVolumeOptions{
1181+
ID: &volumeID,
1182+
}
1183+
result, _, err := m.IBMVPCClient.GetVolume(&options)
1184+
if err != nil {
1185+
return "", fmt.Errorf("could not fetch volume status: %w", err)
1186+
}
1187+
return *result.Status, err
1188+
}
1189+
1190+
// CreateVolume creates a new Volume and attaches it to the instance.
1191+
func (m *MachineScope) CreateVolume(vpcVolume *infrav1.VPCVolume) (string, error) {
1192+
volumeOptions := vpcv1.CreateVolumeOptions{}
1193+
var resourceGroupID string
1194+
if m.IBMVPCCluster.Status.ResourceGroup != nil {
1195+
resourceGroupID = m.IBMVPCCluster.Status.ResourceGroup.ID
1196+
} else {
1197+
resourceGroupID = m.IBMVPCCluster.Spec.ResourceGroup
1198+
}
1199+
// TODO: EncryptionKeyCRN is not supported for now, the field is omitted from the manifest
1200+
volumeOptions.VolumePrototype = &vpcv1.VolumePrototype{
1201+
ResourceGroup: &vpcv1.ResourceGroupIdentityByID{
1202+
ID: &resourceGroupID,
1203+
},
1204+
Profile: &vpcv1.VolumeProfileIdentityByName{
1205+
Name: &vpcVolume.Profile,
1206+
},
1207+
Zone: &vpcv1.ZoneIdentity{
1208+
Name: &m.IBMVPCMachine.Spec.Zone,
1209+
},
1210+
Capacity: &vpcVolume.SizeGiB,
1211+
}
1212+
1213+
if vpcVolume.Profile == "custom" {
1214+
volumeOptions.VolumePrototype.(*vpcv1.VolumePrototype).Iops = &vpcVolume.Iops
1215+
}
1216+
1217+
volumeResult, _, err := m.IBMVPCClient.CreateVolume(&volumeOptions)
1218+
if err != nil {
1219+
return "", fmt.Errorf("error while creating volume: %w", err)
1220+
}
1221+
1222+
return *volumeResult.ID, nil
1223+
}
1224+
1225+
// AttachVolume attaches the given volume to the instance.
1226+
func (m *MachineScope) AttachVolume(deleteOnInstanceDelete bool, volumeID, volumeName string) error {
1227+
attachmentOptions := vpcv1.CreateInstanceVolumeAttachmentOptions{
1228+
InstanceID: &m.IBMVPCMachine.Status.InstanceID,
1229+
Volume: &vpcv1.VolumeAttachmentPrototypeVolume{
1230+
ID: &volumeID,
1231+
},
1232+
DeleteVolumeOnInstanceDelete: &deleteOnInstanceDelete,
1233+
Name: &volumeName,
1234+
}
1235+
1236+
_, _, err := m.IBMVPCClient.AttachVolumeToInstance(&attachmentOptions)
1237+
if err != nil {
1238+
err = fmt.Errorf("error while attaching volume to instance: %w", err)
1239+
}
1240+
return err
1241+
}

cloud/scope/machine_test.go

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ import (
4242
. "github.com/onsi/gomega"
4343
)
4444

45+
var (
46+
volumeName = "foo-volume"
47+
volumeID = "foo-volume-id"
48+
)
49+
4550
func newVPCMachine(clusterName, machineName string) *infrav1.IBMVPCMachine {
4651
return &infrav1.IBMVPCMachine{
4752
ObjectMeta: metav1.ObjectMeta{
@@ -1094,3 +1099,183 @@ func TestDeleteVPCLoadBalancerPoolMember(t *testing.T) {
10941099
})
10951100
})
10961101
}
1102+
1103+
func TestGetVolumeAttachments(t *testing.T) {
1104+
setup := func(t *testing.T) (*gomock.Controller, *mock.MockVpc) {
1105+
t.Helper()
1106+
return gomock.NewController(t), mock.NewMockVpc(gomock.NewController(t))
1107+
}
1108+
1109+
vpcMachine := infrav1.IBMVPCMachine{
1110+
Status: infrav1.IBMVPCMachineStatus{
1111+
InstanceID: "foo-instance-id",
1112+
},
1113+
}
1114+
volumeAttachmentName := "foo-volume-attachment"
1115+
1116+
testVolumeAttachments := vpcv1.VolumeAttachmentCollection{
1117+
VolumeAttachments: []vpcv1.VolumeAttachment{{
1118+
Name: &volumeAttachmentName,
1119+
},
1120+
{
1121+
Name: &volumeName,
1122+
}},
1123+
}
1124+
1125+
t.Run("Return List of Volume Attachments for Machine", func(t *testing.T) {
1126+
g := NewWithT(t)
1127+
mockController, mockVPC := setup(t)
1128+
t.Cleanup(mockController.Finish)
1129+
scope := setupMachineScope(clusterName, machineName, mockVPC)
1130+
scope.IBMVPCMachine.Status = vpcMachine.Status
1131+
mockVPC.EXPECT().GetVolumeAttachments(gomock.AssignableToTypeOf(&vpcv1.ListInstanceVolumeAttachmentsOptions{})).Return(&testVolumeAttachments, nil, nil)
1132+
attachments, err := scope.GetVolumeAttachments()
1133+
g.Expect(attachments).To(Equal(testVolumeAttachments.VolumeAttachments))
1134+
g.Expect(err).Should(Succeed())
1135+
})
1136+
1137+
t.Run("Return Error when GetVolumeAttachments fails", func(t *testing.T) {
1138+
g := NewWithT(t)
1139+
mockController, mockVPC := setup(t)
1140+
t.Cleanup(mockController.Finish)
1141+
scope := setupMachineScope(clusterName, machineName, mockVPC)
1142+
scope.IBMVPCMachine.Status = vpcMachine.Status
1143+
mockVPC.EXPECT().GetVolumeAttachments(gomock.AssignableToTypeOf(&vpcv1.ListInstanceVolumeAttachmentsOptions{})).Return(nil, nil, errors.New("Error when getting volume attachments"))
1144+
attachments, err := scope.GetVolumeAttachments()
1145+
g.Expect(attachments).To(BeNil())
1146+
g.Expect(err).ShouldNot(Succeed())
1147+
})
1148+
}
1149+
1150+
func TestGetVolumeState(t *testing.T) {
1151+
setup := func(t *testing.T) (*gomock.Controller, *mock.MockVpc) {
1152+
t.Helper()
1153+
return gomock.NewController(t), mock.NewMockVpc(gomock.NewController(t))
1154+
}
1155+
1156+
volumeStatus := vpcv1.VolumeStatusPendingConst
1157+
1158+
vpcMachine := infrav1.IBMVPCMachine{
1159+
Status: infrav1.IBMVPCMachineStatus{
1160+
InstanceID: "foo-instance-id",
1161+
},
1162+
}
1163+
1164+
vpcVolume := vpcv1.Volume{
1165+
Name: &volumeName,
1166+
ID: &volumeID,
1167+
Status: &volumeStatus,
1168+
}
1169+
volumeFetchError := errors.New("error while fetching volume")
1170+
1171+
t.Run("Return correct volume state", func(t *testing.T) {
1172+
g := NewWithT(t)
1173+
mockController, mockVPC := setup(t)
1174+
t.Cleanup(mockController.Finish)
1175+
scope := setupMachineScope(clusterName, machineName, mockVPC)
1176+
scope.IBMVPCMachine.Status = vpcMachine.Status
1177+
mockVPC.EXPECT().GetVolume(gomock.AssignableToTypeOf(&vpcv1.GetVolumeOptions{})).Return(&vpcVolume, nil, nil)
1178+
state, err := scope.GetVolumeState(volumeID)
1179+
g.Expect(err).To(BeNil())
1180+
g.Expect(state).To(Equal(volumeStatus))
1181+
})
1182+
1183+
t.Run("Return error when GetVolumeState returns error", func(t *testing.T) {
1184+
g := NewWithT(t)
1185+
mockController, mockVPC := setup(t)
1186+
t.Cleanup(mockController.Finish)
1187+
scope := setupMachineScope(clusterName, machineName, mockVPC)
1188+
scope.IBMVPCMachine.Status = vpcMachine.Status
1189+
mockVPC.EXPECT().GetVolume(gomock.AssignableToTypeOf(&vpcv1.GetVolumeOptions{})).Return(nil, nil, volumeFetchError)
1190+
state, err := scope.GetVolumeState(volumeID)
1191+
g.Expect(state).To(BeZero())
1192+
g.Expect(errors.Is(err, volumeFetchError)).To(BeTrue())
1193+
})
1194+
}
1195+
1196+
func TestCreateVolume(t *testing.T) {
1197+
setup := func(t *testing.T) (*gomock.Controller, *mock.MockVpc) {
1198+
t.Helper()
1199+
return gomock.NewController(t), mock.NewMockVpc(gomock.NewController(t))
1200+
}
1201+
1202+
vpcMachine := infrav1.IBMVPCMachine{
1203+
Status: infrav1.IBMVPCMachineStatus{
1204+
InstanceID: "foo-instance-id",
1205+
},
1206+
}
1207+
1208+
infraVolume := infrav1.VPCVolume{
1209+
Name: volumeName,
1210+
Profile: "custom",
1211+
Iops: 100,
1212+
SizeGiB: 50,
1213+
}
1214+
pendingState := vpcv1.VolumeStatusPendingConst
1215+
1216+
vpcVolume := vpcv1.Volume{
1217+
Name: &volumeName,
1218+
ID: &volumeID,
1219+
Status: &pendingState,
1220+
}
1221+
1222+
volumeCreationError := errors.New("error while creating volume")
1223+
t.Run("Volume creation is successful", func(t *testing.T) {
1224+
g := NewWithT(t)
1225+
mockController, mockVPC := setup(t)
1226+
t.Cleanup(mockController.Finish)
1227+
scope := setupMachineScope(clusterName, machineName, mockVPC)
1228+
mockVPC.EXPECT().CreateVolume(gomock.AssignableToTypeOf(&vpcv1.CreateVolumeOptions{})).Return(&vpcVolume, nil, nil)
1229+
id, err := scope.CreateVolume(&infraVolume)
1230+
g.Expect(err).Should(Succeed())
1231+
g.Expect(id).Should(Equal(volumeID))
1232+
})
1233+
t.Run("Volume creation fails", func(t *testing.T) {
1234+
g := NewWithT(t)
1235+
mockController, mockVPC := setup(t)
1236+
t.Cleanup(mockController.Finish)
1237+
scope := setupMachineScope(clusterName, machineName, mockVPC)
1238+
scope.IBMVPCMachine.Status = vpcMachine.Status
1239+
mockVPC.EXPECT().CreateVolume(gomock.AssignableToTypeOf(&vpcv1.CreateVolumeOptions{})).Return(nil, nil, volumeCreationError)
1240+
id, err := scope.CreateVolume(&infraVolume)
1241+
g.Expect(err).ShouldNot(Succeed())
1242+
g.Expect(errors.Is(err, volumeCreationError)).To(BeTrue())
1243+
g.Expect(id).To(BeZero())
1244+
})
1245+
}
1246+
1247+
func TestAttachVolume(t *testing.T) {
1248+
setup := func(t *testing.T) (*gomock.Controller, *mock.MockVpc) {
1249+
t.Helper()
1250+
return gomock.NewController(t), mock.NewMockVpc(gomock.NewController(t))
1251+
}
1252+
1253+
deleteOnInstanceDelete := true
1254+
vpcMachine := infrav1.IBMVPCMachine{
1255+
Status: infrav1.IBMVPCMachineStatus{
1256+
InstanceID: "foo-instance-id",
1257+
},
1258+
}
1259+
volumeAttachmentError := errors.New("error while attaching volume")
1260+
t.Run("Volume attachment is successful", func(t *testing.T) {
1261+
g := NewWithT(t)
1262+
mockController, mockVPC := setup(t)
1263+
t.Cleanup(mockController.Finish)
1264+
scope := setupMachineScope(clusterName, machineName, mockVPC)
1265+
scope.IBMVPCMachine.Status = vpcMachine.Status
1266+
mockVPC.EXPECT().AttachVolumeToInstance(gomock.AssignableToTypeOf(&vpcv1.CreateInstanceVolumeAttachmentOptions{})).Return(nil, nil, nil)
1267+
err := scope.AttachVolume(deleteOnInstanceDelete, volumeID, volumeName)
1268+
g.Expect(err).Should(Succeed())
1269+
})
1270+
t.Run("Volume attachment fails", func(t *testing.T) {
1271+
g := NewWithT(t)
1272+
mockController, mockVPC := setup(t)
1273+
t.Cleanup(mockController.Finish)
1274+
scope := setupMachineScope(clusterName, machineName, mockVPC)
1275+
scope.IBMVPCMachine.Status = vpcMachine.Status
1276+
mockVPC.EXPECT().AttachVolumeToInstance(gomock.AssignableToTypeOf(&vpcv1.CreateInstanceVolumeAttachmentOptions{})).Return(nil, nil, volumeAttachmentError)
1277+
err := scope.AttachVolume(deleteOnInstanceDelete, volumeID, volumeName)
1278+
g.Expect(err).ShouldNot(Succeed())
1279+
g.Expect(errors.Is(err, volumeAttachmentError)).To(BeTrue())
1280+
})
1281+
}

0 commit comments

Comments
 (0)