blob: c1a094d3c7790f921dd4912725967e2f83291dd2 [file] [log] [blame]
Carlos Amedee87d10202020-06-02 16:56:13 -04001// 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
5package buildlet
6
7import (
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 durring the
20// lifecycle of a buildlet. This is a partial implementation of the AWSClient found at
21// `golang.org/x/internal/cloud`.
22type 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.
29type EC2Client struct {
30 client awsClient
31}
32
33// NewEC2Client creates a new EC2Client.
34func 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.
Carlos Amedeef8a16ea2021-12-14 18:17:24 -050042func (c *EC2Client) StartNewVM(ctx context.Context, buildEnv *buildenv.Environment, hconf *dashboard.HostConfig, vmName, hostType string, opts *VMOpts) (Client, error) {
Carlos Amedee87d10202020-06-02 16:56:13 -040043 // 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 enviornment")
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 }
Carlos Amedee87d10202020-06-02 16:56:13 -040061 if opts.DeleteIn == 0 {
62 opts.DeleteIn = 30 * time.Minute
63 }
64
65 vmConfig := configureVM(buildEnv, hconf, vmName, hostType, opts)
66
67 vm, err := c.createVM(ctx, vmConfig, opts)
68 if err != nil {
69 return nil, err
70 }
Carlos Amedee5a0d4622020-08-07 12:22:05 -040071 if err = c.waitUntilVMExists(ctx, vm.ID, opts); err != nil {
Carlos Amedee87d10202020-06-02 16:56:13 -040072 return nil, err
73 }
74 // once the VM is up and running then all of the configuration data is available
75 // when the API is querried for the VM.
76 vm, err = c.client.Instance(ctx, vm.ID)
77 if err != nil {
78 return nil, fmt.Errorf("unable to retrieve instance %q information: %w", vm.ID, err)
79 }
80 buildletURL, ipPort, err := ec2BuildletParams(vm, opts)
81 if err != nil {
82 return nil, err
83 }
84 return buildletClient(ctx, buildletURL, ipPort, opts)
85}
86
87// createVM submits a request for the creation of a VM.
88func (c *EC2Client) createVM(ctx context.Context, config *cloud.EC2VMConfiguration, opts *VMOpts) (*cloud.Instance, error) {
89 if config == nil || opts == nil {
90 return nil, errors.New("invalid parameter")
91 }
92 inst, err := c.client.CreateInstance(ctx, config)
93 if err != nil {
94 return nil, fmt.Errorf("unable to create instance: %w", err)
95 }
96 condRun(opts.OnInstanceRequested)
97 return inst, nil
98}
99
Carlos Amedee5a0d4622020-08-07 12:22:05 -0400100// waitUntilVMExists submits a request which waits until an instance exists before returning.
101func (c *EC2Client) waitUntilVMExists(ctx context.Context, instID string, opts *VMOpts) error {
Carlos Amedee87d10202020-06-02 16:56:13 -0400102 if err := c.client.WaitUntilInstanceRunning(ctx, instID); err != nil {
103 return fmt.Errorf("failed waiting for vm instance: %w", err)
104 }
105 condRun(opts.OnInstanceCreated)
106 return nil
107}
108
109// configureVM creates a configuration for an EC2 VM instance.
110func configureVM(buildEnv *buildenv.Environment, hconf *dashboard.HostConfig, vmName, hostType string, opts *VMOpts) *cloud.EC2VMConfiguration {
111 return &cloud.EC2VMConfiguration{
112 Description: opts.Description,
113 ImageID: hconf.VMImage,
114 Name: vmName,
115 SSHKeyID: "ec2-go-builders",
116 SecurityGroups: []string{buildEnv.AWSSecurityGroup},
117 Tags: make(map[string]string),
118 Type: hconf.MachineType(),
119 UserData: vmUserDataSpec(buildEnv, hconf, vmName, hostType, opts),
120 Zone: opts.Zone,
121 }
122}
123
124func vmUserDataSpec(buildEnv *buildenv.Environment, hconf *dashboard.HostConfig, vmName, hostType string, opts *VMOpts) string {
125 // add custom metadata to the user data.
126 ud := cloud.EC2UserData{
127 BuildletName: vmName,
128 BuildletBinaryURL: hconf.BuildletBinaryURL(buildEnv),
129 BuildletHostType: hostType,
130 BuildletImageURL: hconf.ContainerVMImage(),
131 Metadata: make(map[string]string),
132 TLSCert: opts.TLS.CertPEM,
133 TLSKey: opts.TLS.KeyPEM,
134 TLSPassword: opts.TLS.Password(),
135 }
136 for k, v := range opts.Meta {
137 ud.Metadata[k] = v
138 }
139 return ud.EncodedString()
140}
141
142// ec2BuildletParams returns the necessary information to connect to an EC2 buildlet. A
143// buildlet URL and an IP address port are required to connect to a buildlet.
144func ec2BuildletParams(inst *cloud.Instance, opts *VMOpts) (string, string, error) {
145 if inst.IPAddressExternal == "" {
146 return "", "", errors.New("external IP address is not set")
147 }
148 extIP := inst.IPAddressExternal
149 buildletURL := fmt.Sprintf("https://%s", extIP)
150 ipPort := net.JoinHostPort(extIP, "443")
151
152 if opts.OnGotEC2InstanceInfo != nil {
153 opts.OnGotEC2InstanceInfo(inst)
154 }
155 return buildletURL, ipPort, nil
156}