golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/cloud/aws.go (about)

     1  // Copyright 2020 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package cloud
     6  
     7  import (
     8  	"context"
     9  	"encoding/base64"
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"log"
    14  	"time"
    15  
    16  	"github.com/aws/aws-sdk-go/aws"
    17  	"github.com/aws/aws-sdk-go/aws/credentials"
    18  	"github.com/aws/aws-sdk-go/aws/request"
    19  	"github.com/aws/aws-sdk-go/aws/session"
    20  	"github.com/aws/aws-sdk-go/service/ec2"
    21  	"github.com/aws/aws-sdk-go/service/servicequotas"
    22  )
    23  
    24  const (
    25  	// tagName denotes the text used for Name tags.
    26  	tagName = "Name"
    27  	// tagDescription denotes the text used for Description tags.
    28  	tagDescription = "Description"
    29  )
    30  
    31  const (
    32  	// QuotaCodeCPUOnDemand is the quota code for on-demand CPUs.
    33  	QuotaCodeCPUOnDemand = "L-1216C47A"
    34  	// QuotaServiceEC2 is the service code for the EC2 service.
    35  	QuotaServiceEC2 = "ec2"
    36  )
    37  
    38  // vmClient defines the interface used to call the backing EC2 service. This is a partial interface
    39  // based on the EC2 package defined at github.com/aws/aws-sdk-go/service/ec2.
    40  type vmClient interface {
    41  	DescribeInstancesPagesWithContext(context.Context, *ec2.DescribeInstancesInput, func(*ec2.DescribeInstancesOutput, bool) bool, ...request.Option) error
    42  	DescribeInstancesWithContext(context.Context, *ec2.DescribeInstancesInput, ...request.Option) (*ec2.DescribeInstancesOutput, error)
    43  	RunInstancesWithContext(context.Context, *ec2.RunInstancesInput, ...request.Option) (*ec2.Reservation, error)
    44  	TerminateInstancesWithContext(context.Context, *ec2.TerminateInstancesInput, ...request.Option) (*ec2.TerminateInstancesOutput, error)
    45  	WaitUntilInstanceRunningWithContext(context.Context, *ec2.DescribeInstancesInput, ...request.WaiterOption) error
    46  	DescribeInstanceTypesPagesWithContext(context.Context, *ec2.DescribeInstanceTypesInput, func(*ec2.DescribeInstanceTypesOutput, bool) bool, ...request.Option) error
    47  }
    48  
    49  // quotaClient defines the interface used to call the backing service quotas service. This
    50  // is a partial interface based on the service quota package defined at
    51  // github.com/aws/aws-sdk-go/service/servicequotas.
    52  type quotaClient interface {
    53  	GetServiceQuota(*servicequotas.GetServiceQuotaInput) (*servicequotas.GetServiceQuotaOutput, error)
    54  }
    55  
    56  // EC2VMConfiguration is the configuration needed for an EC2 instance.
    57  type EC2VMConfiguration struct {
    58  	// Description is a user defined description of the instance. It is displayed
    59  	// on the AWS UI. It is an optional field.
    60  	Description string
    61  	// ImageID is the ID of the image used to launch the instance. It is a required field.
    62  	ImageID string
    63  	// Name is a user defined name for the instance. It is displayed on the AWS UI. It is
    64  	// an optional field.
    65  	Name string
    66  	// SSHKeyID is the name of the SSH key pair to use for access. It is a required field.
    67  	SSHKeyID string
    68  	// SecurityGroups contains the names of the security groups to be applied to the VM. If none
    69  	// are provided the default security group will be used.
    70  	SecurityGroups []string
    71  	// Tags the tags to apply to the resources during launch.
    72  	Tags map[string]string
    73  	// Type is the type of instance.
    74  	Type string
    75  	// UserData is the user data to make available to the instance. This data is available
    76  	// on the VM via the metadata endpoints. It must be a base64-encoded string. User
    77  	// data is limited to 16 KB.
    78  	UserData string
    79  	// Zone the Availability Zone of the instance.
    80  	Zone string
    81  }
    82  
    83  // Instance is a virtual machine.
    84  type Instance struct {
    85  	// CPUCount is the number of VCPUs the instance is configured with.
    86  	CPUCount int64
    87  	// CreatedAt is the time when the instance was launched.
    88  	CreatedAt time.Time
    89  	// Description is a user defined description of the instance.
    90  	Description string
    91  	// ID is the instance ID.
    92  	ID string
    93  	// IPAddressExternal is the public IPv4 address assigned to the instance.
    94  	IPAddressExternal string
    95  	// IPAddressInternal is the private IPv4 address assigned to the instance.
    96  	IPAddressInternal string
    97  	// ImageID is The ID of the AMI(image)  used to launch the instance.
    98  	ImageID string
    99  	// Name is a user defined name for the instance.
   100  	Name string
   101  	// SSHKeyID is the name of the SSH key pair to use for access. It is a required field.
   102  	SSHKeyID string
   103  	// SecurityGroups is the security groups for the instance.
   104  	SecurityGroups []string
   105  	// State contains the state of the instance.
   106  	State string
   107  	// Tags contains tags assigned to the instance.
   108  	Tags map[string]string
   109  	// Type is the name of instance type.
   110  	Type string
   111  	// Zone is the availability zone where the instance is deployed.
   112  	Zone string
   113  }
   114  
   115  // AWSClient is a client for AWS services.
   116  type AWSClient struct {
   117  	ec2Client   vmClient
   118  	quotaClient quotaClient
   119  }
   120  
   121  // AWSOpt is an optional configuration setting for the AWSClient.
   122  type AWSOpt func(*AWSClient)
   123  
   124  // NewAWSClient creates a new AWS client.
   125  func NewAWSClient(region, keyID, accessKey string, opts ...AWSOpt) (*AWSClient, error) {
   126  	s, err := session.NewSession(&aws.Config{
   127  		Region:      aws.String(region),
   128  		Credentials: credentials.NewStaticCredentials(keyID, accessKey, ""), // Token is only required for STS
   129  	})
   130  	if err != nil {
   131  		return nil, fmt.Errorf("failed to create AWS session: %v", err)
   132  	}
   133  	c := &AWSClient{
   134  		ec2Client:   ec2.New(s),
   135  		quotaClient: servicequotas.New(s),
   136  	}
   137  	for _, opt := range opts {
   138  		opt(c)
   139  	}
   140  	return c, nil
   141  }
   142  
   143  // Instance retrieves an EC2 instance by instance ID.
   144  func (ac *AWSClient) Instance(ctx context.Context, instID string) (*Instance, error) {
   145  	dio, err := ac.ec2Client.DescribeInstancesWithContext(ctx, &ec2.DescribeInstancesInput{
   146  		InstanceIds: []*string{aws.String(instID)},
   147  	})
   148  	if err != nil {
   149  		return nil, fmt.Errorf("unable to retrieve instance %q information: %w", instID, err)
   150  	}
   151  
   152  	if dio == nil || len(dio.Reservations) != 1 || len(dio.Reservations[0].Instances) != 1 {
   153  		return nil, errors.New("describe instances output does not contain a valid instance")
   154  	}
   155  	ec2Inst := dio.Reservations[0].Instances[0]
   156  	return ec2ToInstance(ec2Inst), err
   157  }
   158  
   159  // RunningInstances retrieves all EC2 instances in a region which have not been terminated or stopped.
   160  func (ac *AWSClient) RunningInstances(ctx context.Context) ([]*Instance, error) {
   161  	instances := make([]*Instance, 0)
   162  
   163  	fn := func(page *ec2.DescribeInstancesOutput, lastPage bool) bool {
   164  		for _, res := range page.Reservations {
   165  			for _, inst := range res.Instances {
   166  				instances = append(instances, ec2ToInstance(inst))
   167  			}
   168  		}
   169  		return true
   170  	}
   171  	err := ac.ec2Client.DescribeInstancesPagesWithContext(ctx, &ec2.DescribeInstancesInput{
   172  		Filters: []*ec2.Filter{
   173  			&ec2.Filter{
   174  				Name:   aws.String("instance-state-name"),
   175  				Values: []*string{aws.String(ec2.InstanceStateNameRunning), aws.String(ec2.InstanceStateNamePending)},
   176  			},
   177  		},
   178  	}, fn)
   179  	if err != nil {
   180  		return nil, err
   181  	}
   182  	return instances, nil
   183  }
   184  
   185  // CreateInstance creates an EC2 VM instance.
   186  func (ac *AWSClient) CreateInstance(ctx context.Context, config *EC2VMConfiguration) (*Instance, error) {
   187  	if config == nil {
   188  		return nil, errors.New("unable to create a VM with a nil instance")
   189  	}
   190  	runResult, err := ac.ec2Client.RunInstancesWithContext(ctx, vmConfig(config))
   191  	if err != nil {
   192  		return nil, fmt.Errorf("unable to create instance: %w", err)
   193  	}
   194  	if runResult == nil || len(runResult.Instances) != 1 {
   195  		return nil, fmt.Errorf("unexpected number of instances. want 1; got %d", len(runResult.Instances))
   196  	}
   197  	return ec2ToInstance(runResult.Instances[0]), nil
   198  }
   199  
   200  // DestroyInstances terminates EC2 VM instances.
   201  func (ac *AWSClient) DestroyInstances(ctx context.Context, instIDs ...string) error {
   202  	ids := aws.StringSlice(instIDs)
   203  	_, err := ac.ec2Client.TerminateInstancesWithContext(ctx, &ec2.TerminateInstancesInput{
   204  		InstanceIds: ids,
   205  	})
   206  	if err != nil {
   207  		return fmt.Errorf("unable to destroy vm: %w", err)
   208  	}
   209  	return err
   210  }
   211  
   212  // WaitUntilInstanceRunning waits until a stopping condition is met. The stopping conditions are:
   213  // - The requested instance state is `running`.
   214  // - The passed in context is cancelled or the deadline expires.
   215  // - 40 requests are made with a 15 second delay between each request.
   216  func (ac *AWSClient) WaitUntilInstanceRunning(ctx context.Context, instID string) error {
   217  	err := ac.ec2Client.WaitUntilInstanceRunningWithContext(ctx, &ec2.DescribeInstancesInput{
   218  		InstanceIds: []*string{aws.String(instID)},
   219  	})
   220  	if err != nil {
   221  		return fmt.Errorf("failed waiting for vm instance: %w", err)
   222  	}
   223  	return err
   224  }
   225  
   226  // InstanceType contains information about an EC2 vm instance type.
   227  type InstanceType struct {
   228  	// Type is the textual label used to describe an instance type.
   229  	Type string
   230  	// CPU is the Default vCPU count.
   231  	CPU int64
   232  }
   233  
   234  // InstanceTypesARM retrieves all EC2 instance types in a region which support the
   235  // ARM64 architecture.
   236  func (ac *AWSClient) InstanceTypesARM(ctx context.Context) ([]*InstanceType, error) {
   237  	var its []*InstanceType
   238  	contains := func(strs []*string, want string) bool {
   239  		for _, s := range strs {
   240  			if aws.StringValue(s) == want {
   241  				return true
   242  			}
   243  		}
   244  		return false
   245  	}
   246  	fn := func(page *ec2.DescribeInstanceTypesOutput, lastPage bool) bool {
   247  		for _, it := range page.InstanceTypes {
   248  			if !contains(it.ProcessorInfo.SupportedArchitectures, "arm64") {
   249  				continue
   250  			}
   251  			its = append(its, &InstanceType{
   252  				Type: aws.StringValue(it.InstanceType),
   253  				CPU:  aws.Int64Value(it.VCpuInfo.DefaultVCpus),
   254  			})
   255  		}
   256  		return true
   257  	}
   258  	err := ac.ec2Client.DescribeInstanceTypesPagesWithContext(ctx, &ec2.DescribeInstanceTypesInput{}, fn)
   259  	if err != nil {
   260  		return nil, fmt.Errorf("failed to retrieve arm64 instance types: %w", err)
   261  	}
   262  	return its, nil
   263  }
   264  
   265  // Quota retrieves the requested service quota for the service.
   266  func (ac *AWSClient) Quota(ctx context.Context, service, code string) (int64, error) {
   267  	// TODO(golang.org/issue/36841): use ctx
   268  	sq, err := ac.quotaClient.GetServiceQuota(&servicequotas.GetServiceQuotaInput{
   269  		QuotaCode:   aws.String(code),
   270  		ServiceCode: aws.String(service),
   271  	})
   272  	if err != nil {
   273  		return 0, fmt.Errorf("failed to retrieve quota: %w", err)
   274  	}
   275  	return int64(aws.Float64Value(sq.Quota.Value)), nil
   276  }
   277  
   278  // ec2ToInstance converts an `ec2.Instance` to an `Instance`
   279  func ec2ToInstance(inst *ec2.Instance) *Instance {
   280  	secGroup := make([]string, 0, len(inst.SecurityGroups))
   281  	for _, sg := range inst.SecurityGroups {
   282  		secGroup = append(secGroup, aws.StringValue(sg.GroupId))
   283  	}
   284  	i := &Instance{
   285  		CreatedAt:         aws.TimeValue(inst.LaunchTime),
   286  		ID:                *inst.InstanceId,
   287  		IPAddressExternal: aws.StringValue(inst.PublicIpAddress),
   288  		IPAddressInternal: aws.StringValue(inst.PrivateIpAddress),
   289  		ImageID:           aws.StringValue(inst.ImageId),
   290  		SSHKeyID:          aws.StringValue(inst.KeyName),
   291  		SecurityGroups:    secGroup,
   292  		State:             aws.StringValue(inst.State.Name),
   293  		Tags:              make(map[string]string),
   294  		Type:              aws.StringValue(inst.InstanceType),
   295  	}
   296  	if inst.Placement != nil {
   297  		i.Zone = aws.StringValue(inst.Placement.AvailabilityZone)
   298  	}
   299  	if inst.CpuOptions != nil {
   300  		i.CPUCount = aws.Int64Value(inst.CpuOptions.CoreCount)
   301  	}
   302  	for _, tag := range inst.Tags {
   303  		switch *tag.Key {
   304  		case tagName:
   305  			i.Name = *tag.Value
   306  		case tagDescription:
   307  			i.Description = *tag.Value
   308  		default:
   309  			i.Tags[*tag.Key] = *tag.Value
   310  		}
   311  	}
   312  	return i
   313  }
   314  
   315  // vmConfig converts a configuration into a request to create an instance.
   316  func vmConfig(config *EC2VMConfiguration) *ec2.RunInstancesInput {
   317  	ri := &ec2.RunInstancesInput{
   318  		ImageId:      aws.String(config.ImageID),
   319  		InstanceType: aws.String(config.Type),
   320  		MinCount:     aws.Int64(1),
   321  		MaxCount:     aws.Int64(1),
   322  		Placement: &ec2.Placement{
   323  			AvailabilityZone: aws.String(config.Zone),
   324  		},
   325  		KeyName:                           aws.String(config.SSHKeyID),
   326  		InstanceInitiatedShutdownBehavior: aws.String(ec2.ShutdownBehaviorTerminate),
   327  		TagSpecifications: []*ec2.TagSpecification{
   328  			&ec2.TagSpecification{
   329  				ResourceType: aws.String("instance"),
   330  				Tags: []*ec2.Tag{
   331  					&ec2.Tag{
   332  						Key:   aws.String(tagName),
   333  						Value: aws.String(config.Name),
   334  					},
   335  					&ec2.Tag{
   336  						Key:   aws.String(tagDescription),
   337  						Value: aws.String(config.Description),
   338  					},
   339  				},
   340  			},
   341  		},
   342  		SecurityGroups: aws.StringSlice(config.SecurityGroups),
   343  		UserData:       aws.String(config.UserData),
   344  	}
   345  	for k, v := range config.Tags {
   346  		ri.TagSpecifications[0].Tags = append(ri.TagSpecifications[0].Tags, &ec2.Tag{
   347  			Key:   aws.String(k),
   348  			Value: aws.String(v),
   349  		})
   350  	}
   351  	return ri
   352  }
   353  
   354  // EC2UserData is stored in the user data for each EC2 instance. This is
   355  // used to store metadata about the running instance. The buildlet will retrieve
   356  // this on EC2 instances before allowing connections from the coordinator.
   357  type EC2UserData struct {
   358  	// BuildletBinaryURL is the url to the buildlet binary stored on GCS.
   359  	BuildletBinaryURL string `json:"buildlet_binary_url,omitempty"`
   360  	// BuildletHostType is the host type used by the buildlet. For example, `host-linux-arm64-aws`.
   361  	BuildletHostType string `json:"buildlet_host_type,omitempty"`
   362  	// BuildletImageURL is the url for the buildlet container image.
   363  	BuildletImageURL string `json:"buildlet_image_url,omitempty"`
   364  	// BuildletName is the name which should be passed onto the buildlet.
   365  	BuildletName string `json:"buildlet_name,omitempty"`
   366  	// Metadata provides a location for arbitrary metadata to be stored.
   367  	Metadata map[string]string `json:"metadata,omitempty"`
   368  	// TLSCert is the TLS certificate used by the buildlet.
   369  	TLSCert string `json:"tls_cert,omitempty"`
   370  	// TLSKey is the TLS key used by the buildlet.
   371  	TLSKey string `json:"tls_key,omitempty"`
   372  	// TLSPassword contains the SHA1 of the TLS key used by the buildlet for basic authentication.
   373  	TLSPassword string `json:"tls_password,omitempty"`
   374  }
   375  
   376  // EncodedString converts `EC2UserData` into JSON which is base64 encoded.
   377  // User data must be base64 encoded upon creation.
   378  func (ud *EC2UserData) EncodedString() string {
   379  	jsonUserData, err := json.Marshal(ud)
   380  	if err != nil {
   381  		log.Printf("unable to marshal user data: %v", err)
   382  	}
   383  	return base64.StdEncoding.EncodeToString([]byte(jsonUserData))
   384  }