mirror of https://github.com/kubernetes/kops.git
1794 lines
49 KiB
Go
1794 lines
49 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 spotinsttasks
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go/service/ec2"
|
|
"github.com/spotinst/spotinst-sdk-go/service/elastigroup/providers/aws"
|
|
"github.com/spotinst/spotinst-sdk-go/spotinst/client"
|
|
"github.com/spotinst/spotinst-sdk-go/spotinst/util/stringutil"
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/util/sets"
|
|
"k8s.io/klog"
|
|
"k8s.io/kops/pkg/resources/spotinst"
|
|
"k8s.io/kops/upup/pkg/fi"
|
|
"k8s.io/kops/upup/pkg/fi/cloudup/awstasks"
|
|
"k8s.io/kops/upup/pkg/fi/cloudup/awsup"
|
|
"k8s.io/kops/upup/pkg/fi/cloudup/terraform"
|
|
"k8s.io/kops/upup/pkg/fi/utils"
|
|
)
|
|
|
|
// +kops:fitask
|
|
type Elastigroup struct {
|
|
Name *string
|
|
Lifecycle *fi.Lifecycle
|
|
|
|
ID *string
|
|
MinSize *int64
|
|
MaxSize *int64
|
|
SpotPercentage *float64
|
|
UtilizeReservedInstances *bool
|
|
FallbackToOnDemand *bool
|
|
DrainingTimeout *int64
|
|
HealthCheckType *string
|
|
Product *string
|
|
Orientation *string
|
|
Tags map[string]string
|
|
UserData *fi.ResourceHolder
|
|
ImageID *string
|
|
OnDemandInstanceType *string
|
|
SpotInstanceTypes []string
|
|
IAMInstanceProfile *awstasks.IAMInstanceProfile
|
|
LoadBalancer *awstasks.LoadBalancer
|
|
SSHKey *awstasks.SSHKey
|
|
Subnets []*awstasks.Subnet
|
|
SecurityGroups []*awstasks.SecurityGroup
|
|
Monitoring *bool
|
|
AssociatePublicIP *bool
|
|
Tenancy *string
|
|
RootVolumeOpts *RootVolumeOpts
|
|
AutoScalerOpts *AutoScalerOpts
|
|
}
|
|
|
|
type RootVolumeOpts struct {
|
|
Type *string
|
|
Size *int32
|
|
IOPS *int32
|
|
Optimization *bool
|
|
}
|
|
|
|
type AutoScalerOpts struct {
|
|
Enabled *bool
|
|
ClusterID *string
|
|
Cooldown *int
|
|
Labels map[string]string
|
|
Taints []*corev1.Taint
|
|
Headroom *AutoScalerHeadroomOpts
|
|
Down *AutoScalerDownOpts
|
|
}
|
|
|
|
type AutoScalerHeadroomOpts struct {
|
|
CPUPerUnit *int
|
|
GPUPerUnit *int
|
|
MemPerUnit *int
|
|
NumOfUnits *int
|
|
}
|
|
|
|
type AutoScalerDownOpts struct {
|
|
MaxPercentage *int
|
|
EvaluationPeriods *int
|
|
}
|
|
|
|
var _ fi.Task = &Elastigroup{}
|
|
var _ fi.CompareWithID = &Elastigroup{}
|
|
var _ fi.HasDependencies = &Elastigroup{}
|
|
|
|
func (e *Elastigroup) CompareWithID() *string {
|
|
return e.Name
|
|
}
|
|
|
|
func (e *Elastigroup) GetDependencies(tasks map[string]fi.Task) []fi.Task {
|
|
var deps []fi.Task
|
|
|
|
if e.IAMInstanceProfile != nil {
|
|
deps = append(deps, e.IAMInstanceProfile)
|
|
}
|
|
|
|
if e.LoadBalancer != nil {
|
|
deps = append(deps, e.LoadBalancer)
|
|
}
|
|
|
|
if e.SSHKey != nil {
|
|
deps = append(deps, e.SSHKey)
|
|
}
|
|
|
|
if e.Subnets != nil {
|
|
for _, subnet := range e.Subnets {
|
|
deps = append(deps, subnet)
|
|
}
|
|
}
|
|
|
|
if e.SecurityGroups != nil {
|
|
for _, sg := range e.SecurityGroups {
|
|
deps = append(deps, sg)
|
|
}
|
|
}
|
|
|
|
if e.UserData != nil {
|
|
deps = append(deps, e.UserData.GetDependencies(tasks)...)
|
|
}
|
|
|
|
return deps
|
|
}
|
|
|
|
func (e *Elastigroup) find(svc spotinst.InstanceGroupService, name string) (*aws.Group, error) {
|
|
klog.V(4).Infof("Attempting to find Elastigroup: %q", name)
|
|
|
|
groups, err := svc.List(context.Background())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("spotinst: failed to find elastigroup %s: %v", name, err)
|
|
}
|
|
|
|
var out *aws.Group
|
|
for _, group := range groups {
|
|
if group.Name() == name {
|
|
out = group.Obj().(*aws.Group)
|
|
break
|
|
}
|
|
}
|
|
if out == nil {
|
|
return nil, fmt.Errorf("spotinst: failed to find elastigroup %q", name)
|
|
}
|
|
|
|
klog.V(4).Infof("Elastigroup/%s: %s", name, stringutil.Stringify(out))
|
|
return out, nil
|
|
}
|
|
|
|
var _ fi.HasCheckExisting = &Elastigroup{}
|
|
|
|
func (e *Elastigroup) Find(c *fi.Context) (*Elastigroup, error) {
|
|
cloud := c.Cloud.(awsup.AWSCloud)
|
|
|
|
group, err := e.find(cloud.Spotinst().Elastigroup(), *e.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
actual := &Elastigroup{}
|
|
actual.ID = group.ID
|
|
actual.Name = group.Name
|
|
|
|
// Capacity.
|
|
{
|
|
actual.MinSize = fi.Int64(int64(fi.IntValue(group.Capacity.Minimum)))
|
|
actual.MaxSize = fi.Int64(int64(fi.IntValue(group.Capacity.Maximum)))
|
|
}
|
|
|
|
// Strategy.
|
|
{
|
|
actual.SpotPercentage = group.Strategy.Risk
|
|
actual.Orientation = group.Strategy.AvailabilityVsCost
|
|
actual.FallbackToOnDemand = group.Strategy.FallbackToOnDemand
|
|
actual.UtilizeReservedInstances = group.Strategy.UtilizeReservedInstances
|
|
|
|
if group.Strategy.DrainingTimeout != nil {
|
|
actual.DrainingTimeout = fi.Int64(int64(fi.IntValue(group.Strategy.DrainingTimeout)))
|
|
}
|
|
}
|
|
|
|
// Compute.
|
|
{
|
|
compute := group.Compute
|
|
actual.Product = compute.Product
|
|
|
|
// Instance types.
|
|
{
|
|
actual.OnDemandInstanceType = compute.InstanceTypes.OnDemand
|
|
actual.SpotInstanceTypes = compute.InstanceTypes.Spot
|
|
}
|
|
|
|
// Subnets.
|
|
{
|
|
for _, zone := range compute.AvailabilityZones {
|
|
if zone.SubnetID != nil {
|
|
actual.Subnets = append(actual.Subnets,
|
|
&awstasks.Subnet{ID: zone.SubnetID})
|
|
}
|
|
}
|
|
if subnetSlicesEqualIgnoreOrder(actual.Subnets, e.Subnets) {
|
|
actual.Subnets = e.Subnets
|
|
}
|
|
}
|
|
}
|
|
|
|
// Launch specification.
|
|
{
|
|
lc := group.Compute.LaunchSpecification
|
|
|
|
// Image.
|
|
{
|
|
actual.ImageID = lc.ImageID
|
|
|
|
if e.ImageID != nil && actual.ImageID != nil &&
|
|
fi.StringValue(actual.ImageID) != fi.StringValue(e.ImageID) {
|
|
image, err := resolveImage(cloud, fi.StringValue(e.ImageID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if fi.StringValue(image.ImageId) == fi.StringValue(lc.ImageID) {
|
|
actual.ImageID = e.ImageID
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tags.
|
|
{
|
|
if lc.Tags != nil && len(lc.Tags) > 0 {
|
|
actual.Tags = make(map[string]string)
|
|
for _, tag := range lc.Tags {
|
|
actual.Tags[fi.StringValue(tag.Key)] = fi.StringValue(tag.Value)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Security groups.
|
|
{
|
|
if lc.SecurityGroupIDs != nil {
|
|
for _, sgID := range lc.SecurityGroupIDs {
|
|
actual.SecurityGroups = append(actual.SecurityGroups,
|
|
&awstasks.SecurityGroup{ID: fi.String(sgID)})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Root volume options.
|
|
{
|
|
// Block device mappings.
|
|
{
|
|
if lc.BlockDeviceMappings != nil {
|
|
for _, b := range lc.BlockDeviceMappings {
|
|
if b.EBS == nil || b.EBS.SnapshotID != nil {
|
|
continue // not the root
|
|
}
|
|
if actual.RootVolumeOpts == nil {
|
|
actual.RootVolumeOpts = new(RootVolumeOpts)
|
|
}
|
|
if b.EBS.IOPS != nil {
|
|
actual.RootVolumeOpts.IOPS = fi.Int32(int32(fi.IntValue(b.EBS.IOPS)))
|
|
}
|
|
|
|
actual.RootVolumeOpts.Type = b.EBS.VolumeType
|
|
actual.RootVolumeOpts.Size = fi.Int32(int32(fi.IntValue(b.EBS.VolumeSize)))
|
|
}
|
|
}
|
|
}
|
|
|
|
// EBS optimization.
|
|
{
|
|
if fi.BoolValue(lc.EBSOptimized) {
|
|
if actual.RootVolumeOpts == nil {
|
|
actual.RootVolumeOpts = new(RootVolumeOpts)
|
|
}
|
|
|
|
actual.RootVolumeOpts.Optimization = lc.EBSOptimized
|
|
}
|
|
}
|
|
}
|
|
|
|
// User data.
|
|
{
|
|
var userData []byte
|
|
|
|
if lc.UserData != nil {
|
|
userData, err = base64.StdEncoding.DecodeString(fi.StringValue(lc.UserData))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
actual.UserData = fi.WrapResource(fi.NewStringResource(string(userData)))
|
|
}
|
|
|
|
// Network interfaces.
|
|
{
|
|
associatePublicIP := false
|
|
|
|
if lc.NetworkInterfaces != nil && len(lc.NetworkInterfaces) > 0 {
|
|
for _, iface := range lc.NetworkInterfaces {
|
|
if fi.BoolValue(iface.AssociatePublicIPAddress) {
|
|
associatePublicIP = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
actual.AssociatePublicIP = fi.Bool(associatePublicIP)
|
|
}
|
|
|
|
// Load balancer.
|
|
{
|
|
if lc.LoadBalancersConfig != nil && len(lc.LoadBalancersConfig.LoadBalancers) > 0 {
|
|
lbs := lc.LoadBalancersConfig.LoadBalancers
|
|
actual.LoadBalancer = &awstasks.LoadBalancer{Name: lbs[0].Name}
|
|
|
|
if e.LoadBalancer != nil && actual.LoadBalancer != nil &&
|
|
fi.StringValue(actual.LoadBalancer.Name) != fi.StringValue(e.LoadBalancer.Name) {
|
|
elb, err := awstasks.FindLoadBalancerByNameTag(cloud, fi.StringValue(e.LoadBalancer.Name))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if fi.StringValue(elb.LoadBalancerName) == fi.StringValue(lbs[0].Name) {
|
|
actual.LoadBalancer = e.LoadBalancer
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// IAM instance profile.
|
|
if lc.IAMInstanceProfile != nil {
|
|
actual.IAMInstanceProfile = &awstasks.IAMInstanceProfile{Name: lc.IAMInstanceProfile.Name}
|
|
}
|
|
|
|
// SSH key.
|
|
if lc.KeyPair != nil {
|
|
actual.SSHKey = &awstasks.SSHKey{Name: lc.KeyPair}
|
|
}
|
|
|
|
// Tenancy.
|
|
if lc.Tenancy != nil {
|
|
actual.Tenancy = lc.Tenancy
|
|
}
|
|
|
|
// Monitoring.
|
|
if lc.Monitoring != nil {
|
|
actual.Monitoring = lc.Monitoring
|
|
}
|
|
|
|
// Health check.
|
|
if lc.HealthCheckType != nil {
|
|
actual.HealthCheckType = lc.HealthCheckType
|
|
}
|
|
}
|
|
|
|
// Auto Scaler.
|
|
{
|
|
if group.Integration != nil && group.Integration.Kubernetes != nil {
|
|
integration := group.Integration.Kubernetes
|
|
|
|
actual.AutoScalerOpts = new(AutoScalerOpts)
|
|
actual.AutoScalerOpts.ClusterID = integration.ClusterIdentifier
|
|
|
|
if integration.AutoScale != nil {
|
|
actual.AutoScalerOpts.Enabled = integration.AutoScale.IsEnabled
|
|
actual.AutoScalerOpts.Cooldown = integration.AutoScale.Cooldown
|
|
|
|
// Headroom.
|
|
if headroom := integration.AutoScale.Headroom; headroom != nil {
|
|
actual.AutoScalerOpts.Headroom = new(AutoScalerHeadroomOpts)
|
|
|
|
if v := fi.IntValue(headroom.CPUPerUnit); v > 0 {
|
|
actual.AutoScalerOpts.Headroom.CPUPerUnit = headroom.CPUPerUnit
|
|
}
|
|
if v := fi.IntValue(headroom.GPUPerUnit); v > 0 {
|
|
actual.AutoScalerOpts.Headroom.GPUPerUnit = headroom.GPUPerUnit
|
|
}
|
|
if v := fi.IntValue(headroom.MemoryPerUnit); v > 0 {
|
|
actual.AutoScalerOpts.Headroom.MemPerUnit = headroom.MemoryPerUnit
|
|
}
|
|
if v := fi.IntValue(headroom.NumOfUnits); v > 0 {
|
|
actual.AutoScalerOpts.Headroom.NumOfUnits = headroom.NumOfUnits
|
|
}
|
|
}
|
|
|
|
// Scale down.
|
|
if down := integration.AutoScale.Down; down != nil {
|
|
actual.AutoScalerOpts.Down = &AutoScalerDownOpts{
|
|
MaxPercentage: down.MaxScaleDownPercentage,
|
|
EvaluationPeriods: down.EvaluationPeriods,
|
|
}
|
|
}
|
|
|
|
// Labels.
|
|
if labels := integration.AutoScale.Labels; labels != nil {
|
|
actual.AutoScalerOpts.Labels = make(map[string]string)
|
|
|
|
for _, label := range labels {
|
|
actual.AutoScalerOpts.Labels[fi.StringValue(label.Key)] = fi.StringValue(label.Value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Avoid spurious changes
|
|
actual.Lifecycle = e.Lifecycle
|
|
|
|
return actual, nil
|
|
}
|
|
|
|
func (e *Elastigroup) CheckExisting(c *fi.Context) bool {
|
|
cloud := c.Cloud.(awsup.AWSCloud)
|
|
group, err := e.find(cloud.Spotinst().Elastigroup(), *e.Name)
|
|
return err == nil && group != nil
|
|
}
|
|
|
|
func (e *Elastigroup) Run(c *fi.Context) error {
|
|
return fi.DefaultDeltaRunMethod(e, c)
|
|
}
|
|
|
|
func (s *Elastigroup) CheckChanges(a, e, changes *Elastigroup) error {
|
|
if e.Name == nil {
|
|
return fi.RequiredField("Name")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (eg *Elastigroup) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *Elastigroup) error {
|
|
return eg.createOrUpdate(t.Cloud.(awsup.AWSCloud), a, e, changes)
|
|
}
|
|
|
|
func (eg *Elastigroup) createOrUpdate(cloud awsup.AWSCloud, a, e, changes *Elastigroup) error {
|
|
if a == nil {
|
|
return eg.create(cloud, a, e, changes)
|
|
} else {
|
|
return eg.update(cloud, a, e, changes)
|
|
}
|
|
}
|
|
|
|
func (_ *Elastigroup) create(cloud awsup.AWSCloud, a, e, changes *Elastigroup) error {
|
|
klog.V(2).Infof("Creating Elastigroup %q", *e.Name)
|
|
e.applyDefaults()
|
|
|
|
group := &aws.Group{
|
|
Capacity: new(aws.Capacity),
|
|
Strategy: new(aws.Strategy),
|
|
Compute: &aws.Compute{
|
|
LaunchSpecification: new(aws.LaunchSpecification),
|
|
InstanceTypes: new(aws.InstanceTypes),
|
|
},
|
|
}
|
|
|
|
// General.
|
|
{
|
|
group.SetName(e.Name)
|
|
group.SetDescription(e.Name)
|
|
}
|
|
|
|
// Capacity.
|
|
{
|
|
group.Capacity.SetTarget(fi.Int(int(*e.MinSize)))
|
|
group.Capacity.SetMinimum(fi.Int(int(*e.MinSize)))
|
|
group.Capacity.SetMaximum(fi.Int(int(*e.MaxSize)))
|
|
}
|
|
|
|
// Strategy.
|
|
{
|
|
group.Strategy.SetRisk(e.SpotPercentage)
|
|
group.Strategy.SetAvailabilityVsCost(fi.String(string(normalizeOrientation(e.Orientation))))
|
|
group.Strategy.SetFallbackToOnDemand(e.FallbackToOnDemand)
|
|
group.Strategy.SetUtilizeReservedInstances(e.UtilizeReservedInstances)
|
|
|
|
if e.DrainingTimeout != nil {
|
|
group.Strategy.SetDrainingTimeout(fi.Int(int(*e.DrainingTimeout)))
|
|
}
|
|
}
|
|
|
|
// Compute.
|
|
{
|
|
group.Compute.SetProduct(e.Product)
|
|
|
|
// Instance types.
|
|
{
|
|
group.Compute.InstanceTypes.SetOnDemand(e.OnDemandInstanceType)
|
|
group.Compute.InstanceTypes.SetSpot(e.SpotInstanceTypes)
|
|
}
|
|
|
|
// Availability zones.
|
|
{
|
|
zones := make([]*aws.AvailabilityZone, len(e.Subnets))
|
|
for i, subnet := range e.Subnets {
|
|
zone := new(aws.AvailabilityZone)
|
|
zone.SetName(subnet.AvailabilityZone)
|
|
zone.SetSubnetId(subnet.ID)
|
|
zones[i] = zone
|
|
}
|
|
group.Compute.SetAvailabilityZones(zones)
|
|
}
|
|
|
|
// Launch Specification.
|
|
{
|
|
group.Compute.LaunchSpecification.SetMonitoring(e.Monitoring)
|
|
group.Compute.LaunchSpecification.SetKeyPair(e.SSHKey.Name)
|
|
|
|
if e.Tenancy != nil {
|
|
group.Compute.LaunchSpecification.SetTenancy(e.Tenancy)
|
|
}
|
|
|
|
// Block device mappings.
|
|
{
|
|
rootDevices, err := e.buildRootDevice(cloud)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ephemeralDevices, err := e.buildEphemeralDevices(cloud, e.OnDemandInstanceType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(rootDevices) != 0 || len(ephemeralDevices) != 0 {
|
|
var mappings []*aws.BlockDeviceMapping
|
|
for device, bdm := range rootDevices {
|
|
mappings = append(mappings, e.buildBlockDeviceMapping(device, bdm))
|
|
}
|
|
for device, bdm := range ephemeralDevices {
|
|
mappings = append(mappings, e.buildBlockDeviceMapping(device, bdm))
|
|
}
|
|
if len(mappings) > 0 {
|
|
group.Compute.LaunchSpecification.SetBlockDeviceMappings(mappings)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Image.
|
|
{
|
|
image, err := resolveImage(cloud, fi.StringValue(e.ImageID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
group.Compute.LaunchSpecification.SetImageId(image.ImageId)
|
|
}
|
|
|
|
// User data.
|
|
{
|
|
if e.UserData != nil {
|
|
userData, err := e.UserData.AsString()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(userData) > 0 {
|
|
encoded := base64.StdEncoding.EncodeToString([]byte(userData))
|
|
group.Compute.LaunchSpecification.SetUserData(fi.String(encoded))
|
|
}
|
|
}
|
|
}
|
|
|
|
// IAM instance profile.
|
|
{
|
|
if e.IAMInstanceProfile != nil {
|
|
iprof := new(aws.IAMInstanceProfile)
|
|
iprof.SetName(e.IAMInstanceProfile.GetName())
|
|
group.Compute.LaunchSpecification.SetIAMInstanceProfile(iprof)
|
|
}
|
|
}
|
|
|
|
// Security groups.
|
|
{
|
|
if e.SecurityGroups != nil {
|
|
securityGroupIDs := make([]string, len(e.SecurityGroups))
|
|
for i, sg := range e.SecurityGroups {
|
|
securityGroupIDs[i] = *sg.ID
|
|
}
|
|
group.Compute.LaunchSpecification.SetSecurityGroupIDs(securityGroupIDs)
|
|
}
|
|
}
|
|
|
|
// Public IP.
|
|
{
|
|
if e.AssociatePublicIP != nil {
|
|
iface := &aws.NetworkInterface{
|
|
Description: fi.String("eth0"),
|
|
DeviceIndex: fi.Int(0),
|
|
DeleteOnTermination: fi.Bool(true),
|
|
AssociatePublicIPAddress: e.AssociatePublicIP,
|
|
}
|
|
|
|
group.Compute.LaunchSpecification.SetNetworkInterfaces([]*aws.NetworkInterface{iface})
|
|
}
|
|
}
|
|
|
|
// Load balancer.
|
|
{
|
|
if e.LoadBalancer != nil {
|
|
elb, err := awstasks.FindLoadBalancerByNameTag(cloud, fi.StringValue(e.LoadBalancer.Name))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if elb != nil {
|
|
lb := new(aws.LoadBalancer)
|
|
lb.SetName(elb.LoadBalancerName)
|
|
lb.SetType(fi.String("CLASSIC"))
|
|
|
|
cfg := new(aws.LoadBalancersConfig)
|
|
cfg.SetLoadBalancers([]*aws.LoadBalancer{lb})
|
|
|
|
group.Compute.LaunchSpecification.SetLoadBalancersConfig(cfg)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tags.
|
|
{
|
|
if e.Tags != nil {
|
|
group.Compute.LaunchSpecification.SetTags(e.buildTags())
|
|
}
|
|
}
|
|
|
|
// Health check.
|
|
{
|
|
if e.HealthCheckType != nil {
|
|
group.Compute.LaunchSpecification.SetHealthCheckType(e.HealthCheckType)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Auto Scaler.
|
|
{
|
|
if opts := e.AutoScalerOpts; opts != nil {
|
|
k8s := new(aws.KubernetesIntegration)
|
|
k8s.SetIntegrationMode(fi.String("pod"))
|
|
k8s.SetClusterIdentifier(opts.ClusterID)
|
|
|
|
if opts.Enabled != nil {
|
|
autoScaler := new(aws.AutoScaleKubernetes)
|
|
autoScaler.IsEnabled = opts.Enabled
|
|
autoScaler.IsAutoConfig = fi.Bool(true)
|
|
autoScaler.Cooldown = opts.Cooldown
|
|
|
|
// Headroom.
|
|
if headroom := opts.Headroom; headroom != nil {
|
|
autoScaler.IsAutoConfig = fi.Bool(false)
|
|
autoScaler.Headroom = &aws.AutoScaleHeadroom{
|
|
CPUPerUnit: headroom.CPUPerUnit,
|
|
GPUPerUnit: headroom.GPUPerUnit,
|
|
MemoryPerUnit: headroom.MemPerUnit,
|
|
NumOfUnits: headroom.NumOfUnits,
|
|
}
|
|
}
|
|
|
|
// Scale down.
|
|
if down := opts.Down; down != nil {
|
|
autoScaler.Down = &aws.AutoScaleDown{
|
|
MaxScaleDownPercentage: down.MaxPercentage,
|
|
EvaluationPeriods: down.EvaluationPeriods,
|
|
}
|
|
}
|
|
|
|
// Labels.
|
|
if labels := opts.Labels; labels != nil {
|
|
autoScaler.Labels = e.buildAutoScaleLabels(labels)
|
|
}
|
|
|
|
k8s.SetAutoScale(autoScaler)
|
|
}
|
|
|
|
integration := new(aws.Integration)
|
|
integration.SetKubernetes(k8s)
|
|
|
|
group.SetIntegration(integration)
|
|
}
|
|
}
|
|
|
|
attempt := 0
|
|
maxAttempts := 10
|
|
|
|
readyLoop:
|
|
for {
|
|
attempt++
|
|
klog.V(2).Infof("(%d/%d) Attempting to create Elastigroup: %q, config: %s",
|
|
attempt, maxAttempts, *e.Name, stringutil.Stringify(group))
|
|
|
|
// Wait for IAM instance profile to be ready.
|
|
time.Sleep(10 * time.Second)
|
|
|
|
// Wrap the raw object as an Elastigroup.
|
|
eg, err := spotinst.NewElastigroup(cloud.ProviderID(), group)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create the Elastigroup.
|
|
id, err := cloud.Spotinst().Elastigroup().Create(context.Background(), eg)
|
|
if err == nil {
|
|
e.ID = fi.String(id)
|
|
break
|
|
}
|
|
|
|
if errs, ok := err.(client.Errors); ok {
|
|
for _, err := range errs {
|
|
if strings.Contains(err.Message, "Invalid IAM Instance Profile name") {
|
|
if attempt > maxAttempts {
|
|
return fmt.Errorf("IAM instance profile not yet created/propagated (original error: %v)", err)
|
|
}
|
|
|
|
klog.V(4).Infof("Got an error indicating that the IAM instance profile %q is not ready %q", fi.StringValue(e.IAMInstanceProfile.Name), err)
|
|
klog.Infof("Waiting for IAM instance profile %q to be ready", fi.StringValue(e.IAMInstanceProfile.Name))
|
|
goto readyLoop
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("spotinst: failed to create elastigroup: %v", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (_ *Elastigroup) update(cloud awsup.AWSCloud, a, e, changes *Elastigroup) error {
|
|
klog.V(2).Infof("Updating Elastigroup %q", *e.Name)
|
|
|
|
actual, err := e.find(cloud.Spotinst().Elastigroup(), *e.Name)
|
|
if err != nil {
|
|
klog.Errorf("Unable to resolve Elastigroup %q, error: %v", *e.Name, err)
|
|
return err
|
|
}
|
|
|
|
var changed bool
|
|
group := new(aws.Group)
|
|
group.SetId(actual.ID)
|
|
|
|
// Strategy.
|
|
{
|
|
// Spot percentage.
|
|
if changes.SpotPercentage != nil {
|
|
if group.Strategy == nil {
|
|
group.Strategy = new(aws.Strategy)
|
|
}
|
|
|
|
group.Strategy.SetRisk(e.SpotPercentage)
|
|
changes.SpotPercentage = nil
|
|
changed = true
|
|
}
|
|
|
|
// Orientation.
|
|
if changes.Orientation != nil {
|
|
if group.Strategy == nil {
|
|
group.Strategy = new(aws.Strategy)
|
|
}
|
|
|
|
group.Strategy.SetAvailabilityVsCost(fi.String(string(normalizeOrientation(e.Orientation))))
|
|
changes.Orientation = nil
|
|
changed = true
|
|
}
|
|
|
|
// Fallback to on-demand.
|
|
if changes.FallbackToOnDemand != nil {
|
|
if group.Strategy == nil {
|
|
group.Strategy = new(aws.Strategy)
|
|
}
|
|
|
|
group.Strategy.SetFallbackToOnDemand(e.FallbackToOnDemand)
|
|
changes.FallbackToOnDemand = nil
|
|
changed = true
|
|
}
|
|
|
|
// Utilize reserved instances.
|
|
if changes.UtilizeReservedInstances != nil {
|
|
if group.Strategy == nil {
|
|
group.Strategy = new(aws.Strategy)
|
|
}
|
|
|
|
group.Strategy.SetUtilizeReservedInstances(e.UtilizeReservedInstances)
|
|
changes.UtilizeReservedInstances = nil
|
|
changed = true
|
|
}
|
|
|
|
// Draining timeout.
|
|
if changes.DrainingTimeout != nil {
|
|
if group.Strategy == nil {
|
|
group.Strategy = new(aws.Strategy)
|
|
}
|
|
|
|
group.Strategy.SetDrainingTimeout(fi.Int(int(*e.DrainingTimeout)))
|
|
changes.DrainingTimeout = nil
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
// Compute.
|
|
{
|
|
// Product.
|
|
if changes.Product != nil {
|
|
if group.Compute == nil {
|
|
group.Compute = new(aws.Compute)
|
|
}
|
|
|
|
group.Compute.SetProduct(e.Product)
|
|
changes.Product = nil
|
|
changed = true
|
|
}
|
|
|
|
// On-demand instance type.
|
|
{
|
|
if changes.OnDemandInstanceType != nil {
|
|
if group.Compute == nil {
|
|
group.Compute = new(aws.Compute)
|
|
}
|
|
if group.Compute.InstanceTypes == nil {
|
|
group.Compute.InstanceTypes = new(aws.InstanceTypes)
|
|
}
|
|
|
|
group.Compute.InstanceTypes.SetOnDemand(e.OnDemandInstanceType)
|
|
changes.OnDemandInstanceType = nil
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
// Spot instance types.
|
|
{
|
|
if changes.SpotInstanceTypes != nil {
|
|
if group.Compute == nil {
|
|
group.Compute = new(aws.Compute)
|
|
}
|
|
if group.Compute.InstanceTypes == nil {
|
|
group.Compute.InstanceTypes = new(aws.InstanceTypes)
|
|
}
|
|
|
|
types := make([]string, len(e.SpotInstanceTypes))
|
|
copy(types, e.SpotInstanceTypes)
|
|
|
|
group.Compute.InstanceTypes.SetSpot(types)
|
|
changes.SpotInstanceTypes = nil
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
// Availability zones.
|
|
{
|
|
if changes.Subnets != nil {
|
|
if group.Compute == nil {
|
|
group.Compute = new(aws.Compute)
|
|
}
|
|
|
|
zones := make([]*aws.AvailabilityZone, len(e.Subnets))
|
|
for i, subnet := range e.Subnets {
|
|
zone := new(aws.AvailabilityZone)
|
|
zone.SetName(subnet.AvailabilityZone)
|
|
zone.SetSubnetId(subnet.ID)
|
|
zones[i] = zone
|
|
}
|
|
|
|
group.Compute.SetAvailabilityZones(zones)
|
|
changes.Subnets = nil
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
// Launch specification.
|
|
{
|
|
// Security groups.
|
|
{
|
|
if changes.SecurityGroups != nil {
|
|
if group.Compute == nil {
|
|
group.Compute = new(aws.Compute)
|
|
}
|
|
if group.Compute.LaunchSpecification == nil {
|
|
group.Compute.LaunchSpecification = new(aws.LaunchSpecification)
|
|
}
|
|
|
|
securityGroupIDs := make([]string, len(e.SecurityGroups))
|
|
for i, sg := range e.SecurityGroups {
|
|
securityGroupIDs[i] = *sg.ID
|
|
}
|
|
|
|
group.Compute.LaunchSpecification.SetSecurityGroupIDs(securityGroupIDs)
|
|
changes.SecurityGroups = nil
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
// User data.
|
|
{
|
|
if changes.UserData != nil {
|
|
userData, err := e.UserData.AsString()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(userData) > 0 {
|
|
if group.Compute == nil {
|
|
group.Compute = new(aws.Compute)
|
|
}
|
|
if group.Compute.LaunchSpecification == nil {
|
|
group.Compute.LaunchSpecification = new(aws.LaunchSpecification)
|
|
}
|
|
|
|
encoded := base64.StdEncoding.EncodeToString([]byte(userData))
|
|
group.Compute.LaunchSpecification.SetUserData(fi.String(encoded))
|
|
changed = true
|
|
}
|
|
|
|
changes.UserData = nil
|
|
}
|
|
}
|
|
|
|
// Network interfaces.
|
|
{
|
|
if changes.AssociatePublicIP != nil {
|
|
if group.Compute == nil {
|
|
group.Compute = new(aws.Compute)
|
|
}
|
|
if group.Compute.LaunchSpecification == nil {
|
|
group.Compute.LaunchSpecification = new(aws.LaunchSpecification)
|
|
}
|
|
|
|
iface := &aws.NetworkInterface{
|
|
Description: fi.String("eth0"),
|
|
DeviceIndex: fi.Int(0),
|
|
DeleteOnTermination: fi.Bool(true),
|
|
AssociatePublicIPAddress: changes.AssociatePublicIP,
|
|
}
|
|
|
|
group.Compute.LaunchSpecification.SetNetworkInterfaces([]*aws.NetworkInterface{iface})
|
|
changes.AssociatePublicIP = nil
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
// Root volume options.
|
|
{
|
|
if opts := changes.RootVolumeOpts; opts != nil {
|
|
|
|
// Block device mappings.
|
|
{
|
|
if opts.Type != nil || opts.Size != nil || opts.IOPS != nil {
|
|
rootDevices, err := e.buildRootDevice(cloud)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ephemeralDevices, err := e.buildEphemeralDevices(cloud, e.OnDemandInstanceType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(rootDevices) != 0 || len(ephemeralDevices) != 0 {
|
|
var mappings []*aws.BlockDeviceMapping
|
|
for device, bdm := range rootDevices {
|
|
mappings = append(mappings, e.buildBlockDeviceMapping(device, bdm))
|
|
}
|
|
for device, bdm := range ephemeralDevices {
|
|
mappings = append(mappings, e.buildBlockDeviceMapping(device, bdm))
|
|
}
|
|
if len(mappings) > 0 {
|
|
if group.Compute == nil {
|
|
group.Compute = new(aws.Compute)
|
|
}
|
|
if group.Compute.LaunchSpecification == nil {
|
|
group.Compute.LaunchSpecification = new(aws.LaunchSpecification)
|
|
}
|
|
|
|
group.Compute.LaunchSpecification.SetBlockDeviceMappings(mappings)
|
|
changed = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// EBS optimization.
|
|
{
|
|
if opts.Optimization != nil {
|
|
if group.Compute == nil {
|
|
group.Compute = new(aws.Compute)
|
|
}
|
|
if group.Compute.LaunchSpecification == nil {
|
|
group.Compute.LaunchSpecification = new(aws.LaunchSpecification)
|
|
}
|
|
|
|
group.Compute.LaunchSpecification.SetEBSOptimized(e.RootVolumeOpts.Optimization)
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
changes.RootVolumeOpts = nil
|
|
}
|
|
}
|
|
|
|
// Image.
|
|
{
|
|
if changes.ImageID != nil {
|
|
image, err := resolveImage(cloud, fi.StringValue(e.ImageID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if *actual.Compute.LaunchSpecification.ImageID != *image.ImageId {
|
|
if group.Compute == nil {
|
|
group.Compute = new(aws.Compute)
|
|
}
|
|
if group.Compute.LaunchSpecification == nil {
|
|
group.Compute.LaunchSpecification = new(aws.LaunchSpecification)
|
|
}
|
|
|
|
group.Compute.LaunchSpecification.SetImageId(image.ImageId)
|
|
changed = true
|
|
}
|
|
|
|
changes.ImageID = nil
|
|
}
|
|
}
|
|
|
|
// Tags.
|
|
{
|
|
if changes.Tags != nil {
|
|
if group.Compute == nil {
|
|
group.Compute = new(aws.Compute)
|
|
}
|
|
if group.Compute.LaunchSpecification == nil {
|
|
group.Compute.LaunchSpecification = new(aws.LaunchSpecification)
|
|
}
|
|
|
|
group.Compute.LaunchSpecification.SetTags(e.buildTags())
|
|
changes.Tags = nil
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
// IAM instance profile.
|
|
{
|
|
if changes.IAMInstanceProfile != nil {
|
|
if group.Compute == nil {
|
|
group.Compute = new(aws.Compute)
|
|
}
|
|
if group.Compute.LaunchSpecification == nil {
|
|
group.Compute.LaunchSpecification = new(aws.LaunchSpecification)
|
|
}
|
|
|
|
iprof := new(aws.IAMInstanceProfile)
|
|
iprof.SetName(e.IAMInstanceProfile.GetName())
|
|
|
|
group.Compute.LaunchSpecification.SetIAMInstanceProfile(iprof)
|
|
changes.IAMInstanceProfile = nil
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
// Monitoring.
|
|
{
|
|
if changes.Monitoring != nil {
|
|
if group.Compute == nil {
|
|
group.Compute = new(aws.Compute)
|
|
}
|
|
if group.Compute.LaunchSpecification == nil {
|
|
group.Compute.LaunchSpecification = new(aws.LaunchSpecification)
|
|
}
|
|
|
|
group.Compute.LaunchSpecification.SetMonitoring(e.Monitoring)
|
|
changes.Monitoring = nil
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
// SSH key.
|
|
{
|
|
if changes.SSHKey != nil {
|
|
if group.Compute == nil {
|
|
group.Compute = new(aws.Compute)
|
|
}
|
|
if group.Compute.LaunchSpecification == nil {
|
|
group.Compute.LaunchSpecification = new(aws.LaunchSpecification)
|
|
}
|
|
|
|
group.Compute.LaunchSpecification.SetKeyPair(e.SSHKey.Name)
|
|
changes.SSHKey = nil
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
// Load balancer.
|
|
{
|
|
if changes.LoadBalancer != nil {
|
|
elb, err := awstasks.FindLoadBalancerByNameTag(cloud, fi.StringValue(e.LoadBalancer.Name))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if elb != nil {
|
|
lb := new(aws.LoadBalancer)
|
|
lb.SetName(elb.LoadBalancerName)
|
|
lb.SetType(fi.String("CLASSIC"))
|
|
|
|
cfg := new(aws.LoadBalancersConfig)
|
|
cfg.SetLoadBalancers([]*aws.LoadBalancer{lb})
|
|
|
|
if group.Compute == nil {
|
|
group.Compute = new(aws.Compute)
|
|
}
|
|
if group.Compute.LaunchSpecification == nil {
|
|
group.Compute.LaunchSpecification = new(aws.LaunchSpecification)
|
|
}
|
|
|
|
group.Compute.LaunchSpecification.SetLoadBalancersConfig(cfg)
|
|
changes.LoadBalancer = nil
|
|
changed = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tenancy.
|
|
{
|
|
if changes.Tenancy != nil {
|
|
if group.Compute == nil {
|
|
group.Compute = new(aws.Compute)
|
|
}
|
|
if group.Compute.LaunchSpecification == nil {
|
|
group.Compute.LaunchSpecification = new(aws.LaunchSpecification)
|
|
}
|
|
|
|
group.Compute.LaunchSpecification.SetTenancy(e.Tenancy)
|
|
changes.Tenancy = nil
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
// Health check.
|
|
{
|
|
if changes.HealthCheckType != nil {
|
|
if group.Compute == nil {
|
|
group.Compute = new(aws.Compute)
|
|
}
|
|
if group.Compute.LaunchSpecification == nil {
|
|
group.Compute.LaunchSpecification = new(aws.LaunchSpecification)
|
|
}
|
|
|
|
group.Compute.LaunchSpecification.SetHealthCheckType(e.HealthCheckType)
|
|
changes.HealthCheckType = nil
|
|
changed = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Capacity.
|
|
{
|
|
if changes.MinSize != nil {
|
|
if group.Capacity == nil {
|
|
group.Capacity = new(aws.Capacity)
|
|
}
|
|
|
|
group.Capacity.SetMinimum(fi.Int(int(*e.MinSize)))
|
|
changes.MinSize = nil
|
|
changed = true
|
|
|
|
// Scale up the target capacity, if needed.
|
|
if int64(*actual.Capacity.Target) < *e.MinSize {
|
|
group.Capacity.SetTarget(fi.Int(int(*e.MinSize)))
|
|
}
|
|
}
|
|
if changes.MaxSize != nil {
|
|
if group.Capacity == nil {
|
|
group.Capacity = new(aws.Capacity)
|
|
}
|
|
|
|
group.Capacity.SetMaximum(fi.Int(int(*e.MaxSize)))
|
|
changes.MaxSize = nil
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
// Auto Scaler.
|
|
{
|
|
if opts := changes.AutoScalerOpts; opts != nil {
|
|
if opts.Enabled != nil {
|
|
autoScaler := new(aws.AutoScaleKubernetes)
|
|
autoScaler.IsEnabled = e.AutoScalerOpts.Enabled
|
|
autoScaler.Cooldown = e.AutoScalerOpts.Cooldown
|
|
|
|
// Headroom.
|
|
if headroom := opts.Headroom; headroom != nil {
|
|
autoScaler.IsAutoConfig = fi.Bool(false)
|
|
autoScaler.Headroom = &aws.AutoScaleHeadroom{
|
|
CPUPerUnit: e.AutoScalerOpts.Headroom.CPUPerUnit,
|
|
GPUPerUnit: e.AutoScalerOpts.Headroom.GPUPerUnit,
|
|
MemoryPerUnit: e.AutoScalerOpts.Headroom.MemPerUnit,
|
|
NumOfUnits: e.AutoScalerOpts.Headroom.NumOfUnits,
|
|
}
|
|
} else if a.AutoScalerOpts != nil && a.AutoScalerOpts.Headroom != nil {
|
|
autoScaler.IsAutoConfig = fi.Bool(true)
|
|
autoScaler.SetHeadroom(nil)
|
|
}
|
|
|
|
// Scale down.
|
|
if down := opts.Down; down != nil {
|
|
autoScaler.Down = &aws.AutoScaleDown{
|
|
MaxScaleDownPercentage: down.MaxPercentage,
|
|
EvaluationPeriods: down.EvaluationPeriods,
|
|
}
|
|
} else if a.AutoScalerOpts.Down != nil {
|
|
autoScaler.SetDown(nil)
|
|
}
|
|
|
|
// Labels.
|
|
if labels := opts.Labels; labels != nil {
|
|
autoScaler.Labels = e.buildAutoScaleLabels(e.AutoScalerOpts.Labels)
|
|
} else if a.AutoScalerOpts.Labels != nil {
|
|
autoScaler.SetLabels(nil)
|
|
}
|
|
|
|
k8s := new(aws.KubernetesIntegration)
|
|
k8s.SetAutoScale(autoScaler)
|
|
|
|
integration := new(aws.Integration)
|
|
integration.SetKubernetes(k8s)
|
|
|
|
group.SetIntegration(integration)
|
|
changed = true
|
|
}
|
|
|
|
changes.AutoScalerOpts = nil
|
|
}
|
|
}
|
|
|
|
empty := &Elastigroup{}
|
|
if !reflect.DeepEqual(empty, changes) {
|
|
klog.Warningf("Not all changes applied to Elastigroup %q: %v", *group.ID, changes)
|
|
}
|
|
|
|
if !changed {
|
|
klog.V(2).Infof("No changes detected in Elastigroup %q", *group.ID)
|
|
return nil
|
|
}
|
|
|
|
klog.V(2).Infof("Updating Elastigroup %q (config: %s)", *group.ID, stringutil.Stringify(group))
|
|
|
|
// Wrap the raw object as an Elastigroup.
|
|
eg, err := spotinst.NewElastigroup(cloud.ProviderID(), group)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update the Elastigroup.
|
|
if err := cloud.Spotinst().Elastigroup().Update(context.Background(), eg); err != nil {
|
|
return fmt.Errorf("spotinst: failed to update elastigroup: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type terraformElastigroup struct {
|
|
Name *string `json:"name,omitempty" cty:"name"`
|
|
Description *string `json:"description,omitempty" cty:"description"`
|
|
Product *string `json:"product,omitempty" cty:"product"`
|
|
Region *string `json:"region,omitempty" cty:"region"`
|
|
SubnetIDs []*terraform.Literal `json:"subnet_ids,omitempty" cty:"subnet_ids"`
|
|
LoadBalancers []*terraform.Literal `json:"elastic_load_balancers,omitempty" cty:"elastic_load_balancers"`
|
|
NetworkInterfaces []*terraformElastigroupNetworkInterface `json:"network_interface,omitempty" cty:"network_interface"`
|
|
RootBlockDevice *terraformElastigroupBlockDevice `json:"ebs_block_device,omitempty" cty:"ebs_block_device"`
|
|
EphemeralBlockDevice []*terraformElastigroupBlockDevice `json:"ephemeral_block_device,omitempty" cty:"ephemeral_block_device"`
|
|
Integration *terraformElastigroupIntegration `json:"integration_kubernetes,omitempty" cty:"integration_kubernetes"`
|
|
Tags []*terraformKV `json:"tags,omitempty" cty:"tags"`
|
|
Lifecycle *terraformLifecycle `json:"lifecycle,omitempty" cty:"lifecycle"`
|
|
|
|
*terraformElastigroupCapacity
|
|
*terraformElastigroupStrategy
|
|
*terraformElastigroupInstanceTypes
|
|
*terraformElastigroupLaunchSpec
|
|
}
|
|
|
|
type terraformElastigroupCapacity struct {
|
|
MinSize *int64 `json:"min_size,omitempty" cty:"min_size"`
|
|
MaxSize *int64 `json:"max_size,omitempty" cty:"max_size"`
|
|
DesiredCapacity *int64 `json:"desired_capacity,omitempty" cty:"desired_capacity"`
|
|
CapacityUnit *string `json:"capacity_unit,omitempty" cty:"capacity_unit"`
|
|
}
|
|
|
|
type terraformElastigroupStrategy struct {
|
|
SpotPercentage *float64 `json:"spot_percentage,omitempty" cty:"spot_percentage"`
|
|
Orientation *string `json:"orientation,omitempty" cty:"orientation"`
|
|
FallbackToOnDemand *bool `json:"fallback_to_ondemand,omitempty" cty:"fallback_to_ondemand"`
|
|
UtilizeReservedInstances *bool `json:"utilize_reserved_instances,omitempty" cty:"utilize_reserved_instances"`
|
|
DrainingTimeout *int64 `json:"draining_timeout,omitempty" cty:"draining_timeout"`
|
|
}
|
|
|
|
type terraformElastigroupInstanceTypes struct {
|
|
OnDemand *string `json:"instance_types_ondemand,omitempty" cty:"instance_types_ondemand"`
|
|
Spot []string `json:"instance_types_spot,omitempty" cty:"instance_types_spot"`
|
|
}
|
|
|
|
type terraformElastigroupLaunchSpec struct {
|
|
Monitoring *bool `json:"enable_monitoring,omitempty" cty:"enable_monitoring"`
|
|
EBSOptimized *bool `json:"ebs_optimized,omitempty" cty:"ebs_optimized"`
|
|
ImageID *string `json:"image_id,omitempty" cty:"image_id"`
|
|
HealthCheckType *string `json:"health_check_type,omitempty" cty:"health_check_type"`
|
|
SecurityGroups []*terraform.Literal `json:"security_groups,omitempty" cty:"security_groups"`
|
|
UserData *terraform.Literal `json:"user_data,omitempty" cty:"user_data"`
|
|
IAMInstanceProfile *terraform.Literal `json:"iam_instance_profile,omitempty" cty:"iam_instance_profile"`
|
|
KeyName *terraform.Literal `json:"key_name,omitempty" cty:"key_name"`
|
|
}
|
|
|
|
type terraformElastigroupBlockDevice struct {
|
|
DeviceName *string `json:"device_name,omitempty" cty:"device_name"`
|
|
VirtualName *string `json:"virtual_name,omitempty" cty:"virtual_name"`
|
|
VolumeType *string `json:"volume_type,omitempty" cty:"volume_type"`
|
|
VolumeSize *int64 `json:"volume_size,omitempty" cty:"volume_size"`
|
|
DeleteOnTermination *bool `json:"delete_on_termination,omitempty" cty:"delete_on_termination"`
|
|
}
|
|
|
|
type terraformElastigroupNetworkInterface struct {
|
|
Description *string `json:"description,omitempty" cty:"description"`
|
|
DeviceIndex *int `json:"device_index,omitempty" cty:"device_index"`
|
|
AssociatePublicIPAddress *bool `json:"associate_public_ip_address,omitempty" cty:"associate_public_ip_address"`
|
|
DeleteOnTermination *bool `json:"delete_on_termination,omitempty" cty:"delete_on_termination"`
|
|
}
|
|
|
|
type terraformElastigroupIntegration struct {
|
|
IntegrationMode *string `json:"integration_mode,omitempty" cty:"integration_mode"`
|
|
ClusterIdentifier *string `json:"cluster_identifier,omitempty" cty:"cluster_identifier"`
|
|
|
|
*terraformAutoScaler
|
|
}
|
|
|
|
type terraformAutoScaler struct {
|
|
Enabled *bool `json:"autoscale_is_enabled,omitempty" cty:"autoscale_is_enabled"`
|
|
AutoConfig *bool `json:"autoscale_is_auto_config,omitempty" cty:"autoscale_is_auto_config"`
|
|
Cooldown *int `json:"autoscale_cooldown,omitempty" cty:"autoscale_cooldown"`
|
|
Headroom *terraformAutoScalerHeadroom `json:"autoscale_headroom,omitempty" cty:"autoscale_headroom"`
|
|
Down *terraformAutoScalerDown `json:"autoscale_down,omitempty" cty:"autoscale_down"`
|
|
Labels []*terraformKV `json:"autoscale_labels,omitempty" cty:"autoscale_labels"`
|
|
}
|
|
|
|
type terraformAutoScalerHeadroom struct {
|
|
CPUPerUnit *int `json:"cpu_per_unit,omitempty" cty:"cpu_per_unit"`
|
|
GPUPerUnit *int `json:"gpu_per_unit,omitempty" cty:"gpu_per_unit"`
|
|
MemPerUnit *int `json:"memory_per_unit,omitempty" cty:"memory_per_unit"`
|
|
NumOfUnits *int `json:"num_of_units,omitempty" cty:"num_of_units"`
|
|
}
|
|
|
|
type terraformAutoScalerDown struct {
|
|
MaxPercentage *int `json:"max_scale_down_percentage,omitempty" cty:"max_scale_down_percentage"`
|
|
EvaluationPeriods *int `json:"evaluation_periods,omitempty" cty:"evaluation_periods"`
|
|
}
|
|
|
|
type terraformKV struct {
|
|
Key *string `json:"key" cty:"key"`
|
|
Value *string `json:"value" cty:"value"`
|
|
}
|
|
|
|
type terraformLifecycle struct {
|
|
IgnoreChanges []string `json:"ignore_changes,omitempty" cty:"ignore_changes"`
|
|
}
|
|
|
|
func (_ *Elastigroup) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *Elastigroup) error {
|
|
cloud := t.Cloud.(awsup.AWSCloud)
|
|
e.applyDefaults()
|
|
|
|
tf := &terraformElastigroup{
|
|
Name: e.Name,
|
|
Description: e.Name,
|
|
Product: e.Product,
|
|
Region: fi.String(cloud.Region()),
|
|
terraformElastigroupCapacity: &terraformElastigroupCapacity{
|
|
DesiredCapacity: e.MinSize,
|
|
MinSize: e.MinSize,
|
|
MaxSize: e.MaxSize,
|
|
CapacityUnit: fi.String("instance"),
|
|
},
|
|
terraformElastigroupStrategy: &terraformElastigroupStrategy{
|
|
SpotPercentage: e.SpotPercentage,
|
|
Orientation: fi.String(string(normalizeOrientation(e.Orientation))),
|
|
FallbackToOnDemand: e.FallbackToOnDemand,
|
|
UtilizeReservedInstances: e.UtilizeReservedInstances,
|
|
DrainingTimeout: e.DrainingTimeout,
|
|
},
|
|
terraformElastigroupInstanceTypes: &terraformElastigroupInstanceTypes{
|
|
OnDemand: e.OnDemandInstanceType,
|
|
Spot: e.SpotInstanceTypes,
|
|
},
|
|
terraformElastigroupLaunchSpec: &terraformElastigroupLaunchSpec{},
|
|
}
|
|
|
|
// Image.
|
|
if e.ImageID != nil {
|
|
image, err := resolveImage(cloud, fi.StringValue(e.ImageID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tf.ImageID = image.ImageId
|
|
}
|
|
|
|
var role string
|
|
for key := range e.Tags {
|
|
if strings.HasPrefix(key, awstasks.CloudTagInstanceGroupRolePrefix) {
|
|
suffix := strings.TrimPrefix(key, awstasks.CloudTagInstanceGroupRolePrefix)
|
|
if role != "" && role != suffix {
|
|
return fmt.Errorf("spotinst: found multiple role tags %q vs %q", role, suffix)
|
|
}
|
|
role = suffix
|
|
}
|
|
}
|
|
|
|
// Security groups.
|
|
if e.SecurityGroups != nil {
|
|
for _, sg := range e.SecurityGroups {
|
|
tf.SecurityGroups = append(tf.SecurityGroups, sg.TerraformLink())
|
|
if role != "" {
|
|
if err := t.AddOutputVariableArray(role+"_security_groups", sg.TerraformLink()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// User data.
|
|
if e.UserData != nil {
|
|
var err error
|
|
tf.UserData, err = t.AddFile("spotinst_elastigroup_aws", *e.Name, "user_data", e.UserData, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// IAM instance profile.
|
|
if e.IAMInstanceProfile != nil {
|
|
tf.IAMInstanceProfile = e.IAMInstanceProfile.TerraformLink()
|
|
}
|
|
|
|
// Monitoring.
|
|
if e.Monitoring != nil {
|
|
tf.Monitoring = e.Monitoring
|
|
}
|
|
|
|
// Health check.
|
|
if e.HealthCheckType != nil {
|
|
tf.HealthCheckType = e.HealthCheckType
|
|
}
|
|
|
|
// SSH key.
|
|
if e.SSHKey != nil {
|
|
tf.KeyName = e.SSHKey.TerraformLink()
|
|
}
|
|
|
|
// Subnets.
|
|
if e.Subnets != nil {
|
|
for _, subnet := range e.Subnets {
|
|
tf.SubnetIDs = append(tf.SubnetIDs, subnet.TerraformLink())
|
|
if role != "" {
|
|
if err := t.AddOutputVariableArray(role+"_subnet_ids", subnet.TerraformLink()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Load balancer.
|
|
if e.LoadBalancer != nil {
|
|
tf.LoadBalancers = append(tf.LoadBalancers, e.LoadBalancer.TerraformLink())
|
|
}
|
|
|
|
// Public IP.
|
|
if e.AssociatePublicIP != nil {
|
|
tf.NetworkInterfaces = append(tf.NetworkInterfaces, &terraformElastigroupNetworkInterface{
|
|
Description: fi.String("eth0"),
|
|
DeviceIndex: fi.Int(0),
|
|
DeleteOnTermination: fi.Bool(true),
|
|
AssociatePublicIPAddress: e.AssociatePublicIP,
|
|
})
|
|
}
|
|
|
|
// Root volume options.
|
|
{
|
|
if opts := e.RootVolumeOpts; opts != nil {
|
|
|
|
// Block device mappings.
|
|
{
|
|
rootDevices, err := e.buildRootDevice(t.Cloud.(awsup.AWSCloud))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ephemeralDevices, err := e.buildEphemeralDevices(cloud, e.OnDemandInstanceType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(rootDevices) != 0 {
|
|
if len(rootDevices) != 1 {
|
|
return fmt.Errorf("unexpectedly found multiple root devices")
|
|
}
|
|
|
|
for name, bdm := range rootDevices {
|
|
tf.RootBlockDevice = &terraformElastigroupBlockDevice{
|
|
DeviceName: fi.String(name),
|
|
VolumeType: bdm.EbsVolumeType,
|
|
VolumeSize: bdm.EbsVolumeSize,
|
|
DeleteOnTermination: fi.Bool(true),
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(ephemeralDevices) != 0 {
|
|
tf.EphemeralBlockDevice = []*terraformElastigroupBlockDevice{}
|
|
for _, deviceName := range sets.StringKeySet(ephemeralDevices).List() {
|
|
bdm := ephemeralDevices[deviceName]
|
|
tf.EphemeralBlockDevice = append(tf.EphemeralBlockDevice, &terraformElastigroupBlockDevice{
|
|
VirtualName: bdm.VirtualName,
|
|
DeviceName: fi.String(deviceName),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// EBS optimization.
|
|
{
|
|
if opts.Optimization != nil {
|
|
tf.EBSOptimized = opts.Optimization
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Auto Scaler.
|
|
{
|
|
if opts := e.AutoScalerOpts; opts != nil {
|
|
tf.Integration = &terraformElastigroupIntegration{
|
|
IntegrationMode: fi.String("pod"),
|
|
ClusterIdentifier: opts.ClusterID,
|
|
}
|
|
|
|
if opts.Enabled != nil {
|
|
tf.Integration.terraformAutoScaler = &terraformAutoScaler{
|
|
Enabled: opts.Enabled,
|
|
AutoConfig: fi.Bool(true),
|
|
Cooldown: opts.Cooldown,
|
|
}
|
|
|
|
// Headroom.
|
|
if headroom := opts.Headroom; headroom != nil {
|
|
tf.Integration.AutoConfig = fi.Bool(false)
|
|
tf.Integration.Headroom = &terraformAutoScalerHeadroom{
|
|
CPUPerUnit: headroom.CPUPerUnit,
|
|
GPUPerUnit: headroom.GPUPerUnit,
|
|
MemPerUnit: headroom.MemPerUnit,
|
|
NumOfUnits: headroom.NumOfUnits,
|
|
}
|
|
}
|
|
|
|
// Scale down.
|
|
if down := opts.Down; down != nil {
|
|
tf.Integration.Down = &terraformAutoScalerDown{
|
|
MaxPercentage: down.MaxPercentage,
|
|
EvaluationPeriods: down.EvaluationPeriods,
|
|
}
|
|
}
|
|
|
|
// Labels.
|
|
if labels := opts.Labels; labels != nil {
|
|
tf.Integration.Labels = make([]*terraformKV, 0, len(labels))
|
|
for k, v := range labels {
|
|
tf.Integration.Labels = append(tf.Integration.Labels, &terraformKV{
|
|
Key: fi.String(k),
|
|
Value: fi.String(v),
|
|
})
|
|
}
|
|
}
|
|
|
|
// Ignore capacity changes because the auto scaler updates the
|
|
// desired capacity overtime.
|
|
if fi.BoolValue(tf.Integration.Enabled) {
|
|
tf.Lifecycle = &terraformLifecycle{
|
|
IgnoreChanges: []string{
|
|
"desired_capacity",
|
|
},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tags.
|
|
{
|
|
if e.Tags != nil {
|
|
tags := e.buildTags()
|
|
for _, tag := range tags {
|
|
tf.Tags = append(tf.Tags, &terraformKV{
|
|
Key: tag.Key,
|
|
Value: tag.Value,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return t.RenderResource("spotinst_elastigroup_aws", *e.Name, tf)
|
|
}
|
|
|
|
func (e *Elastigroup) TerraformLink() *terraform.Literal {
|
|
return terraform.LiteralProperty("spotinst_elastigroup_aws", *e.Name, "id")
|
|
}
|
|
|
|
func (e *Elastigroup) buildTags() []*aws.Tag {
|
|
tags := make([]*aws.Tag, 0, len(e.Tags))
|
|
|
|
for key, value := range e.Tags {
|
|
tags = append(tags, &aws.Tag{
|
|
Key: fi.String(key),
|
|
Value: fi.String(value),
|
|
})
|
|
}
|
|
|
|
return tags
|
|
}
|
|
|
|
func (e *Elastigroup) buildAutoScaleLabels(labelsMap map[string]string) []*aws.AutoScaleLabel {
|
|
labels := make([]*aws.AutoScaleLabel, 0, len(labelsMap))
|
|
for key, value := range labelsMap {
|
|
labels = append(labels, &aws.AutoScaleLabel{
|
|
Key: fi.String(key),
|
|
Value: fi.String(value),
|
|
})
|
|
}
|
|
|
|
return labels
|
|
}
|
|
|
|
func (e *Elastigroup) buildEphemeralDevices(c awsup.AWSCloud, instanceTypeName *string) (map[string]*awstasks.BlockDeviceMapping, error) {
|
|
if instanceTypeName == nil {
|
|
return nil, fi.RequiredField("InstanceType")
|
|
}
|
|
|
|
instanceType, err := awsup.GetMachineTypeInfo(c, *instanceTypeName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
blockDeviceMappings := make(map[string]*awstasks.BlockDeviceMapping)
|
|
for _, ed := range instanceType.EphemeralDevices() {
|
|
m := &awstasks.BlockDeviceMapping{
|
|
VirtualName: fi.String(ed.VirtualName),
|
|
}
|
|
blockDeviceMappings[ed.DeviceName] = m
|
|
}
|
|
|
|
return blockDeviceMappings, nil
|
|
}
|
|
|
|
func (e *Elastigroup) buildRootDevice(cloud awsup.AWSCloud) (map[string]*awstasks.BlockDeviceMapping, error) {
|
|
image, err := resolveImage(cloud, fi.StringValue(e.ImageID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rootDeviceName := fi.StringValue(image.RootDeviceName)
|
|
blockDeviceMappings := make(map[string]*awstasks.BlockDeviceMapping)
|
|
|
|
rootDeviceMapping := &awstasks.BlockDeviceMapping{
|
|
EbsDeleteOnTermination: fi.Bool(true),
|
|
EbsVolumeSize: fi.Int64(int64(fi.Int32Value(e.RootVolumeOpts.Size))),
|
|
EbsVolumeType: e.RootVolumeOpts.Type,
|
|
}
|
|
|
|
// The parameter IOPS is not supported for gp2 volumes.
|
|
if e.RootVolumeOpts.IOPS != nil && fi.StringValue(e.RootVolumeOpts.Type) != "gp2" {
|
|
rootDeviceMapping.EbsVolumeIops = fi.Int64(int64(fi.Int32Value(e.RootVolumeOpts.IOPS)))
|
|
}
|
|
|
|
blockDeviceMappings[rootDeviceName] = rootDeviceMapping
|
|
|
|
return blockDeviceMappings, nil
|
|
}
|
|
|
|
func (e *Elastigroup) buildBlockDeviceMapping(deviceName string, i *awstasks.BlockDeviceMapping) *aws.BlockDeviceMapping {
|
|
o := &aws.BlockDeviceMapping{}
|
|
o.DeviceName = fi.String(deviceName)
|
|
o.VirtualName = i.VirtualName
|
|
|
|
if i.EbsDeleteOnTermination != nil || i.EbsVolumeSize != nil || i.EbsVolumeType != nil {
|
|
o.EBS = &aws.EBS{}
|
|
o.EBS.DeleteOnTermination = i.EbsDeleteOnTermination
|
|
o.EBS.VolumeSize = fi.Int(int(fi.Int64Value(i.EbsVolumeSize)))
|
|
o.EBS.VolumeType = i.EbsVolumeType
|
|
|
|
// The parameter IOPS is not supported for gp2 volumes.
|
|
if i.EbsVolumeIops != nil && fi.StringValue(i.EbsVolumeType) != "gp2" {
|
|
o.EBS.IOPS = fi.Int(int(fi.Int64Value(i.EbsVolumeIops)))
|
|
}
|
|
}
|
|
|
|
return o
|
|
}
|
|
|
|
func (e *Elastigroup) applyDefaults() {
|
|
if e.FallbackToOnDemand == nil {
|
|
e.FallbackToOnDemand = fi.Bool(true)
|
|
}
|
|
|
|
if e.UtilizeReservedInstances == nil {
|
|
e.UtilizeReservedInstances = fi.Bool(true)
|
|
}
|
|
|
|
if e.Product == nil || (e.Product != nil && fi.StringValue(e.Product) == "") {
|
|
e.Product = fi.String("Linux/UNIX")
|
|
}
|
|
|
|
if e.Orientation == nil || (e.Orientation != nil && fi.StringValue(e.Orientation) == "") {
|
|
e.Orientation = fi.String("balanced")
|
|
}
|
|
|
|
if e.Monitoring == nil {
|
|
e.Monitoring = fi.Bool(false)
|
|
}
|
|
|
|
if e.HealthCheckType == nil {
|
|
e.HealthCheckType = fi.String("K8S_NODE")
|
|
}
|
|
}
|
|
|
|
func resolveImage(cloud awsup.AWSCloud, name string) (*ec2.Image, error) {
|
|
image, err := cloud.ResolveImage(name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("spotinst: unable to resolve image %q: %v", name, err)
|
|
} else if image == nil {
|
|
return nil, fmt.Errorf("spotinst: unable to resolve image %q: not found", name)
|
|
}
|
|
|
|
return image, nil
|
|
}
|
|
|
|
func subnetSlicesEqualIgnoreOrder(l, r []*awstasks.Subnet) bool {
|
|
var lIDs []string
|
|
for _, s := range l {
|
|
lIDs = append(lIDs, *s.ID)
|
|
}
|
|
|
|
var rIDs []string
|
|
for _, s := range r {
|
|
if s.ID == nil {
|
|
klog.V(4).Infof("Subnet ID not set; returning not-equal: %v", s)
|
|
return false
|
|
}
|
|
rIDs = append(rIDs, *s.ID)
|
|
}
|
|
|
|
return utils.StringSlicesEqualIgnoreOrder(lIDs, rIDs)
|
|
}
|
|
|
|
type Orientation string
|
|
|
|
const (
|
|
OrientationBalanced Orientation = "balanced"
|
|
OrientationCost Orientation = "costOriented"
|
|
OrientationAvailability Orientation = "availabilityOriented"
|
|
OrientationEqualZoneDistribution Orientation = "equalAzDistribution"
|
|
)
|
|
|
|
func normalizeOrientation(orientation *string) Orientation {
|
|
out := OrientationBalanced
|
|
|
|
// Fast path.
|
|
if orientation == nil {
|
|
return out
|
|
}
|
|
|
|
switch *orientation {
|
|
case "cost":
|
|
out = OrientationCost
|
|
case "availability":
|
|
out = OrientationAvailability
|
|
case "equal-distribution":
|
|
out = OrientationEqualZoneDistribution
|
|
}
|
|
|
|
return out
|
|
}
|