/*
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 awsmodel

import (
	"fmt"
	"strconv"
	"strings"

	corev1 "k8s.io/api/core/v1"
	"k8s.io/klog/v2"
	"k8s.io/kops/pkg/apis/kops"
	"k8s.io/kops/pkg/featureflag"
	"k8s.io/kops/pkg/model"
	"k8s.io/kops/pkg/model/defaults"
	"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/spotinsttasks"
)

const (
	// SpotInstanceGroupLabelHybrid is the metadata label used on the instance group
	// to specify that the Spotinst provider should be used to upon creation.
	SpotInstanceGroupLabelHybrid  = "spotinst.io/hybrid"
	SpotInstanceGroupLabelManaged = "spotinst.io/managed" // for backward compatibility

	// SpotInstanceGroupLabelSpotPercentage is the metadata label used on the
	// instance group to specify the percentage of Spot instances that
	// should spin up from the target capacity.
	SpotInstanceGroupLabelSpotPercentage = "spotinst.io/spot-percentage"

	// SpotInstanceGroupLabelOrientation is the metadata label used on the
	// instance group to specify which orientation should be used.
	SpotInstanceGroupLabelOrientation = "spotinst.io/orientation"

	// SpotInstanceGroupLabelUtilizeReservedInstances is the metadata label used
	// on the instance group to specify whether reserved instances should be
	// utilized.
	SpotInstanceGroupLabelUtilizeReservedInstances = "spotinst.io/utilize-reserved-instances"

	// SpotInstanceGroupLabelUtilizeCommitments is the metadata label used
	// on the instance group to specify whether commitments should be utilized.
	SpotInstanceGroupLabelUtilizeCommitments = "spotinst.io/utilize-commitments"

	// SpotInstanceGroupLabelFallbackToOnDemand is the metadata label used on the
	// instance group to specify whether fallback to on-demand instances should
	// be enabled.
	SpotInstanceGroupLabelFallbackToOnDemand = "spotinst.io/fallback-to-ondemand"

	// SpotInstanceGroupLabelDrainingTimeout is the metadata label used on the
	// instance group to specify a period of time, in seconds, after a node
	// is marked for termination during which on running pods remains active.
	SpotInstanceGroupLabelDrainingTimeout = "spotinst.io/draining-timeout"

	// SpotInstanceGroupLabelGracePeriod is the metadata label used on the
	// instance group to specify a period of time, in seconds, that Ocean
	// should wait before applying instance health checks.
	SpotInstanceGroupLabelGracePeriod = "spotinst.io/grace-period"

	// SpotInstanceGroupLabelHealthCheckType is the metadata label used on the
	// instance group to specify the type of the health check that should be used.
	SpotInstanceGroupLabelHealthCheckType = "spotinst.io/health-check-type"

	// SpotInstanceGroupLabelOceanDefaultLaunchSpec is the metadata label used on the
	// instance group to specify whether to use the SpotInstanceGroup's spec as the default
	// Launch Spec for the Ocean cluster.
	SpotInstanceGroupLabelOceanDefaultLaunchSpec = "spotinst.io/ocean-default-launchspec"

	// SpotInstanceGroupLabelOceanInstanceTypes[White|Black]list are the metadata labels
	// used on the instance group to specify whether to whitelist or blacklist
	// specific instance types.
	SpotInstanceGroupLabelOceanInstanceTypesWhitelist = "spotinst.io/ocean-instance-types-whitelist"
	SpotInstanceGroupLabelOceanInstanceTypesBlacklist = "spotinst.io/ocean-instance-types-blacklist"
	SpotInstanceGroupLabelOceanInstanceTypes          = "spotinst.io/ocean-instance-types" // launchspec

	// SpotInstanceGroupLabelAutoScalerDisabled is the metadata label used on the
	// instance group to specify whether the auto scaler should be enabled.
	SpotInstanceGroupLabelAutoScalerDisabled = "spotinst.io/autoscaler-disabled"

	// SpotInstanceGroupLabelAutoScalerDefaultNodeLabels is the metadata label used on the
	// instance group to specify whether default node labels should be set for
	// the auto scaler.
	SpotInstanceGroupLabelAutoScalerDefaultNodeLabels = "spotinst.io/autoscaler-default-node-labels"

	// SpotInstanceGroupLabelAutoScalerAuto* are the metadata labels used on the
	// instance group to specify whether headroom resources should be
	// automatically configured and optimized.
	SpotInstanceGroupLabelAutoScalerAutoConfig             = "spotinst.io/autoscaler-auto-config"
	SpotInstanceGroupLabelAutoScalerAutoHeadroomPercentage = "spotinst.io/autoscaler-auto-headroom-percentage"

	// SpotInstanceGroupLabelAutoScalerHeadroom* are the metadata labels used on the
	// instance group to specify the headroom configuration used by the auto scaler.
	SpotInstanceGroupLabelAutoScalerHeadroomCPUPerUnit = "spotinst.io/autoscaler-headroom-cpu-per-unit"
	SpotInstanceGroupLabelAutoScalerHeadroomGPUPerUnit = "spotinst.io/autoscaler-headroom-gpu-per-unit"
	SpotInstanceGroupLabelAutoScalerHeadroomMemPerUnit = "spotinst.io/autoscaler-headroom-mem-per-unit"
	SpotInstanceGroupLabelAutoScalerHeadroomNumOfUnits = "spotinst.io/autoscaler-headroom-num-of-units"

	// SpotInstanceGroupLabelAutoScalerCooldown is the metadata label used on the
	// instance group to specify the cooldown period (in seconds) for scaling actions.
	SpotInstanceGroupLabelAutoScalerCooldown = "spotinst.io/autoscaler-cooldown"

	// SpotInstanceGroupLabelOtherArchitectureImages  Identifier of other architecture image in AWS.
	//For each architecture type (amd64, arm64) only one AMI is allowed,first image is from  config.InstanceGroup.spec.image
	SpotInstanceGroupLabelOtherArchitectureImages = "spotinst.io/other-architecture-images"

	// SpotInstanceGroupLabelAutoScalerScaleDown* are the metadata labels used on the
	// instance group to specify the scale down configuration used by the auto scaler.
	SpotInstanceGroupLabelAutoScalerScaleDownMaxPercentage     = "spotinst.io/autoscaler-scale-down-max-percentage"
	SpotInstanceGroupLabelAutoScalerScaleDownEvaluationPeriods = "spotinst.io/autoscaler-scale-down-evaluation-periods"

	// SpotInstanceGroupLabelAutoScalerResourceLimits* are the metadata labels used on the
	// instance group to specify the resource limits configuration used by the auto scaler.
	SpotInstanceGroupLabelAutoScalerResourceLimitsMaxVCPU   = "spotinst.io/autoscaler-resource-limits-max-vcpu"
	SpotInstanceGroupLabelAutoScalerResourceLimitsMaxMemory = "spotinst.io/autoscaler-resource-limits-max-memory"

	// InstanceGroupLabelRestrictScaleDown is the metadata label used on the
	// instance group to specify whether the scale-down activities should be restricted.
	SpotInstanceGroupLabelRestrictScaleDown = "spotinst.io/restrict-scale-down"

	// SpotClusterLabelSpreadNodesBy is the cloud  label used on the
	// cluster spec to specify how Ocean will spread the nodes across markets by this value
	SpotClusterLabelSpreadNodesBy = "spotinst.io/strategy-cluster-spread-nodes-by"

	// SpotClusterLabelStrategyClusterOrientationAvailabilityVsCost is the metadata label used on the
	// instance group to specify how to optimize towards  continuity and/or cost-effective infrastructure
	SpotClusterLabelStrategyClusterOrientationAvailabilityVsCost = "spotinst.io/strategy-cluster-orientation-availability-vs-cost"

	// SpotClusterLabelResourceTagSpecificationVolumes
	// Specify if Volume resources will be tagged with Virtual Node Group tags or Ocean tags.
	SpotClusterLabelResourceTagSpecificationVolumes = "spotinst.io/resource-tag-specification-volumes"

	// SpotClusterLabelAutoScalerAggressiveScaleDown
	// configure the aggressive scale down feature, the default is false. cluster.autoScaler.down.aggressiveScaleDown.isEnabled
	SpotClusterLabelAutoScalerAggressiveScaleDown = "spotinst.io/autoscaler-aggressive-scale-down"
)

// SpotInstanceGroupModelBuilder configures SpotInstanceGroup objects
type SpotInstanceGroupModelBuilder struct {
	*AWSModelContext
	BootstrapScriptBuilder *model.BootstrapScriptBuilder
	Lifecycle              fi.Lifecycle
	SecurityLifecycle      fi.Lifecycle
}

var _ fi.CloudupModelBuilder = &SpotInstanceGroupModelBuilder{}

func (b *SpotInstanceGroupModelBuilder) Build(c *fi.CloudupModelBuilderContext) error {
	var nodeSpotInstanceGroups []*kops.InstanceGroup
	var err error

	for _, ig := range b.InstanceGroups {
		name := b.AutoscalingGroupName(ig)

		if featureflag.SpotinstHybrid.Enabled() {
			if !HybridInstanceGroup(ig) {
				klog.V(2).Infof("Skipping instance group: %q", name)
				continue
			}
		}

		klog.V(2).Infof("Building instance group: %q", name)
		switch ig.Spec.Role {

		// Create both Master and Bastion instance groups as Elastigroups.
		case kops.InstanceGroupRoleControlPlane, kops.InstanceGroupRoleBastion:
			err = b.buildElastigroup(c, ig)

		// Create Node instance groups as Elastigroups or a single Ocean with
		// multiple LaunchSpecs.
		case kops.InstanceGroupRoleNode:
			if featureflag.SpotinstOcean.Enabled() {
				nodeSpotInstanceGroups = append(nodeSpotInstanceGroups, ig)
			} else {
				err = b.buildElastigroup(c, ig)
			}

		default:
			err = fmt.Errorf("spotinst: unexpected instance group role: %s", ig.Spec.Role)
		}

		if err != nil {
			return fmt.Errorf("spotinst: error building elastigroup: %v", err)
		}
	}

	if len(nodeSpotInstanceGroups) > 0 {
		if err = b.buildOcean(c, nodeSpotInstanceGroups...); err != nil {
			return fmt.Errorf("spotinst: error building ocean: %v", err)
		}
	}

	return nil
}

func (b *SpotInstanceGroupModelBuilder) buildElastigroup(c *fi.CloudupModelBuilderContext, ig *kops.InstanceGroup) (err error) {
	klog.V(4).Infof("Building instance group as Elastigroup: %q", b.AutoscalingGroupName(ig))
	group := &spotinsttasks.Elastigroup{
		Lifecycle:            b.Lifecycle,
		Name:                 new(b.AutoscalingGroupName(ig)),
		Region:               new(b.Region),
		ImageID:              new(ig.Spec.Image),
		OnDemandInstanceType: new(strings.Split(ig.Spec.MachineType, ",")[0]),
		SpotInstanceTypes:    strings.Split(ig.Spec.MachineType, ","),
	}

	// Cloud config.
	if aws := b.Cluster.Spec.CloudProvider.AWS; aws != nil {
		group.Product = aws.SpotinstProduct
		group.Orientation = aws.SpotinstOrientation
		nth := aws.NodeTerminationHandler
		if nth != nil && nth.Enabled != nil && *nth.Enabled {
			return fmt.Errorf("can't build elastigroup while nodeTerminationHandler flag is on. " +
				"using nodeTerminationHandler will interfere with Ocean Kubernetes controller .\n" +
				"Please add the following configuration to cluster config \n nodeTerminationHandler:\n   enabled: false ")
		}
	}

	// Strategy.
	for k, v := range ig.ObjectMeta.Labels {
		switch k {
		case SpotInstanceGroupLabelSpotPercentage:
			group.SpotPercentage, err = parseFloat(v)
			if err != nil {
				return err
			}

		case SpotInstanceGroupLabelOrientation:
			group.Orientation = new(v)

		case SpotInstanceGroupLabelUtilizeReservedInstances:
			group.UtilizeReservedInstances, err = parseBool(v)
			if err != nil {
				return err
			}

		case SpotInstanceGroupLabelUtilizeCommitments:
			group.UtilizeCommitments, err = parseBool(v)
			if err != nil {
				return err
			}

		case SpotInstanceGroupLabelFallbackToOnDemand:
			group.FallbackToOnDemand, err = parseBool(v)
			if err != nil {
				return err
			}

		case SpotInstanceGroupLabelDrainingTimeout:
			group.DrainingTimeout, err = parseInt(v)
			if err != nil {
				return err
			}

		case SpotInstanceGroupLabelHealthCheckType:
			group.HealthCheckType = new(strings.ToUpper(v))
		}
	}

	// Spot percentage.
	if group.SpotPercentage == nil {
		group.SpotPercentage = defaultSpotPercentage(ig)
	}

	// Instance profile.
	group.IAMInstanceProfile, err = b.LinkToIAMInstanceProfile(ig)
	if err != nil {
		return fmt.Errorf("error building iam instance profile: %v", err)
	}

	// Root volume.
	group.RootVolumeOpts, err = b.buildRootVolumeOpts(ig)
	if err != nil {
		return fmt.Errorf("error building root volume options: %v", err)
	}

	// Tenancy.
	if ig.Spec.Tenancy != "" {
		group.Tenancy = new(ig.Spec.Tenancy)
	}

	// Security groups.
	group.SecurityGroups, err = b.buildSecurityGroups(c, ig)
	if err != nil {
		return fmt.Errorf("error building security groups: %v", err)
	}

	// SSH key.
	group.SSHKey, err = b.LinkToSSHKey()
	if err != nil {
		return fmt.Errorf("error building ssh key: %v", err)
	}

	// Load balancers.
	group.LoadBalancers, group.TargetGroups, err = b.buildLoadBalancers(c, ig)
	if err != nil {
		return fmt.Errorf("error building load balancers: %v", err)
	}

	// User data.
	group.UserData, err = b.BootstrapScriptBuilder.ResourceNodeUp(c, ig)
	if err != nil {
		return fmt.Errorf("error building user data: %v", err)
	}

	// Public IP.
	group.AssociatePublicIPAddress, err = b.buildPublicIPOpts(ig)
	if err != nil {
		return fmt.Errorf("error building public ip options: %v", err)
	}

	// Subnets.
	group.Subnets, err = b.buildSubnets(ig)
	if err != nil {
		return fmt.Errorf("error building subnets: %v", err)
	}

	// Capacity.
	group.MinSize, group.MaxSize = b.buildCapacity(ig)

	// Monitoring.
	group.Monitoring = ig.Spec.DetailedInstanceMonitoring

	// Tags.
	group.Tags, err = b.buildTags(ig)
	if err != nil {
		return fmt.Errorf("error building cloud tags: %v", err)
	}

	// Auto Scaler.
	group.AutoScalerOpts, err = b.buildAutoScalerOpts(b.ClusterName(), ig)
	if err != nil {
		return fmt.Errorf("error building auto scaler options: %v", err)
	}
	if group.AutoScalerOpts != nil { // remove unsupported options
		group.AutoScalerOpts.Taints = nil
	}

	// Instance Metadata Options
	group.InstanceMetadataOptions = b.buildInstanceMetadataOptions(ig)

	klog.V(4).Infof("Adding task: Elastigroup/%s", fi.ValueOf(group.Name))
	c.AddTask(group)

	return nil
}

func (b *SpotInstanceGroupModelBuilder) buildOcean(c *fi.CloudupModelBuilderContext, igs ...*kops.InstanceGroup) (err error) {
	klog.V(4).Infof("Building instance group as Ocean: %q", "nodes."+b.ClusterName())
	ocean := &spotinsttasks.Ocean{
		Lifecycle: b.Lifecycle,
		Name:      new("nodes." + b.ClusterName()),
	}

	if featureflag.SpotinstOceanTemplate.Enabled() {
		ocean.UseAsTemplateOnly = new(true)
	}

	if len(igs) == 0 {
		return nil
	}
	var ig *kops.InstanceGroup
	for _, g := range igs {
		for k, v := range g.ObjectMeta.Labels {
			if k == SpotInstanceGroupLabelOceanDefaultLaunchSpec {
				defaultLaunchSpec, err := parseBool(v)
				if err != nil {
					continue
				}
				if fi.ValueOf(defaultLaunchSpec) {
					if ig != nil {
						return fmt.Errorf("unable to detect default launch spec: "+
							"multiple instance groups labeled with `%s: \"true\"`",
							SpotInstanceGroupLabelOceanDefaultLaunchSpec)
					}
					ig = g.DeepCopy()
					break
				}
			}
		}
	}
	if ig == nil {
		ig = igs[0].DeepCopy()
	}

	klog.V(4).Infof("Detected default launch spec: %q", b.AutoscalingGroupName(ig))

	for k, v := range b.Cluster.Labels {
		switch k {
		case SpotClusterLabelSpreadNodesBy:
			ocean.SpreadNodesBy = new(v)
		case SpotClusterLabelStrategyClusterOrientationAvailabilityVsCost:
			ocean.AvailabilityVsCost = new(string(spotinsttasks.NormalizeClusterOrientation(&v)))
		case SpotClusterLabelResourceTagSpecificationVolumes:
			ocean.ResourceTagSpecificationVolumes, err = parseBool(v)
			if err != nil {
				return err
			}
		case SpotClusterLabelAutoScalerAggressiveScaleDown:
			ocean.AutoScalerAggressiveScaleDown, err = parseBool(v)
			if err != nil {
				return err
			}
		}
	}

	// Image.
	ocean.ImageID = new(ig.Spec.Image)

	// Strategy and instance types.
	for k, v := range ig.ObjectMeta.Labels {
		switch k {
		case SpotInstanceGroupLabelUtilizeReservedInstances:
			ocean.UtilizeReservedInstances, err = parseBool(v)
			if err != nil {
				return err
			}

		case SpotInstanceGroupLabelUtilizeCommitments:
			ocean.UtilizeCommitments, err = parseBool(v)
			if err != nil {
				return err
			}

		case SpotInstanceGroupLabelFallbackToOnDemand:
			ocean.FallbackToOnDemand, err = parseBool(v)
			if err != nil {
				return err
			}

		case SpotInstanceGroupLabelGracePeriod:
			ocean.GracePeriod, err = parseInt(v)
			if err != nil {
				return err
			}

		case SpotInstanceGroupLabelDrainingTimeout:
			ocean.DrainingTimeout, err = parseInt(v)
			if err != nil {
				return err
			}

		case SpotInstanceGroupLabelOceanInstanceTypesWhitelist:
			ocean.InstanceTypesWhitelist, err = parseStringSlice(v)
			if err != nil {
				return err
			}

		case SpotInstanceGroupLabelOceanInstanceTypesBlacklist:
			ocean.InstanceTypesBlacklist, err = parseStringSlice(v)
			if err != nil {
				return err
			}
		}
	}

	// Monitoring.
	ocean.Monitoring = ig.Spec.DetailedInstanceMonitoring

	// Security groups.
	ocean.SecurityGroups, err = b.buildSecurityGroups(c, ig)
	if err != nil {
		return fmt.Errorf("error building security groups: %v", err)
	}

	// SSH key.
	ocean.SSHKey, err = b.LinkToSSHKey()
	if err != nil {
		return fmt.Errorf("error building ssh key: %v", err)
	}

	// Subnets.
	ocean.Subnets, err = b.buildSubnets(ig)
	if err != nil {
		return fmt.Errorf("error building subnets: %v", err)
	}

	// Auto Scaler.
	ocean.AutoScalerOpts, err = b.buildAutoScalerOpts(b.ClusterName(), ig)
	if err != nil {
		return fmt.Errorf("error building auto scaler options: %v", err)
	}
	if ocean.AutoScalerOpts != nil { // remove unsupported options
		ocean.AutoScalerOpts.Labels = nil
		ocean.AutoScalerOpts.Taints = nil
		ocean.AutoScalerOpts.Headroom = nil
	}

	// Instance Metadata Options
	ocean.InstanceMetadataOptions = b.buildInstanceMetadataOptions(ig)

	if !fi.ValueOf(ocean.UseAsTemplateOnly) {
		// Capacity.
		ocean.MinSize = new(int64(0))
		ocean.MaxSize = new(int64(0))

		// User data.
		ocean.UserData, err = b.BootstrapScriptBuilder.ResourceNodeUp(c, ig)
		if err != nil {
			return fmt.Errorf("error building user data: %v", err)
		}

		// Instance profile.
		ocean.IAMInstanceProfile, err = b.LinkToIAMInstanceProfile(ig)
		if err != nil {
			return fmt.Errorf("error building iam instance profile: %v", err)
		}

		// Root volume.
		rootVolumeOpts, err := b.buildRootVolumeOpts(ig)
		if err != nil {
			return fmt.Errorf("error building root volume options: %v", err)
		}
		if rootVolumeOpts != nil {
			ocean.RootVolumeOpts = rootVolumeOpts
			ocean.RootVolumeOpts.Type = nil // not supported in Ocean
		}

		// Public IP.
		ocean.AssociatePublicIPAddress, err = b.buildPublicIPOpts(ig)
		if err != nil {
			return fmt.Errorf("error building public ip options: %v", err)
		}

		// Tags.
		ocean.Tags, err = b.buildTags(ig)
		if err != nil {
			return fmt.Errorf("error building cloud tags: %v", err)
		}
	}

	// Create a Launch Spec for each instance group.
	for _, g := range igs {
		if err := b.buildLaunchSpec(c, g, ig, ocean); err != nil {
			return fmt.Errorf("error building launch spec: %v", err)
		}
	}

	klog.V(4).Infof("Adding task: Ocean/%s", fi.ValueOf(ocean.Name))
	c.AddTask(ocean)
	klog.V(4).Infof("Finish task: Ocean/%s", fi.ValueOf(ocean.Name))
	return nil
}

func (b *SpotInstanceGroupModelBuilder) buildLaunchSpec(c *fi.CloudupModelBuilderContext,
	ig, igOcean *kops.InstanceGroup, ocean *spotinsttasks.Ocean) (err error) {
	klog.V(4).Infof("Building instance group as LaunchSpec: %q", b.AutoscalingGroupName(ig))
	launchSpec := &spotinsttasks.LaunchSpec{
		Name:      new(b.AutoscalingGroupName(ig)),
		Lifecycle: b.Lifecycle,
		ImageID:   new(ig.Spec.Image),
		Ocean:     ocean, // link to Ocean
	}

	// Instance types and strategy.
	for k, v := range ig.ObjectMeta.Labels {
		switch k {
		case SpotInstanceGroupLabelOceanInstanceTypesWhitelist, SpotInstanceGroupLabelOceanInstanceTypes:
			launchSpec.InstanceTypes, err = parseStringSlice(v)
			if err != nil {
				return err
			}

		case SpotInstanceGroupLabelSpotPercentage:
			launchSpec.SpotPercentage, err = parseInt(v)
			if err != nil {
				return err
			}

		case SpotInstanceGroupLabelRestrictScaleDown:
			launchSpec.RestrictScaleDown, err = parseBool(v)
			if err != nil {
				return err
			}

		case SpotInstanceGroupLabelOtherArchitectureImages:
			launchSpec.OtherArchitectureImages, err = parseStringSlice(v)
			if err != nil {
				return err
			}
		}
	}

	policy := ig.Spec.MixedInstancesPolicy
	if len(launchSpec.InstanceTypes) == 0 && policy != nil && len(policy.Instances) > 0 {
		launchSpec.InstanceTypes = policy.Instances
	}

	// Capacity.
	minSize, maxSize := b.buildCapacity(ig)
	if !fi.ValueOf(ocean.UseAsTemplateOnly) {
		ocean.MinSize = new(fi.ValueOf(ocean.MinSize) + fi.ValueOf(minSize))
		ocean.MaxSize = new(fi.ValueOf(ocean.MaxSize) + fi.ValueOf(maxSize))
	}

	launchSpec.MinSize = minSize
	launchSpec.MaxSize = maxSize

	// User data.
	if ig.Name == igOcean.Name && !featureflag.SpotinstOceanTemplate.Enabled() {
		launchSpec.UserData = ocean.UserData
	} else {
		launchSpec.UserData, err = b.BootstrapScriptBuilder.ResourceNodeUp(c, ig)
		if err != nil {
			return fmt.Errorf("error building user data: %v", err)
		}
	}

	// Instance profile.
	launchSpec.IAMInstanceProfile, err = b.LinkToIAMInstanceProfile(ig)
	if err != nil {
		return fmt.Errorf("error building iam instance profile: %v", err)
	}

	// Root volume.
	rootVolumeOpts, err := b.buildRootVolumeOpts(ig)
	if err != nil {
		return fmt.Errorf("error building root volume options: %v", err)
	}
	if rootVolumeOpts != nil { // remove unsupported options
		launchSpec.RootVolumeOpts = rootVolumeOpts
		launchSpec.RootVolumeOpts.Optimization = nil
	}

	// Public IP.
	launchSpec.AssociatePublicIPAddress, err = b.buildPublicIPOpts(ig)
	if err != nil {
		return fmt.Errorf("error building public ip options: %v", err)
	}

	// Security groups.
	launchSpec.SecurityGroups, err = b.buildSecurityGroups(c, ig)
	if err != nil {
		return fmt.Errorf("error building security groups: %v", err)
	}

	// Subnets.
	launchSpec.Subnets, err = b.buildSubnets(ig)
	if err != nil {
		return fmt.Errorf("error building subnets: %v", err)
	}

	// Tags.
	launchSpec.Tags, err = b.buildTags(ig)
	if err != nil {
		return fmt.Errorf("error building cloud tags: %v", err)
	}

	// Auto Scaler.
	autoScalerOpts, err := b.buildAutoScalerOpts(b.ClusterName(), ig)
	if err != nil {
		return fmt.Errorf("error building auto scaler options: %v", err)
	}
	if autoScalerOpts != nil { // remove unsupported options
		autoScalerOpts.Enabled = nil
		autoScalerOpts.AutoConfig = nil
		autoScalerOpts.AutoHeadroomPercentage = nil
		autoScalerOpts.ClusterID = nil
		autoScalerOpts.Cooldown = nil
		autoScalerOpts.Down = nil

		if autoScalerOpts.Labels != nil || autoScalerOpts.Taints != nil || autoScalerOpts.Headroom != nil {
			launchSpec.AutoScalerOpts = autoScalerOpts
		}
	}

	//  Instance Metadata Options
	launchSpec.InstanceMetadataOptions = b.buildInstanceMetadataOptions(ig)

	klog.V(4).Infof("Adding task: LaunchSpec/%s", fi.ValueOf(launchSpec.Name))
	c.AddTask(launchSpec)

	return nil
}

func (b *SpotInstanceGroupModelBuilder) buildSecurityGroups(c *fi.CloudupModelBuilderContext,
	ig *kops.InstanceGroup) ([]*awstasks.SecurityGroup, error) {
	securityGroups := []*awstasks.SecurityGroup{
		b.LinkToSecurityGroup(ig.Spec.Role),
	}

	for _, id := range ig.Spec.AdditionalSecurityGroups {
		sg := &awstasks.SecurityGroup{
			Lifecycle: b.SecurityLifecycle,
			ID:        new(id),
			Name:      new(id),
			Shared:    new(true),
		}
		c.EnsureTask(sg)
		securityGroups = append(securityGroups, sg)
	}

	return securityGroups, nil
}

func (b *SpotInstanceGroupModelBuilder) buildSubnets(ig *kops.InstanceGroup) ([]*awstasks.Subnet, error) {
	subnets, err := b.GatherSubnets(ig)
	if err != nil {
		return nil, err
	}
	if len(subnets) == 0 {
		return nil, fmt.Errorf("could not determine any subnets for SpotInstanceGroup %q; subnets was %s", ig.ObjectMeta.Name, ig.Spec.Subnets)
	}

	out := make([]*awstasks.Subnet, len(subnets))
	for i, subnet := range subnets {
		out[i] = b.LinkToSubnet(subnet)
	}

	return out, nil
}

func (b *SpotInstanceGroupModelBuilder) buildPublicIPOpts(ig *kops.InstanceGroup) (*bool, error) {
	subnetMap := make(map[string]*kops.ClusterSubnetSpec)
	for i := range b.Cluster.Spec.Networking.Subnets {
		subnet := &b.Cluster.Spec.Networking.Subnets[i]
		subnetMap[subnet.Name] = subnet
	}

	var subnetType kops.SubnetType
	for _, subnetName := range ig.Spec.Subnets {
		subnet := subnetMap[subnetName]
		if subnet == nil {
			return nil, fmt.Errorf("SpotInstanceGroup %q uses subnet %q that does not exist", ig.ObjectMeta.Name, subnetName)
		}
		if subnetType != "" && subnetType != subnet.Type {
			return nil, fmt.Errorf("SpotInstanceGroup %q cannot be in subnets of different Type", ig.ObjectMeta.Name)
		}
		subnetType = subnet.Type
	}

	var associatePublicIP bool
	switch subnetType {
	case kops.SubnetTypePublic, kops.SubnetTypeUtility:
		associatePublicIP = true
		if ig.Spec.AssociatePublicIP != nil {
			associatePublicIP = *ig.Spec.AssociatePublicIP
		}
	case kops.SubnetTypeDualStack, kops.SubnetTypePrivate:
		associatePublicIP = false
		if ig.Spec.AssociatePublicIP != nil {
			if *ig.Spec.AssociatePublicIP {
				klog.Warningf("Ignoring AssociatePublicIPAddress=true for private SpotInstanceGroup %q", ig.ObjectMeta.Name)
			}
		}
	default:
		return nil, fmt.Errorf("unknown subnet type %q", subnetType)
	}

	return new(associatePublicIP), nil
}

func (b *SpotInstanceGroupModelBuilder) buildRootVolumeOpts(ig *kops.InstanceGroup) (*spotinsttasks.RootVolumeOpts, error) {
	opts := new(spotinsttasks.RootVolumeOpts)

	var size int32
	var typ string
	var iops int32
	var throughput int32
	if ig.Spec.RootVolume != nil {
		// Optimization.
		{
			if fi.ValueOf(ig.Spec.RootVolume.Optimization) {
				opts.Optimization = ig.Spec.RootVolume.Optimization
			}
		}

		// Encryption.
		{
			if fi.ValueOf(ig.Spec.RootVolume.Encryption) {
				opts.Encryption = ig.Spec.RootVolume.Encryption
			}
		}

		size = fi.ValueOf(ig.Spec.RootVolume.Size)
		typ = fi.ValueOf(ig.Spec.RootVolume.Type)
		iops = fi.ValueOf(ig.Spec.RootVolume.IOPS)
		throughput = fi.ValueOf(ig.Spec.RootVolume.Throughput)
	}

	if size == 0 {
		var err error
		size, err = defaults.DefaultInstanceGroupVolumeSize(ig.Spec.Role)
		if err != nil {
			return nil, err
		}
	}
	opts.Size = new(int64(size))

	if typ == "" {
		typ = "gp2"
	}
	opts.Type = new(typ)

	if iops > 0 {
		opts.IOPS = new(int64(iops))
	}

	if throughput > 0 {
		opts.Throughput = new(int64(throughput))
	}

	return opts, nil
}

func (b *SpotInstanceGroupModelBuilder) buildCapacity(ig *kops.InstanceGroup) (*int64, *int64) {
	minSize := int32(1)
	if ig.Spec.MinSize != nil {
		minSize = fi.ValueOf(ig.Spec.MinSize)
	} else if ig.Spec.Role.HasNode() {
		minSize = 2
	}

	maxSize := int32(1)
	if ig.Spec.MaxSize != nil {
		maxSize = *ig.Spec.MaxSize
	} else if ig.Spec.Role.HasNode() {
		maxSize = 2
	}

	return new(int64(minSize)), new(int64(maxSize))
}

func (b *SpotInstanceGroupModelBuilder) buildLoadBalancers(c *fi.CloudupModelBuilderContext,
	ig *kops.InstanceGroup) ([]*awstasks.ClassicLoadBalancer, []*awstasks.TargetGroup, error) {
	var loadBalancers []*awstasks.ClassicLoadBalancer
	var targetGroups []*awstasks.TargetGroup

	if b.UseLoadBalancerForAPI() && ig.RunsAPIServer() {
		targetGroups = append(targetGroups, b.LinkToTargetGroup("tcp"))
		if b.Cluster.Spec.API.LoadBalancer.SSLCertificate != "" {
			targetGroups = append(targetGroups, b.LinkToTargetGroup("tls"))
		}
	}

	if ig.Spec.Role.HasBastion() {
		targetGroups = append(targetGroups, b.LinkToTargetGroup("bastion"))
	}

	for _, extLB := range ig.Spec.ExternalLoadBalancers {
		if extLB.LoadBalancerName != nil {
			lb := &awstasks.ClassicLoadBalancer{
				Name:             extLB.LoadBalancerName,
				LoadBalancerName: extLB.LoadBalancerName,
				Shared:           new(true),
			}
			loadBalancers = append(loadBalancers, lb)
			c.EnsureTask(lb)
		}
		if extLB.TargetGroupARN != nil {
			targetGroupName, err := awsup.NameForExternalTargetGroup(fi.ValueOf(extLB.TargetGroupARN))
			if err != nil {
				return nil, nil, err
			}
			tg := &awstasks.TargetGroup{
				Name:   new(ig.Name + "-" + targetGroupName),
				ARN:    extLB.TargetGroupARN,
				Shared: new(true),
			}
			targetGroups = append(targetGroups, tg)
			c.AddTask(tg)
		}
	}

	return loadBalancers, targetGroups, nil
}

func (b *SpotInstanceGroupModelBuilder) buildTags(ig *kops.InstanceGroup) (map[string]string, error) {
	tags, err := b.CloudTagsForInstanceGroup(ig)
	if err != nil {
		return nil, err
	}
	return tags, nil
}

func (b *SpotInstanceGroupModelBuilder) buildAutoScalerOpts(clusterID string, ig *kops.InstanceGroup) (*spotinsttasks.AutoScalerOpts, error) {
	opts := &spotinsttasks.AutoScalerOpts{
		ClusterID: new(clusterID),
	}

	switch ig.Spec.Role {
	case kops.InstanceGroupRoleControlPlane:
		return opts, nil

	case kops.InstanceGroupRoleBastion:
		return nil, nil
	}

	// Enable the auto scaler for Node instance groups.
	opts.Enabled = new(true)
	opts.AutoConfig = new(true)

	// Parse instance group labels.
	var defaultNodeLabels bool
	for k, v := range ig.ObjectMeta.Labels {
		switch k {
		case SpotInstanceGroupLabelAutoScalerDisabled:
			{
				v, err := parseBool(v)
				if err != nil {
					return nil, err
				}
				opts.Enabled = new(!fi.ValueOf(v))
			}

		case SpotInstanceGroupLabelAutoScalerDefaultNodeLabels:
			{
				v, err := parseBool(v)
				if err != nil {
					return nil, err
				}
				defaultNodeLabels = fi.ValueOf(v)
			}

		case SpotInstanceGroupLabelAutoScalerCooldown:
			{
				v, err := parseInt(v)
				if err != nil {
					return nil, err
				}
				opts.Cooldown = new(int(fi.ValueOf(v)))
			}

		case SpotInstanceGroupLabelAutoScalerAutoConfig:
			{
				v, err := parseBool(v)
				if err != nil {
					return nil, err
				}
				opts.AutoConfig = v
			}

		case SpotInstanceGroupLabelAutoScalerAutoHeadroomPercentage:
			{
				v, err := parseInt(v)
				if err != nil {
					return nil, err
				}
				opts.AutoHeadroomPercentage = new(int(fi.ValueOf(v)))
			}

		case SpotInstanceGroupLabelAutoScalerHeadroomCPUPerUnit:
			{
				v, err := parseInt(v)
				if err != nil {
					return nil, err
				}
				if opts.Headroom == nil {
					opts.Headroom = new(spotinsttasks.AutoScalerHeadroomOpts)
				}
				opts.Headroom.CPUPerUnit = new(int(fi.ValueOf(v)))
			}

		case SpotInstanceGroupLabelAutoScalerHeadroomGPUPerUnit:
			{
				v, err := parseInt(v)
				if err != nil {
					return nil, err
				}
				if opts.Headroom == nil {
					opts.Headroom = new(spotinsttasks.AutoScalerHeadroomOpts)
				}
				opts.Headroom.GPUPerUnit = new(int(fi.ValueOf(v)))
			}

		case SpotInstanceGroupLabelAutoScalerHeadroomMemPerUnit:
			{
				v, err := parseInt(v)
				if err != nil {
					return nil, err
				}
				if opts.Headroom == nil {
					opts.Headroom = new(spotinsttasks.AutoScalerHeadroomOpts)
				}
				opts.Headroom.MemPerUnit = new(int(fi.ValueOf(v)))
			}

		case SpotInstanceGroupLabelAutoScalerHeadroomNumOfUnits:
			{
				v, err := parseInt(v)
				if err != nil {
					return nil, err
				}
				if opts.Headroom == nil {
					opts.Headroom = new(spotinsttasks.AutoScalerHeadroomOpts)
				}
				opts.Headroom.NumOfUnits = new(int(fi.ValueOf(v)))
			}

		case SpotInstanceGroupLabelAutoScalerScaleDownMaxPercentage:
			{
				v, err := parseFloat(v)
				if err != nil {
					return nil, err
				}
				if opts.Down == nil {
					opts.Down = new(spotinsttasks.AutoScalerDownOpts)
				}
				opts.Down.MaxPercentage = v
			}

		case SpotInstanceGroupLabelAutoScalerScaleDownEvaluationPeriods:
			{
				v, err := parseInt(v)
				if err != nil {
					return nil, err
				}
				if opts.Down == nil {
					opts.Down = new(spotinsttasks.AutoScalerDownOpts)
				}
				opts.Down.EvaluationPeriods = new(int(fi.ValueOf(v)))
			}

		case SpotInstanceGroupLabelAutoScalerResourceLimitsMaxVCPU:
			{
				v, err := parseInt(v)
				if err != nil {
					return nil, err
				}
				if opts.ResourceLimits == nil {
					opts.ResourceLimits = new(spotinsttasks.AutoScalerResourceLimitsOpts)
				}
				opts.ResourceLimits.MaxVCPU = new(int(fi.ValueOf(v)))
			}

		case SpotInstanceGroupLabelAutoScalerResourceLimitsMaxMemory:
			{
				v, err := parseInt(v)
				if err != nil {
					return nil, err
				}
				if opts.ResourceLimits == nil {
					opts.ResourceLimits = new(spotinsttasks.AutoScalerResourceLimitsOpts)
				}
				opts.ResourceLimits.MaxMemory = new(int(fi.ValueOf(v)))
			}
		}
	}

	// Configure Elastigroup defaults to avoid state drifts.
	if !featureflag.SpotinstOcean.Enabled() {
		if opts.Cooldown == nil {
			opts.Cooldown = new(300)
		}
		if opts.Down != nil && opts.Down.EvaluationPeriods == nil {
			opts.Down.EvaluationPeriods = new(5)
		}
	}

	// Configure node labels.
	labels := make(map[string]string)
	for k, v := range ig.Spec.NodeLabels {
		if strings.HasPrefix(k, kops.NodeLabelInstanceGroup) && !defaultNodeLabels {
			continue
		}
		labels[k] = v
	}
	if len(labels) > 0 {
		opts.Labels = labels
	}

	// Configure node taints.
	taints, err := parseTaints(ig.Spec.Taints)
	if err != nil {
		return nil, err
	}
	if len(taints) > 0 {
		opts.Taints = taints
	}

	return opts, nil
}

func (b *SpotInstanceGroupModelBuilder) buildInstanceMetadataOptions(ig *kops.InstanceGroup) *spotinsttasks.InstanceMetadataOptions {
	if ig.Spec.InstanceMetadata != nil {
		opt := new(spotinsttasks.InstanceMetadataOptions)
		opt.HTTPPutResponseHopLimit = new(fi.ValueOf(ig.Spec.InstanceMetadata.HTTPPutResponseHopLimit))
		opt.HTTPTokens = new(fi.ValueOf(ig.Spec.InstanceMetadata.HTTPTokens))
		return opt
	}
	return nil
}

func parseBool(str string) (*bool, error) {
	v, err := strconv.ParseBool(str)
	if err != nil {
		return nil, fmt.Errorf("unexpected boolean value: %q", str)
	}
	return &v, nil
}

func parseFloat(str string) (*float64, error) {
	v, err := strconv.ParseFloat(str, 64)
	if err != nil {
		return nil, fmt.Errorf("unexpected float value: %q", str)
	}
	return &v, nil
}

func parseInt(str string) (*int64, error) {
	v, err := strconv.ParseInt(str, 10, 64)
	if err != nil {
		return nil, fmt.Errorf("unexpected integer value: %q", str)
	}
	return &v, nil
}

func parseTaints(taintSpecs []string) ([]*corev1.Taint, error) {
	var taints []*corev1.Taint

	for _, taintSpec := range taintSpecs {
		taint, err := parseTaint(taintSpec)
		if err != nil {
			return nil, err
		}
		taints = append(taints, taint)
	}

	return taints, nil
}

func parseTaint(taintSpec string) (*corev1.Taint, error) {
	var taint corev1.Taint

	parts := strings.Split(taintSpec, ":")
	switch len(parts) {
	case 1:
		taint.Key = parts[0]
	case 2:
		taint.Effect = corev1.TaintEffect(parts[1])
		partsKV := strings.Split(parts[0], "=")
		if len(partsKV) > 2 {
			return nil, fmt.Errorf("invalid taint spec: %v", taintSpec)
		}
		taint.Key = partsKV[0]
		if len(partsKV) == 2 {
			taint.Value = partsKV[1]
		}
	default:
		return nil, fmt.Errorf("invalid taint spec: %v", taintSpec)
	}

	return &taint, nil
}

func parseStringSlice(str string) ([]string, error) {
	v := strings.Split(str, ",")
	for i, s := range v {
		v[i] = strings.TrimSpace(s)
	}
	return v, nil
}

func defaultSpotPercentage(ig *kops.InstanceGroup) *float64 {
	var percentage float64

	switch ig.Spec.Role {
	case kops.InstanceGroupRoleControlPlane, kops.InstanceGroupRoleBastion:
		percentage = 0
	case kops.InstanceGroupRoleNode:
		percentage = 100
	}

	return &percentage
}

// HybridInstanceGroup indicates whether the instance group labeled with
// a metadata label `spotinst.io/hybrid` which means the Spotinst provider
// should be used to upon creation if the `SpotinstHybrid` feature flag is on.
func HybridInstanceGroup(ig *kops.InstanceGroup) bool {
	v, ok := ig.ObjectMeta.Labels[SpotInstanceGroupLabelHybrid]
	if !ok {
		v = ig.ObjectMeta.Labels[SpotInstanceGroupLabelManaged]
	}

	hybrid, _ := strconv.ParseBool(v)
	return hybrid
}
