Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -931,8 +931,22 @@ spec:
name of the managed machine pool.
type: string
instanceType:
description: InstanceType specifies the AWS instance type
description: |-
InstanceType specifies the AWS instance type.
This field is deprecated. Use InstanceTypes instead.
type: string
instanceTypes:
description: |-
InstanceTypes specifies a list of AWS instance types for the node group.
The order of instance types specified determines priority of picking that instance type.
This is also influenced by the CapacityType. For On-Demand capacity, the allocation strategy
uses the order of instance types to determine which instance type to use first when fulfilling capacity.
See AWS documentation https://docs.aws.amazon.com/eks/latest/userguide/managed-node-groups.html#managed-node-group-capacity-types
for more information.
At most one of InstanceType or InstanceTypes may be specified.
items:
type: string
type: array
labels:
additionalProperties:
type: string
Expand Down
24 changes: 23 additions & 1 deletion exp/api/v1beta1/conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,14 @@ func (src *AWSManagedMachinePool) ConvertTo(dstRaw conversion.Hub) error {
dst.Spec.RolePath = restored.Spec.RolePath
dst.Spec.RolePermissionsBoundary = restored.Spec.RolePermissionsBoundary

// Restore InstanceType and InstanceTypes from the hub version
// v1beta1 doesn't have InstanceTypes, but v1beta2 does
// We preserve the exact state through the annotation-based conversion data
// This includes cases where both fields are set (even though webhook would reject it)
// to ensure fuzzy conversion tests pass
dst.Spec.InstanceType = restored.Spec.InstanceType
dst.Spec.InstanceTypes = restored.Spec.InstanceTypes

return nil
}

Expand All @@ -160,12 +168,26 @@ func (r *AWSManagedMachinePool) ConvertFrom(srcRaw conversion.Hub) error {
return err
}

// Preserve v1beta2 data through annotation
// This ensures fuzzy conversion tests pass
return utilconversion.MarshalData(src, r)
}

// Convert_v1beta2_AWSManagedMachinePoolSpec_To_v1beta1_AWSManagedMachinePoolSpec is a conversion function.
func Convert_v1beta2_AWSManagedMachinePoolSpec_To_v1beta1_AWSManagedMachinePoolSpec(in *expinfrav1.AWSManagedMachinePoolSpec, out *AWSManagedMachinePoolSpec, s apiconversion.Scope) error {
return autoConvert_v1beta2_AWSManagedMachinePoolSpec_To_v1beta1_AWSManagedMachinePoolSpec(in, out, s)
if err := autoConvert_v1beta2_AWSManagedMachinePoolSpec_To_v1beta1_AWSManagedMachinePoolSpec(in, out, s); err != nil {
return err
}

// Convert v1beta2 InstanceTypes or InstanceType to v1beta1 InstanceType
// Prefer InstanceTypes if set (use first element), otherwise use InstanceType
if len(in.InstanceTypes) > 0 {
out.InstanceType = &in.InstanceTypes[0]
} else if in.InstanceType != nil {
out.InstanceType = in.InstanceType
}

return nil
}

func Convert_v1beta2_AWSMachinePoolStatus_To_v1beta1_AWSMachinePoolStatus(in *expinfrav1.AWSMachinePoolStatus, out *AWSMachinePoolStatus, s apiconversion.Scope) error {
Expand Down
1 change: 1 addition & 0 deletions exp/api/v1beta1/zz_generated.conversion.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 12 additions & 1 deletion exp/api/v1beta2/awsmanagedmachinepool_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,21 @@ type AWSManagedMachinePoolSpec struct {
// +optional
DiskSize *int32 `json:"diskSize,omitempty"`

// InstanceType specifies the AWS instance type
// InstanceType specifies the AWS instance type.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets mark InstanceType as deprecated as well. Long term, we can remove the field and just rely on the InstanceTypes field.

// This field is deprecated. Use InstanceTypes instead.
// +optional
InstanceType *string `json:"instanceType,omitempty"`

// InstanceTypes specifies a list of AWS instance types for the node group.
// The order of instance types specified determines priority of picking that instance type.
// This is also influenced by the CapacityType. For On-Demand capacity, the allocation strategy
// uses the order of instance types to determine which instance type to use first when fulfilling capacity.
// See AWS documentation https://docs.aws.amazon.com/eks/latest/userguide/managed-node-groups.html#managed-node-group-capacity-types
// for more information.
// At most one of InstanceType or InstanceTypes may be specified.
// +optional
InstanceTypes []string `json:"instanceTypes,omitempty"`

// Scaling specifies scaling for the ASG behind this pool
// +optional
Scaling *ManagedMachinePoolScaling `json:"scaling,omitempty"`
Expand Down
30 changes: 30 additions & 0 deletions exp/api/v1beta2/awsmanagedmachinepool_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,20 @@ func (r *AWSManagedMachinePool) validateRemoteAccess() field.ErrorList {
return allErrs
}

func (r *AWSManagedMachinePool) validateInstanceTypes() field.ErrorList {
var allErrs field.ErrorList

// InstanceType and InstanceTypes are mutually exclusive
if r.Spec.InstanceType != nil && len(r.Spec.InstanceTypes) > 0 {
allErrs = append(allErrs, field.Invalid(
field.NewPath("spec", "InstanceTypes"),
r.Spec.InstanceTypes,
"cannot specify both instanceType and instanceTypes. Use instanceTypes for multiple instance types or instanceType for a single instance type"))
}

return allErrs
}

func (r *AWSManagedMachinePool) validateLaunchTemplate() field.ErrorList {
var allErrs field.ErrorList
if r.Spec.AWSLaunchTemplate == nil {
Expand All @@ -133,6 +147,9 @@ func (r *AWSManagedMachinePool) validateLaunchTemplate() field.ErrorList {
if r.Spec.InstanceType != nil {
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "InstanceType"), r.Spec.InstanceType, "InstanceType cannot be specified when LaunchTemplate is specified"))
}
if len(r.Spec.InstanceTypes) > 0 {
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "InstanceTypes"), r.Spec.InstanceTypes, "InstanceTypes cannot be specified when LaunchTemplate is specified"))
}
if r.Spec.DiskSize != nil {
allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "DiskSize"), r.Spec.DiskSize, "DiskSize cannot be specified when LaunchTemplate is specified"))
}
Expand Down Expand Up @@ -171,6 +188,9 @@ func (*awsManagedMachinePoolWebhook) ValidateCreate(_ context.Context, obj runti
if errs := r.validateNodegroupUpdateConfig(); len(errs) > 0 {
allErrs = append(allErrs, errs...)
}
if errs := r.validateInstanceTypes(); len(errs) > 0 {
allErrs = append(allErrs, errs...)
}
if errs := r.validateLaunchTemplate(); len(errs) > 0 {
allErrs = append(allErrs, errs...)
}
Expand All @@ -180,6 +200,11 @@ func (*awsManagedMachinePoolWebhook) ValidateCreate(_ context.Context, obj runti

allErrs = append(allErrs, r.Spec.AdditionalTags.Validate()...)

// Log deprecation warning for InstanceType field
if r.Spec.InstanceType != nil {
mmpLog.Info("spec.instanceType is deprecated, use spec.instanceTypes instead", "managed-machine-pool", klog.KObj(r))
}

if len(allErrs) == 0 {
return nil, nil
}
Expand Down Expand Up @@ -216,6 +241,9 @@ func (*awsManagedMachinePoolWebhook) ValidateUpdate(_ context.Context, oldObj, n
if errs := r.validateNodegroupUpdateConfig(); len(errs) > 0 {
allErrs = append(allErrs, errs...)
}
if errs := r.validateInstanceTypes(); len(errs) > 0 {
allErrs = append(allErrs, errs...)
}
if errs := r.validateLaunchTemplate(); len(errs) > 0 {
allErrs = append(allErrs, errs...)
}
Expand Down Expand Up @@ -265,6 +293,8 @@ func (r *AWSManagedMachinePool) validateImmutable(old *AWSManagedMachinePool) fi
appendErrorIfMutated(old.Spec.SubnetIDs, r.Spec.SubnetIDs, "subnetIDs")
appendErrorIfSetAndMutated(old.Spec.RoleName, r.Spec.RoleName, "roleName")
appendErrorIfMutated(old.Spec.DiskSize, r.Spec.DiskSize, "diskSize")
appendErrorIfMutated(old.Spec.InstanceType, r.Spec.InstanceType, "instanceType")
appendErrorIfMutated(old.Spec.InstanceTypes, r.Spec.InstanceTypes, "instanceTypes")
appendErrorIfMutated(old.Spec.AMIType, r.Spec.AMIType, "amiType")
appendErrorIfMutated(old.Spec.RemoteAccess, r.Spec.RemoteAccess, "remoteAccess")
appendErrorIfSetAndMutated(old.Spec.CapacityType, r.Spec.CapacityType, "capacityType")
Expand Down
139 changes: 139 additions & 0 deletions exp/api/v1beta2/awsmanagedmachinepool_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,37 @@ func TestAWSManagedMachinePoolValidateCreate(t *testing.T) {
},
wantErr: false,
},
{
name: "both instanceType and instanceTypes are specified",
pool: &AWSManagedMachinePool{
Spec: AWSManagedMachinePoolSpec{
EKSNodegroupName: "eks-node-group-4",
InstanceTypes: []string{"m5.xlarge", "m5.2xlarge"},
InstanceType: ptr.To("m5.xlarge"),
},
},
wantErr: true,
},
{
name: "only instanceTypes is accepted",
pool: &AWSManagedMachinePool{
Spec: AWSManagedMachinePoolSpec{
EKSNodegroupName: "eks-node-group-5",
InstanceTypes: []string{"m5.xlarge", "m5.2xlarge"},
},
},
wantErr: false,
},
{
name: "only instanceType is accepted",
pool: &AWSManagedMachinePool{
Spec: AWSManagedMachinePoolSpec{
EKSNodegroupName: "eks-node-group-6",
InstanceType: ptr.To("m5.xlarge"),
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down Expand Up @@ -693,6 +724,114 @@ func TestAWSManagedMachinePoolValidateUpdate(t *testing.T) {
},
wantErr: false,
},
{
name: "adding instanceType is rejected",
old: &AWSManagedMachinePool{
Spec: AWSManagedMachinePoolSpec{
EKSNodegroupName: "eks-node-group-1",
},
},
new: &AWSManagedMachinePool{
Spec: AWSManagedMachinePoolSpec{
EKSNodegroupName: "eks-node-group-1",
InstanceType: ptr.To("m5.xlarge"),
},
},
wantErr: true,
},
{
name: "removing instanceType is rejected",
old: &AWSManagedMachinePool{
Spec: AWSManagedMachinePoolSpec{
EKSNodegroupName: "eks-node-group-1",
InstanceType: ptr.To("m5.xlarge"),
},
},
new: &AWSManagedMachinePool{
Spec: AWSManagedMachinePoolSpec{
EKSNodegroupName: "eks-node-group-1",
},
},
wantErr: true,
},
{
name: "changing instanceType is rejected",
old: &AWSManagedMachinePool{
Spec: AWSManagedMachinePoolSpec{
EKSNodegroupName: "eks-node-group-1",
InstanceType: ptr.To("m5.xlarge"),
},
},
new: &AWSManagedMachinePool{
Spec: AWSManagedMachinePoolSpec{
EKSNodegroupName: "eks-node-group-1",
InstanceType: ptr.To("m5.2xlarge"),
},
},
wantErr: true,
},
{
name: "adding instanceTypes is rejected",
old: &AWSManagedMachinePool{
Spec: AWSManagedMachinePoolSpec{
EKSNodegroupName: "eks-node-group-1",
},
},
new: &AWSManagedMachinePool{
Spec: AWSManagedMachinePoolSpec{
EKSNodegroupName: "eks-node-group-1",
InstanceTypes: []string{"m5.xlarge", "m5.2xlarge"},
},
},
wantErr: true,
},
{
name: "removing instanceTypes is rejected",
old: &AWSManagedMachinePool{
Spec: AWSManagedMachinePoolSpec{
EKSNodegroupName: "eks-node-group-1",
InstanceTypes: []string{"m5.xlarge", "m5.2xlarge"},
},
},
new: &AWSManagedMachinePool{
Spec: AWSManagedMachinePoolSpec{
EKSNodegroupName: "eks-node-group-1",
},
},
wantErr: true,
},
{
name: "changing instanceTypes is rejected",
old: &AWSManagedMachinePool{
Spec: AWSManagedMachinePoolSpec{
EKSNodegroupName: "eks-node-group-1",
InstanceTypes: []string{"m5.xlarge", "m5.2xlarge"},
},
},
new: &AWSManagedMachinePool{
Spec: AWSManagedMachinePoolSpec{
EKSNodegroupName: "eks-node-group-1",
InstanceTypes: []string{"m5.xlarge", "m6.xlarge"},
},
},
wantErr: true,
},
{
name: "adding both instanceType and instanceTypes is rejected",
old: &AWSManagedMachinePool{
Spec: AWSManagedMachinePoolSpec{
EKSNodegroupName: "eks-node-group-1",
},
},
new: &AWSManagedMachinePool{
Spec: AWSManagedMachinePoolSpec{
EKSNodegroupName: "eks-node-group-1",
InstanceType: ptr.To("m5.xlarge"),
InstanceTypes: []string{"m5.xlarge", "m6.xlarge"},
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
5 changes: 5 additions & 0 deletions exp/api/v1beta2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion pkg/cloud/services/eks/nodegroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,11 @@ func (s *NodegroupService) createNodegroup(ctx context.Context) (*ekstypes.Nodeg
if managedPool.DiskSize != nil {
input.DiskSize = managedPool.DiskSize
}
if managedPool.InstanceType != nil {
// Support both InstanceTypes (preferred for multiple types) and InstanceType (for single type)
// Webhook validation ensures they are mutually exclusive
if len(managedPool.InstanceTypes) > 0 {
input.InstanceTypes = managedPool.InstanceTypes
} else if managedPool.InstanceType != nil {
input.InstanceTypes = []string{aws.ToString(managedPool.InstanceType)}
}
if len(managedPool.Taints) > 0 {
Expand Down
39 changes: 39 additions & 0 deletions test-awsmanagedmachinepool.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
kind: AWSManagedMachinePool
metadata:
name: test-pool-multiple-instance-types
namespace: default
spec:
eksNodegroupName: test-nodegroup
# Use instanceTypes (plural) to specify multiple instance types
# This enhances availability when using Spot instances
instanceTypes:
- t3.medium
- t3a.medium
- t2.medium
capacityType: spot
scaling:
minSize: 1
maxSize: 10
updateConfig:
maxUnavailable: 1
tags:
usage: test
---
apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
kind: AWSManagedMachinePool
metadata:
name: test-pool-single-instance-type
namespace: default
spec:
eksNodegroupName: test-nodegroup-single
# Use instanceType (singular) for a single instance type (backward compatible)
instanceType: t3.medium
capacityType: on-demand
scaling:
minSize: 1
maxSize: 5
updateConfig:
maxUnavailable: 1
tags:
usage: test