golang.org/x/build@v0.0.0-20240506185731-218518f32b70/buildlet/ec2.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 buildlet
     6  
     7  import (
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"net"
    12  	"time"
    13  
    14  	"golang.org/x/build/buildenv"
    15  	"golang.org/x/build/dashboard"
    16  	"golang.org/x/build/internal/cloud"
    17  )
    18  
    19  // awsClient represents the AWS specific calls made during the
    20  // lifecycle of a buildlet. This is a partial implementation of the AWSClient found at
    21  // `golang.org/x/internal/cloud`.
    22  type awsClient interface {
    23  	Instance(ctx context.Context, instID string) (*cloud.Instance, error)
    24  	CreateInstance(ctx context.Context, config *cloud.EC2VMConfiguration) (*cloud.Instance, error)
    25  	WaitUntilInstanceRunning(ctx context.Context, instID string) error
    26  }
    27  
    28  // EC2Client is the client used to create buildlets on EC2.
    29  type EC2Client struct {
    30  	client awsClient
    31  }
    32  
    33  // NewEC2Client creates a new EC2Client.
    34  func NewEC2Client(client *cloud.AWSClient) *EC2Client {
    35  	return &EC2Client{
    36  		client: client,
    37  	}
    38  }
    39  
    40  // StartNewVM boots a new VM on EC2, waits until the client is accepting connections
    41  // on the configured port and returns a buildlet client configured communicate with it.
    42  func (c *EC2Client) StartNewVM(ctx context.Context, buildEnv *buildenv.Environment, hconf *dashboard.HostConfig, vmName, hostType string, opts *VMOpts) (Client, error) {
    43  	// check required params
    44  	if opts == nil || opts.TLS.IsZero() {
    45  		return nil, errors.New("TLS keypair is not set")
    46  	}
    47  	if buildEnv == nil {
    48  		return nil, errors.New("invalid build environment")
    49  	}
    50  	if hconf == nil {
    51  		return nil, errors.New("invalid host configuration")
    52  	}
    53  	if vmName == "" || hostType == "" {
    54  		return nil, fmt.Errorf("invalid vmName: %q and hostType: %q", vmName, hostType)
    55  	}
    56  
    57  	// configure defaults
    58  	if opts.Description == "" {
    59  		opts.Description = fmt.Sprintf("Go Builder for %s", hostType)
    60  	}
    61  	if opts.DeleteIn == 0 {
    62  		// Note: This implements a short default in the rare case the caller doesn't care.
    63  		opts.DeleteIn = 30 * time.Minute
    64  	}
    65  
    66  	vmConfig := configureVM(buildEnv, hconf, vmName, hostType, opts)
    67  
    68  	vm, err := c.createVM(ctx, vmConfig, opts)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  	if err = c.waitUntilVMExists(ctx, vm.ID, opts); err != nil {
    73  		return nil, err
    74  	}
    75  	// once the VM is up and running then all of the configuration data is available
    76  	// when the API is querried for the VM.
    77  	vm, err = c.client.Instance(ctx, vm.ID)
    78  	if err != nil {
    79  		return nil, fmt.Errorf("unable to retrieve instance %q information: %w", vm.ID, err)
    80  	}
    81  	buildletURL, ipPort, err := ec2BuildletParams(vm, opts)
    82  	if err != nil {
    83  		return nil, err
    84  	}
    85  	return buildletClient(ctx, buildletURL, ipPort, opts)
    86  }
    87  
    88  // createVM submits a request for the creation of a VM.
    89  func (c *EC2Client) createVM(ctx context.Context, config *cloud.EC2VMConfiguration, opts *VMOpts) (*cloud.Instance, error) {
    90  	if config == nil || opts == nil {
    91  		return nil, errors.New("invalid parameter")
    92  	}
    93  	inst, err := c.client.CreateInstance(ctx, config)
    94  	if err != nil {
    95  		return nil, fmt.Errorf("unable to create instance: %w", err)
    96  	}
    97  	condRun(opts.OnInstanceRequested)
    98  	return inst, nil
    99  }
   100  
   101  // waitUntilVMExists submits a request which waits until an instance exists before returning.
   102  func (c *EC2Client) waitUntilVMExists(ctx context.Context, instID string, opts *VMOpts) error {
   103  	if err := c.client.WaitUntilInstanceRunning(ctx, instID); err != nil {
   104  		return fmt.Errorf("failed waiting for vm instance: %w", err)
   105  	}
   106  	condRun(opts.OnInstanceCreated)
   107  	return nil
   108  }
   109  
   110  // configureVM creates a configuration for an EC2 VM instance.
   111  func configureVM(buildEnv *buildenv.Environment, hconf *dashboard.HostConfig, vmName, hostType string, opts *VMOpts) *cloud.EC2VMConfiguration {
   112  	return &cloud.EC2VMConfiguration{
   113  		Description:    opts.Description,
   114  		ImageID:        hconf.VMImage,
   115  		Name:           vmName,
   116  		SSHKeyID:       "ec2-go-builders",
   117  		SecurityGroups: []string{buildEnv.AWSSecurityGroup},
   118  		Tags:           make(map[string]string),
   119  		Type:           hconf.MachineType(),
   120  		UserData:       vmUserDataSpec(buildEnv, hconf, vmName, hostType, opts),
   121  		Zone:           opts.Zone,
   122  	}
   123  }
   124  
   125  func vmUserDataSpec(buildEnv *buildenv.Environment, hconf *dashboard.HostConfig, vmName, hostType string, opts *VMOpts) string {
   126  	// add custom metadata to the user data.
   127  	ud := cloud.EC2UserData{
   128  		BuildletName:      vmName,
   129  		BuildletBinaryURL: hconf.BuildletBinaryURL(buildEnv),
   130  		BuildletHostType:  hostType,
   131  		BuildletImageURL:  hconf.ContainerVMImage(),
   132  		Metadata:          make(map[string]string),
   133  		TLSCert:           opts.TLS.CertPEM,
   134  		TLSKey:            opts.TLS.KeyPEM,
   135  		TLSPassword:       opts.TLS.Password(),
   136  	}
   137  	for k, v := range opts.Meta {
   138  		ud.Metadata[k] = v
   139  	}
   140  	return ud.EncodedString()
   141  }
   142  
   143  // ec2BuildletParams returns the necessary information to connect to an EC2 buildlet. A
   144  // buildlet URL and an IP address port are required to connect to a buildlet.
   145  func ec2BuildletParams(inst *cloud.Instance, opts *VMOpts) (string, string, error) {
   146  	if inst.IPAddressExternal == "" {
   147  		return "", "", errors.New("external IP address is not set")
   148  	}
   149  	extIP := inst.IPAddressExternal
   150  	buildletURL := fmt.Sprintf("https://%s", extIP)
   151  	ipPort := net.JoinHostPort(extIP, "443")
   152  
   153  	if opts.OnGotEC2InstanceInfo != nil {
   154  		opts.OnGotEC2InstanceInfo(inst)
   155  	}
   156  	return buildletURL, ipPort, nil
   157  }