From a2d76a72634f881e8d04f9d25f81789f993a9821 Mon Sep 17 00:00:00 2001 From: BraeTroutman Date: Fri, 1 Aug 2025 12:50:32 -0400 Subject: [PATCH 1/4] update v1beta2 with CapacityReservationPreference update all the API types that map to EC2 RunInstance operations, allowing for the specification of CapacityReservationPreference as well as validations to ensure that these values play well with AWS specifications for use of CapacityReservationPreference with CapacityReservationID --- api/v1beta1/awscluster_conversion.go | 1 + api/v1beta1/awsmachine_conversion.go | 2 + api/v1beta1/zz_generated.conversion.go | 2 + api/v1beta2/awsmachine_types.go | 7 + api/v1beta2/awsmachine_webhook.go | 9 ++ api/v1beta2/awsmachine_webhook_test.go | 32 ++++ api/v1beta2/types.go | 23 +++ ...ster.x-k8s.io_awsmanagedcontrolplanes.yaml | 22 +++ ...tructure.cluster.x-k8s.io_awsclusters.yaml | 11 ++ ...ture.cluster.x-k8s.io_awsmachinepools.yaml | 11 ++ ...tructure.cluster.x-k8s.io_awsmachines.yaml | 11 ++ ....cluster.x-k8s.io_awsmachinetemplates.yaml | 11 ++ ...uster.x-k8s.io_awsmanagedmachinepools.yaml | 11 ++ exp/api/v1beta1/conversion.go | 8 + exp/api/v1beta1/zz_generated.conversion.go | 1 + exp/api/v1beta2/awsmachinepool_webhook.go | 11 ++ .../v1beta2/awsmachinepool_webhook_test.go | 13 +- exp/api/v1beta2/types.go | 7 + pkg/cloud/services/ec2/instances.go | 19 ++- pkg/cloud/services/ec2/instances_test.go | 152 +++++++++++++++++- pkg/cloud/services/ec2/launchtemplate.go | 45 ++++++ pkg/cloud/services/ec2/launchtemplate_test.go | 128 ++++++++++----- 22 files changed, 488 insertions(+), 49 deletions(-) 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..742e4ef36e 100644 --- a/api/v1beta2/awsmachine_types.go +++ b/api/v1beta2/awsmachine_types.go @@ -245,6 +245,13 @@ 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" (default): The instance may make 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 + // +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..a426cba7b1 100644 --- a/api/v1beta2/types.go +++ b/api/v1beta2/types.go @@ -285,8 +285,31 @@ 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" (default): The instance may make 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 + // +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 is a CapacityReservationPreference enum value + CapacityReservationPreferenceNone CapacityReservationPreference = "None" + + // CapacityReservationPreferenceOnly is a CapacityReservationPreference enum value + CapacityReservationPreferenceOnly CapacityReservationPreference = "CapacityReservationsOnly" + + // CapacityReservationPreferenceOpen is a CapacityReservationPreference enum value + 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..2fbd2ac5b9 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,17 @@ spec: description: CapacityReservationID specifies the target Capacity Reservation into which the instance should be launched. type: string + capacityReservationPreference: + description: |- + CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: + "Open" (default): The instance may make 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 + enum: + - None + - CapacityReservationsOnly + - Open + type: string ebsOptimized: description: Indicates whether the instance is optimized for Amazon EBS I/O. @@ -3410,6 +3421,17 @@ spec: description: CapacityReservationID specifies the target Capacity Reservation into which the instance should be launched. type: string + capacityReservationPreference: + description: |- + CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: + "Open" (default): The instance may make 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 + enum: + - None + - CapacityReservationsOnly + - Open + 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..a9defbaf76 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,17 @@ spec: description: CapacityReservationID specifies the target Capacity Reservation into which the instance should be launched. type: string + capacityReservationPreference: + description: |- + CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: + "Open" (default): The instance may make 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 + enum: + - None + - CapacityReservationsOnly + - Open + 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..b302db0d26 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,17 @@ spec: description: CapacityReservationID specifies the target Capacity Reservation into which the instance should be launched. type: string + capacityReservationPreference: + description: |- + CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: + "Open" (default): The instance may make 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 + enum: + - None + - CapacityReservationsOnly + - Open + 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..c5428c329c 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,17 @@ spec: description: CapacityReservationID specifies the target Capacity Reservation into which the instance should be launched. type: string + capacityReservationPreference: + description: |- + CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: + "Open" (default): The instance may make 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 + enum: + - None + - CapacityReservationsOnly + - Open + 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..ea034f6596 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,17 @@ spec: description: CapacityReservationID specifies the target Capacity Reservation into which the instance should be launched. type: string + capacityReservationPreference: + description: |- + CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: + "Open" (default): The instance may make 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 + enum: + - None + - CapacityReservationsOnly + - Open + 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..7fbbd1269f 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,17 @@ spec: description: CapacityReservationID specifies the target Capacity Reservation into which the instance should be launched. type: string + capacityReservationPreference: + description: |- + CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: + "Open" (default): The instance may make 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 + enum: + - None + - CapacityReservationsOnly + - Open + 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..efa9ee44a0 100644 --- a/exp/api/v1beta2/types.go +++ b/exp/api/v1beta2/types.go @@ -146,6 +146,13 @@ 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" (default): The instance may make 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 + // +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/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..188aea9527 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,32 @@ func SDKToSpotMarketOptions(instanceMarketOptions *types.LaunchTemplateInstanceM return result } +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 "" + } +} + +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 From bc23ec5ede41d946792fc5f6d8b6364dc34fd5d0 Mon Sep 17 00:00:00 2001 From: BraeTroutman Date: Fri, 8 Aug 2025 10:24:26 -0400 Subject: [PATCH 2/4] correct linter errors --- api/v1beta2/awsmachine_types.go | 2 +- api/v1beta2/types.go | 2 +- exp/api/v1beta2/types.go | 2 +- pkg/cloud/services/ec2/helper_test.go | 39 ------------------------ pkg/cloud/services/ec2/launchtemplate.go | 4 +++ 5 files changed, 7 insertions(+), 42 deletions(-) diff --git a/api/v1beta2/awsmachine_types.go b/api/v1beta2/awsmachine_types.go index 742e4ef36e..420280282c 100644 --- a/api/v1beta2/awsmachine_types.go +++ b/api/v1beta2/awsmachine_types.go @@ -247,7 +247,7 @@ type AWSMachineSpec struct { HostAffinity *string `json:"hostAffinity,omitempty"` // CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: - // "Open" (default): The instance may make make use of open Capacity Reservations that match its AZ and InstanceType + // "Open" (default): 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 // +optional diff --git a/api/v1beta2/types.go b/api/v1beta2/types.go index a426cba7b1..6d1b3d6785 100644 --- a/api/v1beta2/types.go +++ b/api/v1beta2/types.go @@ -287,7 +287,7 @@ type Instance struct { HostID *string `json:"hostID,omitempty"` // CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: - // "Open" (default): The instance may make make use of open Capacity Reservations that match its AZ and InstanceType + // "Open" (default): 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 // +optional diff --git a/exp/api/v1beta2/types.go b/exp/api/v1beta2/types.go index efa9ee44a0..f1221ba9d3 100644 --- a/exp/api/v1beta2/types.go +++ b/exp/api/v1beta2/types.go @@ -148,7 +148,7 @@ type AWSLaunchTemplate struct { MarketType infrav1.MarketType `json:"marketType,omitempty"` // CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: - // "Open" (default): The instance may make make use of open Capacity Reservations that match its AZ and InstanceType + // "Open" (default): 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 // +optional 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/launchtemplate.go b/pkg/cloud/services/ec2/launchtemplate.go index 188aea9527..ad664ebe6d 100644 --- a/pkg/cloud/services/ec2/launchtemplate.go +++ b/pkg/cloud/services/ec2/launchtemplate.go @@ -848,6 +848,8 @@ 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: @@ -861,6 +863,8 @@ func SDKToCapacityReservationPreference(preference types.CapacityReservationPref } } +// 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: From aa0e22144d5ad0c43bd9c37e0efdf6958baac455 Mon Sep 17 00:00:00 2001 From: BraeTroutman Date: Fri, 8 Aug 2025 10:35:53 -0400 Subject: [PATCH 3/4] revise and regenerate doc comments and kubebuilder validations --- api/v1beta2/types.go | 11 +++--- ...ster.x-k8s.io_awsmanagedcontrolplanes.yaml | 34 +++++++++++++------ ...tructure.cluster.x-k8s.io_awsclusters.yaml | 17 +++++++--- ...ture.cluster.x-k8s.io_awsmachinepools.yaml | 3 +- ...tructure.cluster.x-k8s.io_awsmachines.yaml | 3 +- ....cluster.x-k8s.io_awsmachinetemplates.yaml | 3 +- ...uster.x-k8s.io_awsmanagedmachinepools.yaml | 3 +- 7 files changed, 50 insertions(+), 24 deletions(-) diff --git a/api/v1beta2/types.go b/api/v1beta2/types.go index 6d1b3d6785..0f8d155515 100644 --- a/api/v1beta2/types.go +++ b/api/v1beta2/types.go @@ -287,26 +287,27 @@ type Instance struct { HostID *string `json:"hostID,omitempty"` // CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: - // "Open" (default): The instance may make use of open Capacity Reservations that match its AZ and InstanceType + // "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 +// +kubebuilder:validation:Enum:="";None;CapacityReservationsOnly;Open type CapacityReservationPreference string const ( - // CapacityReservationPreferenceNone is a CapacityReservationPreference enum value + // CapacityReservationPreferenceNone the instance may not make use of any Capacity Reservations. This is to conserve open reservations for desired workloads CapacityReservationPreferenceNone CapacityReservationPreference = "None" - // CapacityReservationPreferenceOnly is a CapacityReservationPreference enum value + // CapacityReservationPreferenceOnly the instance will only run if matched or targeted to a Capacity Reservation CapacityReservationPreferenceOnly CapacityReservationPreference = "CapacityReservationsOnly" - // CapacityReservationPreferenceOpen is a CapacityReservationPreference enum value + // CapacityReservationPreferenceOpen the instance may make use of open Capacity Reservations that match its AZ and InstanceType. CapacityReservationPreferenceOpen CapacityReservationPreference = "Open" ) 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 2fbd2ac5b9..4bc4ca8e70 100644 --- a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml +++ b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml @@ -1215,15 +1215,22 @@ spec: 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" (default): The instance may make make use of open Capacity Reservations that match its AZ and InstanceType + "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 - enum: - - None - - CapacityReservationsOnly - - Open type: string ebsOptimized: description: Indicates whether the instance is optimized for Amazon @@ -3422,15 +3429,22 @@ spec: 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" (default): The instance may make make use of open Capacity Reservations that match its AZ and InstanceType + "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 - enum: - - None - - CapacityReservationsOnly - - Open type: string ebsOptimized: description: Indicates whether the instance is optimized for Amazon 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 a9defbaf76..4e0e03d0bf 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml @@ -2198,15 +2198,22 @@ spec: 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" (default): The instance may make make use of open Capacity Reservations that match its AZ and InstanceType + "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 - enum: - - None - - CapacityReservationsOnly - - Open type: string ebsOptimized: description: Indicates whether the instance is optimized for Amazon 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 b302db0d26..158ac91b67 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinepools.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinepools.yaml @@ -647,10 +647,11 @@ spec: capacityReservationPreference: description: |- CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: - "Open" (default): The instance may make make use of open Capacity Reservations that match its AZ and InstanceType + "Open" (default): 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 enum: + - "" - None - CapacityReservationsOnly - Open 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 c5428c329c..1e106bf563 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml @@ -644,10 +644,11 @@ spec: capacityReservationPreference: description: |- CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: - "Open" (default): The instance may make make use of open Capacity Reservations that match its AZ and InstanceType + "Open" (default): 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 enum: + - "" - None - CapacityReservationsOnly - Open 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 ea034f6596..bf5ac473fb 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml @@ -563,10 +563,11 @@ spec: capacityReservationPreference: description: |- CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: - "Open" (default): The instance may make make use of open Capacity Reservations that match its AZ and InstanceType + "Open" (default): 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 enum: + - "" - None - CapacityReservationsOnly - Open 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 7fbbd1269f..eb260d8d3a 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedmachinepools.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedmachinepools.yaml @@ -656,10 +656,11 @@ spec: capacityReservationPreference: description: |- CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: - "Open" (default): The instance may make make use of open Capacity Reservations that match its AZ and InstanceType + "Open" (default): 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 enum: + - "" - None - CapacityReservationsOnly - Open From 108a81f49f7f66109cb87206879072698de78531 Mon Sep 17 00:00:00 2001 From: BraeTroutman Date: Mon, 11 Aug 2025 12:28:22 -0400 Subject: [PATCH 4/4] ensure kubebuilder enum enforcement on all fields making use of CapacityReservationPreference type --- api/v1beta2/awsmachine_types.go | 3 ++- ...cture.cluster.x-k8s.io_awsmachinepools.yaml | 18 ++++++++++++------ ...structure.cluster.x-k8s.io_awsmachines.yaml | 18 ++++++++++++------ ...e.cluster.x-k8s.io_awsmachinetemplates.yaml | 18 ++++++++++++------ ...luster.x-k8s.io_awsmanagedmachinepools.yaml | 18 ++++++++++++------ exp/api/v1beta2/types.go | 3 ++- 6 files changed, 52 insertions(+), 26 deletions(-) diff --git a/api/v1beta2/awsmachine_types.go b/api/v1beta2/awsmachine_types.go index 420280282c..16f85dbe15 100644 --- a/api/v1beta2/awsmachine_types.go +++ b/api/v1beta2/awsmachine_types.go @@ -247,9 +247,10 @@ type AWSMachineSpec struct { HostAffinity *string `json:"hostAffinity,omitempty"` // CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: - // "Open" (default): The instance may make use of open Capacity Reservations that match its AZ and InstanceType + // "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"` } 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 158ac91b67..893efb3e32 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinepools.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinepools.yaml @@ -645,16 +645,22 @@ spec: 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" (default): The instance may make use of open Capacity Reservations that match its AZ and InstanceType + "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 - enum: - - "" - - None - - CapacityReservationsOnly - - Open type: string iamInstanceProfile: description: |- 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 1e106bf563..6ca5ccb7a2 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml @@ -642,16 +642,22 @@ spec: 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" (default): The instance may make use of open Capacity Reservations that match its AZ and InstanceType + "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 - enum: - - "" - - None - - CapacityReservationsOnly - - Open type: string cloudInit: description: |- 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 bf5ac473fb..faff683e2e 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml @@ -561,16 +561,22 @@ spec: 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" (default): The instance may make use of open Capacity Reservations that match its AZ and InstanceType + "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 - enum: - - "" - - None - - CapacityReservationsOnly - - Open type: string cloudInit: description: |- 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 eb260d8d3a..e0504a4c40 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedmachinepools.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedmachinepools.yaml @@ -654,16 +654,22 @@ spec: 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" (default): The instance may make use of open Capacity Reservations that match its AZ and InstanceType + "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 - enum: - - "" - - None - - CapacityReservationsOnly - - Open type: string iamInstanceProfile: description: |- diff --git a/exp/api/v1beta2/types.go b/exp/api/v1beta2/types.go index f1221ba9d3..b4eca931a8 100644 --- a/exp/api/v1beta2/types.go +++ b/exp/api/v1beta2/types.go @@ -148,9 +148,10 @@ type AWSLaunchTemplate struct { MarketType infrav1.MarketType `json:"marketType,omitempty"` // CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include: - // "Open" (default): The instance may make use of open Capacity Reservations that match its AZ and InstanceType + // "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"` }