From f9418a281e7e0f470320c0b4425d0a54d8c99d92 Mon Sep 17 00:00:00 2001 From: Martina Iglesias Fernandez Date: Tue, 7 May 2019 16:05:52 +0200 Subject: [PATCH] Add support for spot instances nodegroups --- docs/08-spot-instances.yaml | 22 +++ examples/01-simple-cluster.yaml | 2 +- pkg/apis/eksctl.io/v1alpha5/types.go | 16 +++ pkg/apis/eksctl.io/v1alpha5/validation.go | 44 ++++++ .../eksctl.io/v1alpha5/validation_test.go | 89 ++++++++++++ pkg/apis/eksctl.io/v1alpha5/vpc.go | 2 +- .../v1alpha5/zz_generated.deepcopy.go | 61 +++++++++ pkg/cfn/builder/api_test.go | 74 +++++++++- pkg/cfn/builder/nodegroup.go | 128 +++++++++++++----- 9 files changed, 402 insertions(+), 36 deletions(-) create mode 100644 docs/08-spot-instances.yaml diff --git a/docs/08-spot-instances.yaml b/docs/08-spot-instances.yaml new file mode 100644 index 00000000000..f904203d9fc --- /dev/null +++ b/docs/08-spot-instances.yaml @@ -0,0 +1,22 @@ +# A simple example of ClusterConfig object: +--- +apiVersion: eksctl.io/v1alpha5 +kind: ClusterConfig + +metadata: + name: martina-test-spotinst-mixed-9 + region: eu-central-1 + +nodeGroups: + - name: ng-6 + ssh: + publicKeyPath: ~/.ssh/id_rsa_tests.pub + minSize: 2 + maxSize: 5 + mixedInstances: + maxPrice: 0.017 + instanceTypes: ["t3.small", "t3.medium", "t3.small"] # Required because we need more instance types + onDemandBaseCapacity: 0 + percentageAboveBase: 100 + spotInstancePools: 2 + diff --git a/examples/01-simple-cluster.yaml b/examples/01-simple-cluster.yaml index decba975b5a..9a2c1bca2eb 100644 --- a/examples/01-simple-cluster.yaml +++ b/examples/01-simple-cluster.yaml @@ -1,5 +1,5 @@ # A simple example of ClusterConfig object: ---- +--- apiVersion: eksctl.io/v1alpha5 kind: ClusterConfig diff --git a/pkg/apis/eksctl.io/v1alpha5/types.go b/pkg/apis/eksctl.io/v1alpha5/types.go index 73e986082a3..6446229fd84 100644 --- a/pkg/apis/eksctl.io/v1alpha5/types.go +++ b/pkg/apis/eksctl.io/v1alpha5/types.go @@ -378,6 +378,8 @@ type NodeGroup struct { AMIFamily string `json:"amiFamily,omitempty"` // +optional InstanceType string `json:"instanceType,omitempty"` + //+optional + InstancesDistribution *NodeGroupInstancesDistribution `json:"instancesDistribution,omitempty"` // +optional AvailabilityZones []string `json:"availabilityZones,omitempty"` // +optional @@ -489,4 +491,18 @@ type ( // +optional PublicKeyName *string `json:"publicKeyName,omitempty"` } + + // NodeGroupInstancesDistribution holds the configuration for spot instances + NodeGroupInstancesDistribution struct { + //+required + InstanceTypes []string `json:"instanceTypes,omitEmpty"` + // +optional + MaxPrice *float64 `json:"maxPrice,omitempty"` + //+optional + OnDemandBaseCapacity *int `json:"onDemandBaseCapacity,omitEmpty"` + //+optional + PercentageAboveBase *int `json:"percentageAboveBase,omitEmpty"` + //+optional + SpotInstancePools *int `json:"spotInstancePools,omitEmpty"` + } ) diff --git a/pkg/apis/eksctl.io/v1alpha5/validation.go b/pkg/apis/eksctl.io/v1alpha5/validation.go index c036eb0aeb3..78a5a6e77c1 100644 --- a/pkg/apis/eksctl.io/v1alpha5/validation.go +++ b/pkg/apis/eksctl.io/v1alpha5/validation.go @@ -146,6 +146,50 @@ func ValidateNodeGroup(i int, ng *NodeGroup) error { } } + if err := validateInstancesDistribution(ng); err != nil { + return err + } + + return nil +} + +// TODO What happens with the other instance type. Should we use it as well, ignore it, or fail when both are specified? +// TODO or should we change it to be an array? +func validateInstancesDistribution(ng *NodeGroup) error { + if ng.InstancesDistribution == nil { + return nil + } + + if ng.InstanceType != "" && ng.InstanceType != "mixed" { + return fmt.Errorf("instanceType should be \"mixed\" or unset when using the mixed instances feature") + } + + distribution := ng.InstancesDistribution + if distribution.InstanceTypes == nil || len(distribution.InstanceTypes) == 0 { + return fmt.Errorf("at least two instance types have to be specified for mixed nodegroups") + } + + allInstanceTypes := make(map[string]bool) + for _, instanceType := range distribution.InstanceTypes { + allInstanceTypes[instanceType] = true + } + + if len(allInstanceTypes) < 2 || len(allInstanceTypes) > 20 { + return fmt.Errorf("mixed nodegroups should have between 2 and 20 different instance types") + } + + if distribution.OnDemandBaseCapacity != nil && *distribution.OnDemandBaseCapacity < 0 { + return fmt.Errorf("onDemandBaseCapacity should be 0 or more") + } + + if distribution.PercentageAboveBase != nil && (*distribution.PercentageAboveBase < 0 || *distribution.PercentageAboveBase > 100) { + return fmt.Errorf("percentageAboveBase should be between 0 and 100") + } + + if distribution.SpotInstancePools != nil && (*distribution.SpotInstancePools < 1 || *distribution.SpotInstancePools > 20) { + return fmt.Errorf("spotInstancePools should be between 1 and 20") + } + return nil } diff --git a/pkg/apis/eksctl.io/v1alpha5/validation_test.go b/pkg/apis/eksctl.io/v1alpha5/validation_test.go index ae8b29f66c1..d1723b1ed35 100644 --- a/pkg/apis/eksctl.io/v1alpha5/validation_test.go +++ b/pkg/apis/eksctl.io/v1alpha5/validation_test.go @@ -46,9 +46,98 @@ var _ = Describe("ConfigFile ssh flags validation", func() { err := validateNodeGroupSSH(nil) Expect(err).ToNot(HaveOccurred()) }) + + Context("Instances distribution", func() { + + var ng *NodeGroup + BeforeEach(func() { + ng = &NodeGroup{ + InstancesDistribution: &NodeGroupInstancesDistribution{ + InstanceTypes: []string{"t3.medium", "t3.large"}, + }, + } + }) + + It("It doesn't panic when instance distribution is not enabled", func() { + ng.InstancesDistribution = nil + err := validateInstancesDistribution(ng) + Expect(err).ToNot(HaveOccurred()) + }) + + It("It fails when instance distribution is enabled and instanceType is not empty or \"mixed\"", func() { + err := validateInstancesDistribution(ng) + Expect(err).ToNot(HaveOccurred()) + + ng.InstanceType = "t3.small" + + err = validateInstancesDistribution(ng) + Expect(err).To(HaveOccurred()) + }) + + It("It fails when the instance distribution doesn't have at least 2 different instance types", func() { + ng.InstanceType = "mixed" + ng.InstancesDistribution.InstanceTypes = []string{"t3.medium", "t3.medium"} + + err := validateInstancesDistribution(ng) + Expect(err).To(HaveOccurred()) + + ng.InstanceType = "mixed" + ng.InstancesDistribution.InstanceTypes = []string{"t3.medium", "t3.small"} + + err = validateInstancesDistribution(ng) + Expect(err).ToNot(HaveOccurred()) + }) + + It("It fails when the onDemandBaseCapacity is not above 0", func() { + ng.InstancesDistribution.OnDemandBaseCapacity = newInt(-1) + + err := validateInstancesDistribution(ng) + Expect(err).To(HaveOccurred()) + + ng.InstancesDistribution.OnDemandBaseCapacity = newInt(1) + + err = validateInstancesDistribution(ng) + Expect(err).ToNot(HaveOccurred()) + }) + + It("It fails when the spotInstancePools is not between 1 and 20", func() { + ng.InstancesDistribution.SpotInstancePools = newInt(0) + + err := validateInstancesDistribution(ng) + Expect(err).To(HaveOccurred()) + + ng.InstancesDistribution.SpotInstancePools = newInt(21) + err = validateInstancesDistribution(ng) + Expect(err).To(HaveOccurred()) + + ng.InstancesDistribution.SpotInstancePools = newInt(2) + err = validateInstancesDistribution(ng) + Expect(err).ToNot(HaveOccurred()) + }) + + It("It fails when the percentageAboveBase is not between 0 and 100", func() { + ng.InstancesDistribution.PercentageAboveBase = newInt(-1) + + err := validateInstancesDistribution(ng) + Expect(err).To(HaveOccurred()) + + ng.InstancesDistribution.PercentageAboveBase = newInt(101) + err = validateInstancesDistribution(ng) + Expect(err).To(HaveOccurred()) + + ng.InstancesDistribution.PercentageAboveBase = newInt(50) + err = validateInstancesDistribution(ng) + Expect(err).ToNot(HaveOccurred()) + }) + }) }) func checkItDetectsError(SSHConfig *NodeGroupSSH) { err := validateNodeGroupSSH(SSHConfig) Expect(err).To(HaveOccurred()) } + +func newInt(value int) *int { + v := value + return &v +} diff --git a/pkg/apis/eksctl.io/v1alpha5/vpc.go b/pkg/apis/eksctl.io/v1alpha5/vpc.go index 0358903702c..78a981b730c 100644 --- a/pkg/apis/eksctl.io/v1alpha5/vpc.go +++ b/pkg/apis/eksctl.io/v1alpha5/vpc.go @@ -155,7 +155,7 @@ func (c *ClusterConfig) HasSufficientPublicSubnets() bool { } var errInsufficientSubnets = fmt.Errorf( - "inssuficient number of subnets, at least %dx public and/or %dx private subnets are required", + "insufficient number of subnets, at least %dx public and/or %dx private subnets are required", MinRequiredSubnets, MinRequiredSubnets) // HasSufficientSubnets validates if there is a sufficient number diff --git a/pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go b/pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go index 3403c81e9dc..6ee454f046f 100644 --- a/pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go +++ b/pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go @@ -265,6 +265,11 @@ func (in *Network) DeepCopy() *Network { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NodeGroup) DeepCopyInto(out *NodeGroup) { *out = *in + if in.InstancesDistribution != nil { + in, out := &in.InstancesDistribution, &out.InstancesDistribution + *out = new(NodeGroupInstancesDistribution) + (*in).DeepCopyInto(*out) + } if in.AvailabilityZones != nil { in, out := &in.AvailabilityZones, &out.AvailabilityZones *out = make([]string, len(*in)) @@ -297,6 +302,21 @@ func (in *NodeGroup) DeepCopyInto(out *NodeGroup) { *out = new(int) **out = **in } + if in.VolumeSize != nil { + in, out := &in.VolumeSize, &out.VolumeSize + *out = new(int) + **out = **in + } + if in.VolumeType != nil { + in, out := &in.VolumeType, &out.VolumeType + *out = new(string) + **out = **in + } + if in.VolumeName != nil { + in, out := &in.VolumeName, &out.VolumeName + *out = new(string) + **out = **in + } if in.Labels != nil { in, out := &in.Labels, &out.Labels *out = make(map[string]string, len(*in)) @@ -427,6 +447,47 @@ func (in *NodeGroupIAMAddonPolicies) DeepCopy() *NodeGroupIAMAddonPolicies { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodeGroupInstancesDistribution) DeepCopyInto(out *NodeGroupInstancesDistribution) { + *out = *in + if in.InstanceTypes != nil { + in, out := &in.InstanceTypes, &out.InstanceTypes + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.MaxPrice != nil { + in, out := &in.MaxPrice, &out.MaxPrice + *out = new(float64) + **out = **in + } + if in.OnDemandBaseCapacity != nil { + in, out := &in.OnDemandBaseCapacity, &out.OnDemandBaseCapacity + *out = new(int) + **out = **in + } + if in.PercentageAboveBase != nil { + in, out := &in.PercentageAboveBase, &out.PercentageAboveBase + *out = new(int) + **out = **in + } + if in.SpotInstancePools != nil { + in, out := &in.SpotInstancePools, &out.SpotInstancePools + *out = new(int) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeGroupInstancesDistribution. +func (in *NodeGroupInstancesDistribution) DeepCopy() *NodeGroupInstancesDistribution { + if in == nil { + return nil + } + out := new(NodeGroupInstancesDistribution) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NodeGroupSGs) DeepCopyInto(out *NodeGroupSGs) { *out = *in diff --git a/pkg/cfn/builder/api_test.go b/pkg/cfn/builder/api_test.go index 55c07bef8d0..5ac2e74797d 100644 --- a/pkg/cfn/builder/api_test.go +++ b/pkg/cfn/builder/api_test.go @@ -89,6 +89,23 @@ type Properties struct { SecurityGroupIds []interface{} SubnetIds []interface{} } + MixedInstancesPolicy *struct { + LaunchTemplate struct { + LaunchTemplateSpecification struct { + LaunchTemplateName map[string]string + Version map[string]string + Overrides []struct { + InstanceType string + } + } + } + InstancesDistribution struct { + OnDemandBaseCapacity string + OnDemandPercentageAboveBaseCapacity string + SpotMaxPrice string + SpotInstancePools string + } + } } type LaunchTemplateData struct { @@ -99,6 +116,13 @@ type LaunchTemplateData struct { DeviceIndex int AssociatePublicIpAddress bool } + InstanceMarketOptions *struct { + MarketType string + SpotOptions struct { + SpotInstanceType string + MaxPrice string + } + } } type Template struct { @@ -1074,7 +1098,7 @@ var _ = Describe("CloudFormation template builder API", func() { }) }) - Context("NodeGroup with cutom role and profile", func() { + Context("NodeGroup with custom role and profile", func() { cfg, ng := newClusterConfigAndNodegroup(true) ng.IAM.InstanceRoleARN = "arn:role" @@ -2102,6 +2126,54 @@ var _ = Describe("CloudFormation template builder API", func() { }) }) + + Context("Nodegroup with Mixed instances", func() { + cfg, ng := newClusterConfigAndNodegroup(true) + + maxSpotPrice := 0.045 + baseCap := 40 + percentageOnDemand := 20 + pools := 3 + ng.InstancesDistribution = &api.NodeGroupInstancesDistribution{ + MaxPrice: &maxSpotPrice, + InstanceTypes: []string{"m5.large", "m5a.xlarge"}, + OnDemandBaseCapacity: &baseCap, + PercentageAboveBase: &percentageOnDemand, + SpotInstancePools: &pools, + } + + zero := 0 + ng.MinSize = &zero + ng.MaxSize = &zero + + build(cfg, "eksctl-test-spot-cluster", ng) + + roundtrip() + + It("should have mixed instances with correct max price", func() { + Expect(ngTemplate.Resources).To(HaveKey("NodeGroupLaunchTemplate")) + + launchTemplateData := getLaunchTemplateData(ngTemplate) + Expect(launchTemplateData.InstanceMarketOptions).To(BeNil()) + + nodeGroupProperties := getNodeGroupProperties(ngTemplate) + Expect(nodeGroupProperties.MinSize).To(Equal("0")) + Expect(nodeGroupProperties.MaxSize).To(Equal("0")) + Expect(nodeGroupProperties.DesiredCapacity).To(Equal("")) + + Expect(nodeGroupProperties.MixedInstancesPolicy).To(Not(BeNil())) + Expect(nodeGroupProperties.MixedInstancesPolicy.LaunchTemplate.LaunchTemplateSpecification.LaunchTemplateName["Fn::Sub"]).To(Equal("${AWS::StackName}")) + Expect(nodeGroupProperties.MixedInstancesPolicy.LaunchTemplate.LaunchTemplateSpecification.Version["Fn::GetAtt"]).To(Equal("NodeGroupLaunchTemplate.LatestVersionNumber")) + Expect(nodeGroupProperties.MixedInstancesPolicy.LaunchTemplate).To(Not(BeNil())) + + Expect(nodeGroupProperties.MixedInstancesPolicy.InstancesDistribution).To(Not(BeNil())) + Expect(nodeGroupProperties.MixedInstancesPolicy.InstancesDistribution.OnDemandBaseCapacity).To(Equal("40")) + Expect(nodeGroupProperties.MixedInstancesPolicy.InstancesDistribution.OnDemandPercentageAboveBaseCapacity).To(Equal("20")) + Expect(nodeGroupProperties.MixedInstancesPolicy.InstancesDistribution.SpotInstancePools).To(Equal("3")) + Expect(nodeGroupProperties.MixedInstancesPolicy.InstancesDistribution.SpotMaxPrice).To(Equal("0.045000")) + + }) + }) }) func setSubnets(cfg *api.ClusterConfig) { diff --git a/pkg/cfn/builder/nodegroup.go b/pkg/cfn/builder/nodegroup.go index f522a3c12c0..f57b8e5085f 100644 --- a/pkg/cfn/builder/nodegroup.go +++ b/pkg/cfn/builder/nodegroup.go @@ -108,19 +108,7 @@ func (n *NodeGroupResourceSet) newResource(name string, resource interface{}) *g func (n *NodeGroupResourceSet) addResourcesForNodeGroup() error { launchTemplateName := gfn.MakeFnSubString(fmt.Sprintf("${%s}", gfn.StackName)) - launchTemplateData := &gfn.AWSEC2LaunchTemplate_LaunchTemplateData{ - IamInstanceProfile: &gfn.AWSEC2LaunchTemplate_IamInstanceProfile{ - Arn: n.instanceProfileARN, - }, - ImageId: gfn.NewString(n.spec.AMI), - InstanceType: gfn.NewString(n.spec.InstanceType), - UserData: n.userData, - NetworkInterfaces: []gfn.AWSEC2LaunchTemplate_NetworkInterface{{ - AssociatePublicIpAddress: gfn.NewBoolean(!n.spec.PrivateNetworking), - DeviceIndex: gfn.NewInteger(0), - Groups: n.securityGroups, - }}, - } + launchTemplateData := newLaunchTemplateData(n) if api.IsEnabled(n.spec.SSH.Allow) && api.IsSetAndNonEmptyString(n.spec.SSH.PublicKeyName) { launchTemplateData.KeyName = gfn.NewString(*n.spec.SSH.PublicKeyName) @@ -196,41 +184,115 @@ func (n *NodeGroupResourceSet) addResourcesForNodeGroup() error { }, ) } - ngProps := map[string]interface{}{ - "LaunchTemplate": map[string]interface{}{ - "LaunchTemplateName": launchTemplateName, - "Version": gfn.MakeFnGetAttString("NodeGroupLaunchTemplate.LatestVersionNumber"), + + asg := nodeGroupResource(launchTemplateName, &vpcZoneIdentifier, tags, n.spec) + n.newResource("NodeGroup", asg) + + return nil +} + +// GetAllOutputs collects all outputs of the node group +func (n *NodeGroupResourceSet) GetAllOutputs(stack cfn.Stack) error { + return n.rs.GetAllOutputs(stack) +} + +func newLaunchTemplateData(n *NodeGroupResourceSet) *gfn.AWSEC2LaunchTemplate_LaunchTemplateData { + launchTemplateData := &gfn.AWSEC2LaunchTemplate_LaunchTemplateData{ + IamInstanceProfile: &gfn.AWSEC2LaunchTemplate_IamInstanceProfile{ + Arn: n.instanceProfileARN, }, - "VPCZoneIdentifier": vpcZoneIdentifier, + ImageId: gfn.NewString(n.spec.AMI), + InstanceType: gfn.NewString(n.spec.InstanceType), + UserData: n.userData, + NetworkInterfaces: []gfn.AWSEC2LaunchTemplate_NetworkInterface{{ + AssociatePublicIpAddress: gfn.NewBoolean(!n.spec.PrivateNetworking), + DeviceIndex: gfn.NewInteger(0), + Groups: n.securityGroups, + }}, + } + + return launchTemplateData +} + +func nodeGroupResource(launchTemplateName *gfn.Value, vpcZoneIdentifier *interface{}, tags []map[string]interface{}, ng *api.NodeGroup) *awsCloudFormationResource { + ngProps := map[string]interface{}{ + "VPCZoneIdentifier": *vpcZoneIdentifier, "Tags": tags, } - if n.spec.DesiredCapacity != nil { - ngProps["DesiredCapacity"] = fmt.Sprintf("%d", *n.spec.DesiredCapacity) + if ng.DesiredCapacity != nil { + ngProps["DesiredCapacity"] = fmt.Sprintf("%d", *ng.DesiredCapacity) } - if n.spec.MinSize != nil { - ngProps["MinSize"] = fmt.Sprintf("%d", *n.spec.MinSize) + if ng.MinSize != nil { + ngProps["MinSize"] = fmt.Sprintf("%d", *ng.MinSize) } - if n.spec.MaxSize != nil { - ngProps["MaxSize"] = fmt.Sprintf("%d", *n.spec.MaxSize) + if ng.MaxSize != nil { + ngProps["MaxSize"] = fmt.Sprintf("%d", *ng.MaxSize) } - if len(n.spec.TargetGroupARNs) > 0 { - ngProps["TargetGroupARNs"] = n.spec.TargetGroupARNs + if len(ng.TargetGroupARNs) > 0 { + ngProps["TargetGroupARNs"] = ng.TargetGroupARNs + } + if hasMixedInstances(ng) { + ngProps["MixedInstancesPolicy"] = *mixedInstancesPolicy(launchTemplateName, ng) + } else { + ngProps["LaunchTemplate"] = map[string]interface{}{ + "LaunchTemplateName": launchTemplateName, + "Version": gfn.MakeFnGetAttString("NodeGroupLaunchTemplate.LatestVersionNumber"), + } } - n.newResource("NodeGroup", &awsCloudFormationResource{ + + return &awsCloudFormationResource{ Type: "AWS::AutoScaling::AutoScalingGroup", Properties: ngProps, UpdatePolicy: map[string]map[string]string{ "AutoScalingRollingUpdate": { - "MinInstancesInService": "1", + "MinInstancesInService": "0", "MaxBatchSize": "1", }, }, - }) + } +} - return nil +func mixedInstancesPolicy(launchTemplateName *gfn.Value, ng *api.NodeGroup) *map[string]interface{} { + instanceTypes := ng.InstancesDistribution.InstanceTypes + overrides := make([]map[string]string, len(instanceTypes)) + + for i, instanceType := range instanceTypes { + overrides[i] = map[string]string{ + "InstanceType": instanceType, + } + } + policy := map[string]interface{}{ + "LaunchTemplate": map[string]interface{}{ + "LaunchTemplateSpecification": map[string]interface{}{ + "LaunchTemplateName": launchTemplateName, + "Version": gfn.MakeFnGetAttString("NodeGroupLaunchTemplate.LatestVersionNumber"), + }, + + "Overrides": overrides, + }, + } + + instancesDistribution := map[string]string{} + + // Only set the price if it was specified so otherwise AWS picks "on-demand price" as the default + if ng.InstancesDistribution.MaxPrice != nil { + instancesDistribution["SpotMaxPrice"] = fmt.Sprintf("%f", *ng.InstancesDistribution.MaxPrice) + } + if ng.InstancesDistribution.OnDemandBaseCapacity != nil { + instancesDistribution["OnDemandBaseCapacity"] = fmt.Sprintf("%d", *ng.InstancesDistribution.OnDemandBaseCapacity) + } + if ng.InstancesDistribution.PercentageAboveBase != nil { + instancesDistribution["OnDemandPercentageAboveBaseCapacity"] = fmt.Sprintf("%d", *ng.InstancesDistribution.PercentageAboveBase) + } + if ng.InstancesDistribution.SpotInstancePools != nil { + instancesDistribution["SpotInstancePools"] = fmt.Sprintf("%d", *ng.InstancesDistribution.SpotInstancePools) + } + + policy["InstancesDistribution"] = instancesDistribution + + return &policy } -// GetAllOutputs collects all outputs of the node group -func (n *NodeGroupResourceSet) GetAllOutputs(stack cfn.Stack) error { - return n.rs.GetAllOutputs(stack) +func hasMixedInstances(ng *api.NodeGroup) bool { + return ng.InstancesDistribution != nil && ng.InstancesDistribution.InstanceTypes != nil && len(ng.InstancesDistribution.InstanceTypes) != 0 }