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