github.com/coreos/mantle@v0.13.0/platform/api/aws/ec2.go (about)

     1  // Copyright 2016 CoreOS, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package aws
    16  
    17  import (
    18  	"encoding/base64"
    19  	"fmt"
    20  	"strings"
    21  	"time"
    22  
    23  	"github.com/aws/aws-sdk-go/aws"
    24  	"github.com/aws/aws-sdk-go/aws/awserr"
    25  	"github.com/aws/aws-sdk-go/service/ec2"
    26  
    27  	"github.com/coreos/mantle/util"
    28  )
    29  
    30  func (a *API) AddKey(name, key string) error {
    31  	_, err := a.ec2.ImportKeyPair(&ec2.ImportKeyPairInput{
    32  		KeyName:           &name,
    33  		PublicKeyMaterial: []byte(key),
    34  	})
    35  
    36  	return err
    37  }
    38  
    39  func (a *API) DeleteKey(name string) error {
    40  	_, err := a.ec2.DeleteKeyPair(&ec2.DeleteKeyPairInput{
    41  		KeyName: &name,
    42  	})
    43  
    44  	return err
    45  }
    46  
    47  // CreateInstances creates EC2 instances with a given name tag, optional ssh key name, user data. The image ID, instance type, and security group set in the API will be used. CreateInstances will block until all instances are running and have an IP address.
    48  func (a *API) CreateInstances(name, keyname, userdata string, count uint64) ([]*ec2.Instance, error) {
    49  	cnt := int64(count)
    50  
    51  	var ud *string
    52  	if len(userdata) > 0 {
    53  		tud := base64.StdEncoding.EncodeToString([]byte(userdata))
    54  		ud = &tud
    55  	}
    56  
    57  	err := a.ensureInstanceProfile(a.opts.IAMInstanceProfile)
    58  	if err != nil {
    59  		return nil, fmt.Errorf("error verifying IAM instance profile: %v", err)
    60  	}
    61  
    62  	sgId, err := a.getSecurityGroupID(a.opts.SecurityGroup)
    63  	if err != nil {
    64  		return nil, fmt.Errorf("error resolving security group: %v", err)
    65  	}
    66  
    67  	vpcId, err := a.getVPCID(sgId)
    68  	if err != nil {
    69  		return nil, fmt.Errorf("error resolving vpc: %v", err)
    70  	}
    71  
    72  	subnetId, err := a.getSubnetID(vpcId)
    73  	if err != nil {
    74  		return nil, fmt.Errorf("error resolving subnet: %v", err)
    75  	}
    76  
    77  	key := &keyname
    78  	if keyname == "" {
    79  		key = nil
    80  	}
    81  	inst := ec2.RunInstancesInput{
    82  		ImageId:          &a.opts.AMI,
    83  		MinCount:         &cnt,
    84  		MaxCount:         &cnt,
    85  		KeyName:          key,
    86  		InstanceType:     &a.opts.InstanceType,
    87  		SecurityGroupIds: []*string{&sgId},
    88  		SubnetId:         &subnetId,
    89  		UserData:         ud,
    90  		IamInstanceProfile: &ec2.IamInstanceProfileSpecification{
    91  			Name: &a.opts.IAMInstanceProfile,
    92  		},
    93  		TagSpecifications: []*ec2.TagSpecification{
    94  			&ec2.TagSpecification{
    95  				ResourceType: aws.String(ec2.ResourceTypeInstance),
    96  				Tags: []*ec2.Tag{
    97  					&ec2.Tag{
    98  						Key:   aws.String("Name"),
    99  						Value: aws.String(name),
   100  					},
   101  					&ec2.Tag{
   102  						Key:   aws.String("CreatedBy"),
   103  						Value: aws.String("mantle"),
   104  					},
   105  				},
   106  			},
   107  		},
   108  	}
   109  
   110  	var reservations *ec2.Reservation
   111  	err = util.RetryConditional(5, 5*time.Second, func(err error) bool {
   112  		// due to AWS' eventual consistency despite ensuring that the IAM Instance
   113  		// Profile has been created it may not be available to ec2 yet.
   114  		if awsErr, ok := err.(awserr.Error); ok && (awsErr.Code() == "InvalidParameterValue" && strings.Contains(awsErr.Message(), "iamInstanceProfile.name")) {
   115  			return true
   116  		}
   117  		return false
   118  	}, func() error {
   119  		var ierr error
   120  		reservations, ierr = a.ec2.RunInstances(&inst)
   121  		return ierr
   122  	})
   123  	if err != nil {
   124  		return nil, fmt.Errorf("error running instances: %v", err)
   125  	}
   126  
   127  	ids := make([]string, len(reservations.Instances))
   128  	for i, inst := range reservations.Instances {
   129  		ids[i] = *inst.InstanceId
   130  	}
   131  
   132  	// loop until all machines are online
   133  	var insts []*ec2.Instance
   134  
   135  	// 10 minutes is a pretty reasonable timeframe for AWS instances to work.
   136  	timeout := 10 * time.Minute
   137  	// don't make api calls too quickly, or we will hit the rate limit
   138  	delay := 10 * time.Second
   139  	err = util.WaitUntilReady(timeout, delay, func() (bool, error) {
   140  		desc, err := a.ec2.DescribeInstances(&ec2.DescribeInstancesInput{
   141  			InstanceIds: aws.StringSlice(ids),
   142  		})
   143  		if err != nil {
   144  			// Keep retrying if the InstanceID disappears momentarily
   145  			if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "InvalidInstanceID.NotFound" {
   146  				plog.Debugf("instance ID not found, retrying: %v", err)
   147  				return false, nil
   148  			}
   149  			return false, err
   150  		}
   151  		insts = desc.Reservations[0].Instances
   152  
   153  		for _, i := range insts {
   154  			if *i.State.Name != ec2.InstanceStateNameRunning || i.PublicIpAddress == nil {
   155  				return false, nil
   156  			}
   157  		}
   158  		return true, nil
   159  	})
   160  	if err != nil {
   161  		a.TerminateInstances(ids)
   162  		return nil, fmt.Errorf("waiting for instances to run: %v", err)
   163  	}
   164  
   165  	return insts, nil
   166  }
   167  
   168  // gcEC2 will terminate ec2 instances older than gracePeriod.
   169  // It will only operate on ec2 instances tagged with 'mantle' to avoid stomping
   170  // on other resources in the account.
   171  func (a *API) gcEC2(gracePeriod time.Duration) error {
   172  	durationAgo := time.Now().Add(-1 * gracePeriod)
   173  
   174  	instances, err := a.ec2.DescribeInstances(&ec2.DescribeInstancesInput{
   175  		Filters: []*ec2.Filter{
   176  			&ec2.Filter{
   177  				Name:   aws.String("tag:CreatedBy"),
   178  				Values: aws.StringSlice([]string{"mantle"}),
   179  			},
   180  		},
   181  	})
   182  	if err != nil {
   183  		return fmt.Errorf("error describing instances: %v", err)
   184  	}
   185  
   186  	toTerminate := []string{}
   187  
   188  	for _, reservation := range instances.Reservations {
   189  		for _, instance := range reservation.Instances {
   190  			if instance.LaunchTime.After(durationAgo) {
   191  				plog.Debugf("ec2: skipping instance %s due to being too new", *instance.InstanceId)
   192  				// Skip, still too new
   193  				continue
   194  			}
   195  
   196  			if instance.State != nil {
   197  				switch *instance.State.Name {
   198  				case ec2.InstanceStateNamePending, ec2.InstanceStateNameRunning, ec2.InstanceStateNameStopped:
   199  					toTerminate = append(toTerminate, *instance.InstanceId)
   200  				case ec2.InstanceStateNameTerminated, ec2.InstanceStateNameShuttingDown:
   201  				default:
   202  					plog.Infof("ec2: skipping instance in state %s", *instance.State.Name)
   203  				}
   204  			} else {
   205  				plog.Warningf("ec2 instance had no state: %s", *instance.InstanceId)
   206  			}
   207  		}
   208  	}
   209  
   210  	return a.TerminateInstances(toTerminate)
   211  }
   212  
   213  // TerminateInstances schedules EC2 instances to be terminated.
   214  func (a *API) TerminateInstances(ids []string) error {
   215  	if len(ids) == 0 {
   216  		return nil
   217  	}
   218  	input := &ec2.TerminateInstancesInput{
   219  		InstanceIds: aws.StringSlice(ids),
   220  	}
   221  
   222  	if _, err := a.ec2.TerminateInstances(input); err != nil {
   223  		return err
   224  	}
   225  
   226  	return nil
   227  }
   228  
   229  func (a *API) CreateTags(resources []string, tags map[string]string) error {
   230  	if len(tags) == 0 {
   231  		return nil
   232  	}
   233  
   234  	tagObjs := make([]*ec2.Tag, 0, len(tags))
   235  	for key, value := range tags {
   236  		tagObjs = append(tagObjs, &ec2.Tag{
   237  			Key:   aws.String(key),
   238  			Value: aws.String(value),
   239  		})
   240  	}
   241  	_, err := a.ec2.CreateTags(&ec2.CreateTagsInput{
   242  		Resources: aws.StringSlice(resources),
   243  		Tags:      tagObjs,
   244  	})
   245  	if err != nil {
   246  		return fmt.Errorf("error creating tags: %v", err)
   247  	}
   248  	return err
   249  }
   250  
   251  // GetConsoleOutput returns the console output. Returns "", nil if no logs
   252  // are available.
   253  func (a *API) GetConsoleOutput(instanceID string) (string, error) {
   254  	res, err := a.ec2.GetConsoleOutput(&ec2.GetConsoleOutputInput{
   255  		InstanceId: aws.String(instanceID),
   256  	})
   257  	if err != nil {
   258  		return "", fmt.Errorf("couldn't get console output of %v: %v", instanceID, err)
   259  	}
   260  
   261  	if res.Output == nil {
   262  		return "", nil
   263  	}
   264  
   265  	decoded, err := base64.StdEncoding.DecodeString(*res.Output)
   266  	if err != nil {
   267  		return "", fmt.Errorf("couldn't decode console output of %v: %v", instanceID, err)
   268  	}
   269  
   270  	return string(decoded), nil
   271  }