diff --git a/api/v1beta1/awscluster_conversion.go b/api/v1beta1/awscluster_conversion.go index 33aff027e5..0e0d8eadde 100644 --- a/api/v1beta1/awscluster_conversion.go +++ b/api/v1beta1/awscluster_conversion.go @@ -65,6 +65,7 @@ func (src *AWSCluster) ConvertTo(dstRaw conversion.Hub) error { dst.Status.Bastion.MarketType = restored.Status.Bastion.MarketType dst.Status.Bastion.HostAffinity = restored.Status.Bastion.HostAffinity dst.Status.Bastion.HostID = restored.Status.Bastion.HostID + dst.Status.Bastion.CapacityReservationPreference = restored.Status.Bastion.CapacityReservationPreference } dst.Spec.Partition = restored.Spec.Partition diff --git a/api/v1beta1/awsmachine_conversion.go b/api/v1beta1/awsmachine_conversion.go index 4cd5a66850..e9a9e329a1 100644 --- a/api/v1beta1/awsmachine_conversion.go +++ b/api/v1beta1/awsmachine_conversion.go @@ -46,6 +46,7 @@ func (src *AWSMachine) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.MarketType = restored.Spec.MarketType dst.Spec.HostID = restored.Spec.HostID dst.Spec.HostAffinity = restored.Spec.HostAffinity + dst.Spec.CapacityReservationPreference = restored.Spec.CapacityReservationPreference dst.Spec.NetworkInterfaceType = restored.Spec.NetworkInterfaceType if restored.Spec.ElasticIPPool != nil { if dst.Spec.ElasticIPPool == nil { @@ -112,6 +113,7 @@ func (r *AWSMachineTemplate) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Template.Spec.MarketType = restored.Spec.Template.Spec.MarketType dst.Spec.Template.Spec.HostID = restored.Spec.Template.Spec.HostID dst.Spec.Template.Spec.HostAffinity = restored.Spec.Template.Spec.HostAffinity + dst.Spec.Template.Spec.CapacityReservationPreference = restored.Spec.Template.Spec.CapacityReservationPreference dst.Spec.Template.Spec.NetworkInterfaceType = restored.Spec.Template.Spec.NetworkInterfaceType if restored.Spec.Template.Spec.ElasticIPPool != nil { if dst.Spec.Template.Spec.ElasticIPPool == nil { diff --git a/api/v1beta1/zz_generated.conversion.go b/api/v1beta1/zz_generated.conversion.go index ad9cc57bea..2045a5af76 100644 --- a/api/v1beta1/zz_generated.conversion.go +++ b/api/v1beta1/zz_generated.conversion.go @@ -1451,6 +1451,7 @@ func autoConvert_v1beta2_AWSMachineSpec_To_v1beta1_AWSMachineSpec(in *v1beta2.AW // WARNING: in.MarketType requires manual conversion: does not exist in peer-type // WARNING: in.HostID requires manual conversion: does not exist in peer-type // WARNING: in.HostAffinity requires manual conversion: does not exist in peer-type + // WARNING: in.CapacityReservationPreference requires manual conversion: does not exist in peer-type return nil } @@ -2061,6 +2062,7 @@ func autoConvert_v1beta2_Instance_To_v1beta1_Instance(in *v1beta2.Instance, out // WARNING: in.MarketType requires manual conversion: does not exist in peer-type // WARNING: in.HostAffinity requires manual conversion: does not exist in peer-type // WARNING: in.HostID requires manual conversion: does not exist in peer-type + // WARNING: in.CapacityReservationPreference requires manual conversion: does not exist in peer-type return nil } diff --git a/api/v1beta2/awsmachine_types.go b/api/v1beta2/awsmachine_types.go index 93c019b1b6..16f85dbe15 100644 --- a/api/v1beta2/awsmachine_types.go +++ b/api/v1beta2/awsmachine_types.go @@ -245,6 +245,14 @@ type AWSMachineSpec struct { // +optional // +kubebuilder:validation:Enum:=default;host HostAffinity *string `json:"hostAffinity,omitempty"` + + // CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: + // "Open": The instance may make use of open Capacity Reservations that match its AZ and InstanceType + // "None": The instance may not make use of any Capacity Reservations. This is to conserve open reservations for desired workloads + // "CapacityReservationsOnly": The instance will only run if matched or targeted to a Capacity Reservation + // +kubebuilder:validation:Enum="";None;CapacityReservationsOnly;Open + // +optional + CapacityReservationPreference CapacityReservationPreference `json:"capacityReservationPreference,omitempty"` } // CloudInit defines options related to the bootstrapping systems where diff --git a/api/v1beta2/awsmachine_webhook.go b/api/v1beta2/awsmachine_webhook.go index af6d68eba4..ae905fcdd2 100644 --- a/api/v1beta2/awsmachine_webhook.go +++ b/api/v1beta2/awsmachine_webhook.go @@ -79,6 +79,7 @@ func (*awsMachineWebhook) ValidateCreate(_ context.Context, obj runtime.Object) allErrs = append(allErrs, r.Spec.AdditionalTags.Validate()...) allErrs = append(allErrs, r.validateNetworkElasticIPPool()...) allErrs = append(allErrs, r.validateInstanceMarketType()...) + allErrs = append(allErrs, r.validateCapacityReservation()...) return nil, aggregateObjErrors(r.GroupVersionKind().GroupKind(), r.Name, allErrs) } @@ -380,6 +381,14 @@ func (r *AWSMachine) validateNetworkElasticIPPool() field.ErrorList { return allErrs } +func (r *AWSMachine) validateCapacityReservation() field.ErrorList { + var allErrs field.ErrorList + if r.Spec.CapacityReservationID != nil && r.Spec.CapacityReservationPreference != CapacityReservationPreferenceOnly && r.Spec.CapacityReservationPreference != "" { + allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "capacityReservationPreference"), "when a reservation ID is specified, capacityReservationPreference may only be `capacity-reservations-only` or empty")) + } + return allErrs +} + func (r *AWSMachine) validateInstanceMarketType() field.ErrorList { var allErrs field.ErrorList if r.Spec.MarketType == MarketTypeCapacityBlock && r.Spec.SpotMarketOptions != nil { diff --git a/api/v1beta2/awsmachine_webhook_test.go b/api/v1beta2/awsmachine_webhook_test.go index 3eb5b6931a..a3680f5dea 100644 --- a/api/v1beta2/awsmachine_webhook_test.go +++ b/api/v1beta2/awsmachine_webhook_test.go @@ -261,6 +261,38 @@ func TestAWSMachineCreate(t *testing.T) { }, wantErr: false, }, + { + name: "invalid case, CapacityReservationId is set and CapacityReservationPreference is not `capacity-reservation-only`", + machine: &AWSMachine{ + Spec: AWSMachineSpec{ + InstanceType: "test", + CapacityReservationID: aws.String("cr-12345678901234567"), + CapacityReservationPreference: CapacityReservationPreferenceNone, + }, + }, + wantErr: true, + }, + { + name: "valid CapacityReservationId is set and CapacityReservationPreference is not specified", + machine: &AWSMachine{ + Spec: AWSMachineSpec{ + InstanceType: "test", + CapacityReservationID: aws.String("cr-12345678901234567"), + }, + }, + wantErr: false, + }, + { + name: "valid CapacityReservationId is set and CapacityReservationPreference is `capacity-reservation-only`", + machine: &AWSMachine{ + Spec: AWSMachineSpec{ + InstanceType: "test", + CapacityReservationID: aws.String("cr-12345678901234567"), + CapacityReservationPreference: CapacityReservationPreferenceOnly, + }, + }, + wantErr: false, + }, { name: "empty instance type not allowed", machine: &AWSMachine{ diff --git a/api/v1beta2/types.go b/api/v1beta2/types.go index 143a806861..0f8d155515 100644 --- a/api/v1beta2/types.go +++ b/api/v1beta2/types.go @@ -285,8 +285,32 @@ type Instance struct { // HostID specifies the dedicated host on which the instance should be started. // +optional HostID *string `json:"hostID,omitempty"` + + // CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: + // "Open": The instance may make use of open Capacity Reservations that match its AZ and InstanceType + // "None": The instance may not make use of any Capacity Reservations. This is to conserve open reservations for desired workloads + // "CapacityReservationsOnly": The instance will only run if matched or targeted to a Capacity Reservation + // +kubebuilder:validation:Enum="";None;CapacityReservationsOnly;Open + // +optional + CapacityReservationPreference CapacityReservationPreference `json:"capacityReservationPreference,omitempty"` } +// CapacityReservationPreference describes the preferred use of capacity reservations +// of an instance +// +kubebuilder:validation:Enum:="";None;CapacityReservationsOnly;Open +type CapacityReservationPreference string + +const ( + // CapacityReservationPreferenceNone the instance may not make use of any Capacity Reservations. This is to conserve open reservations for desired workloads + CapacityReservationPreferenceNone CapacityReservationPreference = "None" + + // CapacityReservationPreferenceOnly the instance will only run if matched or targeted to a Capacity Reservation + CapacityReservationPreferenceOnly CapacityReservationPreference = "CapacityReservationsOnly" + + // CapacityReservationPreferenceOpen the instance may make use of open Capacity Reservations that match its AZ and InstanceType. + CapacityReservationPreferenceOpen CapacityReservationPreference = "Open" +) + // MarketType describes the market type of an Instance // +kubebuilder:validation:Enum:=OnDemand;Spot;CapacityBlock type MarketType string diff --git a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml index 9365590c24..4bc4ca8e70 100644 --- a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml +++ b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml @@ -1214,6 +1214,24 @@ spec: description: CapacityReservationID specifies the target Capacity Reservation into which the instance should be launched. type: string + capacityReservationPreference: + allOf: + - enum: + - "" + - None + - CapacityReservationsOnly + - Open + - enum: + - "" + - None + - CapacityReservationsOnly + - Open + description: |- + CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: + "Open": The instance may make use of open Capacity Reservations that match its AZ and InstanceType + "None": The instance may not make use of any Capacity Reservations. This is to conserve open reservations for desired workloads + "CapacityReservationsOnly": The instance will only run if matched or targeted to a Capacity Reservation + type: string ebsOptimized: description: Indicates whether the instance is optimized for Amazon EBS I/O. @@ -3410,6 +3428,24 @@ spec: description: CapacityReservationID specifies the target Capacity Reservation into which the instance should be launched. type: string + capacityReservationPreference: + allOf: + - enum: + - "" + - None + - CapacityReservationsOnly + - Open + - enum: + - "" + - None + - CapacityReservationsOnly + - Open + description: |- + CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: + "Open": The instance may make use of open Capacity Reservations that match its AZ and InstanceType + "None": The instance may not make use of any Capacity Reservations. This is to conserve open reservations for desired workloads + "CapacityReservationsOnly": The instance will only run if matched or targeted to a Capacity Reservation + type: string ebsOptimized: description: Indicates whether the instance is optimized for Amazon EBS I/O. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml index 534a4ebcd0..4e0e03d0bf 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml @@ -2197,6 +2197,24 @@ spec: description: CapacityReservationID specifies the target Capacity Reservation into which the instance should be launched. type: string + capacityReservationPreference: + allOf: + - enum: + - "" + - None + - CapacityReservationsOnly + - Open + - enum: + - "" + - None + - CapacityReservationsOnly + - Open + description: |- + CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: + "Open": The instance may make use of open Capacity Reservations that match its AZ and InstanceType + "None": The instance may not make use of any Capacity Reservations. This is to conserve open reservations for desired workloads + "CapacityReservationsOnly": The instance will only run if matched or targeted to a Capacity Reservation + type: string ebsOptimized: description: Indicates whether the instance is optimized for Amazon EBS I/O. 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 0caebcc4d1..893efb3e32 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinepools.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinepools.yaml @@ -644,6 +644,24 @@ spec: description: CapacityReservationID specifies the target Capacity Reservation into which the instance should be launched. type: string + capacityReservationPreference: + allOf: + - enum: + - "" + - None + - CapacityReservationsOnly + - Open + - enum: + - "" + - None + - CapacityReservationsOnly + - Open + description: |- + CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: + "Open": The instance may make use of open Capacity Reservations that match its AZ and InstanceType + "None": The instance may not make use of any Capacity Reservations. This is to conserve open reservations for desired workloads + "CapacityReservationsOnly": The instance will only run if matched or targeted to a Capacity Reservation + type: string iamInstanceProfile: description: |- The name or the Amazon Resource Name (ARN) of the instance profile associated diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml index 4a0b10b5a3..6ca5ccb7a2 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml @@ -641,6 +641,24 @@ spec: description: CapacityReservationID specifies the target Capacity Reservation into which the instance should be launched. type: string + capacityReservationPreference: + allOf: + - enum: + - "" + - None + - CapacityReservationsOnly + - Open + - enum: + - "" + - None + - CapacityReservationsOnly + - Open + description: |- + CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: + "Open": The instance may make use of open Capacity Reservations that match its AZ and InstanceType + "None": The instance may not make use of any Capacity Reservations. This is to conserve open reservations for desired workloads + "CapacityReservationsOnly": The instance will only run if matched or targeted to a Capacity Reservation + type: string cloudInit: description: |- CloudInit defines options related to the bootstrapping systems where diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml index fdac94f540..faff683e2e 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml @@ -560,6 +560,24 @@ spec: description: CapacityReservationID specifies the target Capacity Reservation into which the instance should be launched. type: string + capacityReservationPreference: + allOf: + - enum: + - "" + - None + - CapacityReservationsOnly + - Open + - enum: + - "" + - None + - CapacityReservationsOnly + - Open + description: |- + CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: + "Open": The instance may make use of open Capacity Reservations that match its AZ and InstanceType + "None": The instance may not make use of any Capacity Reservations. This is to conserve open reservations for desired workloads + "CapacityReservationsOnly": The instance will only run if matched or targeted to a Capacity Reservation + type: string cloudInit: description: |- CloudInit defines options related to the bootstrapping systems where 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 f2e4991888..e0504a4c40 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedmachinepools.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedmachinepools.yaml @@ -653,6 +653,24 @@ spec: description: CapacityReservationID specifies the target Capacity Reservation into which the instance should be launched. type: string + capacityReservationPreference: + allOf: + - enum: + - "" + - None + - CapacityReservationsOnly + - Open + - enum: + - "" + - None + - CapacityReservationsOnly + - Open + description: |- + CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: + "Open": The instance may make use of open Capacity Reservations that match its AZ and InstanceType + "None": The instance may not make use of any Capacity Reservations. This is to conserve open reservations for desired workloads + "CapacityReservationsOnly": The instance will only run if matched or targeted to a Capacity Reservation + type: string iamInstanceProfile: description: |- The name or the Amazon Resource Name (ARN) of the instance profile associated diff --git a/exp/api/v1beta1/conversion.go b/exp/api/v1beta1/conversion.go index 002a2c6fe0..cf4040a456 100644 --- a/exp/api/v1beta1/conversion.go +++ b/exp/api/v1beta1/conversion.go @@ -72,6 +72,10 @@ func (src *AWSMachinePool) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.AWSLaunchTemplate.MarketType = restored.Spec.AWSLaunchTemplate.MarketType } + if preference := restored.Spec.AWSLaunchTemplate.CapacityReservationPreference; preference != "" { + dst.Spec.AWSLaunchTemplate.CapacityReservationPreference = preference + } + dst.Spec.DefaultInstanceWarmup = restored.Spec.DefaultInstanceWarmup dst.Spec.AWSLaunchTemplate.NonRootVolumes = restored.Spec.AWSLaunchTemplate.NonRootVolumes return nil @@ -130,6 +134,10 @@ func (src *AWSManagedMachinePool) ConvertTo(dstRaw conversion.Hub) error { if restored.Spec.AWSLaunchTemplate.MarketType != "" { dst.Spec.AWSLaunchTemplate.MarketType = restored.Spec.AWSLaunchTemplate.MarketType } + + if preference := restored.Spec.AWSLaunchTemplate.CapacityReservationPreference; preference != "" { + dst.Spec.AWSLaunchTemplate.CapacityReservationPreference = preference + } } if restored.Spec.AvailabilityZoneSubnetType != nil { dst.Spec.AvailabilityZoneSubnetType = restored.Spec.AvailabilityZoneSubnetType diff --git a/exp/api/v1beta1/zz_generated.conversion.go b/exp/api/v1beta1/zz_generated.conversion.go index b974ea9dc0..933a08f716 100644 --- a/exp/api/v1beta1/zz_generated.conversion.go +++ b/exp/api/v1beta1/zz_generated.conversion.go @@ -418,6 +418,7 @@ func autoConvert_v1beta2_AWSLaunchTemplate_To_v1beta1_AWSLaunchTemplate(in *v1be // WARNING: in.PrivateDNSName requires manual conversion: does not exist in peer-type // WARNING: in.CapacityReservationID requires manual conversion: does not exist in peer-type // WARNING: in.MarketType requires manual conversion: does not exist in peer-type + // WARNING: in.CapacityReservationPreference requires manual conversion: does not exist in peer-type return nil } diff --git a/exp/api/v1beta2/awsmachinepool_webhook.go b/exp/api/v1beta2/awsmachinepool_webhook.go index b784ffc628..3b8cf9fdac 100644 --- a/exp/api/v1beta2/awsmachinepool_webhook.go +++ b/exp/api/v1beta2/awsmachinepool_webhook.go @@ -211,6 +211,7 @@ func (*AWSMachinePoolWebhook) ValidateCreate(_ context.Context, obj runtime.Obje allErrs = append(allErrs, r.validateSpotInstances()...) allErrs = append(allErrs, r.validateRefreshPreferences()...) allErrs = append(allErrs, r.validateInstanceMarketType()...) + allErrs = append(allErrs, r.validateCapacityReservation()...) allErrs = append(allErrs, r.validateLifecycleHooks()...) allErrs = append(allErrs, r.validateIgnition()...) @@ -225,6 +226,16 @@ func (*AWSMachinePoolWebhook) ValidateCreate(_ context.Context, obj runtime.Obje ) } +func (r *AWSMachinePool) validateCapacityReservation() field.ErrorList { + var allErrs field.ErrorList + if r.Spec.AWSLaunchTemplate.CapacityReservationID != nil && + r.Spec.AWSLaunchTemplate.CapacityReservationPreference != infrav1.CapacityReservationPreferenceOnly && + r.Spec.AWSLaunchTemplate.CapacityReservationPreference != "" { + allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "capacityReservationPreference"), "when a reservation ID is specified, capacityReservationPreference may only be `capacity-reservations-only` or empty")) + } + return allErrs +} + func (r *AWSMachinePool) validateInstanceMarketType() field.ErrorList { var allErrs field.ErrorList if r.Spec.AWSLaunchTemplate.MarketType == infrav1.MarketTypeCapacityBlock && r.Spec.AWSLaunchTemplate.SpotMarketOptions != nil { diff --git a/exp/api/v1beta2/awsmachinepool_webhook_test.go b/exp/api/v1beta2/awsmachinepool_webhook_test.go index 3bb7c510b7..9b6a5cae5f 100644 --- a/exp/api/v1beta2/awsmachinepool_webhook_test.go +++ b/exp/api/v1beta2/awsmachinepool_webhook_test.go @@ -303,7 +303,18 @@ func TestAWSMachinePoolValidateCreate(t *testing.T) { }, wantErrToContain: ptr.To("cannot be set to when CapacityReservationID is specified"), }, - + { + name: "with CapacityReservationPreference of `none` and CapacityReservationID is specified", + pool: &AWSMachinePool{ + Spec: AWSMachinePoolSpec{ + AWSLaunchTemplate: AWSLaunchTemplate{ + CapacityReservationID: aws.String("cr-123"), + CapacityReservationPreference: infrav1.CapacityReservationPreferenceNone, + }, + }, + }, + wantErrToContain: ptr.To("when a reservation ID is specified, capacityReservationPreference may only be `capacity-reservations-only` or empty"), + }, { name: "invalid, MarketType set to MarketTypeCapacityBlock and spotMarketOptions are specified", pool: &AWSMachinePool{ diff --git a/exp/api/v1beta2/types.go b/exp/api/v1beta2/types.go index 1b76e0945f..b4eca931a8 100644 --- a/exp/api/v1beta2/types.go +++ b/exp/api/v1beta2/types.go @@ -146,6 +146,14 @@ type AWSLaunchTemplate struct { // If marketType is not specified and spotMarketOptions is provided, the marketType defaults to "Spot". // +optional MarketType infrav1.MarketType `json:"marketType,omitempty"` + + // CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: + // "Open": The instance may make use of open Capacity Reservations that match its AZ and InstanceType + // "None": The instance may not make use of any Capacity Reservations. This is to conserve open reservations for desired workloads + // "CapacityReservationsOnly": The instance will only run if matched or targeted to a Capacity Reservation + // +kubebuilder:validation:Enum="";None;CapacityReservationsOnly;Open + // +optional + CapacityReservationPreference infrav1.CapacityReservationPreference `json:"capacityReservationPreference,omitempty"` } // Overrides are used to override the instance type specified by the launch template with multiple diff --git a/pkg/cloud/services/ec2/helper_test.go b/pkg/cloud/services/ec2/helper_test.go index 2184e12bf9..7e77aa34db 100644 --- a/pkg/cloud/services/ec2/helper_test.go +++ b/pkg/cloud/services/ec2/helper_test.go @@ -61,45 +61,6 @@ func setupMachinePoolScope(cl client.Client, ec2Scope scope.EC2Scope) (*scope.Ma }) } -func setupCapacityBlocksMachinePoolScope(cl client.Client, ec2Scope scope.EC2Scope) (*scope.MachinePoolScope, error) { - return scope.NewMachinePoolScope(scope.MachinePoolScopeParams{ - Client: cl, - InfraCluster: ec2Scope, - Cluster: newCluster(), - MachinePool: newMachinePool(), - AWSMachinePool: newAWSCapacityBlockMachinePool(), - }) -} - -func newAWSCapacityBlockMachinePool() *expinfrav1.AWSMachinePool { - return &expinfrav1.AWSMachinePool{ - TypeMeta: metav1.TypeMeta{ - Kind: "AWSMachinePool", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "aws-mp-name", - Namespace: "aws-mp-ns", - }, - Spec: expinfrav1.AWSMachinePoolSpec{ - AvailabilityZones: []string{"us-east-1"}, - AdditionalTags: infrav1.Tags{}, - AWSLaunchTemplate: expinfrav1.AWSLaunchTemplate{ - Name: "aws-launch-template", - IamInstanceProfile: "instance-profile", - AMI: infrav1.AMIReference{}, - InstanceType: "t3.large", - SSHKeyName: aws.String("default"), - MarketType: infrav1.MarketTypeCapacityBlock, - CapacityReservationID: aws.String("cr-12345678901234567"), - }, - }, - Status: expinfrav1.AWSMachinePoolStatus{ - LaunchTemplateID: "launch-template-id", - }, - } -} - func defaultEC2Tags(name, clusterName string) []types.Tag { return []types.Tag{ { diff --git a/pkg/cloud/services/ec2/instances.go b/pkg/cloud/services/ec2/instances.go index 05175d9908..050772925f 100644 --- a/pkg/cloud/services/ec2/instances.go +++ b/pkg/cloud/services/ec2/instances.go @@ -262,6 +262,8 @@ func (s *Service) CreateInstance(ctx context.Context, scope *scope.MachineScope, input.HostAffinity = scope.AWSMachine.Spec.HostAffinity + input.CapacityReservationPreference = scope.AWSMachine.Spec.CapacityReservationPreference + s.scope.Debug("Running instance", "machine-role", scope.Role()) s.scope.Debug("Running instance with instance metadata options", "metadata options", input.InstanceMetadataOptions) out, err := s.runInstance(scope.Role(), input) @@ -659,7 +661,7 @@ func (s *Service) runInstance(role string, i *infrav1.Instance) (*infrav1.Instan } input.MetadataOptions = getInstanceMetadataOptionsRequest(i.InstanceMetadataOptions) input.PrivateDnsNameOptions = getPrivateDNSNameOptionsRequest(i.PrivateDNSName) - input.CapacityReservationSpecification = getCapacityReservationSpecification(i.CapacityReservationID) + input.CapacityReservationSpecification = getCapacityReservationSpecification(i.CapacityReservationID, i.CapacityReservationPreference) if i.Tenancy != "" { input.Placement = &types.Placement{ @@ -1172,17 +1174,18 @@ func filterGroups(list []string, strToFilter string) (newList []string) { return } -func getCapacityReservationSpecification(capacityReservationID *string) *types.CapacityReservationSpecification { - if capacityReservationID == nil { - // Not targeting any specific Capacity Reservation +func getCapacityReservationSpecification(capacityReservationID *string, capacityReservationPreference infrav1.CapacityReservationPreference) *types.CapacityReservationSpecification { + if capacityReservationID == nil && capacityReservationPreference == "" { return nil } - - return &types.CapacityReservationSpecification{ - CapacityReservationTarget: &types.CapacityReservationTarget{ + var spec types.CapacityReservationSpecification + if capacityReservationID != nil { + spec.CapacityReservationTarget = &types.CapacityReservationTarget{ CapacityReservationId: capacityReservationID, - }, + } } + spec.CapacityReservationPreference = CapacityReservationPreferenceToSDK(capacityReservationPreference) + return &spec } func getInstanceMarketOptionsRequest(i *infrav1.Instance) (*types.InstanceMarketOptionsRequest, error) { diff --git a/pkg/cloud/services/ec2/instances_test.go b/pkg/cloud/services/ec2/instances_test.go index dfc080d5c0..f454a36727 100644 --- a/pkg/cloud/services/ec2/instances_test.go +++ b/pkg/cloud/services/ec2/instances_test.go @@ -5653,6 +5653,130 @@ func TestCreateInstance(t *testing.T) { } }, }, + { + name: "Simple, setting CapacityReservationID and CapacityReservationPreference", + machine: &clusterv1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"set": "node"}, + }, + Spec: clusterv1.MachineSpec{ + Bootstrap: clusterv1.Bootstrap{ + DataSecretName: ptr.To[string]("bootstrap-data"), + }, + }, + }, + machineConfig: &infrav1.AWSMachineSpec{ + AMI: infrav1.AMIReference{ + ID: aws.String("abc"), + }, + InstanceType: "m5.large", + CapacityReservationID: aws.String("cr-12345678901234567"), + CapacityReservationPreference: infrav1.CapacityReservationPreferenceOnly, + }, + awsCluster: &infrav1.AWSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: infrav1.AWSClusterSpec{ + + NetworkSpec: infrav1.NetworkSpec{ + Subnets: infrav1.Subnets{ + infrav1.SubnetSpec{ + ID: "subnet-1", + IsPublic: false, + }, + infrav1.SubnetSpec{ + IsPublic: false, + }, + }, + VPC: infrav1.VPCSpec{ + ID: "vpc-test", + }, + }, + }, + Status: infrav1.AWSClusterStatus{ + Network: infrav1.NetworkStatus{ + SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{ + infrav1.SecurityGroupControlPlane: { + ID: "1", + }, + infrav1.SecurityGroupNode: { + ID: "2", + }, + infrav1.SecurityGroupLB: { + ID: "3", + }, + }, + APIServerELB: infrav1.LoadBalancer{ + DNSName: "test-apiserver.us-east-1.aws", + }, + }, + }, + }, + expect: func(m *mocks.MockEC2APIMockRecorder) { + m. + DescribeInstanceTypes(context.TODO(), gomock.Eq(&ec2.DescribeInstanceTypesInput{ + InstanceTypes: []types.InstanceType{ + types.InstanceTypeM5Large, + }, + })). + Return(&ec2.DescribeInstanceTypesOutput{ + InstanceTypes: []types.InstanceTypeInfo{ + { + ProcessorInfo: &types.ProcessorInfo{ + SupportedArchitectures: []types.ArchitectureType{ + types.ArchitectureTypeX8664, + }, + }, + }, + }, + }, nil) + m. // TODO: Restore these parameters, but with the tags as well + RunInstances(context.TODO(), gomock.Any()). + Return(&ec2.RunInstancesOutput{ + Instances: []types.Instance{ + { + State: &types.InstanceState{ + Name: types.InstanceStateNamePending, + }, + IamInstanceProfile: &types.IamInstanceProfile{ + Arn: aws.String("arn:aws:iam::123456789012:instance-profile/foo"), + }, + InstanceId: aws.String("two"), + InstanceType: types.InstanceTypeM5Large, + SubnetId: aws.String("subnet-1"), + ImageId: aws.String("ami-1"), + RootDeviceName: aws.String("device-1"), + BlockDeviceMappings: []types.InstanceBlockDeviceMapping{ + { + DeviceName: aws.String("device-1"), + Ebs: &types.EbsInstanceBlockDevice{ + VolumeId: aws.String("volume-1"), + }, + }, + }, + Placement: &types.Placement{ + AvailabilityZone: &az, + }, + CapacityReservationId: aws.String("cr-12345678901234567"), + CapacityReservationSpecification: &types.CapacityReservationSpecificationResponse{ + CapacityReservationPreference: types.CapacityReservationPreferenceCapacityReservationsOnly, + }, + InstanceLifecycle: types.InstanceLifecycleTypeScheduled, + }, + }, + }, nil) + m. + DescribeNetworkInterfaces(context.TODO(), gomock.Any()). + Return(&ec2.DescribeNetworkInterfacesOutput{ + NetworkInterfaces: []types.NetworkInterface{}, + NextToken: nil, + }, nil) + }, + check: func(instance *infrav1.Instance, err error) { + if err != nil { + t.Fatalf("did not expect error: %v", err) + } + }, + }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { @@ -6216,9 +6340,10 @@ func TestGetCapacityReservationSpecification(t *testing.T) { mockCapacityReservationID := "cr-123" mockCapacityReservationIDPtr := &mockCapacityReservationID testCases := []struct { - name string - capacityReservationID *string - expectedRequest *types.CapacityReservationSpecification + name string + capacityReservationID *string + capacityReservationPreference infrav1.CapacityReservationPreference + expectedRequest *types.CapacityReservationSpecification }{ { name: "with no CapacityReservationID options specified", @@ -6234,10 +6359,29 @@ func TestGetCapacityReservationSpecification(t *testing.T) { }, }, }, + { + name: "with a valid reservation ID and a preference", + capacityReservationID: mockCapacityReservationIDPtr, + capacityReservationPreference: infrav1.CapacityReservationPreferenceOnly, + expectedRequest: &types.CapacityReservationSpecification{ + CapacityReservationTarget: &types.CapacityReservationTarget{ + CapacityReservationId: aws.String(mockCapacityReservationID), + }, + CapacityReservationPreference: types.CapacityReservationPreferenceCapacityReservationsOnly, + }, + }, + { + name: "with no reservation ID and a preference", + capacityReservationID: nil, + capacityReservationPreference: infrav1.CapacityReservationPreferenceNone, + expectedRequest: &types.CapacityReservationSpecification{ + CapacityReservationPreference: types.CapacityReservationPreferenceNone, + }, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - request := getCapacityReservationSpecification(tc.capacityReservationID) + request := getCapacityReservationSpecification(tc.capacityReservationID, tc.capacityReservationPreference) if !cmp.Equal(request, tc.expectedRequest, cmpopts.IgnoreUnexported(types.CapacityReservationSpecification{}, types.CapacityReservationTarget{})) { t.Errorf("Case: %s. Got: %v, expected: %v", tc.name, request, tc.expectedRequest) } diff --git a/pkg/cloud/services/ec2/launchtemplate.go b/pkg/cloud/services/ec2/launchtemplate.go index f0d9b1bde0..ad664ebe6d 100644 --- a/pkg/cloud/services/ec2/launchtemplate.go +++ b/pkg/cloud/services/ec2/launchtemplate.go @@ -650,6 +650,7 @@ func (s *Service) createLaunchTemplateData(scope scope.LaunchTemplateScope, imag } data.InstanceMarketOptions = instanceMarketOptions data.PrivateDnsNameOptions = getLaunchTemplatePrivateDNSNameOptionsRequest(scope.GetLaunchTemplate().PrivateDNSName) + data.CapacityReservationSpecification = getLaunchTemplateCapacityReservationSpecification(scope.GetLaunchTemplate()) blockDeviceMappings := []types.LaunchTemplateBlockDeviceMappingRequest{} @@ -682,6 +683,24 @@ func (s *Service) createLaunchTemplateData(scope scope.LaunchTemplateScope, imag return data, nil } +func getLaunchTemplateCapacityReservationSpecification(awsLaunchTemplate *expinfrav1.AWSLaunchTemplate) *types.LaunchTemplateCapacityReservationSpecificationRequest { + if awsLaunchTemplate == nil { + return nil + } + if awsLaunchTemplate.CapacityReservationID == nil && awsLaunchTemplate.CapacityReservationPreference == "" { + return nil + } + spec := &types.LaunchTemplateCapacityReservationSpecificationRequest{ + CapacityReservationPreference: CapacityReservationPreferenceToSDK(awsLaunchTemplate.CapacityReservationPreference), + } + if awsLaunchTemplate.CapacityReservationID != nil { + spec.CapacityReservationTarget = &types.CapacityReservationTarget{ + CapacityReservationId: awsLaunchTemplate.CapacityReservationID, + } + } + return spec +} + func volumeToLaunchTemplateBlockDeviceMappingRequest(v *infrav1.Volume) *types.LaunchTemplateBlockDeviceMappingRequest { ltEbsDevice := &types.LaunchTemplateEbsBlockDeviceRequest{ DeleteOnTermination: aws.Bool(true), @@ -829,6 +848,36 @@ func SDKToSpotMarketOptions(instanceMarketOptions *types.LaunchTemplateInstanceM return result } +// SDKToCapacityReservationPreference maps an AWS SDK Capacity Reservation Preference onto the CAPA internal CapacityReservationPreference type. +// inverse to `CapacityReservationPreferenceToSDK`. +func SDKToCapacityReservationPreference(preference types.CapacityReservationPreference) infrav1.CapacityReservationPreference { + switch preference { + case types.CapacityReservationPreferenceCapacityReservationsOnly: + return infrav1.CapacityReservationPreferenceOnly + case types.CapacityReservationPreferenceNone: + return infrav1.CapacityReservationPreferenceNone + case types.CapacityReservationPreferenceOpen: + return infrav1.CapacityReservationPreferenceOpen + default: + return "" + } +} + +// CapacityReservationPreferenceToSDK maps a CAPA internal Capacity Reservation Preference enum type onto the AWS SDK equivalent. +// inverse to `SDKToCapacityReservationPreference`. +func CapacityReservationPreferenceToSDK(preference infrav1.CapacityReservationPreference) types.CapacityReservationPreference { + switch preference { + case infrav1.CapacityReservationPreferenceNone: + return types.CapacityReservationPreferenceNone + case infrav1.CapacityReservationPreferenceOnly: + return types.CapacityReservationPreferenceCapacityReservationsOnly + case infrav1.CapacityReservationPreferenceOpen: + return types.CapacityReservationPreferenceOpen + default: + return "" + } +} + // SDKToLaunchTemplate converts an AWS EC2 SDK instance to the CAPA instance type. func (s *Service) SDKToLaunchTemplate(d types.LaunchTemplateVersion) (*expinfrav1.AWSLaunchTemplate, string, *apimachinerytypes.NamespacedName, *string, error) { v := d.LaunchTemplateData diff --git a/pkg/cloud/services/ec2/launchtemplate_test.go b/pkg/cloud/services/ec2/launchtemplate_test.go index 39098948f5..438ceaef66 100644 --- a/pkg/cloud/services/ec2/launchtemplate_test.go +++ b/pkg/cloud/services/ec2/launchtemplate_test.go @@ -1439,6 +1439,18 @@ func TestLaunchTemplateDataCreation(t *testing.T) { }) } +var LaunchTemplateVersionIgnoreUnexported = cmpopts.IgnoreUnexported( + ec2types.CapacityReservationTarget{}, + ec2types.LaunchTemplateCapacityReservationSpecificationRequest{}, + ec2types.LaunchTemplateIamInstanceProfileSpecificationRequest{}, + ec2types.LaunchTemplateSpotMarketOptionsRequest{}, + ec2types.LaunchTemplateInstanceMarketOptionsRequest{}, + ec2types.Tag{}, + ec2types.LaunchTemplateTagSpecificationRequest{}, + ec2types.RequestLaunchTemplateData{}, + ec2.CreateLaunchTemplateVersionInput{}, +) + func TestCreateLaunchTemplateVersion(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() @@ -1459,6 +1471,7 @@ func TestCreateLaunchTemplateVersion(t *testing.T) { awsResourceReference []infrav1.AWSResourceReference expect func(m *mocks.MockEC2APIMockRecorder) wantErr bool + mpScopeUpdater func(*scope.MachinePoolScope) marketType ec2types.MarketType }{ { @@ -1506,16 +1519,8 @@ func TestCreateLaunchTemplateVersion(t *testing.T) { func(ctx context.Context, arg *ec2.CreateLaunchTemplateVersionInput, requestOptions ...request.Option) { // formatting added to match tags slice during cmp.Equal() formatTagsInput(arg) - if !cmp.Equal(expectedInput, arg, cmpopts.IgnoreUnexported( - ec2types.LaunchTemplateIamInstanceProfileSpecificationRequest{}, - ec2types.LaunchTemplateSpotMarketOptionsRequest{}, - ec2types.LaunchTemplateInstanceMarketOptionsRequest{}, - ec2types.Tag{}, - ec2types.LaunchTemplateTagSpecificationRequest{}, - ec2types.RequestLaunchTemplateData{}, - ec2.CreateLaunchTemplateVersionInput{}, - )) { - t.Fatalf("mismatch in input expected: %+v, but got %+v, diff: %s", expectedInput, arg, cmp.Diff(expectedInput, arg)) + if !cmp.Equal(expectedInput, arg, LaunchTemplateVersionIgnoreUnexported) { + t.Fatalf("mismatch in input expected: %+v, but got %+v, diff: %s", expectedInput, arg, cmp.Diff(expectedInput, arg, LaunchTemplateVersionIgnoreUnexported)) } }) }, @@ -1523,7 +1528,13 @@ func TestCreateLaunchTemplateVersion(t *testing.T) { { name: "Should successfully create launch template version with capacity-block", awsResourceReference: []infrav1.AWSResourceReference{{ID: aws.String("1")}}, - marketType: ec2types.MarketTypeCapacityBlock, + mpScopeUpdater: func(mps *scope.MachinePoolScope) { + spec := mps.AWSMachinePool.Spec + spec.AWSLaunchTemplate.CapacityReservationID = aws.String("cr-12345678901234567") + spec.AWSLaunchTemplate.MarketType = infrav1.MarketTypeCapacityBlock + spec.AWSLaunchTemplate.SpotMarketOptions = nil + mps.AWSMachinePool.Spec = spec + }, expect: func(m *mocks.MockEC2APIMockRecorder) { sgMap := make(map[infrav1.SecurityGroupRole]infrav1.SecurityGroup) sgMap[infrav1.SecurityGroupNode] = infrav1.SecurityGroup{ID: "1"} @@ -1542,6 +1553,11 @@ func TestCreateLaunchTemplateVersion(t *testing.T) { InstanceMarketOptions: &ec2types.LaunchTemplateInstanceMarketOptionsRequest{ MarketType: ec2types.MarketTypeCapacityBlock, }, + CapacityReservationSpecification: &ec2types.LaunchTemplateCapacityReservationSpecificationRequest{ + CapacityReservationTarget: &ec2types.CapacityReservationTarget{ + CapacityReservationId: aws.String("cr-12345678901234567"), + }, + }, TagSpecifications: []ec2types.LaunchTemplateTagSpecificationRequest{ { ResourceType: ec2types.ResourceTypeInstance, @@ -1563,16 +1579,66 @@ func TestCreateLaunchTemplateVersion(t *testing.T) { func(ctx context.Context, arg *ec2.CreateLaunchTemplateVersionInput, requestOptions ...request.Option) { // formatting added to match tags slice during cmp.Equal() formatTagsInput(arg) - if !cmp.Equal(expectedInput, arg, cmpopts.IgnoreUnexported( - ec2types.LaunchTemplateIamInstanceProfileSpecificationRequest{}, - ec2types.LaunchTemplateSpotMarketOptionsRequest{}, - ec2types.LaunchTemplateInstanceMarketOptionsRequest{}, - ec2types.Tag{}, - ec2types.LaunchTemplateTagSpecificationRequest{}, - ec2types.RequestLaunchTemplateData{}, - ec2.CreateLaunchTemplateVersionInput{}, - )) { - t.Fatalf("mismatch in input expected: %+v, but got %+v, diff: %s", expectedInput, arg, cmp.Diff(expectedInput, arg)) + if !cmp.Equal(expectedInput, arg, LaunchTemplateVersionIgnoreUnexported) { + t.Fatalf("mismatch in input expected: %+v, but got %+v, diff: %s", expectedInput, arg, cmp.Diff(expectedInput, arg, LaunchTemplateVersionIgnoreUnexported)) + } + }) + }, + }, + { + name: "Should successfully create launch template version with capacity reservation ID and preference", + awsResourceReference: []infrav1.AWSResourceReference{{ID: aws.String("1")}}, + mpScopeUpdater: func(mps *scope.MachinePoolScope) { + spec := mps.AWSMachinePool.Spec + spec.AWSLaunchTemplate.CapacityReservationID = aws.String("cr-12345678901234567") + spec.AWSLaunchTemplate.CapacityReservationPreference = infrav1.CapacityReservationPreferenceOnly + spec.AWSLaunchTemplate.SpotMarketOptions = nil + mps.AWSMachinePool.Spec = spec + }, + expect: func(m *mocks.MockEC2APIMockRecorder) { + sgMap := make(map[infrav1.SecurityGroupRole]infrav1.SecurityGroup) + sgMap[infrav1.SecurityGroupNode] = infrav1.SecurityGroup{ID: "1"} + sgMap[infrav1.SecurityGroupLB] = infrav1.SecurityGroup{ID: "2"} + + expectedInput := &ec2.CreateLaunchTemplateVersionInput{ + LaunchTemplateData: &ec2types.RequestLaunchTemplateData{ + InstanceType: ec2types.InstanceTypeT3Large, + IamInstanceProfile: &ec2types.LaunchTemplateIamInstanceProfileSpecificationRequest{ + Name: aws.String("instance-profile"), + }, + KeyName: aws.String("default"), + UserData: ptr.To[string](base64.StdEncoding.EncodeToString(userData)), + SecurityGroupIds: []string{"nodeSG", "lbSG", "1"}, + ImageId: aws.String("imageID"), + CapacityReservationSpecification: &ec2types.LaunchTemplateCapacityReservationSpecificationRequest{ + CapacityReservationTarget: &ec2types.CapacityReservationTarget{ + CapacityReservationId: aws.String("cr-12345678901234567"), + }, + CapacityReservationPreference: ec2types.CapacityReservationPreferenceCapacityReservationsOnly, + }, + TagSpecifications: []ec2types.LaunchTemplateTagSpecificationRequest{ + { + ResourceType: ec2types.ResourceTypeInstance, + Tags: defaultEC2AndDataTags("aws-mp-name", "cluster-name", userDataSecretKey, testBootstrapDataHash), + }, + { + ResourceType: ec2types.ResourceTypeVolume, + Tags: defaultEC2Tags("aws-mp-name", "cluster-name"), + }, + }, + }, + LaunchTemplateId: aws.String("launch-template-id"), + } + m.CreateLaunchTemplateVersion(context.TODO(), gomock.AssignableToTypeOf(expectedInput)).Return(&ec2.CreateLaunchTemplateVersionOutput{ + LaunchTemplateVersion: &ec2types.LaunchTemplateVersion{ + LaunchTemplateId: aws.String("launch-template-id"), + }, + }, nil).Do( + func(ctx context.Context, arg *ec2.CreateLaunchTemplateVersionInput, requestOptions ...request.Option) { + // formatting added to match tags slice during cmp.Equal() + formatTagsInput(arg) + if !cmp.Equal(expectedInput, arg, LaunchTemplateVersionIgnoreUnexported) { + t.Fatalf("mismatch in input expected: %+v, but got %+v, diff: %s", expectedInput, arg, cmp.Diff(expectedInput, arg, LaunchTemplateVersionIgnoreUnexported)) } }) }, @@ -1619,15 +1685,7 @@ func TestCreateLaunchTemplateVersion(t *testing.T) { func(ctx context.Context, arg *ec2.CreateLaunchTemplateVersionInput, requestOptions ...request.Option) { // formatting added to match tags slice during cmp.Equal() formatTagsInput(arg) - if !cmp.Equal(expectedInput, arg, cmpopts.IgnoreUnexported( - ec2types.LaunchTemplateIamInstanceProfileSpecificationRequest{}, - ec2types.LaunchTemplateSpotMarketOptionsRequest{}, - ec2types.LaunchTemplateInstanceMarketOptionsRequest{}, - ec2types.Tag{}, - ec2types.LaunchTemplateTagSpecificationRequest{}, - ec2types.RequestLaunchTemplateData{}, - ec2.CreateLaunchTemplateVersionInput{}, - )) { + if !cmp.Equal(expectedInput, arg, LaunchTemplateVersionIgnoreUnexported) { t.Fatalf("mismatch in input expected: %+v, got: %+v", expectedInput, arg) } }) @@ -1645,13 +1703,11 @@ func TestCreateLaunchTemplateVersion(t *testing.T) { cs, err := setupClusterScope(client) g.Expect(err).NotTo(HaveOccurred()) - var ms *scope.MachinePoolScope - if tc.marketType == ec2types.MarketTypeCapacityBlock { - ms, err = setupCapacityBlocksMachinePoolScope(client, cs) - } else { - ms, err = setupMachinePoolScope(client, cs) - } + ms, err := setupMachinePoolScope(client, cs) g.Expect(err).NotTo(HaveOccurred()) + if updateScope := tc.mpScopeUpdater; updateScope != nil { + updateScope(ms) + } ms.AWSMachinePool.Spec.AWSLaunchTemplate.AdditionalSecurityGroups = tc.awsResourceReference