Skip to content

Commit

Permalink
Merge pull request #8802 from thejasbabu/feature/spot-block
Browse files Browse the repository at this point in the history
Add support for Spot block in launch template
  • Loading branch information
k8s-ci-robot committed Apr 1, 2020
2 parents 882d012 + dda8dc3 commit 759e24a
Show file tree
Hide file tree
Showing 17 changed files with 129 additions and 19 deletions.
5 changes: 5 additions & 0 deletions k8s/crds/kops.k8s.io_instancegroups.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,11 @@ spec:
description: SecurityGroupOverride overrides the default security group
created by Kops for this IG (AWS only).
type: string
spotDurationInMinutes:
description: SpotDurationInMinutes indicates this is a spot-block group,
with the specified value as the spot reservation time
format: int64
type: integer
subnets:
description: Subnets is the names of the Subnets (as specified in the
Cluster) where machines in this instance group should be placed
Expand Down
2 changes: 2 additions & 0 deletions pkg/apis/kops/instancegroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ type InstanceGroupSpec struct {
Hooks []HookSpec `json:"hooks,omitempty"`
// MaxPrice indicates this is a spot-pricing group, with the specified value as our max-price bid
MaxPrice *string `json:"maxPrice,omitempty"`
// SpotDurationInMinutes reserves a spot block for the period specified
SpotDurationInMinutes *int64 `json:"spotDurationInMinutes,omitempty"`
// AssociatePublicIP is true if we want instances to have a public IP
AssociatePublicIP *bool `json:"associatePublicIp,omitempty"`
// AdditionalSecurityGroups attaches additional security groups (e.g. i-123456)
Expand Down
2 changes: 2 additions & 0 deletions pkg/apis/kops/v1alpha2/instancegroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ type InstanceGroupSpec struct {
Hooks []HookSpec `json:"hooks,omitempty"`
// MaxPrice indicates this is a spot-pricing group, with the specified value as our max-price bid
MaxPrice *string `json:"maxPrice,omitempty"`
// SpotDurationInMinutes indicates this is a spot-block group, with the specified value as the spot reservation time
SpotDurationInMinutes *int64 `json:"spotDurationInMinutes,omitempty"`
// AssociatePublicIP is true if we want instances to have a public IP
AssociatePublicIP *bool `json:"associatePublicIp,omitempty"`
// AdditionalSecurityGroups attaches additional security groups (e.g. i-123456)
Expand Down
2 changes: 2 additions & 0 deletions pkg/apis/kops/v1alpha2/zz_generated.conversion.go

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

5 changes: 5 additions & 0 deletions pkg/apis/kops/v1alpha2/zz_generated.deepcopy.go

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

13 changes: 13 additions & 0 deletions pkg/apis/kops/validation/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package validation

import (
"fmt"
"strconv"
"strings"

"k8s.io/apimachinery/pkg/util/sets"
Expand Down Expand Up @@ -48,6 +49,8 @@ func awsValidateInstanceGroup(ig *kops.InstanceGroup) field.ErrorList {

allErrs = append(allErrs, awsValidateAMIforNVMe(field.NewPath(ig.GetName(), "spec", "machineType"), ig)...)

allErrs = append(allErrs, awsValidateSpotDurationInMinute(field.NewPath(ig.GetName(), "spec", "spotDurationInMinutes"), ig)...)

return allErrs
}

Expand Down Expand Up @@ -107,3 +110,13 @@ func awsValidateAMIforNVMe(fieldPath *field.Path, ig *kops.InstanceGroup) field.
}
return allErrs
}

func awsValidateSpotDurationInMinute(fieldPath *field.Path, ig *kops.InstanceGroup) field.ErrorList {
allErrs := field.ErrorList{}
if ig.Spec.SpotDurationInMinutes != nil {
validSpotDurations := []string{"60", "120", "180", "240", "300", "360"}
spotDurationStr := strconv.FormatInt(*ig.Spec.SpotDurationInMinutes, 10)
allErrs = append(allErrs, IsValidValue(fieldPath, &spotDurationStr, validSpotDurations)...)
}
return allErrs
}
32 changes: 32 additions & 0 deletions pkg/apis/kops/validation/aws_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package validation
import (
"testing"

"k8s.io/kops/upup/pkg/fi"

v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/kops/pkg/apis/kops"
)
Expand Down Expand Up @@ -102,6 +104,36 @@ func TestValidateInstanceGroupSpec(t *testing.T) {
"Forbidden::test-nodes.spec.machineType",
},
},
{
Input: kops.InstanceGroupSpec{
SpotDurationInMinutes: fi.Int64(55),
},
ExpectedErrors: []string{
"Unsupported value::test-nodes.spec.spotDurationInMinutes",
},
},
{
Input: kops.InstanceGroupSpec{
SpotDurationInMinutes: fi.Int64(380),
},
ExpectedErrors: []string{
"Unsupported value::test-nodes.spec.spotDurationInMinutes",
},
},
{
Input: kops.InstanceGroupSpec{
SpotDurationInMinutes: fi.Int64(125),
},
ExpectedErrors: []string{
"Unsupported value::test-nodes.spec.spotDurationInMinutes",
},
},
{
Input: kops.InstanceGroupSpec{
SpotDurationInMinutes: fi.Int64(120),
},
ExpectedErrors: []string{},
},
}
for _, g := range grid {
ig := &kops.InstanceGroup{
Expand Down
5 changes: 5 additions & 0 deletions pkg/apis/kops/zz_generated.deepcopy.go

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

3 changes: 3 additions & 0 deletions pkg/model/awsmodel/autoscalinggroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ func (b *AutoscalingGroupModelBuilder) buildLaunchTemplateTask(c *fi.ModelBuilde
if ig.Spec.MixedInstancesPolicy == nil {
lt.SpotPrice = lc.SpotPrice
}
if ig.Spec.SpotDurationInMinutes != nil {
lt.SpotDurationInMinutes = ig.Spec.SpotDurationInMinutes
}
return lt, nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,13 @@
"ImageId": "ami-12345678",
"InstanceType": "t3.medium",
"KeyName": "kubernetes.launchtemplates.example.com-c4:a6:ed:9a:a8:89:b9:e2:c3:9c:d6:63:eb:9c:71:57",
"InstanceMarketOptions": {
"MarketType": "spot",
"SpotOptions": {
"BlockDurationMinutes": 120,
"MaxPrice": "0.1"
}
},
"NetworkInterfaces": [
{
"AssociatePublicIpAddress": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ spec:
minSize: 2
role: Node
instanceProtection: true
maxPrice: "0.1"
spotDurationInMinutes: 120
subnets:
- us-test-1b
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,15 @@ resource "aws_launch_template" "nodes-launchtemplates-example-com" {
instance_type = "t3.medium"
key_name = "${aws_key_pair.kubernetes-launchtemplates-example-com-c4a6ed9aa889b9e2c39cd663eb9c7157.id}"

instance_market_options = {
market_type = "spot"

spot_options = {
block_duration_minutes = 120
max_price = "0.1"
}
}

network_interfaces = {
associate_public_ip_address = true
delete_on_termination = true
Expand Down
2 changes: 2 additions & 0 deletions upup/pkg/fi/cloudup/awstasks/launchtemplate.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ type LaunchTemplate struct {
SecurityGroups []*SecurityGroup
// SpotPrice is set to the spot-price bid if this is a spot pricing request
SpotPrice string
// SpotDurationInMinutes is set for requesting spot blocks
SpotDurationInMinutes *int64
// Tags are the keypairs to apply to the instance and volume on launch.
Tags map[string]string
// Tenancy. Can be either default or dedicated.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ type cloudformationLaunchTemplateIAMProfile struct {
}

type cloudformationLaunchTemplateMarketOptionsSpotOptions struct {
// BlockDurationMinutes is required duration in minutes. This value must be a multiple of 60.
BlockDurationMinutes *int64 `json:"BlockDurationMinutes,omitempty"`
// InstancesInterruptionBehavior is the behavior when a Spot Instance is interrupted. Can be hibernate, stop, or terminate
InstancesInterruptionBehavior *string `json:"InstancesInterruptionBehavior,omitempty"`
// MaxPrice is the maximum hourly price you're willing to pay for the Spot Instances
Expand All @@ -74,7 +76,7 @@ type cloudformationLaunchTemplateMarketOptions struct {
// MarketType is the option type
MarketType *string `json:"MarketType,omitempty"`
// SpotOptions are the set of options
SpotOptions []*cloudformationLaunchTemplateMarketOptionsSpotOptions `json:"Options,omitempty"`
SpotOptions *cloudformationLaunchTemplateMarketOptionsSpotOptions `json:"SpotOptions,omitempty"`
}

type cloudformationLaunchTemplateBlockDeviceEBS struct {
Expand Down Expand Up @@ -165,29 +167,33 @@ func (t *LaunchTemplate) RenderCloudformation(target *cloudformation.Cloudformat
image = im.ImageId
}

cf := &cloudformationLaunchTemplate{
LaunchTemplateName: fi.String(fi.StringValue(e.Name)),
LaunchTemplateData: &cloudformationLaunchTemplateData{
EBSOptimized: e.RootVolumeOptimization,
ImageID: image,
InstanceType: e.InstanceType,
NetworkInterfaces: []*cloudformationLaunchTemplateNetworkInterface{
{
AssociatePublicIPAddress: e.AssociatePublicIP,
DeleteOnTermination: fi.Bool(true),
DeviceIndex: fi.Int(0),
},
launchTemplateData := &cloudformationLaunchTemplateData{
EBSOptimized: e.RootVolumeOptimization,
ImageID: image,
InstanceType: e.InstanceType,
NetworkInterfaces: []*cloudformationLaunchTemplateNetworkInterface{
{
AssociatePublicIPAddress: e.AssociatePublicIP,
DeleteOnTermination: fi.Bool(true),
DeviceIndex: fi.Int(0),
},
},
}
data := cf.LaunchTemplateData

if e.SpotPrice != "" {
data.MarketOptions = &cloudformationLaunchTemplateMarketOptions{
MarketType: fi.String("spot"),
SpotOptions: []*cloudformationLaunchTemplateMarketOptionsSpotOptions{{MaxPrice: fi.String(e.SpotPrice)}},
marketSpotOptions := cloudformationLaunchTemplateMarketOptionsSpotOptions{MaxPrice: fi.String(e.SpotPrice)}
if e.SpotDurationInMinutes != nil {
marketSpotOptions.BlockDurationMinutes = e.SpotDurationInMinutes
}
launchTemplateData.MarketOptions = &cloudformationLaunchTemplateMarketOptions{MarketType: fi.String("spot"), SpotOptions: &marketSpotOptions}
}

cf := &cloudformationLaunchTemplate{
LaunchTemplateName: fi.String(fi.StringValue(e.Name)),
LaunchTemplateData: launchTemplateData,
}
data := cf.LaunchTemplateData

for _, x := range e.SecurityGroups {
data.NetworkInterfaces[0].SecurityGroups = append(data.NetworkInterfaces[0].SecurityGroups, x.CloudformationLink())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ func TestLaunchTemplateCloudformationRender(t *testing.T) {
RootVolumeOptimization: fi.Bool(true),
RootVolumeIops: fi.Int64(100),
RootVolumeSize: fi.Int64(64),
SpotPrice: "10",
SpotDurationInMinutes: fi.Int64(120),
SSHKey: &SSHKey{
Name: fi.String("mykey"),
},
Expand All @@ -61,6 +63,13 @@ func TestLaunchTemplateCloudformationRender(t *testing.T) {
},
"InstanceType": "t2.medium",
"KeyName": "mykey",
"InstanceMarketOptions": {
"MarketType": "spot",
"SpotOptions": {
"BlockDurationMinutes": 120,
"MaxPrice": "10"
}
},
"NetworkInterfaces": [
{
"AssociatePublicIpAddress": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,10 +179,14 @@ func (t *LaunchTemplate) RenderTerraform(target *terraform.TerraformTarget, a, e
}

if e.SpotPrice != "" {
marketSpotOptions := terraformLaunchTemplateMarketOptionsSpotOptions{MaxPrice: fi.String(e.SpotPrice)}
if e.SpotDurationInMinutes != nil {
marketSpotOptions.BlockDurationMinutes = e.SpotDurationInMinutes
}
tf.MarketOptions = []*terraformLaunchTemplateMarketOptions{
{
MarketType: fi.String("spot"),
SpotOptions: []*terraformLaunchTemplateMarketOptionsSpotOptions{{MaxPrice: fi.String(e.SpotPrice)}},
SpotOptions: []*terraformLaunchTemplateMarketOptionsSpotOptions{&marketSpotOptions},
},
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func TestLaunchTemplateTerraformRender(t *testing.T) {
InstanceMonitoring: fi.Bool(true),
InstanceType: fi.String("t2.medium"),
SpotPrice: "0.1",
SpotDurationInMinutes: fi.Int64(60),
RootVolumeOptimization: fi.Bool(true),
RootVolumeIops: fi.Int64(100),
RootVolumeSize: fi.Int64(64),
Expand Down Expand Up @@ -72,7 +73,8 @@ resource "aws_launch_template" "test" {
market_type = "spot"
spot_options = {
max_price = "0.1"
block_duration_minutes = 60
max_price = "0.1"
}
}
Expand Down

0 comments on commit 759e24a

Please sign in to comment.