Skip to content

Commit

Permalink
Add support for spot instances nodegroups
Browse files Browse the repository at this point in the history
  • Loading branch information
martina-if committed May 17, 2019
1 parent 876d004 commit f9418a2
Show file tree
Hide file tree
Showing 9 changed files with 402 additions and 36 deletions.
22 changes: 22 additions & 0 deletions 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

2 changes: 1 addition & 1 deletion examples/01-simple-cluster.yaml
@@ -1,5 +1,5 @@
# A simple example of ClusterConfig object:
---
---
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

Expand Down
16 changes: 16 additions & 0 deletions pkg/apis/eksctl.io/v1alpha5/types.go
Expand Up @@ -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
Expand Down Expand Up @@ -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"`
}
)
44 changes: 44 additions & 0 deletions pkg/apis/eksctl.io/v1alpha5/validation.go
Expand Up @@ -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
}

Expand Down
89 changes: 89 additions & 0 deletions pkg/apis/eksctl.io/v1alpha5/validation_test.go
Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion pkg/apis/eksctl.io/v1alpha5/vpc.go
Expand Up @@ -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
Expand Down
61 changes: 61 additions & 0 deletions pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go

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

0 comments on commit f9418a2

Please sign in to comment.