Skip to content

Commit 1f3ceed

Browse files
Added support for additionalVolumes in VPCMachines
1 parent 7640740 commit 1f3ceed

16 files changed

+464
-23
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/common.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package v1beta2
1818

1919
import (
20+
"fmt"
2021
"strconv"
2122

2223
"k8s.io/apimachinery/pkg/util/intstr"
@@ -80,8 +81,23 @@ func defaultIBMVPCMachineSpec(spec *IBMVPCMachineSpec) {
8081
}
8182
}
8283

83-
func validateBootVolume(spec IBMVPCMachineSpec) field.ErrorList {
84+
func validateVolumes(spec IBMVPCMachineSpec) field.ErrorList {
8485
var allErrs field.ErrorList
86+
const customProfile = "custom"
87+
88+
for i := range spec.AdditionalVolumes {
89+
if spec.AdditionalVolumes[i].Profile == customProfile {
90+
if spec.AdditionalVolumes[i].Iops == 0 {
91+
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` "))
92+
}
93+
if spec.AdditionalVolumes[i].SizeGiB == 0 {
94+
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` "))
95+
}
96+
}
97+
if spec.AdditionalVolumes[i].Iops != 0 && spec.AdditionalVolumes[i].Profile != customProfile {
98+
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`"))
99+
}
100+
}
85101

86102
if spec.BootVolume == nil {
87103
return allErrs
@@ -91,7 +107,7 @@ func validateBootVolume(spec IBMVPCMachineSpec) field.ErrorList {
91107
allErrs = append(allErrs, field.Invalid(field.NewPath("spec.bootVolume.sizeGiB"), spec, "valid Boot VPCVolume size is 10 - 250 GB"))
92108
}
93109

94-
if spec.BootVolume.Iops != 0 && spec.BootVolume.Profile != "custom" {
110+
if spec.BootVolume.Iops != 0 && spec.BootVolume.Profile != customProfile {
95111
allErrs = append(allErrs, field.Invalid(field.NewPath("spec.bootVolume.iops"), spec, "iops applicable only to volumes using a profile of type `custom`"))
96112
}
97113

api/v1beta2/common_test.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -110,58 +110,58 @@ func TestValidateIBMPowerVSProcessorValues(t *testing.T) {
110110
}
111111
}
112112

113-
func Test_validateBootVolume(t *testing.T) {
113+
func Test_validateVolumes(t *testing.T) {
114114
tests := []struct {
115115
name string
116116
spec IBMVPCMachineSpec
117117
wantError bool
118118
}{
119119
{
120-
name: "Nil bootvolume",
120+
name: "Nil bootvolume for Boot Volume",
121121
spec: IBMVPCMachineSpec{
122122
BootVolume: nil,
123123
},
124124
wantError: false,
125125
},
126126
{
127-
name: "valid sizeGiB",
127+
name: "valid sizeGiB for Boot Volume",
128128
spec: IBMVPCMachineSpec{
129129
BootVolume: &VPCVolume{SizeGiB: 20},
130130
},
131131
wantError: false,
132132
},
133133
{
134-
name: "Invalid sizeGiB",
134+
name: "Invalid sizeGiB for Boot Volume",
135135
spec: IBMVPCMachineSpec{
136136
BootVolume: &VPCVolume{SizeGiB: 1},
137137
},
138138
wantError: true,
139139
},
140140
{
141-
name: "Valid Iops",
141+
name: "Missing Iops for custom profile for Additional Volume",
142142
spec: IBMVPCMachineSpec{
143-
BootVolume: &VPCVolume{Iops: 1000, Profile: "custom"},
143+
AdditionalVolumes: []*VPCVolume{{Profile: "custom", SizeGiB: 20}},
144144
},
145145
wantError: true,
146146
},
147147
{
148-
name: "Invalid Iops",
148+
name: "Missing SizeGiB for custom profile for Additional Volume",
149149
spec: IBMVPCMachineSpec{
150-
BootVolume: &VPCVolume{Iops: 1234, Profile: "general-purpose"},
150+
AdditionalVolumes: []*VPCVolume{{Profile: "custom", Iops: 4000}},
151151
},
152152
wantError: true,
153153
},
154154
{
155-
name: "Missing Iops for custom profile",
155+
name: "Valid iops and sizeGiB for custom profile",
156156
spec: IBMVPCMachineSpec{
157-
BootVolume: &VPCVolume{Profile: "general-purpose"},
157+
AdditionalVolumes: []*VPCVolume{{Profile: "custom", SizeGiB: 25, Iops: 4000}},
158158
},
159-
wantError: true,
159+
wantError: false,
160160
},
161161
}
162162
for _, tt := range tests {
163163
t.Run(tt.name, func(t *testing.T) {
164-
if err := validateBootVolume(tt.spec); (err != nil) != tt.wantError {
164+
if err := validateVolumes(tt.spec); (err != nil) != tt.wantError {
165165
t.Errorf("validateBootVolume() = %v, wantError %v", err, tt.wantError)
166166
}
167167
})

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/ibmvpcmachine_webhook.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,5 +73,5 @@ func (r *IBMVPCMachine) ValidateDelete() (admission.Warnings, error) {
7373
}
7474

7575
func (r *IBMVPCMachine) validateIBMVPCMachineBootVolume() field.ErrorList {
76-
return validateBootVolume(r.Spec)
76+
return validateVolumes(r.Spec)
7777
}

api/v1beta2/ibmvpcmachinetemplate_webhook.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,5 @@ func (r *IBMVPCMachineTemplate) ValidateDelete() (admission.Warnings, error) {
7272
}
7373

7474
func (r *IBMVPCMachineTemplate) validateIBMVPCMachineBootVolume() field.ErrorList {
75-
return validateBootVolume(r.Spec.Template.Spec)
75+
return validateVolumes(r.Spec.Template.Spec)
7676
}

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: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,3 +1155,60 @@ func (m *MachineScope) APIServerPort() int32 {
11551155
}
11561156
return infrav1beta2.DefaultAPIServerPort
11571157
}
1158+
1159+
// GetVolumeAttachments returns the volumeattachments 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, 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 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+
return err
1214+
}

cloud/scope/machine_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,3 +1096,118 @@ 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+
1112+
dummyVolumeAttachments := vpcv1.VolumeAttachmentCollection{
1113+
VolumeAttachments: []vpcv1.VolumeAttachment{{
1114+
Name: new(string),
1115+
},
1116+
{
1117+
Name: new(string),
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(&dummyVolumeAttachments, nil, nil)
1128+
attachments, err := scope.GetVolumeAttachments()
1129+
g.Expect(attachments).To(Equal(dummyVolumeAttachments.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 := infrav1beta2.IBMVPCMachine{
1156+
Status: infrav1beta2.IBMVPCMachineStatus{
1157+
InstanceID: "foo-instance-id",
1158+
},
1159+
}
1160+
1161+
infraVolume := infrav1beta2.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+
}

0 commit comments

Comments
 (0)