diff --git a/pkg/apis/eksctl.io/v1alpha5/assets/schema.json b/pkg/apis/eksctl.io/v1alpha5/assets/schema.json index a962912d76..dabc3eee1d 100755 --- a/pkg/apis/eksctl.io/v1alpha5/assets/schema.json +++ b/pkg/apis/eksctl.io/v1alpha5/assets/schema.json @@ -749,6 +749,11 @@ "description": "enables [EBS optimization](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-optimized.html)", "x-intellij-html-description": "enables EBS optimization" }, + "cpuCredits": { + "type": "string", + "description": "configures [T3 Unlimited](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/burstable-performance-instances-unlimited-mode.html), valid only for T-type instances. Possible values: 'unlimited' or 'standard'.", + "x-intellij-html-description": "configures T3 Unlimited, valid only for T-type instances. Possible values: 'unlimited' or 'standard'." + }, "iam": { "$ref": "#/definitions/NodeGroupIAM" }, @@ -896,6 +901,7 @@ "securityGroups", "asgMetricsCollection", "ebsOptimized", + "cpuCredits", "volumeType", "volumeName", "volumeEncrypted", diff --git a/pkg/apis/eksctl.io/v1alpha5/types.go b/pkg/apis/eksctl.io/v1alpha5/types.go index f864683e83..a2e7178740 100644 --- a/pkg/apis/eksctl.io/v1alpha5/types.go +++ b/pkg/apis/eksctl.io/v1alpha5/types.go @@ -687,6 +687,10 @@ type NodeGroup struct { // +optional EBSOptimized *bool `json:"ebsOptimized,omitempty"` + // CPUCredits configures [T3 Unlimited](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/burstable-performance-instances-unlimited-mode.html), valid only for T-type instances + // +optional + CPUCredits *string `json:"cpuCredits,omitempty"` + // Valid variants are `VolumeType` constants // +optional VolumeType *string `json:"volumeType,omitempty"` diff --git a/pkg/apis/eksctl.io/v1alpha5/validation.go b/pkg/apis/eksctl.io/v1alpha5/validation.go index eb0374ba4a..ce600a7765 100644 --- a/pkg/apis/eksctl.io/v1alpha5/validation.go +++ b/pkg/apis/eksctl.io/v1alpha5/validation.go @@ -260,6 +260,10 @@ func ValidateNodeGroup(i int, ng *NodeGroup) error { return err } + if err := validateCPUCredits(ng); err != nil { + return err + } + return nil } @@ -466,6 +470,35 @@ func validateInstancesDistribution(ng *NodeGroup) error { return nil } +func validateCPUCredits(ng *NodeGroup) error { + isTInstance := false + instanceTypes := []string{ng.InstanceType} + + if ng.CPUCredits == nil { + return nil + } + + if ng.InstanceType == "mixed" { + instanceTypes = ng.InstancesDistribution.InstanceTypes + } + + for _, instanceType := range instanceTypes { + if strings.HasPrefix(instanceType, "t") { + isTInstance = true + } + } + + if !isTInstance { + return fmt.Errorf("cpuCredits option set for nodegroup, but it has no t2/t3 instance types") + } + + if strings.ToLower(*ng.CPUCredits) != "unlimited" && strings.ToLower(*ng.CPUCredits) != "standard" { + return fmt.Errorf("cpuCredits option accepts only one of 'standard' or 'unlimited'") + } + + return nil +} + func validateNodeGroupSSH(SSH *NodeGroupSSH) error { numSSHFlagsEnabled := countEnabledFields( SSH.PublicKeyPath, diff --git a/pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go b/pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go index ad5ce4472f..a544896c9b 100644 --- a/pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go +++ b/pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go @@ -701,6 +701,11 @@ func (in *NodeGroup) DeepCopyInto(out *NodeGroup) { *out = new(bool) **out = **in } + if in.CPUCredits != nil { + in, out := &in.CPUCredits, &out.CPUCredits + *out = new(string) + **out = **in + } if in.VolumeType != nil { in, out := &in.VolumeType, &out.VolumeType *out = new(string) diff --git a/pkg/cfn/builder/api_test.go b/pkg/cfn/builder/api_test.go index 935003972a..ff2af375f9 100644 --- a/pkg/cfn/builder/api_test.go +++ b/pkg/cfn/builder/api_test.go @@ -132,6 +132,9 @@ type LaunchTemplateData struct { MaxPrice string } } + CreditSpecification *struct { + CPUCredits string + } } type Template struct { @@ -2882,14 +2885,18 @@ var _ = Describe("CloudFormation template builder API", func() { }) + maxSpotPrice := 0.045 + baseCap := 40 + percentageOnDemand := 20 + pools := 3 + spotAllocationStrategy := "lowest-price" + zero := 0 + cpuCreditsUnlimited := "unlimited" + cpuCreditsStandard := "standard" + Context("Nodegroup with Mixed instances", func() { cfg, ng := newClusterConfigAndNodegroup(true) - maxSpotPrice := 0.045 - baseCap := 40 - percentageOnDemand := 20 - pools := 3 - spotAllocationStrategy := "lowest-price" ng.InstanceType = "mixed" ng.InstancesDistribution = &api.NodeGroupInstancesDistribution{ MaxPrice: &maxSpotPrice, @@ -2900,7 +2907,6 @@ var _ = Describe("CloudFormation template builder API", func() { SpotAllocationStrategy: &spotAllocationStrategy, } - zero := 0 ng.MinSize = &zero ng.MaxSize = &zero @@ -2934,6 +2940,69 @@ var _ = Describe("CloudFormation template builder API", func() { }) }) + + Context("NodeGroup{CPUCredits=nil}", func() { + cfg, ng := newClusterConfigAndNodegroup(true) + + build(cfg, "eksctl-test-t3-unlimited", ng) + + roundtrip() + + It("should have correct resources and attributes", func() { + Expect(getLaunchTemplateData(ngTemplate).CreditSpecification).To(BeNil()) + }) + }) + + Context("NodeGroup{CPUCredits=standard InstancesDistribution.InstanceTypes=t3.medium,t3a.medium}", func() { + cfg, ng := newClusterConfigAndNodegroup(true) + + ng.InstanceType = "mixed" + ng.CPUCredits = &cpuCreditsStandard + ng.InstancesDistribution = &api.NodeGroupInstancesDistribution{ + MaxPrice: &maxSpotPrice, + InstanceTypes: []string{"t3.medium", "t3a.medium"}, + OnDemandBaseCapacity: &baseCap, + OnDemandPercentageAboveBaseCapacity: &percentageOnDemand, + SpotInstancePools: &pools, + SpotAllocationStrategy: &spotAllocationStrategy, + } + + build(cfg, "eksctl-test-t3-unlimited", ng) + + roundtrip() + + It("should have correct resources and attributes", func() { + Expect(getLaunchTemplateData(ngTemplate).CreditSpecification).ToNot(BeNil()) + Expect(getLaunchTemplateData(ngTemplate).CreditSpecification.CPUCredits).ToNot(BeNil()) + Expect(getLaunchTemplateData(ngTemplate).CreditSpecification.CPUCredits).To(Equal("standard")) + }) + }) + + Context("NodeGroup{CPUCredits=unlimited InstancesDistribution.InstanceTypes=t3.medium,t3a.medium}", func() { + cfg, ng := newClusterConfigAndNodegroup(true) + + ng.InstanceType = "mixed" + ng.CPUCredits = &cpuCreditsUnlimited + ng.InstancesDistribution = &api.NodeGroupInstancesDistribution{ + MaxPrice: &maxSpotPrice, + InstanceTypes: []string{"t3.medium", "t3a.medium"}, + OnDemandBaseCapacity: &baseCap, + OnDemandPercentageAboveBaseCapacity: &percentageOnDemand, + SpotInstancePools: &pools, + SpotAllocationStrategy: &spotAllocationStrategy, + } + + build(cfg, "eksctl-test-t3-unlimited", ng) + + roundtrip() + + It("should have correct resources and attributes", func() { + Expect(getLaunchTemplateData(ngTemplate).CreditSpecification).ToNot(BeNil()) + Expect(getLaunchTemplateData(ngTemplate).CreditSpecification.CPUCredits).ToNot(BeNil()) + Expect(getLaunchTemplateData(ngTemplate).CreditSpecification.CPUCredits).To(Equal("unlimited")) + }) + }) + }) func setSubnets(cfg *api.ClusterConfig) { diff --git a/pkg/cfn/builder/nodegroup.go b/pkg/cfn/builder/nodegroup.go index 5736fae8cd..ba59ff369d 100644 --- a/pkg/cfn/builder/nodegroup.go +++ b/pkg/cfn/builder/nodegroup.go @@ -267,6 +267,7 @@ func newLaunchTemplateData(n *NodeGroupResourceSet) *gfn.AWSEC2LaunchTemplate_La HttpPutResponseHopLimit: gfn.NewInteger(2), }, } + if !api.HasMixedInstances(n.spec) { launchTemplateData.InstanceType = gfn.NewString(n.spec.InstanceType) } else { @@ -276,6 +277,12 @@ func newLaunchTemplateData(n *NodeGroupResourceSet) *gfn.AWSEC2LaunchTemplate_La launchTemplateData.EbsOptimized = gfn.NewBoolean(*n.spec.EBSOptimized) } + if n.spec.CPUCredits != nil { + launchTemplateData.CreditSpecification = &gfn.AWSEC2LaunchTemplate_CreditSpecification{ + CpuCredits: gfn.NewString(strings.ToLower(*n.spec.CPUCredits)), + } + } + return launchTemplateData }