<commit_before>/*
Copyright 2016 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package awstasks

import (
	"encoding/base64"
	"errors"
	"fmt"
	"math"
	"sort"
	"strings"
	"time"

	"k8s.io/kops/pkg/featureflag"
	"k8s.io/kops/upup/pkg/fi"
	"k8s.io/kops/upup/pkg/fi/cloudup/awsup"
	"k8s.io/kops/upup/pkg/fi/cloudup/cloudformation"
	"k8s.io/kops/upup/pkg/fi/cloudup/terraform"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/autoscaling"
	"github.com/golang/glog"
	"k8s.io/apimachinery/pkg/util/sets"
)

// defaultRetainLaunchConfigurationCount is the number of launch configurations (matching the name prefix) that we should
// keep, we delete older ones
var defaultRetainLaunchConfigurationCount = 3

// RetainLaunchConfigurationCount returns the number of launch configurations to keep
func RetainLaunchConfigurationCount() int {
	if featureflag.KeepLaunchConfigurations.Enabled() {
		return math.MaxInt32
	}
	return defaultRetainLaunchConfigurationCount
}

// LaunchConfiguration is the specification of a launch configuration
type LaunchConfiguration struct {
	// Name is the name of the resource
	Name *string
	// Lifecycle is the lifecycle of the resource
	Lifecycle *fi.Lifecycle

	// AssociatePublicIP indicates if a public ip should be associated
	AssociatePublicIP *bool
	// BlockDeviceMappings is a block device mappings
	BlockDeviceMappings []*BlockDeviceMapping
	// IAMInstanceProfile is the instance profile we should use
	IAMInstanceProfile *IAMInstanceProfile
	// ID is the aws id for the resource
	ID *string
	// ImageID is the AMI id to use
	ImageID *string
	// InstanceMonitoring indicates is monitoring should be enabled
	InstanceMonitoring *bool
	// InstanceType is the aws instance type to use
	InstanceType *string
	// RootVolumeIops, if volume type is io1, then we need to specify the number of Iops.
	RootVolumeIops *int64
	// RootVolumeOptimization enables EBS optimization for an instance
	RootVolumeOptimization *bool
	// RootVolumeSize is the size of the EBS root volume to use, in GB
	RootVolumeSize *int64
	// RootVolumeType is the type of the EBS root volume to use (e.g. gp2)
	RootVolumeType *string
	// SSHKey is the key to use for the instance
	SSHKey *SSHKey
	// SecurityGroups is a collection of security groups
	SecurityGroups []*SecurityGroup
	// SpotPrice is set to the spot-price bid if this is a spot pricing request
	SpotPrice string
	// Tenancy. Can be either default or dedicated.
	Tenancy *string
	// UserData is the userdata for the instances
	UserData *fi.ResourceHolder
}

var _ fi.CompareWithID = &LaunchConfiguration{}

var _ fi.ProducesDeletions = &LaunchConfiguration{}

func (e *LaunchConfiguration) CompareWithID() *string {
	return e.ID
}

// findLaunchConfigurations returns matching LaunchConfigurations, sorted by CreatedTime (ascending)
func (e *LaunchConfiguration) findLaunchConfigurations(c *fi.Context) ([]*autoscaling.LaunchConfiguration, error) {
	cloud := c.Cloud.(awsup.AWSCloud)

	request := &autoscaling.DescribeLaunchConfigurationsInput{}

	prefix := *e.Name + "-"

	var configurations []*autoscaling.LaunchConfiguration
	err := cloud.Autoscaling().DescribeLaunchConfigurationsPages(request, func(page *autoscaling.DescribeLaunchConfigurationsOutput, lastPage bool) bool {
		for _, l := range page.LaunchConfigurations {
			name := aws.StringValue(l.LaunchConfigurationName)
			if strings.HasPrefix(name, prefix) {
				configurations = append(configurations, l)
			}
		}
		return true
	})
	if err != nil {
		return nil, fmt.Errorf("error listing AutoscalingLaunchConfigurations: %v", err)
	}

	sort.Slice(configurations, func(i, j int) bool {
		ti := configurations[i].CreatedTime
		tj := configurations[j].CreatedTime
		if tj == nil {
			return true
		}
		if ti == nil {
			return false
		}
		return ti.UnixNano() < tj.UnixNano()
	})

	return configurations, nil
}

// Find is responsible for finding the launch configuration
func (e *LaunchConfiguration) Find(c *fi.Context) (*LaunchConfiguration, error) {
	cloud := c.Cloud.(awsup.AWSCloud)

	configurations, err := e.findLaunchConfigurations(c)
	if err != nil {
		return nil, err
	}

	if len(configurations) == 0 {
		return nil, nil
	}

	// We pick up the latest launch configuration
	// (TODO: this might not actually be attached to the AutoScalingGroup, if something went wrong previously)
	lc := configurations[len(configurations)-1]

	glog.V(2).Infof("found existing AutoscalingLaunchConfiguration: %q", *lc.LaunchConfigurationName)

	actual := &LaunchConfiguration{
		Name:                   e.Name,
		AssociatePublicIP:      lc.AssociatePublicIpAddress,
		ID:                     lc.LaunchConfigurationName,
		ImageID:                lc.ImageId,
		InstanceMonitoring:     lc.InstanceMonitoring.Enabled,
		InstanceType:           lc.InstanceType,
		RootVolumeOptimization: lc.EbsOptimized,
		SpotPrice:              aws.StringValue(lc.SpotPrice),
		Tenancy:                lc.PlacementTenancy,
	}

	if lc.KeyName != nil {
		actual.SSHKey = &SSHKey{Name: lc.KeyName}
	}

	if lc.IamInstanceProfile != nil {
		actual.IAMInstanceProfile = &IAMInstanceProfile{Name: lc.IamInstanceProfile}
	}

	securityGroups := []*SecurityGroup{}
	for _, sgID := range lc.SecurityGroups {
		securityGroups = append(securityGroups, &SecurityGroup{ID: sgID})
	}
	sort.Sort(OrderSecurityGroupsById(securityGroups))

	actual.SecurityGroups = securityGroups

	// Find the root volume
	for _, b := range lc.BlockDeviceMappings {
		if b.Ebs == nil {
			continue
		}
		// @TODO check if this still work, i.e. will it be the only volume without a snapshot??
		if b.Ebs.SnapshotId != nil {
			actual.RootVolumeSize = b.Ebs.VolumeSize
			actual.RootVolumeType = b.Ebs.VolumeType
			actual.RootVolumeIops = b.Ebs.Iops
		} else {
			_, d := BlockDeviceMappingFromAutoscaling(b)
			actual.BlockDeviceMappings = append(actual.BlockDeviceMappings, d)
		}
	}

	if lc.UserData != nil {
		userData, err := base64.StdEncoding.DecodeString(aws.StringValue(lc.UserData))
		if err != nil {
			return nil, fmt.Errorf("error decoding UserData: %v", err)
		}
		actual.UserData = fi.WrapResource(fi.NewStringResource(string(userData)))
	}

	// Avoid spurious changes on ImageId
	if e.ImageID != nil && actual.ImageID != nil && *actual.ImageID != *e.ImageID {
		image, err := cloud.ResolveImage(*e.ImageID)
		if err != nil {
			glog.Warningf("unable to resolve image: %q: %v", *e.ImageID, err)
		} else if image == nil {
			glog.Warningf("unable to resolve image: %q: not found", *e.ImageID)
		} else if aws.StringValue(image.ImageId) == *actual.ImageID {
			glog.V(4).Infof("Returning matching ImageId as expected name: %q -> %q", *actual.ImageID, *e.ImageID)
			actual.ImageID = e.ImageID
		}
	}

	// Avoid spurious changes<commit_msg>- fixing up the root device discovery<commit_after>/*
Copyright 2016 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package awstasks

import (
	"encoding/base64"
	"errors"
	"fmt"
	"math"
	"sort"
	"strings"
	"time"

	"k8s.io/kops/pkg/featureflag"
	"k8s.io/kops/upup/pkg/fi"
	"k8s.io/kops/upup/pkg/fi/cloudup/awsup"
	"k8s.io/kops/upup/pkg/fi/cloudup/cloudformation"
	"k8s.io/kops/upup/pkg/fi/cloudup/terraform"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/service/autoscaling"
	"github.com/golang/glog"
	"k8s.io/apimachinery/pkg/util/sets"
)

// defaultRetainLaunchConfigurationCount is the number of launch configurations (matching the name prefix) that we should
// keep, we delete older ones
var defaultRetainLaunchConfigurationCount = 3

// RetainLaunchConfigurationCount returns the number of launch configurations to keep
func RetainLaunchConfigurationCount() int {
	if featureflag.KeepLaunchConfigurations.Enabled() {
		return math.MaxInt32
	}
	return defaultRetainLaunchConfigurationCount
}

// LaunchConfiguration is the specification of a launch configuration
type LaunchConfiguration struct {
	// Name is the name of the resource
	Name *string
	// Lifecycle is the lifecycle of the resource
	Lifecycle *fi.Lifecycle

	// AssociatePublicIP indicates if a public ip should be associated
	AssociatePublicIP *bool
	// BlockDeviceMappings is a block device mappings
	BlockDeviceMappings []*BlockDeviceMapping
	// IAMInstanceProfile is the instance profile we should use
	IAMInstanceProfile *IAMInstanceProfile
	// ID is the aws id for the resource
	ID *string
	// ImageID is the AMI id to use
	ImageID *string
	// InstanceMonitoring indicates is monitoring should be enabled
	InstanceMonitoring *bool
	// InstanceType is the aws instance type to use
	InstanceType *string
	// RootVolumeIops, if volume type is io1, then we need to specify the number of Iops.
	RootVolumeIops *int64
	// RootVolumeOptimization enables EBS optimization for an instance
	RootVolumeOptimization *bool
	// RootVolumeSize is the size of the EBS root volume to use, in GB
	RootVolumeSize *int64
	// RootVolumeType is the type of the EBS root volume to use (e.g. gp2)
	RootVolumeType *string
	// SSHKey is the key to use for the instance
	SSHKey *SSHKey
	// SecurityGroups is a collection of security groups
	SecurityGroups []*SecurityGroup
	// SpotPrice is set to the spot-price bid if this is a spot pricing request
	SpotPrice string
	// Tenancy. Can be either default or dedicated.
	Tenancy *string
	// UserData is the userdata for the instances
	UserData *fi.ResourceHolder
}

var _ fi.CompareWithID = &LaunchConfiguration{}

var _ fi.ProducesDeletions = &LaunchConfiguration{}

func (e *LaunchConfiguration) CompareWithID() *string {
	return e.ID
}

// findLaunchConfigurations returns matching LaunchConfigurations, sorted by CreatedTime (ascending)
func (e *LaunchConfiguration) findLaunchConfigurations(c *fi.Context) ([]*autoscaling.LaunchConfiguration, error) {
	cloud := c.Cloud.(awsup.AWSCloud)

	request := &autoscaling.DescribeLaunchConfigurationsInput{}

	prefix := *e.Name + "-"

	var configurations []*autoscaling.LaunchConfiguration
	err := cloud.Autoscaling().DescribeLaunchConfigurationsPages(request, func(page *autoscaling.DescribeLaunchConfigurationsOutput, lastPage bool) bool {
		for _, l := range page.LaunchConfigurations {
			name := aws.StringValue(l.LaunchConfigurationName)
			if strings.HasPrefix(name, prefix) {
				configurations = append(configurations, l)
			}
		}
		return true
	})
	if err != nil {
		return nil, fmt.Errorf("error listing AutoscalingLaunchConfigurations: %v", err)
	}

	sort.Slice(configurations, func(i, j int) bool {
		ti := configurations[i].CreatedTime
		tj := configurations[j].CreatedTime
		if tj == nil {
			return true
		}
		if ti == nil {
			return false
		}
		return ti.UnixNano() < tj.UnixNano()
	})

	return configurations, nil
}

// Find is responsible for finding the launch configuration
func (e *LaunchConfiguration) Find(c *fi.Context) (*LaunchConfiguration, error) {
	cloud := c.Cloud.(awsup.AWSCloud)

	configurations, err := e.findLaunchConfigurations(c)
	if err != nil {
		return nil, err
	}

	if len(configurations) == 0 {
		return nil, nil
	}

	// We pick up the latest launch configuration
	// (TODO: this might not actually be attached to the AutoScalingGroup, if something went wrong previously)
	lc := configurations[len(configurations)-1]

	glog.V(2).Infof("found existing AutoscalingLaunchConfiguration: %q", *lc.LaunchConfigurationName)

	actual := &LaunchConfiguration{
		Name:                   e.Name,
		AssociatePublicIP:      lc.AssociatePublicIpAddress,
		ID:                     lc.LaunchConfigurationName,
		ImageID:                lc.ImageId,
		InstanceMonitoring:     lc.InstanceMonitoring.Enabled,
		InstanceType:           lc.InstanceType,
		RootVolumeOptimization: lc.EbsOptimized,
		SpotPrice:              aws.StringValue(lc.SpotPrice),
		Tenancy:                lc.PlacementTenancy,
	}

	if lc.KeyName != nil {
		actual.SSHKey = &SSHKey{Name: lc.KeyName}
	}

	if lc.IamInstanceProfile != nil {
		actual.IAMInstanceProfile = &IAMInstanceProfile{Name: lc.IamInstanceProfile}
	}

	securityGroups := []*SecurityGroup{}
	for _, sgID := range lc.SecurityGroups {
		securityGroups = append(securityGroups, &SecurityGroup{ID: sgID})
	}
	sort.Sort(OrderSecurityGroupsById(securityGroups))

	actual.SecurityGroups = securityGroups

	// Find the root volume
	for i, b := range lc.BlockDeviceMappings {
		if b.Ebs == nil {
			continue
		}
		switch i {
		case 0:
			actual.RootVolumeSize = b.Ebs.VolumeSize
			actual.RootVolumeType = b.Ebs.VolumeType
			actual.RootVolumeIops = b.Ebs.Iops
		default:
			_, d := BlockDeviceMappingFromAutoscaling(b)
			actual.BlockDeviceMappings = append(actual.BlockDeviceMappings, d)
		}
	}

	if lc.UserData != nil {
		userData, err := base64.StdEncoding.DecodeString(aws.StringValue(lc.UserData))
		if err != nil {
			return nil, fmt.Errorf("error decoding UserData: %v", err)
		}
		actual.UserData = fi.WrapResource(fi.NewStringResource(string(userData)))
	}

	// Avoid spurious changes on ImageId
	if e.ImageID != nil && actual.ImageID != nil && *actual.ImageID != *e.ImageID {
		image, err := cloud.ResolveImage(*e.ImageID)
		if err != nil {
			glog.Warningf("unable to resolve image: %q: %v", *e.ImageID, err)
		} else if image == nil {
			glog.Warningf("unable to resolve image: %q: not found", *e.ImageID)
		} else if aws.StringValue(image.ImageId) == *actual.ImageID {
			glog.V(4).Infof("Returning matching ImageId as expected name: %q -> %q", *actual.ImageID, *e.ImageID)
			actual.ImageID = e.ImageID
		}
	}

	// Avoid spurious changes
{'fraction_non_alphanumeric': 0.0969331112961421, 'fraction_numerical': 0.0043713572023313906, 'mean_word_length': 4.0983374602051645, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1211404', 'n_tokens_mistral': 4727, 'n_tokens_neox': 4414, 'n_words': 1323}