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 (
// awsClient represents the AWS specific calls made during the
// lifecycle of a buildlet. This is a partial implementation of the AWSClient found at
// ``.
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)
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)
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 {
return buildletURL, ipPort, nil