diff --git a/api/v1beta1/awscluster_conversion.go b/api/v1beta1/awscluster_conversion.go index 3fa974f07f..94f8078f96 100644 --- a/api/v1beta1/awscluster_conversion.go +++ b/api/v1beta1/awscluster_conversion.go @@ -60,7 +60,7 @@ func (src *AWSCluster) ConvertTo(dstRaw conversion.Hub) error { dst.Status.Bastion.PrivateDNSName = restored.Status.Bastion.PrivateDNSName dst.Status.Bastion.PublicIPOnLaunch = restored.Status.Bastion.PublicIPOnLaunch dst.Status.Bastion.CapacityReservationID = restored.Status.Bastion.CapacityReservationID - dst.Status.Bastion.UseCapacityBlock = restored.Status.Bastion.UseCapacityBlock + dst.Status.Bastion.MarketType = restored.Status.Bastion.MarketType } dst.Spec.Partition = restored.Spec.Partition diff --git a/api/v1beta1/awsmachine_conversion.go b/api/v1beta1/awsmachine_conversion.go index 4a132dc5cb..829bdc912b 100644 --- a/api/v1beta1/awsmachine_conversion.go +++ b/api/v1beta1/awsmachine_conversion.go @@ -42,7 +42,7 @@ func (src *AWSMachine) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.PrivateDNSName = restored.Spec.PrivateDNSName dst.Spec.SecurityGroupOverrides = restored.Spec.SecurityGroupOverrides dst.Spec.CapacityReservationID = restored.Spec.CapacityReservationID - dst.Spec.UseCapacityBlock = restored.Spec.UseCapacityBlock + dst.Spec.MarketType = restored.Spec.MarketType if restored.Spec.ElasticIPPool != nil { if dst.Spec.ElasticIPPool == nil { dst.Spec.ElasticIPPool = &infrav1.ElasticIPPool{} @@ -105,7 +105,7 @@ func (r *AWSMachineTemplate) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Template.Spec.PrivateDNSName = restored.Spec.Template.Spec.PrivateDNSName dst.Spec.Template.Spec.SecurityGroupOverrides = restored.Spec.Template.Spec.SecurityGroupOverrides dst.Spec.Template.Spec.CapacityReservationID = restored.Spec.Template.Spec.CapacityReservationID - dst.Spec.Template.Spec.UseCapacityBlock = restored.Spec.Template.Spec.UseCapacityBlock + dst.Spec.Template.Spec.MarketType = restored.Spec.Template.Spec.MarketType if restored.Spec.Template.Spec.ElasticIPPool != nil { if dst.Spec.Template.Spec.ElasticIPPool == nil { dst.Spec.Template.Spec.ElasticIPPool = &infrav1.ElasticIPPool{} diff --git a/api/v1beta1/zz_generated.conversion.go b/api/v1beta1/zz_generated.conversion.go index 56ea41f118..286b6e7530 100644 --- a/api/v1beta1/zz_generated.conversion.go +++ b/api/v1beta1/zz_generated.conversion.go @@ -1434,7 +1434,7 @@ func autoConvert_v1beta2_AWSMachineSpec_To_v1beta1_AWSMachineSpec(in *v1beta2.AW out.Tenancy = in.Tenancy // 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.UseCapacityBlock requires manual conversion: does not exist in peer-type + // WARNING: in.MarketType requires manual conversion: does not exist in peer-type return nil } @@ -2037,7 +2037,7 @@ func autoConvert_v1beta2_Instance_To_v1beta1_Instance(in *v1beta2.Instance, out // WARNING: in.PrivateDNSName requires manual conversion: does not exist in peer-type // WARNING: in.PublicIPOnLaunch requires manual conversion: does not exist in peer-type // WARNING: in.CapacityReservationID requires manual conversion: does not exist in peer-type - // WARNING: in.UseCapacityBlock requires manual conversion: does not exist in peer-type + // WARNING: in.MarketType 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 4aed7dd1d1..7ac20897e9 100644 --- a/api/v1beta2/awsmachine_types.go +++ b/api/v1beta2/awsmachine_types.go @@ -198,10 +198,13 @@ type AWSMachineSpec struct { // +optional CapacityReservationID *string `json:"capacityReservationId,omitempty"` - // UseCapacityBlock enables usage of pre-purchased compute capacity (capacity blocks) with AWS Capacity Reservations. - // If enabled, CapacityReservationID must be specified to identify the target reservation. + // MarketType specifies the type of market for the EC2 instance. Valid values include: + // "spot": The instance runs as a Spot instance. When SpotMarketOptions is provided, the MarketType defaults to "spot". + // "capacity-block": The instance utilizes pre-purchased compute capacity (capacity blocks) with AWS Capacity Reservations. + // If this value is selected, CapacityReservationID must be specified to identify the target reservation. + // If MarketType is not specified and SpotMarketOptions is provided, the MarketType defaults to "spot". // +optional - UseCapacityBlock *bool `json:"useCapacityBlock,omitempty"` + MarketType *MarketType `json:"marketType,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 cf729948a4..3e2d9aae32 100644 --- a/api/v1beta2/awsmachine_webhook.go +++ b/api/v1beta2/awsmachine_webhook.go @@ -364,8 +364,8 @@ func (r *AWSMachine) validateNetworkElasticIPPool() field.ErrorList { func (r *AWSMachine) validateInstanceMarketType() field.ErrorList { var allErrs field.ErrorList - if r.Spec.UseCapacityBlock != nil && r.Spec.SpotMarketOptions != nil { - allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "useCapacityBlock"), "useCapacityBlock and spotMarketOptions cannot be used together")) + if ptr.Deref(r.Spec.MarketType, "") == MarketTypeCapacityBlock && r.Spec.SpotMarketOptions != nil { + allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "MarketType"), "MarketType set to capacity-block and spotMarketOptions cannot be used together")) } return allErrs } diff --git a/api/v1beta2/awsmachine_webhook_test.go b/api/v1beta2/awsmachine_webhook_test.go index bf31bff812..876a071628 100644 --- a/api/v1beta2/awsmachine_webhook_test.go +++ b/api/v1beta2/awsmachine_webhook_test.go @@ -218,10 +218,10 @@ func TestAWSMachineCreate(t *testing.T) { wantErr: false, }, { - name: "invalid useCapacityBlock and spotMarketOptions are specified", + name: "invalid case, MarketType set to MarketTypeCapacityBlock and spotMarketOptions are specified", machine: &AWSMachine{ Spec: AWSMachineSpec{ - UseCapacityBlock: aws.Bool(true), + MarketType: ptr.To(MarketTypeCapacityBlock), SpotMarketOptions: &SpotMarketOptions{}, InstanceType: "test", }, @@ -229,11 +229,11 @@ func TestAWSMachineCreate(t *testing.T) { wantErr: true, }, { - name: "valid useCapacityBlock is specified", + name: "valid MarketType set to MarketTypeCapacityBlock is specified", machine: &AWSMachine{ Spec: AWSMachineSpec{ - UseCapacityBlock: aws.Bool(true), - InstanceType: "test", + MarketType: ptr.To(MarketTypeCapacityBlock), + InstanceType: "test", }, }, wantErr: false, diff --git a/api/v1beta2/types.go b/api/v1beta2/types.go index 0fe959f2bf..d60da62e0a 100644 --- a/api/v1beta2/types.go +++ b/api/v1beta2/types.go @@ -262,12 +262,30 @@ type Instance struct { // +optional CapacityReservationID *string `json:"capacityReservationId,omitempty"` - // UseCapacityBlock enables usage of pre-purchased compute capacity (capacity blocks) with AWS Capacity Reservations. - // If enabled, CapacityReservationID must be specified to identify the target reservation. + // MarketType specifies the type of market for the EC2 instance. Valid values include: + // "on-demand" (default): The instance runs as a standard On-Demand instance. + // "spot": The instance runs as a Spot instance. When SpotMarketOptions is provided, the MarketType defaults to "spot". + // "capacity-block": The instance utilizes pre-purchased compute capacity (capacity blocks) with AWS Capacity Reservations. + // If this value is selected, CapacityReservationID must be specified to identify the target reservation. + // If MarketType is not specified and SpotMarketOptions is provided, the MarketType defaults to "spot". // +optional - UseCapacityBlock *bool `json:"useCapacityBlock,omitempty"` + MarketType *MarketType `json:"marketType,omitempty"` } +// MarketType describes the market type of an Instance +type MarketType string + +const ( + // MarketTypeOnDemand is a MarketType enum value + MarketTypeOnDemand MarketType = "on-demand" + + // MarketTypeSpot is a MarketType enum value + MarketTypeSpot MarketType = "spot" + + // MarketTypeCapacityBlock is a MarketType enum value + MarketTypeCapacityBlock MarketType = "capacity-block" +) + // InstanceMetadataState describes the state of InstanceMetadataOptions.HttpEndpoint and InstanceMetadataOptions.InstanceMetadataTags type InstanceMetadataState string diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index 81ff8d302d..2b6bc55664 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -772,9 +772,9 @@ func (in *AWSMachineSpec) DeepCopyInto(out *AWSMachineSpec) { *out = new(string) **out = **in } - if in.UseCapacityBlock != nil { - in, out := &in.UseCapacityBlock, &out.UseCapacityBlock - *out = new(bool) + if in.MarketType != nil { + in, out := &in.MarketType, &out.MarketType + *out = new(MarketType) **out = **in } } @@ -1609,9 +1609,9 @@ func (in *Instance) DeepCopyInto(out *Instance) { *out = new(string) **out = **in } - if in.UseCapacityBlock != nil { - in, out := &in.UseCapacityBlock, &out.UseCapacityBlock - *out = new(bool) + if in.MarketType != nil { + in, out := &in.MarketType, &out.MarketType + *out = new(MarketType) **out = **in } } 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 95bf5b6bdb..b5e24bd32b 100644 --- a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml +++ b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml @@ -1210,6 +1210,15 @@ spec: instanceState: description: The current state of the instance. type: string + marketType: + description: |- + MarketType specifies the type of market for the EC2 instance. Valid values include: + "on-demand" (default): The instance runs as a standard On-Demand instance. + "spot": The instance runs as a Spot instance. When SpotMarketOptions is provided, the MarketType defaults to "spot". + "capacity-block": The instance utilizes pre-purchased compute capacity (capacity blocks) with AWS Capacity Reservations. + If this value is selected, CapacityReservationID must be specified to identify the target reservation. + If MarketType is not specified and SpotMarketOptions is provided, the MarketType defaults to "spot". + type: string networkInterfaces: description: Specifies ENIs attached to instance items: @@ -1376,11 +1385,6 @@ spec: type: description: The instance type. type: string - useCapacityBlock: - description: |- - UseCapacityBlock enables usage of pre-purchased compute capacity (capacity blocks) with AWS Capacity Reservations. - If enabled, CapacityReservationID must be specified to identify the target reservation. - type: boolean userData: description: |- UserData is the raw data script passed to the instance which is run upon bootstrap. @@ -3255,6 +3259,15 @@ spec: instanceState: description: The current state of the instance. type: string + marketType: + description: |- + MarketType specifies the type of market for the EC2 instance. Valid values include: + "on-demand" (default): The instance runs as a standard On-Demand instance. + "spot": The instance runs as a Spot instance. When SpotMarketOptions is provided, the MarketType defaults to "spot". + "capacity-block": The instance utilizes pre-purchased compute capacity (capacity blocks) with AWS Capacity Reservations. + If this value is selected, CapacityReservationID must be specified to identify the target reservation. + If MarketType is not specified and SpotMarketOptions is provided, the MarketType defaults to "spot". + type: string networkInterfaces: description: Specifies ENIs attached to instance items: @@ -3421,11 +3434,6 @@ spec: type: description: The instance type. type: string - useCapacityBlock: - description: |- - UseCapacityBlock enables usage of pre-purchased compute capacity (capacity blocks) with AWS Capacity Reservations. - If enabled, CapacityReservationID must be specified to identify the target reservation. - type: boolean userData: description: |- UserData is the raw data script passed to the instance which is run upon bootstrap. 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 6bb89799c4..bb28e6635d 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml @@ -2177,6 +2177,15 @@ spec: instanceState: description: The current state of the instance. type: string + marketType: + description: |- + MarketType specifies the type of market for the EC2 instance. Valid values include: + "on-demand" (default): The instance runs as a standard On-Demand instance. + "spot": The instance runs as a Spot instance. When SpotMarketOptions is provided, the MarketType defaults to "spot". + "capacity-block": The instance utilizes pre-purchased compute capacity (capacity blocks) with AWS Capacity Reservations. + If this value is selected, CapacityReservationID must be specified to identify the target reservation. + If MarketType is not specified and SpotMarketOptions is provided, the MarketType defaults to "spot". + type: string networkInterfaces: description: Specifies ENIs attached to instance items: @@ -2343,11 +2352,6 @@ spec: type: description: The instance type. type: string - useCapacityBlock: - description: |- - UseCapacityBlock enables usage of pre-purchased compute capacity (capacity blocks) with AWS Capacity Reservations. - If enabled, CapacityReservationID must be specified to identify the target reservation. - type: boolean userData: description: |- UserData is the raw data script passed to the instance which is run upon bootstrap. 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 135fcecfa4..c98fdb148f 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinepools.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinepools.yaml @@ -728,6 +728,14 @@ spec: description: 'InstanceType is the type of instance to create. Example: m4.xlarge' type: string + marketType: + description: |- + MarketType specifies the type of market for the EC2 instance. Valid values include: + "spot": The instance runs as a Spot instance. When SpotMarketOptions is provided, the MarketType defaults to "spot". + "capacity-block": The instance utilizes pre-purchased compute capacity (capacity blocks) with AWS Capacity Reservations. + If this value is selected, CapacityReservationID must be specified to identify the target reservation. + If MarketType is not specified and SpotMarketOptions is provided, the MarketType defaults to "spot". + type: string name: description: The name of the launch template. type: string @@ -850,11 +858,6 @@ spec: SSHKeyName is the name of the ssh key to attach to the instance. Valid values are empty string (do not use SSH keys), a valid SSH key name, or omitted (use the default SSH key name) type: string - useCapacityBlock: - description: |- - UseCapacityBlock enables usage of pre-purchased compute capacity (capacity blocks) with AWS Capacity Reservations. - If enabled, CapacityReservationID must be specified to identify the target reservation. - type: boolean versionNumber: description: |- VersionNumber is the version of the launch template that is applied. 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 77172ac074..6b7dd98b86 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml @@ -878,6 +878,14 @@ spec: m4.xlarge' minLength: 2 type: string + marketType: + description: |- + MarketType specifies the type of market for the EC2 instance. Valid values include: + "spot": The instance runs as a Spot instance. When SpotMarketOptions is provided, the MarketType defaults to "spot". + "capacity-block": The instance utilizes pre-purchased compute capacity (capacity blocks) with AWS Capacity Reservations. + If this value is selected, CapacityReservationID must be specified to identify the target reservation. + If MarketType is not specified and SpotMarketOptions is provided, the MarketType defaults to "spot". + type: string networkInterfaces: description: |- NetworkInterfaces is a list of ENIs to associate with the instance. @@ -1080,11 +1088,6 @@ spec: cloud-init has built-in support for gzip-compressed user data user data stored in aws secret manager is always gzip-compressed. type: boolean - useCapacityBlock: - description: |- - UseCapacityBlock enables usage of pre-purchased compute capacity (capacity blocks) with AWS Capacity Reservations. - If enabled, CapacityReservationID must be specified to identify the target reservation. - type: boolean required: - instanceType type: object 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 18f94fdb50..02a3ae90d0 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml @@ -812,6 +812,14 @@ spec: Example: m4.xlarge' minLength: 2 type: string + marketType: + description: |- + MarketType specifies the type of market for the EC2 instance. Valid values include: + "spot": The instance runs as a Spot instance. When SpotMarketOptions is provided, the MarketType defaults to "spot". + "capacity-block": The instance utilizes pre-purchased compute capacity (capacity blocks) with AWS Capacity Reservations. + If this value is selected, CapacityReservationID must be specified to identify the target reservation. + If MarketType is not specified and SpotMarketOptions is provided, the MarketType defaults to "spot". + type: string networkInterfaces: description: |- NetworkInterfaces is a list of ENIs to associate with the instance. @@ -1021,11 +1029,6 @@ spec: cloud-init has built-in support for gzip-compressed user data user data stored in aws secret manager is always gzip-compressed. type: boolean - useCapacityBlock: - description: |- - UseCapacityBlock enables usage of pre-purchased compute capacity (capacity blocks) with AWS Capacity Reservations. - If enabled, CapacityReservationID must be specified to identify the target reservation. - type: boolean required: - instanceType type: object 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 119dfae34a..d0db326cf4 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedmachinepools.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedmachinepools.yaml @@ -724,6 +724,14 @@ spec: description: 'InstanceType is the type of instance to create. Example: m4.xlarge' type: string + marketType: + description: |- + MarketType specifies the type of market for the EC2 instance. Valid values include: + "spot": The instance runs as a Spot instance. When SpotMarketOptions is provided, the MarketType defaults to "spot". + "capacity-block": The instance utilizes pre-purchased compute capacity (capacity blocks) with AWS Capacity Reservations. + If this value is selected, CapacityReservationID must be specified to identify the target reservation. + If MarketType is not specified and SpotMarketOptions is provided, the MarketType defaults to "spot". + type: string name: description: The name of the launch template. type: string @@ -846,11 +854,6 @@ spec: SSHKeyName is the name of the ssh key to attach to the instance. Valid values are empty string (do not use SSH keys), a valid SSH key name, or omitted (use the default SSH key name) type: string - useCapacityBlock: - description: |- - UseCapacityBlock enables usage of pre-purchased compute capacity (capacity blocks) with AWS Capacity Reservations. - If enabled, CapacityReservationID must be specified to identify the target reservation. - type: boolean versionNumber: description: |- VersionNumber is the version of the launch template that is applied. diff --git a/exp/api/v1beta1/conversion.go b/exp/api/v1beta1/conversion.go index 8ad51f3839..e188ec4f98 100644 --- a/exp/api/v1beta1/conversion.go +++ b/exp/api/v1beta1/conversion.go @@ -61,8 +61,8 @@ func (src *AWSMachinePool) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.AWSLaunchTemplate.CapacityReservationID = restored.Spec.AWSLaunchTemplate.CapacityReservationID } - if restored.Spec.AWSLaunchTemplate.UseCapacityBlock != nil { - dst.Spec.AWSLaunchTemplate.UseCapacityBlock = restored.Spec.AWSLaunchTemplate.UseCapacityBlock + if restored.Spec.AWSLaunchTemplate.MarketType != nil { + dst.Spec.AWSLaunchTemplate.MarketType = restored.Spec.AWSLaunchTemplate.MarketType } dst.Spec.DefaultInstanceWarmup = restored.Spec.DefaultInstanceWarmup @@ -121,8 +121,8 @@ func (src *AWSManagedMachinePool) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.AWSLaunchTemplate.CapacityReservationID = restored.Spec.AWSLaunchTemplate.CapacityReservationID } - if restored.Spec.AWSLaunchTemplate.UseCapacityBlock != nil { - dst.Spec.AWSLaunchTemplate.UseCapacityBlock = restored.Spec.AWSLaunchTemplate.UseCapacityBlock + if restored.Spec.AWSLaunchTemplate.MarketType != nil { + dst.Spec.AWSLaunchTemplate.MarketType = restored.Spec.AWSLaunchTemplate.MarketType } } diff --git a/exp/api/v1beta1/zz_generated.conversion.go b/exp/api/v1beta1/zz_generated.conversion.go index da119ccc0f..3cb29cd7b1 100644 --- a/exp/api/v1beta1/zz_generated.conversion.go +++ b/exp/api/v1beta1/zz_generated.conversion.go @@ -415,7 +415,7 @@ func autoConvert_v1beta2_AWSLaunchTemplate_To_v1beta1_AWSLaunchTemplate(in *v1be // WARNING: in.InstanceMetadataOptions requires manual conversion: does not exist in peer-type // 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.UseCapacityBlock requires manual conversion: does not exist in peer-type + // WARNING: in.MarketType 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 fce1a61031..7652044ef7 100644 --- a/exp/api/v1beta2/awsmachinepool_webhook.go +++ b/exp/api/v1beta2/awsmachinepool_webhook.go @@ -23,6 +23,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/klog/v2" + "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" @@ -191,8 +192,8 @@ func (r *AWSMachinePool) ValidateCreate() (admission.Warnings, error) { func (r *AWSMachinePool) validateInstanceMarketType() field.ErrorList { var allErrs field.ErrorList - if r.Spec.AWSLaunchTemplate.UseCapacityBlock != nil && r.Spec.AWSLaunchTemplate.SpotMarketOptions != nil { - allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "useCapacityBlock"), "useCapacityBlock and spotMarketOptions cannot be used together")) + if ptr.Deref(r.Spec.AWSLaunchTemplate.MarketType, "") == v1beta2.MarketTypeCapacityBlock && r.Spec.AWSLaunchTemplate.SpotMarketOptions != nil { + allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "MarketType"), "setting MarketType to MarketTypeCapacityBlock and spotMarketOptions cannot be used together")) } return allErrs } diff --git a/exp/api/v1beta2/awsmachinepool_webhook_test.go b/exp/api/v1beta2/awsmachinepool_webhook_test.go index 65ecfb21dd..09fcf4399a 100644 --- a/exp/api/v1beta2/awsmachinepool_webhook_test.go +++ b/exp/api/v1beta2/awsmachinepool_webhook_test.go @@ -175,11 +175,11 @@ func TestAWSMachinePoolValidateCreate(t *testing.T) { wantErr: true, }, { - name: "invalid useCapacityBlock and spotMarketOptions are specified", + name: "invalid, MarketType set to MarketTypeCapacityBlock and spotMarketOptions are specified", pool: &AWSMachinePool{ Spec: AWSMachinePoolSpec{ AWSLaunchTemplate: AWSLaunchTemplate{ - UseCapacityBlock: aws.Bool(true), + MarketType: ptr.To(infrav1.MarketTypeCapacityBlock), SpotMarketOptions: &infrav1.SpotMarketOptions{}, }, }, @@ -187,11 +187,11 @@ func TestAWSMachinePoolValidateCreate(t *testing.T) { wantErr: true, }, { - name: "valid useCapacityBlock is specified", + name: "valid MarketType set to MarketTypeCapacityBlock is specified", pool: &AWSMachinePool{ Spec: AWSMachinePoolSpec{ AWSLaunchTemplate: AWSLaunchTemplate{ - UseCapacityBlock: aws.Bool(true), + MarketType: ptr.To(infrav1.MarketTypeCapacityBlock), }, }, }, diff --git a/exp/api/v1beta2/types.go b/exp/api/v1beta2/types.go index 9622786073..6c54c7f3e9 100644 --- a/exp/api/v1beta2/types.go +++ b/exp/api/v1beta2/types.go @@ -133,10 +133,13 @@ type AWSLaunchTemplate struct { // +optional CapacityReservationID *string `json:"capacityReservationId,omitempty"` - // UseCapacityBlock enables usage of pre-purchased compute capacity (capacity blocks) with AWS Capacity Reservations. - // If enabled, CapacityReservationID must be specified to identify the target reservation. + // MarketType specifies the type of market for the EC2 instance. Valid values include: + // "spot": The instance runs as a Spot instance. When SpotMarketOptions is provided, the MarketType defaults to "spot". + // "capacity-block": The instance utilizes pre-purchased compute capacity (capacity blocks) with AWS Capacity Reservations. + // If this value is selected, CapacityReservationID must be specified to identify the target reservation. + // If MarketType is not specified and SpotMarketOptions is provided, the MarketType defaults to "spot". // +optional - UseCapacityBlock *bool `json:"useCapacityBlock,omitempty"` + MarketType *infrav1.MarketType `json:"marketType,omitempty"` } // Overrides are used to override the instance type specified by the launch template with multiple diff --git a/exp/api/v1beta2/zz_generated.deepcopy.go b/exp/api/v1beta2/zz_generated.deepcopy.go index 046361f5c1..7d84353659 100644 --- a/exp/api/v1beta2/zz_generated.deepcopy.go +++ b/exp/api/v1beta2/zz_generated.deepcopy.go @@ -141,9 +141,9 @@ func (in *AWSLaunchTemplate) DeepCopyInto(out *AWSLaunchTemplate) { *out = new(string) **out = **in } - if in.UseCapacityBlock != nil { - in, out := &in.UseCapacityBlock, &out.UseCapacityBlock - *out = new(bool) + if in.MarketType != nil { + in, out := &in.MarketType, &out.MarketType + *out = new(apiv1beta2.MarketType) **out = **in } } diff --git a/pkg/cloud/services/ec2/helper_test.go b/pkg/cloud/services/ec2/helper_test.go index dffb5180e4..93ba62f8f9 100644 --- a/pkg/cloud/services/ec2/helper_test.go +++ b/pkg/cloud/services/ec2/helper_test.go @@ -90,7 +90,7 @@ func newAWSCapacityBlockMachinePool() *expinfrav1.AWSMachinePool { AMI: infrav1.AMIReference{}, InstanceType: "t3.large", SSHKeyName: aws.String("default"), - UseCapacityBlock: aws.Bool(true), + MarketType: ptr.To(infrav1.MarketTypeCapacityBlock), CapacityReservationID: aws.String("cr-12345678901234567"), }, }, diff --git a/pkg/cloud/services/ec2/instances.go b/pkg/cloud/services/ec2/instances.go index f0044d06a7..e7d4ed4d0e 100644 --- a/pkg/cloud/services/ec2/instances.go +++ b/pkg/cloud/services/ec2/instances.go @@ -253,7 +253,7 @@ func (s *Service) CreateInstance(scope *scope.MachineScope, userData []byte, use input.CapacityReservationID = scope.AWSMachine.Spec.CapacityReservationID - input.UseCapacityBlock = scope.AWSMachine.Spec.UseCapacityBlock + input.MarketType = scope.AWSMachine.Spec.MarketType s.scope.Debug("Running instance", "machine-role", scope.Role()) s.scope.Debug("Running instance with instance metadata options", "metadata options", input.InstanceMetadataOptions) @@ -1146,47 +1146,53 @@ func getCapacityReservationSpecification(capacityReservationID *string) *ec2.Cap } func getInstanceMarketOptionsRequest(i *infrav1.Instance) (*ec2.InstanceMarketOptionsRequest, error) { - if i.UseCapacityBlock != nil && i.SpotMarketOptions != nil { + marketType := ptr.Deref(i.MarketType, "") + if marketType != "" && marketType == ec2.MarketTypeCapacityBlock && i.SpotMarketOptions != nil { return nil, errors.New("can't create spot capacity-blocks, remove spot market request") } - // Handle Capacity Block case. - if ptr.Deref(i.UseCapacityBlock, false) { + // Infer MarketType if not explicitly set + if i.SpotMarketOptions != nil && marketType == "" { + marketType = infrav1.MarketTypeSpot + } + + switch marketType { + case infrav1.MarketTypeCapacityBlock: if i.CapacityReservationID == nil { return nil, errors.Errorf("capacityReservationID is required when CapacityBlock is enabled") } return &ec2.InstanceMarketOptionsRequest{ MarketType: aws.String(ec2.MarketTypeCapacityBlock), }, nil - } - // Handle Spot instance case. - if i.SpotMarketOptions == nil { - // Instance is not a Spot instance - return nil, nil - } + case infrav1.MarketTypeSpot: + // Set required values for Spot instances + spotOpts := &ec2.SpotMarketOptions{ + // The following two options ensure that: + // - If an instance is interrupted, it is terminated rather than hibernating or stopping + // - No replacement instance will be created if the instance is interrupted + // - If the spot request cannot immediately be fulfilled, it will not be created + // This behaviour should satisfy the 1:1 mapping of Machines to Instances as + // assumed by the Cluster API. + InstanceInterruptionBehavior: aws.String(ec2.InstanceInterruptionBehaviorTerminate), + SpotInstanceType: aws.String(ec2.SpotInstanceTypeOneTime), + } - // Set required values for Spot instances - spotOpts := &ec2.SpotMarketOptions{ - // The following two options ensure that: - // - If an instance is interrupted, it is terminated rather than hibernating or stopping - // - No replacement instance will be created if the instance is interrupted - // - If the spot request cannot immediately be fulfilled, it will not be created - // This behaviour should satisfy the 1:1 mapping of Machines to Instances as - // assumed by the Cluster API. - InstanceInterruptionBehavior: aws.String(ec2.InstanceInterruptionBehaviorTerminate), - SpotInstanceType: aws.String(ec2.SpotInstanceTypeOneTime), - } + if maxPrice := aws.StringValue(i.SpotMarketOptions.MaxPrice); maxPrice != "" { + spotOpts.MaxPrice = aws.String(maxPrice) + } - maxPrice := ptr.Deref(i.SpotMarketOptions.MaxPrice, "") - if maxPrice != "" { - spotOpts.MaxPrice = aws.String(maxPrice) + return &ec2.InstanceMarketOptionsRequest{ + MarketType: aws.String(ec2.MarketTypeSpot), + SpotOptions: spotOpts, + }, nil + case infrav1.MarketTypeOnDemand, "": + // Instance is on-demand or empty + return nil, nil + default: + // Invalid MarketType provided + return nil, errors.Errorf("invalid MarketType %s, must be spot/capacity-block or empty", marketType) } - - return &ec2.InstanceMarketOptionsRequest{ - MarketType: aws.String(ec2.MarketTypeSpot), - SpotOptions: spotOpts, - }, nil } func getInstanceMetadataOptionsRequest(metadataOptions *infrav1.InstanceMetadataOptions) *ec2.InstanceMetadataOptionsRequest { diff --git a/pkg/cloud/services/ec2/instances_test.go b/pkg/cloud/services/ec2/instances_test.go index 9c33f3bf85..e7b14eb94d 100644 --- a/pkg/cloud/services/ec2/instances_test.go +++ b/pkg/cloud/services/ec2/instances_test.go @@ -5198,7 +5198,7 @@ func TestCreateInstance(t *testing.T) { }, }, { - name: "Simple, setting CapacityBlock and providing CapacityReservationID", + name: "Simple, setting MarketType to MarketTypeCapacityBlock and providing CapacityReservationID", machine: &clusterv1.Machine{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"set": "node"}, @@ -5214,7 +5214,7 @@ func TestCreateInstance(t *testing.T) { ID: aws.String("abc"), }, InstanceType: "m5.large", - UseCapacityBlock: aws.Bool(true), + MarketType: ptr.To(infrav1.MarketTypeCapacityBlock), CapacityReservationID: aws.String("cr-12345678901234567"), }, awsCluster: &infrav1.AWSCluster{ @@ -5319,7 +5319,7 @@ func TestCreateInstance(t *testing.T) { }, }, { - name: "expect error when CapacityBlock set but not providing CapacityReservationID", + name: "expect error when MarketType to MarketTypeCapacityBlock set but not providing CapacityReservationID", machine: &clusterv1.Machine{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"set": "node"}, @@ -5336,7 +5336,7 @@ func TestCreateInstance(t *testing.T) { AMI: infrav1.AMIReference{ ID: aws.String("abc"), }, - UseCapacityBlock: aws.Bool(true), + MarketType: ptr.To(infrav1.MarketTypeCapacityBlock), InstanceType: "m5.large", PlacementGroupPartition: 2, UncompressedUserData: &isUncompressedFalse, @@ -5404,7 +5404,7 @@ func TestCreateInstance(t *testing.T) { }, }, { - name: "Simple, setting CapacityBlock to false and proving CapacityReservationID", + name: "Simple, setting not setting MarketType and proving CapacityReservationID", machine: &clusterv1.Machine{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"set": "node"}, @@ -5420,7 +5420,6 @@ func TestCreateInstance(t *testing.T) { ID: aws.String("abc"), }, InstanceType: "m5.large", - UseCapacityBlock: aws.Bool(false), CapacityReservationID: aws.String("cr-12345678901234567"), }, awsCluster: &infrav1.AWSCluster{ @@ -5506,7 +5505,8 @@ func TestCreateInstance(t *testing.T) { Placement: &ec2.Placement{ AvailabilityZone: &az, }, - InstanceLifecycle: aws.String("scheduled"), + CapacityReservationId: aws.String("cr-12345678901234567"), + InstanceLifecycle: aws.String("scheduled"), }, }, }, nil) @@ -5614,6 +5614,20 @@ func TestGetInstanceMarketOptionsRequest(t *testing.T) { }, expectedError: nil, }, + { + name: "with no MarketType", + expectedRequest: nil, + instance: &infrav1.Instance{}, + expectedError: nil, + }, + { + name: "invalid MarketType specified", + expectedRequest: nil, + instance: &infrav1.Instance{ + MarketType: ptr.To(infrav1.MarketType("inValid")), + }, + expectedError: errors.New("invalid MarketType inValid, must be spot/capacity-block or empty"), + }, { name: "with an empty Spot options specified", instance: &infrav1.Instance{ @@ -5668,18 +5682,18 @@ func TestGetInstanceMarketOptionsRequest(t *testing.T) { expectedError: nil, }, { - name: "with a CapacityBlock specified with capacityReservationID set to nil", + name: "with a MarketType to MarketTypeCapacityBlock specified with capacityReservationID set to nil", instance: &infrav1.Instance{ - UseCapacityBlock: aws.Bool(true), + MarketType: ptr.To(infrav1.MarketTypeCapacityBlock), CapacityReservationID: nil, }, expectedRequest: nil, expectedError: errors.Errorf("capacityReservationID is required when CapacityBlock is enabled"), }, { - name: "with a CapacityBlock set to false with capacityReservationID set to nil", + name: "with a MarketType to MarketTypeCapacityBlock with capacityReservationID set to nil", instance: &infrav1.Instance{ - UseCapacityBlock: aws.Bool(true), + MarketType: ptr.To(infrav1.MarketTypeCapacityBlock), CapacityReservationID: mockCapacityReservationID, }, expectedRequest: &ec2.InstanceMarketOptionsRequest{ @@ -5688,9 +5702,9 @@ func TestGetInstanceMarketOptionsRequest(t *testing.T) { expectedError: nil, }, { - name: "with a CapacityBlock set to false with capacityReservationID set and empty Spot options specified", + name: "with a MarketType to MarketTypeCapacityBlock set with capacityReservationID set and empty Spot options specified", instance: &infrav1.Instance{ - UseCapacityBlock: aws.Bool(true), + MarketType: ptr.To(infrav1.MarketTypeCapacityBlock), SpotMarketOptions: &infrav1.SpotMarketOptions{}, CapacityReservationID: mockCapacityReservationID, }, diff --git a/pkg/cloud/services/ec2/launchtemplate.go b/pkg/cloud/services/ec2/launchtemplate.go index 2383521925..fef6b66a99 100644 --- a/pkg/cloud/services/ec2/launchtemplate.go +++ b/pkg/cloud/services/ec2/launchtemplate.go @@ -967,40 +967,49 @@ func (s *Service) getFilteredSecurityGroupIDs(securityGroup infrav1.AWSResourceR } func getLaunchTemplateInstanceMarketOptionsRequest(i *expinfrav1.AWSLaunchTemplate) (*ec2.LaunchTemplateInstanceMarketOptionsRequest, error) { - if i.UseCapacityBlock != nil && i.SpotMarketOptions != nil { + marketType := ptr.Deref(i.MarketType, "") + if marketType != "" && marketType != infrav1.MarketTypeCapacityBlock && i.SpotMarketOptions != nil { return nil, errors.New("can't create spot capacity-blocks, remove spot market request") } - // Handle Capacity Block case. - if ptr.Deref(i.UseCapacityBlock, false) { + // Infer MarketType if not explicitly set + if i.SpotMarketOptions != nil && marketType == "" { + marketType = infrav1.MarketTypeSpot + } + + switch marketType { + case infrav1.MarketTypeCapacityBlock: + // Handle Capacity Block case. if i.CapacityReservationID == nil { return nil, errors.Errorf("capacityReservationID is required when CapacityBlock is enabled") } return &ec2.LaunchTemplateInstanceMarketOptionsRequest{ MarketType: aws.String(ec2.MarketTypeCapacityBlock), }, nil - } - - if i.SpotMarketOptions == nil { - // Instance is not a Spot instance - return nil, nil - } - // Set required values for Spot instances - spotOptions := &ec2.LaunchTemplateSpotMarketOptionsRequest{} + case infrav1.MarketTypeSpot: + // Set required values for Spot instances + spotOptions := &ec2.LaunchTemplateSpotMarketOptionsRequest{} - // Persistent option is not available for EC2 autoscaling, EC2 makes a one-time request by default and setting request type should not be allowed. - // For one-time requests, only terminate option is available as interruption behavior, and default for spotOptions.SetInstanceInterruptionBehavior() is terminate, so it is not set here explicitly. + // Persistent option is not available for EC2 autoscaling, EC2 makes a one-time request by default and setting request type should not be allowed. + // For one-time requests, only terminate option is available as interruption behavior, and default for spotOptions.SetInstanceInterruptionBehavior() is terminate, so it is not set here explicitly. - if maxPrice := aws.StringValue(i.SpotMarketOptions.MaxPrice); maxPrice != "" { - spotOptions.SetMaxPrice(maxPrice) - } + if maxPrice := aws.StringValue(i.SpotMarketOptions.MaxPrice); maxPrice != "" { + spotOptions.SetMaxPrice(maxPrice) + } - launchTemplateInstanceMarketOptionsRequest := &ec2.LaunchTemplateInstanceMarketOptionsRequest{} - launchTemplateInstanceMarketOptionsRequest.SetMarketType(ec2.MarketTypeSpot) - launchTemplateInstanceMarketOptionsRequest.SetSpotOptions(spotOptions) + launchTemplateInstanceMarketOptionsRequest := &ec2.LaunchTemplateInstanceMarketOptionsRequest{} + launchTemplateInstanceMarketOptionsRequest.SetMarketType(ec2.MarketTypeSpot) + launchTemplateInstanceMarketOptionsRequest.SetSpotOptions(spotOptions) - return launchTemplateInstanceMarketOptionsRequest, nil + return launchTemplateInstanceMarketOptionsRequest, nil + case infrav1.MarketTypeOnDemand, "": + // Instance is on-demand + return nil, nil + default: + // Invalid MarketType provided + return nil, errors.Errorf("invalid MarketType %s, must be spot/capacity-block or empty", marketType) + } } func getLaunchTemplatePrivateDNSNameOptionsRequest(privateDNSName *infrav1.PrivateDNSName) *ec2.LaunchTemplatePrivateDnsNameOptionsRequest { diff --git a/pkg/cloud/services/ec2/launchtemplate_test.go b/pkg/cloud/services/ec2/launchtemplate_test.go index 80b390b8b3..ff3a337f22 100644 --- a/pkg/cloud/services/ec2/launchtemplate_test.go +++ b/pkg/cloud/services/ec2/launchtemplate_test.go @@ -1076,7 +1076,7 @@ func TestCreateLaunchTemplateVersion(t *testing.T) { awsResourceReference []infrav1.AWSResourceReference expect func(m *mocks.MockEC2APIMockRecorder) wantErr bool - useCapacityBlocks bool + MarketType *string }{ { name: "Should successfully creates launch template version", @@ -1132,7 +1132,7 @@ func TestCreateLaunchTemplateVersion(t *testing.T) { { name: "Should successfully create launch template version with capacity-block", awsResourceReference: []infrav1.AWSResourceReference{{ID: aws.String("1")}}, - useCapacityBlocks: true, + MarketType: aws.String(ec2.MarketTypeCapacityBlock), expect: func(m *mocks.MockEC2APIMockRecorder) { sgMap := make(map[infrav1.SecurityGroupRole]infrav1.SecurityGroup) sgMap[infrav1.SecurityGroupNode] = infrav1.SecurityGroup{ID: "1"} @@ -1239,7 +1239,7 @@ func TestCreateLaunchTemplateVersion(t *testing.T) { g.Expect(err).NotTo(HaveOccurred()) var ms *scope.MachinePoolScope - if tc.useCapacityBlocks { + if aws.StringValue(tc.MarketType) == ec2.MarketTypeCapacityBlock { ms, err = setupCapacityBlocksMachinePoolScope(client, cs) } else { ms, err = setupMachinePoolScope(client, cs)