Added support for instance interruption behavior

This commit is contained in:
Martin Tomes 2020-04-29 14:53:17 +02:00
parent 53f16fd9ef
commit c66180bc58
17 changed files with 103 additions and 24 deletions

View File

@ -217,6 +217,10 @@ spec:
image: image:
description: Image is the instance (ami etc) we should use description: Image is the instance (ami etc) we should use
type: string type: string
instanceInterruptionBehavior:
description: InstanceInterrutionBehavior defines if a spot instance
should be terminated, hibernated, or stopped after interruption
type: string
instanceProtection: instanceProtection:
description: InstanceProtection makes new instances in an autoscaling description: InstanceProtection makes new instances in an autoscaling
group protected from scale in group protected from scale in

View File

@ -161,6 +161,9 @@ type InstanceGroupSpec struct {
SysctlParameters []string `json:"sysctlParameters,omitempty"` SysctlParameters []string `json:"sysctlParameters,omitempty"`
// RollingUpdate defines the rolling-update behavior // RollingUpdate defines the rolling-update behavior
RollingUpdate *RollingUpdate `json:"rollingUpdate,omitempty"` RollingUpdate *RollingUpdate `json:"rollingUpdate,omitempty"`
// InstanceInterrutionBehavior defines if a spot instance should be terminated, hibernated,
// or stopped after interruption
InstanceInterruptionBehavior *string `json:"instanceInterruptionBehavior,omitempty"`
} }
const ( const (

View File

@ -157,6 +157,9 @@ type InstanceGroupSpec struct {
SysctlParameters []string `json:"sysctlParameters,omitempty"` SysctlParameters []string `json:"sysctlParameters,omitempty"`
// RollingUpdate defines the rolling-update behavior // RollingUpdate defines the rolling-update behavior
RollingUpdate *RollingUpdate `json:"rollingUpdate,omitempty"` RollingUpdate *RollingUpdate `json:"rollingUpdate,omitempty"`
// InstanceInterrutionBehavior defines if a spot instance should be terminated, hibernated,
// or stopped after interruption
InstanceInterruptionBehavior *string `json:"instanceInterruptionBehavior,omitempty"`
} }
const ( const (

View File

@ -3366,6 +3366,7 @@ func autoConvert_v1alpha2_InstanceGroupSpec_To_kops_InstanceGroupSpec(in *Instan
} else { } else {
out.RollingUpdate = nil out.RollingUpdate = nil
} }
out.InstanceInterruptionBehavior = in.InstanceInterruptionBehavior
return nil return nil
} }
@ -3504,6 +3505,7 @@ func autoConvert_kops_InstanceGroupSpec_To_v1alpha2_InstanceGroupSpec(in *kops.I
} else { } else {
out.RollingUpdate = nil out.RollingUpdate = nil
} }
out.InstanceInterruptionBehavior = in.InstanceInterruptionBehavior
return nil return nil
} }

View File

@ -1787,6 +1787,11 @@ func (in *InstanceGroupSpec) DeepCopyInto(out *InstanceGroupSpec) {
*out = new(RollingUpdate) *out = new(RollingUpdate)
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
if in.InstanceInterruptionBehavior != nil {
in, out := &in.InstanceInterruptionBehavior, &out.InstanceInterruptionBehavior
*out = new(string)
**out = **in
}
return return
} }

View File

@ -51,6 +51,8 @@ func awsValidateInstanceGroup(ig *kops.InstanceGroup) field.ErrorList {
allErrs = append(allErrs, awsValidateSpotDurationInMinute(field.NewPath(ig.GetName(), "spec", "spotDurationInMinutes"), ig)...) allErrs = append(allErrs, awsValidateSpotDurationInMinute(field.NewPath(ig.GetName(), "spec", "spotDurationInMinutes"), ig)...)
allErrs = append(allErrs, awsValidateInstanceInterruptionBehavior(field.NewPath(ig.GetName(), "spec", "instanceInterruptionBehavior"), ig)...)
return allErrs return allErrs
} }
@ -120,3 +122,13 @@ func awsValidateSpotDurationInMinute(fieldPath *field.Path, ig *kops.InstanceGro
} }
return allErrs return allErrs
} }
func awsValidateInstanceInterruptionBehavior(fieldPath *field.Path, ig *kops.InstanceGroup) field.ErrorList {
allErrs := field.ErrorList{}
if ig.Spec.InstanceInterruptionBehavior != nil {
validInterruptionBehaviors := []string{"terminate", "hibernate", "stop"}
instanceInterruptionBehavior := *ig.Spec.InstanceInterruptionBehavior
allErrs = append(allErrs, IsValidValue(fieldPath, &instanceInterruptionBehavior, validInterruptionBehaviors)...)
}
return allErrs
}

View File

@ -134,6 +134,32 @@ func TestValidateInstanceGroupSpec(t *testing.T) {
}, },
ExpectedErrors: []string{}, ExpectedErrors: []string{},
}, },
{
Input: kops.InstanceGroupSpec{
InstanceInterruptionBehavior: fi.String("invalidValue"),
},
ExpectedErrors: []string{
"Unsupported value::test-nodes.spec.instanceInterruptionBehavior",
},
},
{
Input: kops.InstanceGroupSpec{
InstanceInterruptionBehavior: fi.String("terminate"),
},
ExpectedErrors: []string{},
},
{
Input: kops.InstanceGroupSpec{
InstanceInterruptionBehavior: fi.String("hibernate"),
},
ExpectedErrors: []string{},
},
{
Input: kops.InstanceGroupSpec{
InstanceInterruptionBehavior: fi.String("stop"),
},
ExpectedErrors: []string{},
},
} }
for _, g := range grid { for _, g := range grid {
ig := &kops.InstanceGroup{ ig := &kops.InstanceGroup{

View File

@ -1953,6 +1953,11 @@ func (in *InstanceGroupSpec) DeepCopyInto(out *InstanceGroupSpec) {
*out = new(RollingUpdate) *out = new(RollingUpdate)
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
if in.InstanceInterruptionBehavior != nil {
in, out := &in.InstanceInterruptionBehavior, &out.InstanceInterruptionBehavior
*out = new(string)
**out = **in
}
return return
} }

View File

@ -134,6 +134,9 @@ func (b *AutoscalingGroupModelBuilder) buildLaunchTemplateTask(c *fi.ModelBuilde
if ig.Spec.SpotDurationInMinutes != nil { if ig.Spec.SpotDurationInMinutes != nil {
lt.SpotDurationInMinutes = ig.Spec.SpotDurationInMinutes lt.SpotDurationInMinutes = ig.Spec.SpotDurationInMinutes
} }
if ig.Spec.InstanceInterruptionBehavior != nil {
lt.InstanceInterruptionBehavior = ig.Spec.InstanceInterruptionBehavior
}
return lt, nil return lt, nil
} }

View File

@ -606,6 +606,7 @@
"MarketType": "spot", "MarketType": "spot",
"SpotOptions": { "SpotOptions": {
"BlockDurationMinutes": 120, "BlockDurationMinutes": 120,
"InstanceInterruptionBehavior": "hibernate",
"MaxPrice": "0.1" "MaxPrice": "0.1"
} }
}, },

View File

@ -73,6 +73,7 @@ spec:
instanceProtection: true instanceProtection: true
maxPrice: "0.1" maxPrice: "0.1"
spotDurationInMinutes: 120 spotDurationInMinutes: 120
instanceInterruptionBehavior: "hibernate"
subnets: subnets:
- us-test-1b - us-test-1b
--- ---

View File

@ -530,8 +530,9 @@ resource "aws_launch_template" "nodes-launchtemplates-example-com" {
instance_market_options { instance_market_options {
market_type = "spot" market_type = "spot"
spot_options { spot_options {
block_duration_minutes = 120 block_duration_minutes = 120
max_price = "0.1" instance_interruption_behavior = "hibernate"
max_price = "0.1"
} }
} }
instance_type = "t3.medium" instance_type = "t3.medium"

View File

@ -70,6 +70,9 @@ type LaunchTemplate struct {
Tenancy *string Tenancy *string
// UserData is the user data configuration // UserData is the user data configuration
UserData *fi.ResourceHolder UserData *fi.ResourceHolder
// InstanceInterrutionBehavior defines if a spot instance should be terminated, hibernated,
// or stopped after interruption
InstanceInterruptionBehavior *string
} }
var ( var (

View File

@ -64,8 +64,8 @@ type cloudformationLaunchTemplateIAMProfile struct {
type cloudformationLaunchTemplateMarketOptionsSpotOptions struct { type cloudformationLaunchTemplateMarketOptionsSpotOptions struct {
// BlockDurationMinutes is required duration in minutes. This value must be a multiple of 60. // BlockDurationMinutes is required duration in minutes. This value must be a multiple of 60.
BlockDurationMinutes *int64 `json:"BlockDurationMinutes,omitempty"` BlockDurationMinutes *int64 `json:"BlockDurationMinutes,omitempty"`
// InstancesInterruptionBehavior is the behavior when a Spot Instance is interrupted. Can be hibernate, stop, or terminate // InstanceInterruptionBehavior is the behavior when a Spot Instance is interrupted. Can be hibernate, stop, or terminate
InstancesInterruptionBehavior *string `json:"InstancesInterruptionBehavior,omitempty"` InstanceInterruptionBehavior *string `json:"InstanceInterruptionBehavior,omitempty"`
// MaxPrice is the maximum hourly price you're willing to pay for the Spot Instances // MaxPrice is the maximum hourly price you're willing to pay for the Spot Instances
MaxPrice *string `json:"MaxPrice,omitempty"` MaxPrice *string `json:"MaxPrice,omitempty"`
// SpotInstanceType is the Spot Instance request type. Can be one-time, or persistent // SpotInstanceType is the Spot Instance request type. Can be one-time, or persistent
@ -185,6 +185,9 @@ func (t *LaunchTemplate) RenderCloudformation(target *cloudformation.Cloudformat
if e.SpotDurationInMinutes != nil { if e.SpotDurationInMinutes != nil {
marketSpotOptions.BlockDurationMinutes = e.SpotDurationInMinutes marketSpotOptions.BlockDurationMinutes = e.SpotDurationInMinutes
} }
if e.InstanceInterruptionBehavior != nil {
marketSpotOptions.InstanceInterruptionBehavior = e.InstanceInterruptionBehavior
}
launchTemplateData.MarketOptions = &cloudformationLaunchTemplateMarketOptions{MarketType: fi.String("spot"), SpotOptions: &marketSpotOptions} launchTemplateData.MarketOptions = &cloudformationLaunchTemplateMarketOptions{MarketType: fi.String("spot"), SpotOptions: &marketSpotOptions}
} }

View File

@ -31,14 +31,15 @@ func TestLaunchTemplateCloudformationRender(t *testing.T) {
IAMInstanceProfile: &IAMInstanceProfile{ IAMInstanceProfile: &IAMInstanceProfile{
Name: fi.String("nodes"), Name: fi.String("nodes"),
}, },
ID: fi.String("test-11"), ID: fi.String("test-11"),
InstanceMonitoring: fi.Bool(true), InstanceMonitoring: fi.Bool(true),
InstanceType: fi.String("t2.medium"), InstanceType: fi.String("t2.medium"),
RootVolumeOptimization: fi.Bool(true), RootVolumeOptimization: fi.Bool(true),
RootVolumeIops: fi.Int64(100), RootVolumeIops: fi.Int64(100),
RootVolumeSize: fi.Int64(64), RootVolumeSize: fi.Int64(64),
SpotPrice: "10", SpotPrice: "10",
SpotDurationInMinutes: fi.Int64(120), SpotDurationInMinutes: fi.Int64(120),
InstanceInterruptionBehavior: fi.String("hibernate"),
SSHKey: &SSHKey{ SSHKey: &SSHKey{
Name: fi.String("mykey"), Name: fi.String("mykey"),
}, },
@ -67,6 +68,7 @@ func TestLaunchTemplateCloudformationRender(t *testing.T) {
"MarketType": "spot", "MarketType": "spot",
"SpotOptions": { "SpotOptions": {
"BlockDurationMinutes": 120, "BlockDurationMinutes": 120,
"InstanceInterruptionBehavior": "hibernate",
"MaxPrice": "10" "MaxPrice": "10"
} }
}, },

View File

@ -61,8 +61,8 @@ type terraformLaunchTemplateIAMProfile struct {
type terraformLaunchTemplateMarketOptionsSpotOptions struct { type terraformLaunchTemplateMarketOptionsSpotOptions struct {
// BlockDurationMinutes is required duration in minutes. This value must be a multiple of 60. // BlockDurationMinutes is required duration in minutes. This value must be a multiple of 60.
BlockDurationMinutes *int64 `json:"block_duration_minutes,omitempty" cty:"block_duration_minutes"` BlockDurationMinutes *int64 `json:"block_duration_minutes,omitempty" cty:"block_duration_minutes"`
// InstancesInterruptionBehavior is the behavior when a Spot Instance is interrupted. Can be hibernate, stop, or terminate // InstanceInterruptionBehavior is the behavior when a Spot Instance is interrupted. Can be hibernate, stop, or terminate
InstancesInterruptionBehavior *string `json:"instances_interruption_behavior,omitempty" cty:"instances_interruption_behavior"` InstanceInterruptionBehavior *string `json:"instance_interruption_behavior,omitempty" cty:"instance_interruption_behavior"`
// MaxPrice is the maximum hourly price you're willing to pay for the Spot Instances // MaxPrice is the maximum hourly price you're willing to pay for the Spot Instances
MaxPrice *string `json:"max_price,omitempty" cty:"max_price"` MaxPrice *string `json:"max_price,omitempty" cty:"max_price"`
// SpotInstanceType is the Spot Instance request type. Can be one-time, or persistent // SpotInstanceType is the Spot Instance request type. Can be one-time, or persistent
@ -183,6 +183,9 @@ func (t *LaunchTemplate) RenderTerraform(target *terraform.TerraformTarget, a, e
if e.SpotDurationInMinutes != nil { if e.SpotDurationInMinutes != nil {
marketSpotOptions.BlockDurationMinutes = e.SpotDurationInMinutes marketSpotOptions.BlockDurationMinutes = e.SpotDurationInMinutes
} }
if e.InstanceInterruptionBehavior != nil {
marketSpotOptions.InstanceInterruptionBehavior = e.InstanceInterruptionBehavior
}
tf.MarketOptions = []*terraformLaunchTemplateMarketOptions{ tf.MarketOptions = []*terraformLaunchTemplateMarketOptions{
{ {
MarketType: fi.String("spot"), MarketType: fi.String("spot"),

View File

@ -31,14 +31,15 @@ func TestLaunchTemplateTerraformRender(t *testing.T) {
IAMInstanceProfile: &IAMInstanceProfile{ IAMInstanceProfile: &IAMInstanceProfile{
Name: fi.String("nodes"), Name: fi.String("nodes"),
}, },
ID: fi.String("test-11"), ID: fi.String("test-11"),
InstanceMonitoring: fi.Bool(true), InstanceMonitoring: fi.Bool(true),
InstanceType: fi.String("t2.medium"), InstanceType: fi.String("t2.medium"),
SpotPrice: "0.1", SpotPrice: "0.1",
SpotDurationInMinutes: fi.Int64(60), SpotDurationInMinutes: fi.Int64(60),
RootVolumeOptimization: fi.Bool(true), InstanceInterruptionBehavior: fi.String("hibernate"),
RootVolumeIops: fi.Int64(100), RootVolumeOptimization: fi.Bool(true),
RootVolumeSize: fi.Int64(64), RootVolumeIops: fi.Int64(100),
RootVolumeSize: fi.Int64(64),
SSHKey: &SSHKey{ SSHKey: &SSHKey{
Name: fi.String("newkey"), Name: fi.String("newkey"),
PublicKey: fi.WrapResource(fi.NewStringResource("newkey")), PublicKey: fi.WrapResource(fi.NewStringResource("newkey")),
@ -61,8 +62,9 @@ resource "aws_launch_template" "test" {
instance_market_options { instance_market_options {
market_type = "spot" market_type = "spot"
spot_options { spot_options {
block_duration_minutes = 60 block_duration_minutes = 60
max_price = "0.1" instance_interruption_behavior = "hibernate"
max_price = "0.1"
} }
} }
instance_type = "t2.medium" instance_type = "t2.medium"