mirror of https://github.com/kubernetes/kops.git
885 lines
31 KiB
Go
885 lines
31 KiB
Go
/*
|
|
Copyright 2019 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package awstasks
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
|
|
"k8s.io/kops/upup/pkg/fi"
|
|
"k8s.io/kops/upup/pkg/fi/cloudup/awsup"
|
|
"k8s.io/kops/upup/pkg/fi/cloudup/cloudformation"
|
|
"k8s.io/kops/upup/pkg/fi/cloudup/terraform"
|
|
"k8s.io/kops/util/pkg/maps"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/service/autoscaling"
|
|
"k8s.io/klog"
|
|
)
|
|
|
|
// CloudTagInstanceGroupRolePrefix is a cloud tag that defines the instance role
|
|
const CloudTagInstanceGroupRolePrefix = "k8s.io/role/"
|
|
|
|
// AutoscalingGroup provdes the definition for a autoscaling group in aws
|
|
type AutoscalingGroup struct {
|
|
// Name is the name of the ASG
|
|
Name *string
|
|
// Lifecycle is the resource lifecycle
|
|
Lifecycle *fi.Lifecycle
|
|
|
|
// Granularity specifys the granularity of the metrics
|
|
Granularity *string
|
|
// InstanceProtection makes new instances in an autoscaling group protected from scale in
|
|
InstanceProtection *bool
|
|
// LaunchConfiguration is the launch configuration for the autoscaling group
|
|
LaunchConfiguration *LaunchConfiguration
|
|
// LaunchTemplate is the launch template for the asg
|
|
LaunchTemplate *LaunchTemplate
|
|
// MaxSize is the max number of nodes in asg
|
|
MaxSize *int64
|
|
// Metrics is a collection of metrics to monitor
|
|
Metrics []string
|
|
// MinSize is the smallest number of nodes in the asg
|
|
MinSize *int64
|
|
// MixedInstanceOverrides is a collection of instance type to use with fleet policy
|
|
MixedInstanceOverrides []string
|
|
// MixedOnDemandAllocationStrategy is allocation strategy to use for on-demand instances
|
|
MixedOnDemandAllocationStrategy *string
|
|
// MixedOnDemandBase is percentage split of On-Demand Instances and Spot Instances for your
|
|
// additional capacity beyond the base portion
|
|
MixedOnDemandBase *int64
|
|
// MixedOnDemandAboveBase is the percentage split of On-Demand Instances and Spot Instances
|
|
// for your additional capacity beyond the base portion.
|
|
MixedOnDemandAboveBase *int64
|
|
// MixedSpotAllocationStrategy diversifies your Spot capacity across multiple instance types to
|
|
// find the best pricing. Higher Spot availability may result from a larger number of
|
|
// instance types to choose from.
|
|
MixedSpotAllocationStrategy *string
|
|
// MixedSpotInstancePools is the number of Spot pools to use to allocate your Spot capacity (defaults to 2)
|
|
// pools are determined from the different instance types in the Overrides array of LaunchTemplate
|
|
MixedSpotInstancePools *int64
|
|
// MixedSpotMaxPrice is the maximum price per unit hour you are willing to pay for a Spot Instance
|
|
MixedSpotMaxPrice *string
|
|
// Subnets is a collection of subnets to attach the nodes to
|
|
Subnets []*Subnet
|
|
// SuspendProcesses
|
|
SuspendProcesses *[]string
|
|
// Tags is a collection of keypairs to apply to the node on launch
|
|
Tags map[string]string
|
|
}
|
|
|
|
var _ fi.CompareWithID = &AutoscalingGroup{}
|
|
|
|
// CompareWithID returns the ID of the ASG
|
|
func (e *AutoscalingGroup) CompareWithID() *string {
|
|
return e.Name
|
|
}
|
|
|
|
// Find is used to discover the ASG in the cloud provider
|
|
func (e *AutoscalingGroup) Find(c *fi.Context) (*AutoscalingGroup, error) {
|
|
cloud := c.Cloud.(awsup.AWSCloud)
|
|
|
|
g, err := findAutoscalingGroup(cloud, fi.StringValue(e.Name))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if g == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
actual := &AutoscalingGroup{
|
|
Name: g.AutoScalingGroupName,
|
|
MaxSize: g.MaxSize,
|
|
MinSize: g.MinSize,
|
|
}
|
|
|
|
if g.VPCZoneIdentifier != nil {
|
|
subnets := strings.Split(*g.VPCZoneIdentifier, ",")
|
|
for _, subnet := range subnets {
|
|
actual.Subnets = append(actual.Subnets, &Subnet{ID: aws.String(subnet)})
|
|
}
|
|
}
|
|
|
|
for _, enabledMetric := range g.EnabledMetrics {
|
|
actual.Metrics = append(actual.Metrics, aws.StringValue(enabledMetric.Metric))
|
|
actual.Granularity = enabledMetric.Granularity
|
|
}
|
|
sort.Strings(actual.Metrics)
|
|
|
|
if len(g.Tags) != 0 {
|
|
actual.Tags = make(map[string]string)
|
|
for _, tag := range g.Tags {
|
|
actual.Tags[fi.StringValue(tag.Key)] = fi.StringValue(tag.Value)
|
|
}
|
|
}
|
|
|
|
if g.LaunchConfigurationName != nil {
|
|
actual.LaunchConfiguration = &LaunchConfiguration{ID: g.LaunchConfigurationName}
|
|
}
|
|
if g.LaunchTemplate != nil {
|
|
actual.LaunchTemplate = &LaunchTemplate{ID: g.LaunchTemplate.LaunchTemplateName}
|
|
}
|
|
|
|
if g.MixedInstancesPolicy != nil {
|
|
mp := g.MixedInstancesPolicy
|
|
if mp.InstancesDistribution != nil {
|
|
mpd := mp.InstancesDistribution
|
|
actual.MixedOnDemandAboveBase = mpd.OnDemandPercentageAboveBaseCapacity
|
|
actual.MixedOnDemandAllocationStrategy = mpd.OnDemandAllocationStrategy
|
|
actual.MixedOnDemandBase = mpd.OnDemandBaseCapacity
|
|
actual.MixedSpotAllocationStrategy = mpd.SpotAllocationStrategy
|
|
actual.MixedSpotInstancePools = mpd.SpotInstancePools
|
|
actual.MixedSpotMaxPrice = mpd.SpotMaxPrice
|
|
}
|
|
|
|
if g.MixedInstancesPolicy.LaunchTemplate != nil {
|
|
if g.MixedInstancesPolicy.LaunchTemplate.LaunchTemplateSpecification != nil {
|
|
actual.LaunchTemplate = &LaunchTemplate{ID: g.MixedInstancesPolicy.LaunchTemplate.LaunchTemplateSpecification.LaunchTemplateName}
|
|
}
|
|
|
|
for _, n := range g.MixedInstancesPolicy.LaunchTemplate.Overrides {
|
|
actual.MixedInstanceOverrides = append(actual.MixedInstanceOverrides, fi.StringValue(n.InstanceType))
|
|
}
|
|
}
|
|
}
|
|
|
|
if subnetSlicesEqualIgnoreOrder(actual.Subnets, e.Subnets) {
|
|
actual.Subnets = e.Subnets
|
|
}
|
|
|
|
processes := []string{}
|
|
for _, p := range g.SuspendedProcesses {
|
|
processes = append(processes, *p.ProcessName)
|
|
}
|
|
|
|
actual.SuspendProcesses = &processes
|
|
|
|
// Avoid spurious changes
|
|
actual.Lifecycle = e.Lifecycle
|
|
|
|
if g.NewInstancesProtectedFromScaleIn != nil {
|
|
actual.InstanceProtection = g.NewInstancesProtectedFromScaleIn
|
|
}
|
|
|
|
return actual, nil
|
|
}
|
|
|
|
// findAutoscalingGroup is responsilble for finding all the autoscaling groups for us
|
|
func findAutoscalingGroup(cloud awsup.AWSCloud, name string) (*autoscaling.Group, error) {
|
|
request := &autoscaling.DescribeAutoScalingGroupsInput{
|
|
AutoScalingGroupNames: []*string{&name},
|
|
}
|
|
|
|
var found []*autoscaling.Group
|
|
err := cloud.Autoscaling().DescribeAutoScalingGroupsPages(request, func(p *autoscaling.DescribeAutoScalingGroupsOutput, lastPage bool) (shouldContinue bool) {
|
|
for _, g := range p.AutoScalingGroups {
|
|
// Check for "Delete in progress" (the only use .Status). We won't be able to update or create while
|
|
// this is true, but filtering it out here makes the messages slightly clearer.
|
|
if g.Status != nil {
|
|
klog.Warningf("Skipping AutoScalingGroup %v: %v", fi.StringValue(g.AutoScalingGroupName), fi.StringValue(g.Status))
|
|
continue
|
|
}
|
|
|
|
if aws.StringValue(g.AutoScalingGroupName) == name {
|
|
found = append(found, g)
|
|
} else {
|
|
klog.Warningf("Got ASG with unexpected name %q", fi.StringValue(g.AutoScalingGroupName))
|
|
}
|
|
}
|
|
|
|
return true
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error listing AutoscalingGroups: %v", err)
|
|
}
|
|
|
|
switch len(found) {
|
|
case 0:
|
|
return nil, nil
|
|
case 1:
|
|
return found[0], nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("found multiple AutoscalingGroups with name: %q", name)
|
|
}
|
|
|
|
func (e *AutoscalingGroup) normalize(c *fi.Context) error {
|
|
sort.Strings(e.Metrics)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Run is responsible for running the task
|
|
func (e *AutoscalingGroup) Run(c *fi.Context) error {
|
|
err := e.normalize(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.Cloud.(awsup.AWSCloud).AddTags(e.Name, e.Tags)
|
|
|
|
return fi.DefaultDeltaRunMethod(e, c)
|
|
}
|
|
|
|
// CheckChanges is responsible for checking for changes??
|
|
func (e *AutoscalingGroup) CheckChanges(a, ex, changes *AutoscalingGroup) error {
|
|
if a != nil {
|
|
if ex.Name == nil {
|
|
return fi.RequiredField("Name")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RenderAWS is responsible for building the autoscaling group via AWS API
|
|
func (v *AutoscalingGroup) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *AutoscalingGroup) error {
|
|
// @step: did we find an autoscaling group?
|
|
if a == nil {
|
|
klog.V(2).Infof("Creating autoscaling group with name: %s", fi.StringValue(e.Name))
|
|
|
|
request := &autoscaling.CreateAutoScalingGroupInput{
|
|
AutoScalingGroupName: e.Name,
|
|
MinSize: e.MinSize,
|
|
MaxSize: e.MaxSize,
|
|
Tags: v.AutoscalingGroupTags(),
|
|
VPCZoneIdentifier: fi.String(strings.Join(e.AutoscalingGroupSubnets(), ",")),
|
|
}
|
|
|
|
// @check are we using a launchconfiguation
|
|
if e.LaunchConfiguration != nil {
|
|
request.LaunchConfigurationName = e.LaunchConfiguration.ID
|
|
}
|
|
// @check are we using launch template
|
|
if e.LaunchTemplate != nil {
|
|
request.LaunchTemplate = &autoscaling.LaunchTemplateSpecification{
|
|
LaunchTemplateName: e.LaunchTemplate.ID,
|
|
}
|
|
}
|
|
// @check if we are using mixed instance policies
|
|
if e.UseMixedInstancesPolicy() {
|
|
// we can zero this out for now and use the mixed instance policy for definition
|
|
request.LaunchTemplate = nil
|
|
request.MixedInstancesPolicy = &autoscaling.MixedInstancesPolicy{
|
|
InstancesDistribution: &autoscaling.InstancesDistribution{
|
|
OnDemandPercentageAboveBaseCapacity: e.MixedOnDemandAboveBase,
|
|
OnDemandBaseCapacity: e.MixedOnDemandBase,
|
|
SpotAllocationStrategy: e.MixedSpotAllocationStrategy,
|
|
SpotInstancePools: e.MixedSpotInstancePools,
|
|
SpotMaxPrice: e.MixedSpotMaxPrice,
|
|
},
|
|
LaunchTemplate: &autoscaling.LaunchTemplate{
|
|
LaunchTemplateSpecification: &autoscaling.LaunchTemplateSpecification{LaunchTemplateName: e.LaunchTemplate.ID},
|
|
},
|
|
}
|
|
p := request.MixedInstancesPolicy.LaunchTemplate
|
|
for _, x := range e.MixedInstanceOverrides {
|
|
p.Overrides = append(p.Overrides, &autoscaling.LaunchTemplateOverrides{InstanceType: fi.String(x)})
|
|
}
|
|
}
|
|
|
|
// @step: attempt to create the autoscaling group for us
|
|
if _, err := t.Cloud.Autoscaling().CreateAutoScalingGroup(request); err != nil {
|
|
return fmt.Errorf("error creating AutoscalingGroup: %v", err)
|
|
}
|
|
|
|
// @step: attempt to enable the metrics for us
|
|
if _, err := t.Cloud.Autoscaling().EnableMetricsCollection(&autoscaling.EnableMetricsCollectionInput{
|
|
AutoScalingGroupName: e.Name,
|
|
Granularity: e.Granularity,
|
|
Metrics: aws.StringSlice(e.Metrics),
|
|
}); err != nil {
|
|
return fmt.Errorf("error enabling metrics collection for AutoscalingGroup: %v", err)
|
|
}
|
|
|
|
if len(*e.SuspendProcesses) > 0 {
|
|
toSuspend := []*string{}
|
|
for _, p := range *e.SuspendProcesses {
|
|
toSuspend = append(toSuspend, &p)
|
|
}
|
|
|
|
processQuery := &autoscaling.ScalingProcessQuery{}
|
|
processQuery.AutoScalingGroupName = e.Name
|
|
processQuery.ScalingProcesses = toSuspend
|
|
|
|
if _, err := t.Cloud.Autoscaling().SuspendProcesses(processQuery); err != nil {
|
|
return fmt.Errorf("error suspending processes: %v", err)
|
|
}
|
|
}
|
|
|
|
if e.InstanceProtection != nil {
|
|
request.NewInstancesProtectedFromScaleIn = e.InstanceProtection
|
|
}
|
|
} else {
|
|
// @logic: else we have found a autoscaling group and we need to evaluate the difference
|
|
request := &autoscaling.UpdateAutoScalingGroupInput{
|
|
AutoScalingGroupName: e.Name,
|
|
}
|
|
|
|
if changes.LaunchConfiguration != nil {
|
|
request.LaunchConfigurationName = e.LaunchConfiguration.ID
|
|
changes.LaunchConfiguration = nil
|
|
}
|
|
setup := func(req *autoscaling.UpdateAutoScalingGroupInput) *autoscaling.MixedInstancesPolicy {
|
|
if req.MixedInstancesPolicy == nil {
|
|
req.MixedInstancesPolicy = &autoscaling.MixedInstancesPolicy{
|
|
InstancesDistribution: &autoscaling.InstancesDistribution{},
|
|
}
|
|
}
|
|
|
|
return req.MixedInstancesPolicy
|
|
}
|
|
|
|
if changes.LaunchTemplate != nil {
|
|
// @note: at the moment we are only using launch templates when using mixed instance policies,
|
|
// but this might change
|
|
setup(request).LaunchTemplate = &autoscaling.LaunchTemplate{
|
|
LaunchTemplateSpecification: &autoscaling.LaunchTemplateSpecification{
|
|
LaunchTemplateName: changes.LaunchTemplate.ID,
|
|
},
|
|
}
|
|
changes.LaunchTemplate = nil
|
|
}
|
|
|
|
if changes.MixedOnDemandAboveBase != nil {
|
|
setup(request).InstancesDistribution.OnDemandPercentageAboveBaseCapacity = e.MixedOnDemandAboveBase
|
|
changes.MixedOnDemandAboveBase = nil
|
|
}
|
|
if changes.MixedOnDemandBase != nil {
|
|
setup(request).InstancesDistribution.OnDemandBaseCapacity = e.MixedOnDemandBase
|
|
changes.MixedOnDemandBase = nil
|
|
}
|
|
if changes.MixedSpotAllocationStrategy != nil {
|
|
setup(request).InstancesDistribution.SpotAllocationStrategy = e.MixedSpotAllocationStrategy
|
|
changes.MixedSpotAllocationStrategy = nil
|
|
}
|
|
if changes.MixedSpotInstancePools != nil {
|
|
setup(request).InstancesDistribution.SpotInstancePools = e.MixedSpotInstancePools
|
|
changes.MixedSpotInstancePools = nil
|
|
}
|
|
if changes.MixedSpotMaxPrice != nil {
|
|
setup(request).InstancesDistribution.SpotMaxPrice = e.MixedSpotMaxPrice
|
|
changes.MixedSpotMaxPrice = nil
|
|
}
|
|
if changes.MixedInstanceOverrides != nil {
|
|
if setup(request).LaunchTemplate == nil {
|
|
setup(request).LaunchTemplate = &autoscaling.LaunchTemplate{
|
|
LaunchTemplateSpecification: &autoscaling.LaunchTemplateSpecification{
|
|
LaunchTemplateName: e.LaunchTemplate.ID,
|
|
},
|
|
}
|
|
}
|
|
|
|
p := request.MixedInstancesPolicy.LaunchTemplate
|
|
for _, x := range changes.MixedInstanceOverrides {
|
|
p.Overrides = append(p.Overrides, &autoscaling.LaunchTemplateOverrides{InstanceType: fi.String(x)})
|
|
}
|
|
changes.MixedInstanceOverrides = nil
|
|
}
|
|
|
|
if changes.MinSize != nil {
|
|
request.MinSize = e.MinSize
|
|
changes.MinSize = nil
|
|
}
|
|
if changes.MaxSize != nil {
|
|
request.MaxSize = e.MaxSize
|
|
changes.MaxSize = nil
|
|
}
|
|
if changes.Subnets != nil {
|
|
request.VPCZoneIdentifier = aws.String(strings.Join(e.AutoscalingGroupSubnets(), ","))
|
|
changes.Subnets = nil
|
|
}
|
|
|
|
var updateTagsRequest *autoscaling.CreateOrUpdateTagsInput
|
|
var deleteTagsRequest *autoscaling.DeleteTagsInput
|
|
if changes.Tags != nil {
|
|
updateTagsRequest = &autoscaling.CreateOrUpdateTagsInput{Tags: e.AutoscalingGroupTags()}
|
|
|
|
if a != nil && len(a.Tags) > 0 {
|
|
deleteTagsRequest = &autoscaling.DeleteTagsInput{}
|
|
deleteTagsRequest.Tags = e.getASGTagsToDelete(a.Tags)
|
|
}
|
|
|
|
changes.Tags = nil
|
|
}
|
|
|
|
if changes.Metrics != nil || changes.Granularity != nil {
|
|
// TODO: Support disabling metrics?
|
|
if len(e.Metrics) != 0 {
|
|
_, err := t.Cloud.Autoscaling().EnableMetricsCollection(&autoscaling.EnableMetricsCollectionInput{
|
|
AutoScalingGroupName: e.Name,
|
|
Granularity: e.Granularity,
|
|
Metrics: aws.StringSlice(e.Metrics),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("error enabling metrics collection for AutoscalingGroup: %v", err)
|
|
}
|
|
changes.Metrics = nil
|
|
changes.Granularity = nil
|
|
}
|
|
}
|
|
|
|
if changes.SuspendProcesses != nil {
|
|
toSuspend := processCompare(e.SuspendProcesses, a.SuspendProcesses)
|
|
toResume := processCompare(a.SuspendProcesses, e.SuspendProcesses)
|
|
|
|
if len(toSuspend) > 0 {
|
|
suspendProcessQuery := &autoscaling.ScalingProcessQuery{}
|
|
suspendProcessQuery.AutoScalingGroupName = e.Name
|
|
suspendProcessQuery.ScalingProcesses = toSuspend
|
|
|
|
_, err := t.Cloud.Autoscaling().SuspendProcesses(suspendProcessQuery)
|
|
if err != nil {
|
|
return fmt.Errorf("error suspending processes: %v", err)
|
|
}
|
|
}
|
|
if len(toResume) > 0 {
|
|
resumeProcessQuery := &autoscaling.ScalingProcessQuery{}
|
|
resumeProcessQuery.AutoScalingGroupName = e.Name
|
|
resumeProcessQuery.ScalingProcesses = toResume
|
|
|
|
_, err := t.Cloud.Autoscaling().ResumeProcesses(resumeProcessQuery)
|
|
if err != nil {
|
|
return fmt.Errorf("error resuming processes: %v", err)
|
|
}
|
|
}
|
|
changes.SuspendProcesses = nil
|
|
}
|
|
|
|
if changes.InstanceProtection != nil {
|
|
request.NewInstancesProtectedFromScaleIn = e.InstanceProtection
|
|
changes.InstanceProtection = nil
|
|
}
|
|
|
|
empty := &AutoscalingGroup{}
|
|
if !reflect.DeepEqual(empty, changes) {
|
|
klog.Warningf("cannot apply changes to AutoScalingGroup: %v", changes)
|
|
}
|
|
|
|
klog.V(2).Infof("Updating autoscaling group %s", fi.StringValue(e.Name))
|
|
|
|
if _, err := t.Cloud.Autoscaling().UpdateAutoScalingGroup(request); err != nil {
|
|
return fmt.Errorf("error updating AutoscalingGroup: %v", err)
|
|
}
|
|
|
|
if deleteTagsRequest != nil && len(deleteTagsRequest.Tags) > 0 {
|
|
if _, err := t.Cloud.Autoscaling().DeleteTags(deleteTagsRequest); err != nil {
|
|
return fmt.Errorf("error deleting old AutoscalingGroup tags: %v", err)
|
|
}
|
|
}
|
|
if updateTagsRequest != nil {
|
|
if _, err := t.Cloud.Autoscaling().CreateOrUpdateTags(updateTagsRequest); err != nil {
|
|
return fmt.Errorf("error updating AutoscalingGroup tags: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UseMixedInstancesPolicy checks if we should add a mixed instances policy to the asg
|
|
func (e *AutoscalingGroup) UseMixedInstancesPolicy() bool {
|
|
if e.LaunchTemplate == nil {
|
|
return false
|
|
}
|
|
// @check if any of the mixed instance policies settings are toggled
|
|
if e.MixedOnDemandAboveBase != nil {
|
|
return true
|
|
}
|
|
if e.MixedOnDemandBase != nil {
|
|
return true
|
|
}
|
|
if e.MixedSpotAllocationStrategy != nil {
|
|
return true
|
|
}
|
|
if e.MixedSpotInstancePools != nil {
|
|
return true
|
|
}
|
|
if len(e.MixedInstanceOverrides) > 0 {
|
|
return true
|
|
}
|
|
if e.MixedSpotMaxPrice != nil {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// AutoscalingGroupTags is responsible for generating the tagging for the asg
|
|
func (e *AutoscalingGroup) AutoscalingGroupTags() []*autoscaling.Tag {
|
|
var list []*autoscaling.Tag
|
|
|
|
for k, v := range e.Tags {
|
|
list = append(list, &autoscaling.Tag{
|
|
Key: aws.String(k),
|
|
Value: aws.String(v),
|
|
ResourceId: e.Name,
|
|
ResourceType: aws.String("auto-scaling-group"),
|
|
PropagateAtLaunch: aws.Bool(true),
|
|
})
|
|
}
|
|
|
|
return list
|
|
}
|
|
|
|
// AutoscalingGroupSubnets returns the subnets list
|
|
func (e *AutoscalingGroup) AutoscalingGroupSubnets() []string {
|
|
var list []string
|
|
|
|
for _, x := range e.Subnets {
|
|
list = append(list, fi.StringValue(x.ID))
|
|
}
|
|
|
|
return list
|
|
}
|
|
|
|
// processCompare returns processes that exist in a but not in b
|
|
func processCompare(a *[]string, b *[]string) []*string {
|
|
notInB := []*string{}
|
|
for _, ap := range *a {
|
|
found := false
|
|
for _, bp := range *b {
|
|
if ap == bp {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
notFound := ap
|
|
notInB = append(notInB, ¬Found)
|
|
}
|
|
}
|
|
return notInB
|
|
}
|
|
|
|
// getASGTagsToDelete loops through the currently set tags and builds a list of
|
|
// tags to be deleted from the Autoscaling Group
|
|
func (e *AutoscalingGroup) getASGTagsToDelete(currentTags map[string]string) []*autoscaling.Tag {
|
|
tagsToDelete := []*autoscaling.Tag{}
|
|
|
|
for k, v := range currentTags {
|
|
if _, ok := e.Tags[k]; !ok {
|
|
tagsToDelete = append(tagsToDelete, &autoscaling.Tag{
|
|
Key: aws.String(k),
|
|
Value: aws.String(v),
|
|
ResourceId: e.Name,
|
|
ResourceType: aws.String("auto-scaling-group"),
|
|
})
|
|
}
|
|
}
|
|
return tagsToDelete
|
|
}
|
|
|
|
type terraformASGTag struct {
|
|
Key *string `json:"key"`
|
|
Value *string `json:"value"`
|
|
PropagateAtLaunch *bool `json:"propagate_at_launch"`
|
|
}
|
|
|
|
type terraformAutoscalingLaunchTemplateSpecification struct {
|
|
// LaunchTemplateID is the ID of the template to use
|
|
LaunchTemplateID *terraform.Literal `json:"launch_template_id,omitempty"`
|
|
// Version is the version of the Launch Template to use
|
|
Version *terraform.Literal `json:"version,omitempty"`
|
|
}
|
|
|
|
type terraformAutoscalingLaunchTemplateOverride struct {
|
|
// InstanceType is the instance to use
|
|
InstanceType *string `json:"instance_type,omitempty"`
|
|
}
|
|
|
|
type terraformAutoscalingLaunchTemplate struct {
|
|
// LaunchTemplateSpecification is the definition for a LT
|
|
LaunchTemplateSpecification []*terraformAutoscalingLaunchTemplateSpecification `json:"launch_template_specification,omitempty"`
|
|
// Override the is machine type override
|
|
Override []*terraformAutoscalingLaunchTemplateOverride `json:"override,omitempty"`
|
|
}
|
|
|
|
type terraformAutoscalingInstanceDistribution struct {
|
|
// OnDemandAllocationStrategy
|
|
OnDemandAllocationStrategy *string `json:"on_demand_allocation_strategy,omitempty"`
|
|
// OnDemandBaseCapacity is the base ondemand requirement
|
|
OnDemandBaseCapacity *int64 `json:"on_demand_base_capacity,omitempty"`
|
|
// OnDemandPercentageAboveBaseCapacity is the percentage above base for on-demand instances
|
|
OnDemandPercentageAboveBaseCapacity *int64 `json:"on_demand_percentage_above_base_capacity,omitempty"`
|
|
// SpotAllocationStrategy is the spot allocation stratergy
|
|
SpotAllocationStrategy *string `json:"spot_allocation_strategy,omitempty"`
|
|
// SpotInstancePool is the number of pools
|
|
SpotInstancePool *int64 `json:"spot_instance_pools,omitempty"`
|
|
// SpotMaxPrice is the max bid on spot instance, defaults to demand value
|
|
SpotMaxPrice *string `json:"spot_max_price,omitempty"`
|
|
}
|
|
|
|
type terraformMixedInstancesPolicy struct {
|
|
// LaunchTemplate is the launch template spec
|
|
LaunchTemplate []*terraformAutoscalingLaunchTemplate `json:"launch_template,omitempty"`
|
|
// InstanceDistribution is the distribution strategy
|
|
InstanceDistribution []*terraformAutoscalingInstanceDistribution `json:"instances_distribution,omitempty"`
|
|
}
|
|
|
|
type terraformAutoscalingGroup struct {
|
|
Name *string `json:"name,omitempty"`
|
|
LaunchConfigurationName *terraform.Literal `json:"launch_configuration,omitempty"`
|
|
MaxSize *int64 `json:"max_size,omitempty"`
|
|
MinSize *int64 `json:"min_size,omitempty"`
|
|
MixedInstancesPolicy []*terraformMixedInstancesPolicy `json:"mixed_instances_policy,omitempty"`
|
|
VPCZoneIdentifier []*terraform.Literal `json:"vpc_zone_identifier,omitempty"`
|
|
Tags []*terraformASGTag `json:"tag,omitempty"`
|
|
MetricsGranularity *string `json:"metrics_granularity,omitempty"`
|
|
EnabledMetrics []*string `json:"enabled_metrics,omitempty"`
|
|
SuspendedProcesses []*string `json:"suspended_processes,omitempty"`
|
|
InstanceProtection *bool `json:"protect_from_scale_in,omitempty"`
|
|
}
|
|
|
|
// RenderTerraform is responsible for rendering the terraform codebase
|
|
func (_ *AutoscalingGroup) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *AutoscalingGroup) error {
|
|
tf := &terraformAutoscalingGroup{
|
|
Name: e.Name,
|
|
MinSize: e.MinSize,
|
|
MaxSize: e.MaxSize,
|
|
MetricsGranularity: e.Granularity,
|
|
EnabledMetrics: aws.StringSlice(e.Metrics),
|
|
InstanceProtection: e.InstanceProtection,
|
|
}
|
|
|
|
for _, s := range e.Subnets {
|
|
tf.VPCZoneIdentifier = append(tf.VPCZoneIdentifier, s.TerraformLink())
|
|
}
|
|
|
|
for _, k := range maps.SortedKeys(e.Tags) {
|
|
v := e.Tags[k]
|
|
tf.Tags = append(tf.Tags, &terraformASGTag{
|
|
Key: fi.String(k),
|
|
Value: fi.String(v),
|
|
PropagateAtLaunch: fi.Bool(true),
|
|
})
|
|
}
|
|
|
|
if e.UseMixedInstancesPolicy() {
|
|
tf.MixedInstancesPolicy = []*terraformMixedInstancesPolicy{
|
|
{
|
|
LaunchTemplate: []*terraformAutoscalingLaunchTemplate{
|
|
{
|
|
LaunchTemplateSpecification: []*terraformAutoscalingLaunchTemplateSpecification{
|
|
{
|
|
LaunchTemplateID: e.LaunchTemplate.TerraformLink(),
|
|
Version: e.LaunchTemplate.VersionLink(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
InstanceDistribution: []*terraformAutoscalingInstanceDistribution{
|
|
{
|
|
OnDemandAllocationStrategy: e.MixedOnDemandAllocationStrategy,
|
|
OnDemandBaseCapacity: e.MixedOnDemandBase,
|
|
OnDemandPercentageAboveBaseCapacity: e.MixedOnDemandAboveBase,
|
|
SpotAllocationStrategy: e.MixedSpotAllocationStrategy,
|
|
SpotInstancePool: e.MixedSpotInstancePools,
|
|
SpotMaxPrice: e.MixedSpotMaxPrice,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, x := range e.MixedInstanceOverrides {
|
|
tf.MixedInstancesPolicy[0].LaunchTemplate[0].Override = append(tf.MixedInstancesPolicy[0].LaunchTemplate[0].Override, &terraformAutoscalingLaunchTemplateOverride{InstanceType: fi.String(x)})
|
|
}
|
|
}
|
|
|
|
role := ""
|
|
for k := range e.Tags {
|
|
if strings.HasPrefix(k, CloudTagInstanceGroupRolePrefix) {
|
|
suffix := strings.TrimPrefix(k, CloudTagInstanceGroupRolePrefix)
|
|
if role != "" && role != suffix {
|
|
return fmt.Errorf("Found multiple role tags: %q vs %q", role, suffix)
|
|
}
|
|
role = suffix
|
|
}
|
|
}
|
|
|
|
if e.LaunchConfiguration != nil {
|
|
tf.LaunchConfigurationName = e.LaunchConfiguration.TerraformLink()
|
|
// Create TF output variable with security group ids
|
|
// This is in the launch configuration, but the ASG has the information about the instance group type
|
|
if role != "" {
|
|
for _, sg := range e.LaunchConfiguration.SecurityGroups {
|
|
if err := t.AddOutputVariableArray(role+"_security_group_ids", sg.TerraformLink()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
} else if e.LaunchTemplate != nil && role != "" {
|
|
for _, sg := range e.LaunchTemplate.SecurityGroups {
|
|
if err := t.AddOutputVariableArray(role+"_security_group_ids", sg.TerraformLink()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if role != "" {
|
|
if err := t.AddOutputVariableArray(role+"_autoscaling_group_ids", e.TerraformLink()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if role == "node" {
|
|
for _, s := range e.Subnets {
|
|
if err := t.AddOutputVariableArray(role+"_subnet_ids", s.TerraformLink()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
var processes []*string
|
|
if e.SuspendProcesses != nil {
|
|
for _, p := range *e.SuspendProcesses {
|
|
processes = append(processes, fi.String(p))
|
|
}
|
|
}
|
|
tf.SuspendedProcesses = processes
|
|
|
|
return t.RenderResource("aws_autoscaling_group", *e.Name, tf)
|
|
}
|
|
|
|
// TerraformLink fills in the property
|
|
func (e *AutoscalingGroup) TerraformLink() *terraform.Literal {
|
|
return terraform.LiteralProperty("aws_autoscaling_group", fi.StringValue(e.Name), "id")
|
|
}
|
|
|
|
type cloudformationASGTag struct {
|
|
Key *string `json:"Key"`
|
|
Value *string `json:"Value"`
|
|
PropagateAtLaunch *bool `json:"PropagateAtLaunch"`
|
|
}
|
|
|
|
type cloudformationASGMetricsCollection struct {
|
|
Granularity *string `json:"Granularity"`
|
|
Metrics []*string `json:"Metrics"`
|
|
}
|
|
|
|
type cloudformationAutoscalingLaunchTemplateSpecification struct {
|
|
// LaunchTemplateName is the name of the template to use
|
|
LaunchTemplateName *string `json:"LaunchTemplateName,omitempty"`
|
|
}
|
|
|
|
type cloudformationAutoscalingLaunchTemplateOverride struct {
|
|
// InstanceType is the instance to use
|
|
InstanceType *string `json:"InstanceType,omitempty"`
|
|
}
|
|
|
|
type cloudformationAutoscalingLaunchTemplate struct {
|
|
// LaunchTemplateSpecification is the definition for a LT
|
|
LaunchTemplateSpecification *cloudformationAutoscalingLaunchTemplateSpecification `json:"LaunchTemplateSpecification,omitempty"`
|
|
// Override the is machine type override
|
|
Overrides []*cloudformationAutoscalingLaunchTemplateOverride `json:"Overrides,omitempty"`
|
|
}
|
|
|
|
type cloudformationAutoscalingInstanceDistribution struct {
|
|
// OnDemandAllocationStrategy
|
|
OnDemandAllocationStrategy *string `json:"InstancesDistribution,omitempty"`
|
|
// OnDemandBaseCapacity is the base ondemand requirement
|
|
OnDemandBaseCapacity *int64 `json:"OnDemandBaseCapacity,omitempty"`
|
|
// OnDemandPercentageAboveBaseCapacity is the percentage above base for on-demand instances
|
|
OnDemandPercentageAboveBaseCapacity *int64 `json:"OnDemandPercentageAboveBaseCapacity,omitempty"`
|
|
// SpotAllocationStrategy is the spot allocation stratergy
|
|
SpotAllocationStrategy *string `json:"SpotAllocationStrategy,omitempty"`
|
|
// SpotInstancePool is the number of pools
|
|
SpotInstancePool *int64 `json:"SpotInstancePool,omitempty"`
|
|
// SpotMaxPrice is the max bid on spot instance, defaults to demand value
|
|
SpotMaxPrice *string `json:"SpotMaxPrice,omitempty"`
|
|
}
|
|
|
|
type cloudformationMixedInstancesPolicy struct {
|
|
// LaunchTemplate is the launch template spec
|
|
LaunchTemplate *cloudformationAutoscalingLaunchTemplate `json:"LaunchTemplate,omitempty"`
|
|
// InstanceDistribution is the distribution strategy
|
|
InstanceDistribution *cloudformationAutoscalingInstanceDistribution `json:"InstancesDistribution,omitempty"`
|
|
}
|
|
|
|
type cloudformationAutoscalingGroup struct {
|
|
Name *string `json:"AutoScalingGroupName,omitempty"`
|
|
LaunchConfigurationName *cloudformation.Literal `json:"LaunchConfigurationName,omitempty"`
|
|
MaxSize *int64 `json:"MaxSize,omitempty"`
|
|
MinSize *int64 `json:"MinSize,omitempty"`
|
|
VPCZoneIdentifier []*cloudformation.Literal `json:"VPCZoneIdentifier,omitempty"`
|
|
Tags []*cloudformationASGTag `json:"Tags,omitempty"`
|
|
MetricsCollection []*cloudformationASGMetricsCollection `json:"MetricsCollection,omitempty"`
|
|
MixedInstancesPolicy *cloudformationMixedInstancesPolicy `json:"MixedInstancesPolicy,omitempty"`
|
|
LoadBalancerNames []*cloudformation.Literal `json:"LoadBalancerNames,omitempty"`
|
|
TargetGroupARNs []*cloudformation.Literal `json:"TargetGroupARNs,omitempty"`
|
|
}
|
|
|
|
// RenderCloudformation is responsible for generating the cloudformation template
|
|
func (_ *AutoscalingGroup) RenderCloudformation(t *cloudformation.CloudformationTarget, a, e, changes *AutoscalingGroup) error {
|
|
cf := &cloudformationAutoscalingGroup{
|
|
Name: e.Name,
|
|
MinSize: e.MinSize,
|
|
MaxSize: e.MaxSize,
|
|
MetricsCollection: []*cloudformationASGMetricsCollection{
|
|
{
|
|
Granularity: e.Granularity,
|
|
Metrics: aws.StringSlice(e.Metrics),
|
|
},
|
|
},
|
|
}
|
|
|
|
if e.UseMixedInstancesPolicy() {
|
|
cf.MixedInstancesPolicy = &cloudformationMixedInstancesPolicy{
|
|
LaunchTemplate: &cloudformationAutoscalingLaunchTemplate{
|
|
LaunchTemplateSpecification: &cloudformationAutoscalingLaunchTemplateSpecification{
|
|
LaunchTemplateName: e.LaunchTemplate.Name,
|
|
},
|
|
},
|
|
InstanceDistribution: &cloudformationAutoscalingInstanceDistribution{
|
|
OnDemandAllocationStrategy: e.MixedOnDemandAllocationStrategy,
|
|
OnDemandBaseCapacity: e.MixedOnDemandBase,
|
|
OnDemandPercentageAboveBaseCapacity: e.MixedOnDemandAboveBase,
|
|
SpotAllocationStrategy: e.MixedSpotAllocationStrategy,
|
|
SpotInstancePool: e.MixedSpotInstancePools,
|
|
SpotMaxPrice: e.MixedSpotMaxPrice,
|
|
},
|
|
}
|
|
|
|
for _, x := range e.MixedInstanceOverrides {
|
|
cf.MixedInstancesPolicy.LaunchTemplate.Overrides = append(cf.MixedInstancesPolicy.LaunchTemplate.Overrides, &cloudformationAutoscalingLaunchTemplateOverride{InstanceType: fi.String(x)})
|
|
}
|
|
}
|
|
|
|
if e.LaunchConfiguration != nil {
|
|
cf.LaunchConfigurationName = e.LaunchConfiguration.CloudformationLink()
|
|
}
|
|
|
|
for _, s := range e.Subnets {
|
|
cf.VPCZoneIdentifier = append(cf.VPCZoneIdentifier, s.CloudformationLink())
|
|
}
|
|
|
|
for _, k := range maps.SortedKeys(e.Tags) {
|
|
v := e.Tags[k]
|
|
cf.Tags = append(cf.Tags, &cloudformationASGTag{
|
|
Key: fi.String(k),
|
|
Value: fi.String(v),
|
|
PropagateAtLaunch: fi.Bool(true),
|
|
})
|
|
}
|
|
|
|
return t.RenderResource("AWS::AutoScaling::AutoScalingGroup", fi.StringValue(e.Name), cf)
|
|
}
|
|
|
|
// CloudformationLink is adds a reference
|
|
func (e *AutoscalingGroup) CloudformationLink() *cloudformation.Literal {
|
|
return cloudformation.Ref("AWS::AutoScaling::AutoScalingGroup", fi.StringValue(e.Name))
|
|
}
|