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 }