From 1c956d2ef6dc50f386d2170a0def62fef45da23a Mon Sep 17 00:00:00 2001 From: Mario Nitchev Date: Mon, 29 Apr 2024 11:12:26 +0300 Subject: [PATCH] Add non root volumes to AWSMachinePool --- ...ture.cluster.x-k8s.io_awsmachinepools.yaml | 44 +++++++++++++++++++ ...uster.x-k8s.io_awsmanagedmachinepools.yaml | 44 +++++++++++++++++++ exp/api/v1beta1/conversion.go | 7 ++- exp/api/v1beta1/zz_generated.conversion.go | 1 + exp/api/v1beta2/awsmachinepool_webhook.go | 26 +++++++++++ exp/api/v1beta2/types.go | 4 ++ exp/api/v1beta2/zz_generated.deepcopy.go | 7 +++ pkg/cloud/services/ec2/launchtemplate.go | 17 +++++-- 8 files changed, 145 insertions(+), 5 deletions(-) diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinepools.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinepools.yaml index 7b6acd1ccc..e28522622c 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinepools.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinepools.yaml @@ -738,6 +738,50 @@ spec: name: description: The name of the launch template. type: string + nonRootVolumes: + description: Configuration options for the non root storage volumes. + items: + description: Volume encapsulates the configuration options for + the storage device. + properties: + deviceName: + description: Device name + type: string + encrypted: + description: Encrypted is whether the volume should be encrypted + or not. + type: boolean + encryptionKey: + description: |- + EncryptionKey is the KMS key to use to encrypt the volume. Can be either a KMS key ID or ARN. + If Encrypted is set and this is omitted, the default AWS key will be used. + The key must already exist and be accessible by the controller. + type: string + iops: + description: IOPS is the number of IOPS requested for the + disk. Not applicable to all types. + format: int64 + type: integer + size: + description: |- + Size specifies size (in Gi) of the storage device. + Must be greater than the image snapshot size or 8 (whichever is greater). + format: int64 + minimum: 8 + type: integer + throughput: + description: Throughput to provision in MiB/s supported + for the volume type. Not applicable to all types. + format: int64 + type: integer + type: + description: Type is the type of the volume (e.g. gp2, io1, + etc...). + type: string + required: + - size + type: object + type: array privateDnsName: description: PrivateDNSName is the options for the instance hostname. properties: diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedmachinepools.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedmachinepools.yaml index 1914b742c8..31ce2b64e4 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedmachinepools.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedmachinepools.yaml @@ -730,6 +730,50 @@ spec: name: description: The name of the launch template. type: string + nonRootVolumes: + description: Configuration options for the non root storage volumes. + items: + description: Volume encapsulates the configuration options for + the storage device. + properties: + deviceName: + description: Device name + type: string + encrypted: + description: Encrypted is whether the volume should be encrypted + or not. + type: boolean + encryptionKey: + description: |- + EncryptionKey is the KMS key to use to encrypt the volume. Can be either a KMS key ID or ARN. + If Encrypted is set and this is omitted, the default AWS key will be used. + The key must already exist and be accessible by the controller. + type: string + iops: + description: IOPS is the number of IOPS requested for the + disk. Not applicable to all types. + format: int64 + type: integer + size: + description: |- + Size specifies size (in Gi) of the storage device. + Must be greater than the image snapshot size or 8 (whichever is greater). + format: int64 + minimum: 8 + type: integer + throughput: + description: Throughput to provision in MiB/s supported + for the volume type. Not applicable to all types. + format: int64 + type: integer + type: + description: Type is the type of the volume (e.g. gp2, io1, + etc...). + type: string + required: + - size + type: object + type: array privateDnsName: description: PrivateDNSName is the options for the instance hostname. properties: diff --git a/exp/api/v1beta1/conversion.go b/exp/api/v1beta1/conversion.go index 16cf651fdf..50a62f6bb4 100644 --- a/exp/api/v1beta1/conversion.go +++ b/exp/api/v1beta1/conversion.go @@ -18,11 +18,12 @@ package v1beta1 import ( apiconversion "k8s.io/apimachinery/pkg/conversion" + utilconversion "sigs.k8s.io/cluster-api/util/conversion" + "sigs.k8s.io/controller-runtime/pkg/conversion" + infrav1beta1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta1" infrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2" infrav1exp "sigs.k8s.io/cluster-api-provider-aws/v2/exp/api/v1beta2" - utilconversion "sigs.k8s.io/cluster-api/util/conversion" - "sigs.k8s.io/controller-runtime/pkg/conversion" ) // ConvertTo converts the v1beta1 AWSMachinePool receiver to a v1beta2 AWSMachinePool. @@ -56,6 +57,7 @@ func (src *AWSMachinePool) ConvertTo(dstRaw conversion.Hub) error { } dst.Spec.DefaultInstanceWarmup = restored.Spec.DefaultInstanceWarmup + dst.Spec.AWSLaunchTemplate.NonRootVolumes = restored.Spec.AWSLaunchTemplate.NonRootVolumes return nil } @@ -101,6 +103,7 @@ func (src *AWSManagedMachinePool) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.AWSLaunchTemplate = restored.Spec.AWSLaunchTemplate } dst.Spec.AWSLaunchTemplate.InstanceMetadataOptions = restored.Spec.AWSLaunchTemplate.InstanceMetadataOptions + dst.Spec.AWSLaunchTemplate.NonRootVolumes = restored.Spec.AWSLaunchTemplate.NonRootVolumes if restored.Spec.AWSLaunchTemplate.PrivateDNSName != nil { dst.Spec.AWSLaunchTemplate.PrivateDNSName = restored.Spec.AWSLaunchTemplate.PrivateDNSName diff --git a/exp/api/v1beta1/zz_generated.conversion.go b/exp/api/v1beta1/zz_generated.conversion.go index 869a3c13d4..4f50a8c20f 100644 --- a/exp/api/v1beta1/zz_generated.conversion.go +++ b/exp/api/v1beta1/zz_generated.conversion.go @@ -402,6 +402,7 @@ func autoConvert_v1beta2_AWSLaunchTemplate_To_v1beta1_AWSLaunchTemplate(in *v1be out.ImageLookupBaseOS = in.ImageLookupBaseOS out.InstanceType = in.InstanceType out.RootVolume = (*apiv1beta2.Volume)(unsafe.Pointer(in.RootVolume)) + // WARNING: in.NonRootVolumes requires manual conversion: does not exist in peer-type out.SSHKeyName = (*string)(unsafe.Pointer(in.SSHKeyName)) out.VersionNumber = (*int64)(unsafe.Pointer(in.VersionNumber)) out.AdditionalSecurityGroups = *(*[]apiv1beta2.AWSResourceReference)(unsafe.Pointer(&in.AdditionalSecurityGroups)) diff --git a/exp/api/v1beta2/awsmachinepool_webhook.go b/exp/api/v1beta2/awsmachinepool_webhook.go index ab434ffb4b..541243c53f 100644 --- a/exp/api/v1beta2/awsmachinepool_webhook.go +++ b/exp/api/v1beta2/awsmachinepool_webhook.go @@ -82,6 +82,31 @@ func (r *AWSMachinePool) validateRootVolume() field.ErrorList { return allErrs } +func (r *AWSMachinePool) validateNonRootVolumes() field.ErrorList { + var allErrs field.ErrorList + + for _, volume := range r.Spec.AWSLaunchTemplate.NonRootVolumes { + if v1beta2.VolumeTypesProvisioned.Has(string(volume.Type)) && volume.IOPS == 0 { + allErrs = append(allErrs, field.Required(field.NewPath("spec.template.spec.nonRootVolumes.iops"), "iops required if type is 'io1' or 'io2'")) + } + + if volume.Throughput != nil { + if volume.Type != v1beta2.VolumeTypeGP3 { + allErrs = append(allErrs, field.Required(field.NewPath("spec.template.spec.nonRootVolumes.throughput"), "throughput is valid only for type 'gp3'")) + } + if *volume.Throughput < 0 { + allErrs = append(allErrs, field.Required(field.NewPath("spec.template.spec.nonRootVolumes.throughput"), "throughput must be nonnegative")) + } + } + + if volume.DeviceName == "" { + allErrs = append(allErrs, field.Required(field.NewPath("spec.template.spec.nonRootVolumes.deviceName"), "non root volume should have device name")) + } + } + + return allErrs +} + func (r *AWSMachinePool) validateSubnets() field.ErrorList { var allErrs field.ErrorList @@ -124,6 +149,7 @@ func (r *AWSMachinePool) ValidateCreate() (admission.Warnings, error) { allErrs = append(allErrs, r.validateDefaultCoolDown()...) allErrs = append(allErrs, r.validateRootVolume()...) + allErrs = append(allErrs, r.validateNonRootVolumes()...) allErrs = append(allErrs, r.Spec.AdditionalTags.Validate()...) allErrs = append(allErrs, r.validateSubnets()...) allErrs = append(allErrs, r.validateAdditionalSecurityGroups()...) diff --git a/exp/api/v1beta2/types.go b/exp/api/v1beta2/types.go index ef589c2951..0bc4009a2e 100644 --- a/exp/api/v1beta2/types.go +++ b/exp/api/v1beta2/types.go @@ -96,6 +96,10 @@ type AWSLaunchTemplate struct { // +optional RootVolume *infrav1.Volume `json:"rootVolume,omitempty"` + // Configuration options for the non root storage volumes. + // +optional + NonRootVolumes []infrav1.Volume `json:"nonRootVolumes,omitempty"` + // SSHKeyName is the name of the ssh key to attach to the instance. Valid values are empty string // (do not use SSH keys), a valid SSH key name, or omitted (use the default SSH key name) // +optional diff --git a/exp/api/v1beta2/zz_generated.deepcopy.go b/exp/api/v1beta2/zz_generated.deepcopy.go index a916ebc059..7048d53196 100644 --- a/exp/api/v1beta2/zz_generated.deepcopy.go +++ b/exp/api/v1beta2/zz_generated.deepcopy.go @@ -96,6 +96,13 @@ func (in *AWSLaunchTemplate) DeepCopyInto(out *AWSLaunchTemplate) { *out = new(apiv1beta2.Volume) (*in).DeepCopyInto(*out) } + if in.NonRootVolumes != nil { + in, out := &in.NonRootVolumes, &out.NonRootVolumes + *out = make([]apiv1beta2.Volume, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.SSHKeyName != nil { in, out := &in.SSHKeyName, &out.SSHKeyName *out = new(string) diff --git a/pkg/cloud/services/ec2/launchtemplate.go b/pkg/cloud/services/ec2/launchtemplate.go index bb04605475..d747c17a6c 100644 --- a/pkg/cloud/services/ec2/launchtemplate.go +++ b/pkg/cloud/services/ec2/launchtemplate.go @@ -520,6 +520,8 @@ func (s *Service) createLaunchTemplateData(scope scope.LaunchTemplateScope, imag data.InstanceMarketOptions = getLaunchTemplateInstanceMarketOptionsRequest(scope.GetLaunchTemplate().SpotMarketOptions) data.PrivateDnsNameOptions = getLaunchTemplatePrivateDNSNameOptionsRequest(scope.GetLaunchTemplate().PrivateDNSName) + blockDeviceMappings := []*ec2.LaunchTemplateBlockDeviceMappingRequest{} + // Set up root volume if lt.RootVolume != nil { rootDeviceName, err := s.checkRootVolume(lt.RootVolume, *data.ImageId) @@ -530,9 +532,18 @@ func (s *Service) createLaunchTemplateData(scope scope.LaunchTemplateScope, imag lt.RootVolume.DeviceName = aws.StringValue(rootDeviceName) req := volumeToLaunchTemplateBlockDeviceMappingRequest(lt.RootVolume) - data.BlockDeviceMappings = []*ec2.LaunchTemplateBlockDeviceMappingRequest{ - req, - } + blockDeviceMappings = append(blockDeviceMappings, req) + } + + for vi := range lt.NonRootVolumes { + nonRootVolume := lt.NonRootVolumes[vi] + + blockDeviceMapping := volumeToLaunchTemplateBlockDeviceMappingRequest(&nonRootVolume) + blockDeviceMappings = append(blockDeviceMappings, blockDeviceMapping) + } + + if len(blockDeviceMappings) > 0 { + data.BlockDeviceMappings = blockDeviceMappings } data.TagSpecifications = s.buildLaunchTemplateTagSpecificationRequest(scope, userDataSecretKey)