mirror of https://github.com/kubernetes/kops.git
1261 lines
32 KiB
Go
1261 lines
32 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/spotinst/spotinst-sdk-go/service/ocean/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/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"
|
|
)
|
|
|
|
// +kops:fitask
|
|
type Ocean struct {
|
|
Name *string
|
|
Lifecycle *fi.Lifecycle
|
|
|
|
ID *string
|
|
MinSize *int64
|
|
MaxSize *int64
|
|
SpotPercentage *float64
|
|
UtilizeReservedInstances *bool
|
|
FallbackToOnDemand *bool
|
|
DrainingTimeout *int64
|
|
GracePeriod *int64
|
|
InstanceTypesWhitelist []string
|
|
InstanceTypesBlacklist []string
|
|
Tags map[string]string
|
|
UserData *fi.ResourceHolder
|
|
ImageID *string
|
|
IAMInstanceProfile *awstasks.IAMInstanceProfile
|
|
SSHKey *awstasks.SSHKey
|
|
Subnets []*awstasks.Subnet
|
|
SecurityGroups []*awstasks.SecurityGroup
|
|
Monitoring *bool
|
|
AssociatePublicIP *bool
|
|
RootVolumeOpts *RootVolumeOpts
|
|
AutoScalerOpts *AutoScalerOpts
|
|
}
|
|
|
|
var _ fi.Task = &Ocean{}
|
|
var _ fi.CompareWithID = &Ocean{}
|
|
var _ fi.HasDependencies = &Ocean{}
|
|
|
|
func (o *Ocean) CompareWithID() *string {
|
|
return o.Name
|
|
}
|
|
|
|
func (o *Ocean) GetDependencies(tasks map[string]fi.Task) []fi.Task {
|
|
var deps []fi.Task
|
|
|
|
if o.IAMInstanceProfile != nil {
|
|
deps = append(deps, o.IAMInstanceProfile)
|
|
}
|
|
|
|
if o.SSHKey != nil {
|
|
deps = append(deps, o.SSHKey)
|
|
}
|
|
|
|
if o.Subnets != nil {
|
|
for _, subnet := range o.Subnets {
|
|
deps = append(deps, subnet)
|
|
}
|
|
}
|
|
|
|
if o.SecurityGroups != nil {
|
|
for _, sg := range o.SecurityGroups {
|
|
deps = append(deps, sg)
|
|
}
|
|
}
|
|
|
|
if o.UserData != nil {
|
|
deps = append(deps, o.UserData.GetDependencies(tasks)...)
|
|
}
|
|
|
|
return deps
|
|
}
|
|
|
|
func (o *Ocean) find(svc spotinst.InstanceGroupService, name string) (*aws.Cluster, error) {
|
|
klog.V(4).Infof("Attempting to find Ocean: %q", name)
|
|
|
|
oceans, err := svc.List(context.Background())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("spotinst: failed to find ocean %q: %v", name, err)
|
|
}
|
|
|
|
var out *aws.Cluster
|
|
for _, ocean := range oceans {
|
|
if ocean.Name() == name {
|
|
out = ocean.Obj().(*aws.Cluster)
|
|
break
|
|
}
|
|
}
|
|
if out == nil {
|
|
return nil, fmt.Errorf("spotinst: failed to find ocean %q", name)
|
|
}
|
|
|
|
klog.V(4).Infof("Ocean/%s: %s", name, stringutil.Stringify(out))
|
|
return out, nil
|
|
}
|
|
|
|
var _ fi.HasCheckExisting = &Ocean{}
|
|
|
|
func (o *Ocean) Find(c *fi.Context) (*Ocean, error) {
|
|
cloud := c.Cloud.(awsup.AWSCloud)
|
|
|
|
ocean, err := o.find(cloud.Spotinst().Ocean(), *o.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
actual := &Ocean{}
|
|
actual.ID = ocean.ID
|
|
actual.Name = ocean.Name
|
|
|
|
// Capacity.
|
|
{
|
|
actual.MinSize = fi.Int64(int64(fi.IntValue(ocean.Capacity.Minimum)))
|
|
actual.MaxSize = fi.Int64(int64(fi.IntValue(ocean.Capacity.Maximum)))
|
|
}
|
|
|
|
// Strategy.
|
|
{
|
|
if strategy := ocean.Strategy; strategy != nil {
|
|
actual.SpotPercentage = strategy.SpotPercentage
|
|
actual.FallbackToOnDemand = strategy.FallbackToOnDemand
|
|
actual.UtilizeReservedInstances = strategy.UtilizeReservedInstances
|
|
|
|
if strategy.DrainingTimeout != nil {
|
|
actual.DrainingTimeout = fi.Int64(int64(fi.IntValue(strategy.DrainingTimeout)))
|
|
}
|
|
|
|
if strategy.GracePeriod != nil {
|
|
actual.GracePeriod = fi.Int64(int64(fi.IntValue(strategy.GracePeriod)))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Compute.
|
|
{
|
|
compute := ocean.Compute
|
|
|
|
// Subnets.
|
|
{
|
|
if subnets := compute.SubnetIDs; subnets != nil {
|
|
for _, subnetID := range subnets {
|
|
actual.Subnets = append(actual.Subnets,
|
|
&awstasks.Subnet{ID: fi.String(subnetID)})
|
|
}
|
|
if subnetSlicesEqualIgnoreOrder(actual.Subnets, o.Subnets) {
|
|
actual.Subnets = o.Subnets
|
|
}
|
|
}
|
|
}
|
|
|
|
// Instance types.
|
|
{
|
|
if itypes := compute.InstanceTypes; itypes != nil {
|
|
// Whitelist.
|
|
if len(itypes.Whitelist) > 0 {
|
|
actual.InstanceTypesWhitelist = itypes.Whitelist
|
|
}
|
|
|
|
// Blacklist.
|
|
if len(itypes.Blacklist) > 0 {
|
|
actual.InstanceTypesBlacklist = itypes.Blacklist
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Launch specification.
|
|
{
|
|
lc := ocean.Compute.LaunchSpecification
|
|
|
|
// Image.
|
|
{
|
|
actual.ImageID = lc.ImageID
|
|
|
|
if o.ImageID != nil && actual.ImageID != nil &&
|
|
fi.StringValue(actual.ImageID) != fi.StringValue(o.ImageID) {
|
|
image, err := resolveImage(cloud, fi.StringValue(o.ImageID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if fi.StringValue(image.ImageId) == fi.StringValue(lc.ImageID) {
|
|
actual.ImageID = o.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)})
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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)))
|
|
}
|
|
|
|
// EBS optimization.
|
|
{
|
|
if fi.BoolValue(lc.EBSOptimized) {
|
|
if actual.RootVolumeOpts == nil {
|
|
actual.RootVolumeOpts = new(RootVolumeOpts)
|
|
}
|
|
|
|
actual.RootVolumeOpts.Optimization = lc.EBSOptimized
|
|
}
|
|
}
|
|
|
|
// 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}
|
|
}
|
|
|
|
// Public IP.
|
|
if lc.AssociatePublicIPAddress != nil {
|
|
actual.AssociatePublicIP = lc.AssociatePublicIPAddress
|
|
}
|
|
|
|
// Root volume options.
|
|
if lc.RootVolumeSize != nil {
|
|
actual.RootVolumeOpts = new(RootVolumeOpts)
|
|
actual.RootVolumeOpts.Size = fi.Int32(int32(*lc.RootVolumeSize))
|
|
}
|
|
|
|
// Monitoring.
|
|
if lc.Monitoring != nil {
|
|
actual.Monitoring = lc.Monitoring
|
|
}
|
|
}
|
|
|
|
// Auto Scaler.
|
|
{
|
|
if ocean.AutoScaler != nil {
|
|
actual.AutoScalerOpts = new(AutoScalerOpts)
|
|
actual.AutoScalerOpts.ClusterID = ocean.ControllerClusterID
|
|
actual.AutoScalerOpts.Enabled = ocean.AutoScaler.IsEnabled
|
|
actual.AutoScalerOpts.Cooldown = ocean.AutoScaler.Cooldown
|
|
|
|
// Headroom.
|
|
if headroom := ocean.AutoScaler.Headroom; headroom != nil {
|
|
actual.AutoScalerOpts.Headroom = &AutoScalerHeadroomOpts{
|
|
CPUPerUnit: headroom.CPUPerUnit,
|
|
GPUPerUnit: headroom.GPUPerUnit,
|
|
MemPerUnit: headroom.MemoryPerUnit,
|
|
NumOfUnits: headroom.NumOfUnits,
|
|
}
|
|
}
|
|
|
|
// Scale down.
|
|
if down := ocean.AutoScaler.Down; down != nil {
|
|
actual.AutoScalerOpts.Down = &AutoScalerDownOpts{
|
|
MaxPercentage: down.MaxScaleDownPercentage,
|
|
EvaluationPeriods: down.EvaluationPeriods,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Avoid spurious changes.
|
|
actual.Lifecycle = o.Lifecycle
|
|
|
|
return actual, nil
|
|
}
|
|
|
|
func (o *Ocean) CheckExisting(c *fi.Context) bool {
|
|
cloud := c.Cloud.(awsup.AWSCloud)
|
|
ocean, err := o.find(cloud.Spotinst().Ocean(), *o.Name)
|
|
return err == nil && ocean != nil
|
|
}
|
|
|
|
func (o *Ocean) Run(c *fi.Context) error {
|
|
return fi.DefaultDeltaRunMethod(o, c)
|
|
}
|
|
|
|
func (s *Ocean) CheckChanges(a, e, changes *Ocean) error {
|
|
if e.Name == nil {
|
|
return fi.RequiredField("Name")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (o *Ocean) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *Ocean) error {
|
|
return o.createOrUpdate(t.Cloud.(awsup.AWSCloud), a, e, changes)
|
|
}
|
|
|
|
func (o *Ocean) createOrUpdate(cloud awsup.AWSCloud, a, e, changes *Ocean) error {
|
|
if a == nil {
|
|
return o.create(cloud, a, e, changes)
|
|
} else {
|
|
return o.update(cloud, a, e, changes)
|
|
}
|
|
}
|
|
|
|
func (_ *Ocean) create(cloud awsup.AWSCloud, a, e, changes *Ocean) error {
|
|
klog.V(2).Infof("Creating Ocean %q", *e.Name)
|
|
e.applyDefaults()
|
|
|
|
ocean := &aws.Cluster{
|
|
Capacity: new(aws.Capacity),
|
|
Strategy: new(aws.Strategy),
|
|
Compute: &aws.Compute{
|
|
LaunchSpecification: new(aws.LaunchSpecification),
|
|
},
|
|
}
|
|
|
|
// General.
|
|
{
|
|
ocean.SetName(e.Name)
|
|
ocean.SetRegion(fi.String(cloud.Region()))
|
|
}
|
|
|
|
// Capacity.
|
|
{
|
|
ocean.Capacity.SetTarget(fi.Int(int(*e.MinSize)))
|
|
ocean.Capacity.SetMinimum(fi.Int(int(*e.MinSize)))
|
|
ocean.Capacity.SetMaximum(fi.Int(int(*e.MaxSize)))
|
|
}
|
|
|
|
// Strategy.
|
|
{
|
|
ocean.Strategy.SetSpotPercentage(e.SpotPercentage)
|
|
ocean.Strategy.SetFallbackToOnDemand(e.FallbackToOnDemand)
|
|
ocean.Strategy.SetUtilizeReservedInstances(e.UtilizeReservedInstances)
|
|
|
|
if e.DrainingTimeout != nil {
|
|
ocean.Strategy.SetDrainingTimeout(fi.Int(int(*e.DrainingTimeout)))
|
|
}
|
|
|
|
if e.GracePeriod != nil {
|
|
ocean.Strategy.SetGracePeriod(fi.Int(int(*e.GracePeriod)))
|
|
}
|
|
}
|
|
|
|
// Compute.
|
|
{
|
|
// Subnets.
|
|
{
|
|
if e.Subnets != nil {
|
|
subnetIDs := make([]string, len(e.Subnets))
|
|
for i, subnet := range e.Subnets {
|
|
subnetIDs[i] = fi.StringValue(subnet.ID)
|
|
}
|
|
ocean.Compute.SetSubnetIDs(subnetIDs)
|
|
}
|
|
}
|
|
|
|
// Instance types.
|
|
{
|
|
itypes := new(aws.InstanceTypes)
|
|
|
|
// Whitelist.
|
|
if e.InstanceTypesWhitelist != nil {
|
|
itypes.SetWhitelist(e.InstanceTypesWhitelist)
|
|
}
|
|
|
|
// Blacklist.
|
|
if e.InstanceTypesBlacklist != nil {
|
|
itypes.SetBlacklist(e.InstanceTypesBlacklist)
|
|
}
|
|
|
|
if len(itypes.Whitelist) > 0 || len(itypes.Blacklist) > 0 {
|
|
ocean.Compute.SetInstanceTypes(itypes)
|
|
}
|
|
}
|
|
|
|
// Launch specification.
|
|
{
|
|
ocean.Compute.LaunchSpecification.SetMonitoring(e.Monitoring)
|
|
ocean.Compute.LaunchSpecification.SetKeyPair(e.SSHKey.Name)
|
|
|
|
// Image.
|
|
{
|
|
if e.ImageID != nil {
|
|
image, err := resolveImage(cloud, fi.StringValue(e.ImageID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ocean.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))
|
|
ocean.Compute.LaunchSpecification.SetUserData(fi.String(encoded))
|
|
}
|
|
}
|
|
}
|
|
|
|
// IAM instance profile.
|
|
{
|
|
if e.IAMInstanceProfile != nil {
|
|
iprof := new(aws.IAMInstanceProfile)
|
|
iprof.SetName(e.IAMInstanceProfile.GetName())
|
|
ocean.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
|
|
}
|
|
ocean.Compute.LaunchSpecification.SetSecurityGroupIDs(securityGroupIDs)
|
|
}
|
|
}
|
|
|
|
// Public IP.
|
|
{
|
|
if e.AssociatePublicIP != nil {
|
|
ocean.Compute.LaunchSpecification.SetAssociatePublicIPAddress(e.AssociatePublicIP)
|
|
}
|
|
}
|
|
|
|
// Root volume options.
|
|
{
|
|
if opts := e.RootVolumeOpts; opts != nil {
|
|
|
|
// Volume size.
|
|
if opts.Size != nil {
|
|
ocean.Compute.LaunchSpecification.SetRootVolumeSize(fi.Int(int(*opts.Size)))
|
|
}
|
|
|
|
// EBS optimization.
|
|
if opts.Optimization != nil {
|
|
ocean.Compute.LaunchSpecification.SetEBSOptimized(opts.Optimization)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tags.
|
|
{
|
|
if e.Tags != nil {
|
|
ocean.Compute.LaunchSpecification.SetTags(e.buildTags())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Auto Scaler.
|
|
{
|
|
if opts := e.AutoScalerOpts; opts != nil {
|
|
ocean.SetControllerClusterId(opts.ClusterID)
|
|
|
|
if opts.Enabled != nil {
|
|
autoScaler := new(aws.AutoScaler)
|
|
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.AutoScalerHeadroom{
|
|
CPUPerUnit: headroom.CPUPerUnit,
|
|
GPUPerUnit: headroom.GPUPerUnit,
|
|
MemoryPerUnit: headroom.MemPerUnit,
|
|
NumOfUnits: headroom.NumOfUnits,
|
|
}
|
|
}
|
|
|
|
// Scale down.
|
|
if down := opts.Down; down != nil {
|
|
autoScaler.Down = &aws.AutoScalerDown{
|
|
MaxScaleDownPercentage: down.MaxPercentage,
|
|
EvaluationPeriods: down.EvaluationPeriods,
|
|
}
|
|
}
|
|
|
|
ocean.SetAutoScaler(autoScaler)
|
|
}
|
|
}
|
|
}
|
|
|
|
attempt := 0
|
|
maxAttempts := 10
|
|
|
|
readyLoop:
|
|
for {
|
|
attempt++
|
|
klog.V(2).Infof("(%d/%d) Attempting to create Ocean: %q, config: %s",
|
|
attempt, maxAttempts, *e.Name, stringutil.Stringify(ocean))
|
|
|
|
// Wait for IAM instance profile to be ready.
|
|
time.Sleep(10 * time.Second)
|
|
|
|
// Wrap the raw object as an Ocean.
|
|
oc, err := spotinst.NewOcean(cloud.ProviderID(), ocean)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create a new Ocean.
|
|
id, err := cloud.Spotinst().Ocean().Create(context.Background(), oc)
|
|
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 ocean: %v", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (_ *Ocean) update(cloud awsup.AWSCloud, a, e, changes *Ocean) error {
|
|
klog.V(2).Infof("Updating Ocean %q", *e.Name)
|
|
|
|
actual, err := e.find(cloud.Spotinst().Ocean(), *e.Name)
|
|
if err != nil {
|
|
klog.Errorf("Unable to resolve Ocean %q, error: %s", *e.Name, err)
|
|
return err
|
|
}
|
|
|
|
var changed bool
|
|
ocean := new(aws.Cluster)
|
|
ocean.SetId(actual.ID)
|
|
|
|
// Strategy.
|
|
{
|
|
// Spot percentage.
|
|
if changes.SpotPercentage != nil {
|
|
if ocean.Strategy == nil {
|
|
ocean.Strategy = new(aws.Strategy)
|
|
}
|
|
|
|
ocean.Strategy.SetSpotPercentage(e.SpotPercentage)
|
|
changes.SpotPercentage = nil
|
|
changed = true
|
|
}
|
|
|
|
// Fallback to on-demand.
|
|
if changes.FallbackToOnDemand != nil {
|
|
if ocean.Strategy == nil {
|
|
ocean.Strategy = new(aws.Strategy)
|
|
}
|
|
|
|
ocean.Strategy.SetFallbackToOnDemand(e.FallbackToOnDemand)
|
|
changes.FallbackToOnDemand = nil
|
|
changed = true
|
|
}
|
|
|
|
// Utilize reserved instances.
|
|
if changes.UtilizeReservedInstances != nil {
|
|
if ocean.Strategy == nil {
|
|
ocean.Strategy = new(aws.Strategy)
|
|
}
|
|
|
|
ocean.Strategy.SetUtilizeReservedInstances(e.UtilizeReservedInstances)
|
|
changes.UtilizeReservedInstances = nil
|
|
changed = true
|
|
}
|
|
|
|
// Draining timeout.
|
|
if changes.DrainingTimeout != nil {
|
|
if ocean.Strategy == nil {
|
|
ocean.Strategy = new(aws.Strategy)
|
|
}
|
|
|
|
ocean.Strategy.SetDrainingTimeout(fi.Int(int(*e.DrainingTimeout)))
|
|
changes.DrainingTimeout = nil
|
|
changed = true
|
|
}
|
|
|
|
// Grace period.
|
|
if changes.GracePeriod != nil {
|
|
if ocean.Strategy == nil {
|
|
ocean.Strategy = new(aws.Strategy)
|
|
}
|
|
|
|
ocean.Strategy.SetGracePeriod(fi.Int(int(*e.GracePeriod)))
|
|
changes.GracePeriod = nil
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
// Compute.
|
|
{
|
|
// Subnets.
|
|
{
|
|
if changes.Subnets != nil {
|
|
if ocean.Compute == nil {
|
|
ocean.Compute = new(aws.Compute)
|
|
}
|
|
|
|
subnetIDs := make([]string, len(e.Subnets))
|
|
for i, subnet := range e.Subnets {
|
|
subnetIDs[i] = fi.StringValue(subnet.ID)
|
|
}
|
|
|
|
ocean.Compute.SetSubnetIDs(subnetIDs)
|
|
changes.Subnets = nil
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
// Instance types.
|
|
{
|
|
// Whitelist.
|
|
{
|
|
if changes.InstanceTypesWhitelist != nil {
|
|
if ocean.Compute == nil {
|
|
ocean.Compute = new(aws.Compute)
|
|
}
|
|
if ocean.Compute.InstanceTypes == nil {
|
|
ocean.Compute.InstanceTypes = new(aws.InstanceTypes)
|
|
}
|
|
|
|
ocean.Compute.InstanceTypes.SetWhitelist(e.InstanceTypesWhitelist)
|
|
changes.InstanceTypesWhitelist = nil
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
// Blacklist.
|
|
{
|
|
if changes.InstanceTypesBlacklist != nil {
|
|
if ocean.Compute == nil {
|
|
ocean.Compute = new(aws.Compute)
|
|
}
|
|
if ocean.Compute.InstanceTypes == nil {
|
|
ocean.Compute.InstanceTypes = new(aws.InstanceTypes)
|
|
}
|
|
|
|
ocean.Compute.InstanceTypes.SetBlacklist(e.InstanceTypesBlacklist)
|
|
changes.InstanceTypesBlacklist = nil
|
|
changed = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Launch specification.
|
|
{
|
|
// Security groups.
|
|
{
|
|
if changes.SecurityGroups != nil {
|
|
if ocean.Compute == nil {
|
|
ocean.Compute = new(aws.Compute)
|
|
}
|
|
if ocean.Compute.LaunchSpecification == nil {
|
|
ocean.Compute.LaunchSpecification = new(aws.LaunchSpecification)
|
|
}
|
|
|
|
securityGroupIDs := make([]string, len(e.SecurityGroups))
|
|
for i, sg := range e.SecurityGroups {
|
|
securityGroupIDs[i] = *sg.ID
|
|
}
|
|
|
|
ocean.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 ocean.Compute == nil {
|
|
ocean.Compute = new(aws.Compute)
|
|
}
|
|
if ocean.Compute.LaunchSpecification == nil {
|
|
ocean.Compute.LaunchSpecification = new(aws.LaunchSpecification)
|
|
}
|
|
|
|
encoded := base64.StdEncoding.EncodeToString([]byte(userData))
|
|
ocean.Compute.LaunchSpecification.SetUserData(fi.String(encoded))
|
|
changed = true
|
|
}
|
|
|
|
changes.UserData = 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 ocean.Compute == nil {
|
|
ocean.Compute = new(aws.Compute)
|
|
}
|
|
if ocean.Compute.LaunchSpecification == nil {
|
|
ocean.Compute.LaunchSpecification = new(aws.LaunchSpecification)
|
|
}
|
|
|
|
ocean.Compute.LaunchSpecification.SetImageId(image.ImageId)
|
|
changed = true
|
|
}
|
|
|
|
changes.ImageID = nil
|
|
}
|
|
}
|
|
|
|
// Tags.
|
|
{
|
|
if changes.Tags != nil {
|
|
if ocean.Compute == nil {
|
|
ocean.Compute = new(aws.Compute)
|
|
}
|
|
if ocean.Compute.LaunchSpecification == nil {
|
|
ocean.Compute.LaunchSpecification = new(aws.LaunchSpecification)
|
|
}
|
|
|
|
ocean.Compute.LaunchSpecification.SetTags(e.buildTags())
|
|
changes.Tags = nil
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
// IAM instance profile.
|
|
{
|
|
if changes.IAMInstanceProfile != nil {
|
|
if ocean.Compute == nil {
|
|
ocean.Compute = new(aws.Compute)
|
|
}
|
|
if ocean.Compute.LaunchSpecification == nil {
|
|
ocean.Compute.LaunchSpecification = new(aws.LaunchSpecification)
|
|
}
|
|
|
|
iprof := new(aws.IAMInstanceProfile)
|
|
iprof.SetName(e.IAMInstanceProfile.GetName())
|
|
|
|
ocean.Compute.LaunchSpecification.SetIAMInstanceProfile(iprof)
|
|
changes.IAMInstanceProfile = nil
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
// Monitoring.
|
|
{
|
|
if changes.Monitoring != nil {
|
|
if ocean.Compute == nil {
|
|
ocean.Compute = new(aws.Compute)
|
|
}
|
|
if ocean.Compute.LaunchSpecification == nil {
|
|
ocean.Compute.LaunchSpecification = new(aws.LaunchSpecification)
|
|
}
|
|
|
|
ocean.Compute.LaunchSpecification.SetMonitoring(e.Monitoring)
|
|
changes.Monitoring = nil
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
// SSH key.
|
|
{
|
|
if changes.SSHKey != nil {
|
|
if ocean.Compute == nil {
|
|
ocean.Compute = new(aws.Compute)
|
|
}
|
|
if ocean.Compute.LaunchSpecification == nil {
|
|
ocean.Compute.LaunchSpecification = new(aws.LaunchSpecification)
|
|
}
|
|
|
|
ocean.Compute.LaunchSpecification.SetKeyPair(e.SSHKey.Name)
|
|
changes.SSHKey = nil
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
// Public IP.
|
|
{
|
|
if changes.AssociatePublicIP != nil {
|
|
if ocean.Compute == nil {
|
|
ocean.Compute = new(aws.Compute)
|
|
}
|
|
if ocean.Compute.LaunchSpecification == nil {
|
|
ocean.Compute.LaunchSpecification = new(aws.LaunchSpecification)
|
|
}
|
|
|
|
ocean.Compute.LaunchSpecification.SetAssociatePublicIPAddress(e.AssociatePublicIP)
|
|
changes.AssociatePublicIP = nil
|
|
changed = true
|
|
}
|
|
}
|
|
|
|
// Root volume options.
|
|
{
|
|
if opts := changes.RootVolumeOpts; opts != nil {
|
|
|
|
// Volume size.
|
|
if opts.Size != nil {
|
|
if ocean.Compute == nil {
|
|
ocean.Compute = new(aws.Compute)
|
|
}
|
|
if ocean.Compute.LaunchSpecification == nil {
|
|
ocean.Compute.LaunchSpecification = new(aws.LaunchSpecification)
|
|
}
|
|
|
|
ocean.Compute.LaunchSpecification.SetRootVolumeSize(fi.Int(int(*opts.Size)))
|
|
changed = true
|
|
}
|
|
|
|
// EBS optimization.
|
|
if opts.Optimization != nil {
|
|
if ocean.Compute == nil {
|
|
ocean.Compute = new(aws.Compute)
|
|
}
|
|
if ocean.Compute.LaunchSpecification == nil {
|
|
ocean.Compute.LaunchSpecification = new(aws.LaunchSpecification)
|
|
}
|
|
|
|
ocean.Compute.LaunchSpecification.SetEBSOptimized(e.RootVolumeOpts.Optimization)
|
|
changed = true
|
|
}
|
|
|
|
changes.RootVolumeOpts = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Capacity.
|
|
{
|
|
if changes.MinSize != nil {
|
|
if ocean.Capacity == nil {
|
|
ocean.Capacity = new(aws.Capacity)
|
|
}
|
|
|
|
ocean.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 {
|
|
ocean.Capacity.SetTarget(fi.Int(int(*e.MinSize)))
|
|
}
|
|
}
|
|
if changes.MaxSize != nil {
|
|
if ocean.Capacity == nil {
|
|
ocean.Capacity = new(aws.Capacity)
|
|
}
|
|
|
|
ocean.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.AutoScaler)
|
|
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.AutoScalerHeadroom{
|
|
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.AutoScalerDown{
|
|
MaxScaleDownPercentage: down.MaxPercentage,
|
|
EvaluationPeriods: down.EvaluationPeriods,
|
|
}
|
|
} else if a.AutoScalerOpts.Down != nil {
|
|
autoScaler.SetDown(nil)
|
|
}
|
|
|
|
ocean.SetAutoScaler(autoScaler)
|
|
changed = true
|
|
}
|
|
|
|
changes.AutoScalerOpts = nil
|
|
}
|
|
}
|
|
|
|
empty := &Ocean{}
|
|
if !reflect.DeepEqual(empty, changes) {
|
|
klog.Warningf("Not all changes applied to Ocean %q: %v", *ocean.ID, changes)
|
|
}
|
|
|
|
if !changed {
|
|
klog.V(2).Infof("No changes detected in Ocean %q", *ocean.ID)
|
|
return nil
|
|
}
|
|
|
|
klog.V(2).Infof("Updating Ocean %q (config: %s)", *ocean.ID, stringutil.Stringify(ocean))
|
|
|
|
// Wrap the raw object as an Ocean.
|
|
oc, err := spotinst.NewOcean(cloud.ProviderID(), ocean)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update an existing Ocean.
|
|
if err := cloud.Spotinst().Ocean().Update(context.Background(), oc); err != nil {
|
|
return fmt.Errorf("spotinst: failed to update ocean: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type terraformOcean struct {
|
|
Name *string `json:"name,omitempty" cty:"name"`
|
|
ControllerClusterID *string `json:"controller_id,omitempty" cty:"controller_id"`
|
|
Region *string `json:"region,omitempty" cty:"region"`
|
|
InstanceTypesWhitelist []string `json:"whitelist,omitempty" cty:"whitelist"`
|
|
InstanceTypesBlacklist []string `json:"blacklist,omitempty" cty:"blacklist"`
|
|
SubnetIDs []*terraform.Literal `json:"subnet_ids,omitempty" cty:"subnet_ids"`
|
|
AutoScaler *terraformAutoScaler `json:"autoscaler,omitempty" cty:"autoscaler"`
|
|
Tags []*terraformKV `json:"tags,omitempty" cty:"tags"`
|
|
Lifecycle *terraformLifecycle `json:"lifecycle,omitempty" cty:"lifecycle"`
|
|
|
|
*terraformOceanCapacity
|
|
*terraformOceanStrategy
|
|
*terraformOceanLaunchSpec
|
|
}
|
|
|
|
type terraformOceanCapacity 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"`
|
|
}
|
|
|
|
type terraformOceanStrategy struct {
|
|
SpotPercentage *float64 `json:"spot_percentage,omitempty" cty:"spot_percentage"`
|
|
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"`
|
|
GracePeriod *int64 `json:"grace_period,omitempty" cty:"grace_period"`
|
|
}
|
|
|
|
type terraformOceanLaunchSpec struct {
|
|
Monitoring *bool `json:"monitoring,omitempty" cty:"monitoring"`
|
|
EBSOptimized *bool `json:"ebs_optimized,omitempty" cty:"ebs_optimized"`
|
|
ImageID *string `json:"image_id,omitempty" cty:"image_id"`
|
|
AssociatePublicIPAddress *bool `json:"associate_public_ip_address,omitempty" cty:"associate_public_ip_address"`
|
|
RootVolumeSize *int32 `json:"root_volume_size,omitempty" cty:"root_volume_size"`
|
|
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"`
|
|
SubnetIDs []*terraform.Literal `json:"subnet_ids,omitempty" cty:"subnet_ids"`
|
|
SecurityGroups []*terraform.Literal `json:"security_groups,omitempty" cty:"security_groups"`
|
|
Taints []*corev1.Taint `json:"taints,omitempty" cty:"taints"`
|
|
Labels []*terraformKV `json:"labels,omitempty" cty:"labels"`
|
|
Tags []*terraformKV `json:"tags,omitempty" cty:"tags"`
|
|
Headrooms []*terraformAutoScalerHeadroom `json:"autoscale_headrooms,omitempty" cty:"autoscale_headrooms"`
|
|
}
|
|
|
|
func (_ *Ocean) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *Ocean) error {
|
|
cloud := t.Cloud.(awsup.AWSCloud)
|
|
e.applyDefaults()
|
|
|
|
tf := &terraformOcean{
|
|
Name: e.Name,
|
|
Region: fi.String(cloud.Region()),
|
|
terraformOceanCapacity: &terraformOceanCapacity{
|
|
DesiredCapacity: e.MinSize,
|
|
MinSize: e.MinSize,
|
|
MaxSize: e.MaxSize,
|
|
},
|
|
terraformOceanStrategy: &terraformOceanStrategy{
|
|
SpotPercentage: e.SpotPercentage,
|
|
FallbackToOnDemand: e.FallbackToOnDemand,
|
|
UtilizeReservedInstances: e.UtilizeReservedInstances,
|
|
DrainingTimeout: e.DrainingTimeout,
|
|
GracePeriod: e.GracePeriod,
|
|
},
|
|
terraformOceanLaunchSpec: &terraformOceanLaunchSpec{},
|
|
}
|
|
|
|
// 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_ocean_aws", *e.Name, "user_data", e.UserData, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Public IP.
|
|
if e.AssociatePublicIP != nil {
|
|
tf.AssociatePublicIPAddress = e.AssociatePublicIP
|
|
}
|
|
|
|
// Root volume options.
|
|
if opts := e.RootVolumeOpts; opts != nil {
|
|
|
|
// Volume size.
|
|
if opts.Size != nil {
|
|
tf.RootVolumeSize = opts.Size
|
|
}
|
|
|
|
// EBS optimization.
|
|
if opts.Optimization != nil {
|
|
tf.EBSOptimized = opts.Optimization
|
|
}
|
|
}
|
|
|
|
// IAM instance profile.
|
|
if e.IAMInstanceProfile != nil {
|
|
tf.IAMInstanceProfile = e.IAMInstanceProfile.TerraformLink()
|
|
}
|
|
|
|
// Monitoring.
|
|
if e.Monitoring != nil {
|
|
tf.Monitoring = e.Monitoring
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Instance types.
|
|
{
|
|
// Whitelist.
|
|
if e.InstanceTypesWhitelist != nil {
|
|
tf.InstanceTypesWhitelist = e.InstanceTypesWhitelist
|
|
}
|
|
|
|
// Blacklist.
|
|
if e.InstanceTypesBlacklist != nil {
|
|
tf.InstanceTypesBlacklist = e.InstanceTypesBlacklist
|
|
}
|
|
}
|
|
|
|
// Auto Scaler.
|
|
{
|
|
if opts := e.AutoScalerOpts; opts != nil {
|
|
tf.ControllerClusterID = opts.ClusterID
|
|
|
|
if opts.Enabled != nil {
|
|
tf.AutoScaler = &terraformAutoScaler{
|
|
Enabled: opts.Enabled,
|
|
AutoConfig: fi.Bool(true),
|
|
Cooldown: opts.Cooldown,
|
|
}
|
|
|
|
// Headroom.
|
|
if headroom := opts.Headroom; headroom != nil {
|
|
tf.AutoScaler.AutoConfig = fi.Bool(false)
|
|
tf.AutoScaler.Headroom = &terraformAutoScalerHeadroom{
|
|
CPUPerUnit: headroom.CPUPerUnit,
|
|
GPUPerUnit: headroom.GPUPerUnit,
|
|
MemPerUnit: headroom.MemPerUnit,
|
|
NumOfUnits: headroom.NumOfUnits,
|
|
}
|
|
}
|
|
|
|
// Scale down.
|
|
if down := opts.Down; down != nil {
|
|
tf.AutoScaler.Down = &terraformAutoScalerDown{
|
|
MaxPercentage: down.MaxPercentage,
|
|
EvaluationPeriods: down.EvaluationPeriods,
|
|
}
|
|
}
|
|
|
|
// Ignore capacity changes because the auto scaler updates the
|
|
// desired capacity overtime.
|
|
if fi.BoolValue(tf.AutoScaler.Enabled) {
|
|
tf.Lifecycle = &terraformLifecycle{
|
|
IgnoreChanges: []string{
|
|
"desired_capacity",
|
|
},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tags.
|
|
{
|
|
if e.Tags != nil {
|
|
for _, tag := range e.buildTags() {
|
|
tf.Tags = append(tf.Tags, &terraformKV{
|
|
Key: tag.Key,
|
|
Value: tag.Value,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return t.RenderResource("spotinst_ocean_aws", *e.Name, tf)
|
|
}
|
|
|
|
func (o *Ocean) TerraformLink() *terraform.Literal {
|
|
return terraform.LiteralProperty("spotinst_ocean_aws", *o.Name, "id")
|
|
}
|
|
|
|
func (o *Ocean) buildTags() []*aws.Tag {
|
|
tags := make([]*aws.Tag, 0, len(o.Tags))
|
|
|
|
for key, value := range o.Tags {
|
|
tags = append(tags, &aws.Tag{
|
|
Key: fi.String(key),
|
|
Value: fi.String(value),
|
|
})
|
|
}
|
|
|
|
return tags
|
|
}
|
|
|
|
func (o *Ocean) applyDefaults() {
|
|
if o.SpotPercentage == nil {
|
|
f := float64(100.0)
|
|
o.SpotPercentage = &f
|
|
}
|
|
|
|
if o.FallbackToOnDemand == nil {
|
|
o.FallbackToOnDemand = fi.Bool(true)
|
|
}
|
|
|
|
if o.UtilizeReservedInstances == nil {
|
|
o.UtilizeReservedInstances = fi.Bool(true)
|
|
}
|
|
|
|
if o.Monitoring == nil {
|
|
o.Monitoring = fi.Bool(false)
|
|
}
|
|
}
|