blob: 02a3efa33f16a57931e9d2110ae3c9280827711a [file] [log] [blame]
// 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
}