buildlet: use cloud client with AWS buildlet
Use the cloud package for AWS functions. Remove the unused
destroyVM function.
Updates golang/go#36841
Change-Id: I00e1a20c904f7c4be6460ac302085b28f518d161
Reviewed-on: https://go-review.googlesource.com/c/build/+/236300
Run-TryBot: Carlos Amedee <carlos@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Alexander Rakoczy <alex@golang.org>
diff --git a/buildlet/aws.go b/buildlet/aws.go
deleted file mode 100644
index 1d96c66..0000000
--- a/buildlet/aws.go
+++ /dev/null
@@ -1,254 +0,0 @@
-// 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"
- "encoding/base64"
- "encoding/json"
- "errors"
- "fmt"
- "log"
- "net"
- "time"
-
- "github.com/aws/aws-sdk-go/aws"
- "github.com/aws/aws-sdk-go/aws/credentials"
- "github.com/aws/aws-sdk-go/aws/request"
- "github.com/aws/aws-sdk-go/aws/session"
- "github.com/aws/aws-sdk-go/service/ec2"
- "golang.org/x/build/buildenv"
- "golang.org/x/build/dashboard"
-)
-
-// EC2UserData is stored in the user data for each EC2 instance. This is
-// used to store metadata about the running instance. The buildlet will retrieve
-// this on EC2 instances before allowing connections from the coordinator.
-type EC2UserData struct {
- BuildletBinaryURL string `json:"buildlet_binary_url,omitempty"`
- BuildletHostType string `json:"buildlet_host_type,omitempty"`
- BuildletImageURL string `json:"buildlet_image_url,omitempty"`
- BuildletName string `json:"buildlet_name,omitempty"`
- Metadata map[string]string `json:"metadata,omitempty"`
- TLSCert string `json:"tls_cert,omitempty"`
- TLSKey string `json:"tls_key,omitempty"`
- TLSPassword string `json:"tls_password,omitempty"`
-}
-
-// ec2Client represents the EC2 specific calls made durring the
-// lifecycle of a buildlet.
-type ec2Client interface {
- DescribeInstancesWithContext(context.Context, *ec2.DescribeInstancesInput, ...request.Option) (*ec2.DescribeInstancesOutput, error)
- RunInstancesWithContext(context.Context, *ec2.RunInstancesInput, ...request.Option) (*ec2.Reservation, error)
- TerminateInstancesWithContext(context.Context, *ec2.TerminateInstancesInput, ...request.Option) (*ec2.TerminateInstancesOutput, error)
- WaitUntilInstanceRunningWithContext(context.Context, *ec2.DescribeInstancesInput, ...request.WaiterOption) error
-}
-
-// AWSClient is the client used to create and destroy buildlets on AWS.
-type AWSClient struct {
- client ec2Client
-}
-
-// NewAWSClient creates a new AWSClient.
-func NewAWSClient(region, keyID, accessKey string) (*AWSClient, error) {
- s, err := session.NewSession(&aws.Config{
- Region: aws.String(region),
- Credentials: credentials.NewStaticCredentials(keyID, accessKey, ""), // Token is only required for STS
- })
- if err != nil {
- return nil, fmt.Errorf("failed to create AWS session: %v", err)
- }
- return &AWSClient{
- client: ec2.New(s),
- }, nil
-}
-
-// 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 *AWSClient) 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 enviornment")
- }
- 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.Zone == "" {
- opts.Zone = buildEnv.RandomEC2VMZone()
- }
- if opts.DeleteIn == 0 {
- opts.DeleteIn = 30 * time.Minute
- }
-
- vmConfig := c.configureVM(buildEnv, hconf, vmName, hostType, opts)
-
- vmID, err := c.createVM(ctx, vmConfig, opts)
- if err != nil {
- return nil, err
- }
- if err = c.WaitUntilVMExists(ctx, vmID, opts); err != nil {
- return nil, err
- }
- vm, err := c.RetrieveVMInfo(ctx, vmID)
- if err != nil {
- return nil, 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 *AWSClient) createVM(ctx context.Context, vmConfig *ec2.RunInstancesInput, opts *VMOpts) (string, error) {
- runResult, err := c.client.RunInstancesWithContext(ctx, vmConfig)
- if err != nil {
- return "", fmt.Errorf("unable to create instance: %w", err)
- }
- condRun(opts.OnInstanceRequested)
- return *runResult.Instances[0].InstanceId, nil
-}
-
-// WaitUntilVMExists submits a request which waits until an instance exists before returning.
-func (c *AWSClient) WaitUntilVMExists(ctx context.Context, instID string, opts *VMOpts) error {
- err := c.client.WaitUntilInstanceRunningWithContext(ctx, &ec2.DescribeInstancesInput{
- InstanceIds: []*string{aws.String(instID)},
- })
- if err != nil {
- return fmt.Errorf("failed waiting for vm instance: %w", err)
- }
- condRun(opts.OnInstanceCreated)
- return err
-}
-
-// RetrieveVMInfo retrives the information about a VM.
-func (c *AWSClient) RetrieveVMInfo(ctx context.Context, instID string) (*ec2.Instance, error) {
- instances, err := c.client.DescribeInstancesWithContext(ctx, &ec2.DescribeInstancesInput{
- InstanceIds: []*string{aws.String(instID)},
- })
- if err != nil {
- return nil, fmt.Errorf("unable to retrieve instance %q information: %w", instID, err)
- }
-
- instance, err := ec2Instance(instances)
- if err != nil {
- return nil, fmt.Errorf("failed to read instance description: %w", err)
- }
- return instance, err
-}
-
-// configureVM creates a configuration for an EC2 VM instance.
-func (c *AWSClient) configureVM(buildEnv *buildenv.Environment, hconf *dashboard.HostConfig, vmName, hostType string, opts *VMOpts) *ec2.RunInstancesInput {
- vmConfig := &ec2.RunInstancesInput{
- ImageId: aws.String(hconf.VMImage),
- InstanceType: aws.String(hconf.MachineType()),
- MinCount: aws.Int64(1),
- MaxCount: aws.Int64(1),
- Placement: &ec2.Placement{
- AvailabilityZone: aws.String(opts.Zone),
- },
- KeyName: aws.String("ec2-go-builders"),
- InstanceInitiatedShutdownBehavior: aws.String("terminate"),
- TagSpecifications: []*ec2.TagSpecification{
- &ec2.TagSpecification{
- ResourceType: aws.String("instance"),
- Tags: []*ec2.Tag{
- &ec2.Tag{
- Key: aws.String("Name"),
- Value: aws.String(vmName),
- },
- &ec2.Tag{
- Key: aws.String("Description"),
- Value: aws.String(opts.Description),
- },
- },
- },
- },
- }
- c.vmUserDataSpec(vmConfig, buildEnv, hconf, vmName, hostType, opts)
- return vmConfig
-}
-
-func (c *AWSClient) vmUserDataSpec(vmConfig *ec2.RunInstancesInput, buildEnv *buildenv.Environment, hconf *dashboard.HostConfig, vmName, hostType string, opts *VMOpts) {
- // add custom metadata to the user data.
- ud := 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
- }
- jsonUserData, err := json.Marshal(ud)
- if err != nil {
- log.Printf("unable to marshal user data: %v", err)
- }
- // user data must be base64 encoded
- jud := base64.StdEncoding.EncodeToString([]byte(jsonUserData))
- vmConfig.SetUserData(jud)
-}
-
-// DestroyVM submits a request to destroy a VM.
-func (c *AWSClient) DestroyVM(ctx context.Context, vmID string) error {
- _, err := c.client.TerminateInstancesWithContext(ctx, &ec2.TerminateInstancesInput{
- InstanceIds: []*string{aws.String(vmID)},
- })
- if err != nil {
- return fmt.Errorf("unable to destroy vm: %w", err)
- }
- return err
-}
-
-// ec2Instance extracts the first instance found in the the describe instances output.
-func ec2Instance(dio *ec2.DescribeInstancesOutput) (*ec2.Instance, error) {
- if dio == nil || dio.Reservations == nil || dio.Reservations[0].Instances == nil {
- return nil, errors.New("describe instances output does not contain a valid instance")
- }
- return dio.Reservations[0].Instances[0], nil
-}
-
-// ec2InstanceIPs returns the internal and external ip addresses for the VM.
-func ec2InstanceIPs(inst *ec2.Instance) (intIP, extIP string, err error) {
- if inst.PrivateIpAddress == nil || *inst.PrivateIpAddress == "" {
- return "", "", errors.New("internal IP address is not set")
- }
- if inst.PublicIpAddress == nil || *inst.PublicIpAddress == "" {
- return "", "", errors.New("external IP address is not set")
- }
- return *inst.PrivateIpAddress, *inst.PublicIpAddress, nil
-}
-
-// 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 *ec2.Instance, opts *VMOpts) (string, string, error) {
- _, extIP, err := ec2InstanceIPs(inst)
- if err != nil {
- return "", "", fmt.Errorf("failed to retrieve IP addresses: %w", err)
- }
- buildletURL := fmt.Sprintf("https://%s", extIP)
- ipPort := net.JoinHostPort(extIP, "443")
-
- if opts.OnGotEC2InstanceInfo != nil {
- opts.OnGotEC2InstanceInfo(inst)
- }
- return buildletURL, ipPort, err
-}
diff --git a/buildlet/aws_test.go b/buildlet/aws_test.go
deleted file mode 100644
index d13871b..0000000
--- a/buildlet/aws_test.go
+++ /dev/null
@@ -1,741 +0,0 @@
-// 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"
- "encoding/base64"
- "encoding/json"
- "errors"
- "testing"
- "time"
-
- "github.com/aws/aws-sdk-go/aws"
- "github.com/aws/aws-sdk-go/aws/request"
- "github.com/aws/aws-sdk-go/service/ec2"
- "github.com/google/go-cmp/cmp"
- "golang.org/x/build/buildenv"
- "golang.org/x/build/dashboard"
-)
-
-type fakeEC2Client struct {
- // returned in describe instances
- PrivateIP *string
- PublicIP *string
-}
-
-func (f *fakeEC2Client) DescribeInstancesWithContext(ctx context.Context, input *ec2.DescribeInstancesInput, opt ...request.Option) (*ec2.DescribeInstancesOutput, error) {
- if ctx == nil || input == nil || len(input.InstanceIds) == 0 {
- return nil, request.ErrInvalidParams{}
- }
- return &ec2.DescribeInstancesOutput{
- Reservations: []*ec2.Reservation{
- &ec2.Reservation{
- Instances: []*ec2.Instance{
- &ec2.Instance{
- InstanceId: input.InstanceIds[0],
- PrivateIpAddress: f.PrivateIP,
- PublicIpAddress: f.PublicIP,
- },
- },
- },
- },
- }, nil
-}
-
-func (f *fakeEC2Client) RunInstancesWithContext(ctx context.Context, input *ec2.RunInstancesInput, opts ...request.Option) (*ec2.Reservation, error) {
- if ctx == nil || input == nil {
- return nil, request.ErrInvalidParams{}
- }
- if input.ImageId == nil || input.InstanceType == nil || input.MinCount == nil || input.Placement == nil {
- return nil, errors.New("invalid instance configuration")
- }
- return &ec2.Reservation{
- Instances: []*ec2.Instance{
- &ec2.Instance{
- ImageId: input.ImageId,
- InstanceType: input.InstanceType,
- InstanceId: aws.String("44"),
- Placement: input.Placement,
- },
- },
- ReservationId: aws.String("res_id"),
- }, nil
-}
-
-func (f *fakeEC2Client) TerminateInstancesWithContext(ctx context.Context, input *ec2.TerminateInstancesInput, opts ...request.Option) (*ec2.TerminateInstancesOutput, error) {
- if ctx == nil || input == nil || len(input.InstanceIds) == 0 {
- return nil, request.ErrInvalidParams{}
- }
- for _, id := range input.InstanceIds {
- if *id == "" {
- return nil, errors.New("invalid instance id")
- }
- }
- return &ec2.TerminateInstancesOutput{
- TerminatingInstances: nil,
- }, nil
-}
-
-func (f *fakeEC2Client) WaitUntilInstanceRunningWithContext(ctx context.Context, input *ec2.DescribeInstancesInput, opt ...request.WaiterOption) error {
- if ctx == nil || input == nil || len(input.InstanceIds) == 0 {
- return request.ErrInvalidParams{}
- }
- return nil
-}
-
-func TestRetrieveVMInfo(t *testing.T) {
- wantVMID := "22"
- ctx := context.Background()
- c := &AWSClient{
- client: &fakeEC2Client{},
- }
- gotInst, gotErr := c.RetrieveVMInfo(ctx, wantVMID)
- if gotErr != nil {
- t.Fatalf("RetrieveVMInfo(%v, %q) failed with error %s", ctx, wantVMID, gotErr)
- }
- if gotInst == nil || *gotInst.InstanceId != wantVMID {
- t.Errorf("RetrieveVMInfo(%v, %q) failed with error %s", ctx, wantVMID, gotErr)
- }
-}
-
-func TestStartNewVM(t *testing.T) {
- kp, err := NewKeyPair()
- if err != nil {
- t.Fatalf("unable to generate key pair: %s", err)
- }
- buildEnv := &buildenv.Environment{}
- hconf := &dashboard.HostConfig{}
- vmName := "sample-vm"
- hostType := "host-sample-os"
- opts := &VMOpts{
- Zone: "us-west",
- ProjectID: "project1",
- TLS: kp,
- Description: "Golang builder for sample",
- Meta: map[string]string{
- "Owner": "george",
- },
- DeleteIn: 45 * time.Second,
- SkipEndpointVerification: true,
- }
- c := &AWSClient{
- client: &fakeEC2Client{
- PrivateIP: aws.String("8.8.8.8"),
- PublicIP: aws.String("9.9.9.9"),
- },
- }
- gotClient, gotErr := c.StartNewVM(context.Background(), buildEnv, hconf, vmName, hostType, opts)
- if gotErr != nil {
- t.Fatalf("error is not nil: %v", gotErr)
- }
- if gotClient == nil {
- t.Fatalf("response is nil")
- }
-}
-
-func TestStartNewVMError(t *testing.T) {
- kp, err := NewKeyPair()
- if err != nil {
- t.Fatalf("unable to generate key pair: %s", err)
- }
-
- testCases := []struct {
- desc string
- buildEnv *buildenv.Environment
- hconf *dashboard.HostConfig
- vmName string
- hostType string
- opts *VMOpts
- }{
- {
- desc: "nil-buildenv",
- hconf: &dashboard.HostConfig{},
- vmName: "sample-vm",
- hostType: "host-sample-os",
- opts: &VMOpts{
- Zone: "us-west",
- ProjectID: "project1",
- TLS: kp,
- Description: "Golang builder for sample",
- Meta: map[string]string{
- "Owner": "george",
- },
- DeleteIn: 45 * time.Second,
- },
- },
- {
- desc: "nil-hconf",
- buildEnv: &buildenv.Environment{},
- vmName: "sample-vm",
- hostType: "host-sample-os",
- opts: &VMOpts{
- Zone: "us-west",
- ProjectID: "project1",
- TLS: kp,
- Description: "Golang builder for sample",
- Meta: map[string]string{
- "Owner": "george",
- },
- DeleteIn: 45 * time.Second,
- },
- },
- {
- desc: "empty-vnName",
- buildEnv: &buildenv.Environment{},
- hconf: &dashboard.HostConfig{},
- vmName: "",
- hostType: "host-sample-os",
- opts: &VMOpts{
- Zone: "us-west",
- ProjectID: "project1",
- TLS: kp,
- Description: "Golang builder for sample",
- Meta: map[string]string{
- "Owner": "george",
- },
- DeleteIn: 45 * time.Second,
- },
- },
- {
- desc: "empty-hostType",
- buildEnv: &buildenv.Environment{},
- hconf: &dashboard.HostConfig{},
- vmName: "sample-vm",
- hostType: "",
- opts: &VMOpts{
- Zone: "us-west",
- ProjectID: "project1",
- TLS: kp,
- Description: "Golang builder for sample",
- Meta: map[string]string{
- "Owner": "george",
- },
- DeleteIn: 45 * time.Second,
- },
- },
- {
- desc: "missing-certs",
- buildEnv: &buildenv.Environment{},
- hconf: &dashboard.HostConfig{},
- vmName: "sample-vm",
- hostType: "host-sample-os",
- opts: &VMOpts{
- Zone: "us-west",
- ProjectID: "project1",
- Description: "Golang builder for sample",
- Meta: map[string]string{
- "Owner": "george",
- },
- DeleteIn: 45 * time.Second,
- },
- },
- {
- desc: "nil-opts",
- buildEnv: &buildenv.Environment{},
- hconf: &dashboard.HostConfig{},
- vmName: "sample-vm",
- hostType: "host-sample-os",
- },
- }
- for _, tc := range testCases {
- t.Run(tc.desc, func(t *testing.T) {
- c := &AWSClient{
- client: &fakeEC2Client{},
- }
- gotClient, gotErr := c.StartNewVM(context.Background(), tc.buildEnv, tc.hconf, tc.vmName, tc.hostType, tc.opts)
- if gotErr == nil {
- t.Errorf("expected error did not occur")
- }
- if gotClient != nil {
- t.Errorf("got %+v; expected nil", gotClient)
- }
- })
- }
-}
-
-func TestWaitUntilInstanceExists(t *testing.T) {
- vmID := "22"
- invoked := false
- opts := &VMOpts{
- OnInstanceCreated: func() {
- invoked = true
- },
- }
- ctx := context.Background()
- c := &AWSClient{
- client: &fakeEC2Client{},
- }
- gotErr := c.WaitUntilVMExists(ctx, vmID, opts)
- if gotErr != nil {
- t.Fatalf("WaitUntilVMExists(%v, %v, %v) failed with error %s", ctx, vmID, opts, gotErr)
- }
- if !invoked {
- t.Errorf("OnInstanceCreated() was not invoked")
- }
-}
-
-func TestCreateVM(t *testing.T) {
- vmConfig := &ec2.RunInstancesInput{
- ImageId: aws.String("foo"),
- InstanceType: aws.String("type-a"),
- MinCount: aws.Int64(15),
- Placement: &ec2.Placement{
- AvailabilityZone: aws.String("eu-15"),
- },
- }
- invoked := false
- opts := &VMOpts{
- OnInstanceRequested: func() {
- invoked = true
- },
- }
- wantVMID := aws.String("44")
-
- c := &AWSClient{
- client: &fakeEC2Client{},
- }
- gotVMID, gotErr := c.createVM(context.Background(), vmConfig, opts)
- if gotErr != nil {
- t.Fatalf("createVM(ctx, %v, %v) failed with %s", vmConfig, opts, gotErr)
- }
- if gotVMID != *wantVMID {
- t.Errorf("createVM(ctx, %v, %v) = %s, nil; want %s, nil", vmConfig, opts, gotVMID, *wantVMID)
- }
- if !invoked {
- t.Errorf("OnInstanceRequested() was not invoked")
- }
-}
-
-func TestCreateVMError(t *testing.T) {
- testCases := []struct {
- desc string
- vmConfig *ec2.RunInstancesInput
- opts *VMOpts
- }{
- {
- desc: "missing-vmConfig",
- },
- {
- desc: "missing-image-id",
- vmConfig: &ec2.RunInstancesInput{
- InstanceType: aws.String("type-a"),
- MinCount: aws.Int64(15),
- Placement: &ec2.Placement{
- AvailabilityZone: aws.String("eu-15"),
- },
- },
- opts: &VMOpts{
- OnInstanceRequested: func() {},
- },
- },
- {
- desc: "missing-instance-id",
- vmConfig: &ec2.RunInstancesInput{
- ImageId: aws.String("foo"),
- MinCount: aws.Int64(15),
- Placement: &ec2.Placement{
- AvailabilityZone: aws.String("eu-15"),
- },
- },
- opts: &VMOpts{
- OnInstanceRequested: func() {},
- },
- },
- {
- desc: "missing-placement",
- vmConfig: &ec2.RunInstancesInput{
- ImageId: aws.String("foo"),
- InstanceType: aws.String("type-a"),
- MinCount: aws.Int64(15),
- },
- opts: &VMOpts{
- OnInstanceRequested: func() {},
- },
- },
- }
- for _, tc := range testCases {
- t.Run(tc.desc, func(t *testing.T) {
- c := &AWSClient{
- client: &fakeEC2Client{},
- }
- gotVMID, gotErr := c.createVM(context.Background(), tc.vmConfig, tc.opts)
- if gotErr == nil {
- t.Errorf("createVM(ctx, %v, %v) = %s, %v; want error", tc.vmConfig, tc.opts, gotVMID, gotErr)
- }
- if gotVMID != "" {
- t.Errorf("createVM(ctx, %v, %v) = %s, %v; %q, error", tc.vmConfig, tc.opts, gotVMID, gotErr, "")
- }
- })
- }
-}
-
-func TestDestroyVM(t *testing.T) {
- testCases := []struct {
- desc string
- ctx context.Context
- vmID string
- wantErr bool
- }{
- {"baseline request", context.Background(), "vm-20", false},
- {"nil context", nil, "vm-20", true},
- {"nil context", context.Background(), "", true},
- }
- for _, tc := range testCases {
- t.Run(tc.desc, func(t *testing.T) {
- c := &AWSClient{
- client: &fakeEC2Client{},
- }
- gotErr := c.DestroyVM(tc.ctx, tc.vmID)
- if (gotErr != nil) != tc.wantErr {
- t.Errorf("DestroyVM(%v, %q) = %v; want error %t", tc.ctx, tc.vmID, gotErr, tc.wantErr)
- }
- })
- }
-}
-
-func TestEC2BuildletParams(t *testing.T) {
- testCases := []struct {
- desc string
- inst *ec2.Instance
- opts *VMOpts
- wantURL string
- wantPort string
- wantCalled bool
- }{
- {
- desc: "base case",
- inst: &ec2.Instance{
- PrivateIpAddress: aws.String("9.9.9.9"),
- PublicIpAddress: aws.String("8.8.8.8"),
- },
- opts: &VMOpts{},
- wantCalled: true,
- wantURL: "https://8.8.8.8",
- wantPort: "8.8.8.8:443",
- },
- }
- for _, tc := range testCases {
- t.Run(tc.desc, func(t *testing.T) {
- gotURL, gotPort, gotErr := ec2BuildletParams(tc.inst, tc.opts)
- if gotErr != nil {
- t.Fatalf("ec2BuildletParams(%v, %v) failed; %v", tc.inst, tc.opts, gotErr)
- }
- if gotURL != tc.wantURL || gotPort != tc.wantPort {
- t.Errorf("ec2BuildletParams(%v, %v) = %q, %q, nil; want %q, %q, nil", tc.inst, tc.opts, gotURL, gotPort, tc.wantURL, tc.wantPort)
- }
- })
- }
-}
-
-func TestConfigureVM(t *testing.T) {
- testCases := []struct {
- desc string
- buildEnv *buildenv.Environment
- hconf *dashboard.HostConfig
- hostType string
- opts *VMOpts
- vmName string
- wantDesc string
- wantImageID string
- wantInstanceType string
- wantName string
- wantZone string
- wantBuildletName string
- wantBuildletImage string
- }{
- {
- desc: "default-values",
- buildEnv: &buildenv.Environment{},
- hconf: &dashboard.HostConfig{
- KonletVMImage: "gcr.io/symbolic-datum-552/gobuilder-arm64-aws",
- },
- vmName: "base_vm",
- hostType: "host-foo-bar",
- opts: &VMOpts{},
- wantInstanceType: "n1-highcpu-2",
- wantName: "base_vm",
- wantBuildletName: "base_vm",
- wantBuildletImage: "gcr.io/symbolic-datum-552/gobuilder-arm64-aws",
- },
- {
- desc: "full-configuration",
- buildEnv: &buildenv.Environment{},
- hconf: &dashboard.HostConfig{
- VMImage: "awesome_image",
- KonletVMImage: "gcr.io/symbolic-datum-552/gobuilder-arm64-aws",
- },
- vmName: "base-vm",
- hostType: "host-foo-bar",
- opts: &VMOpts{
- Zone: "sa-west",
- TLS: KeyPair{
- CertPEM: "abc",
- KeyPEM: "xyz",
- },
- Description: "test description",
- Meta: map[string]string{
- "sample": "value",
- },
- },
- wantDesc: "test description",
- wantImageID: "awesome_image",
- wantInstanceType: "n1-highcpu-2",
- wantName: "base-vm",
- wantZone: "sa-west",
- wantBuildletName: "base-vm",
- wantBuildletImage: "gcr.io/symbolic-datum-552/gobuilder-arm64-aws",
- },
- }
- for _, tc := range testCases {
- t.Run(tc.desc, func(t *testing.T) {
- c := &AWSClient{}
- got := c.configureVM(tc.buildEnv, tc.hconf, tc.vmName, tc.hostType, tc.opts)
- if *got.ImageId != tc.wantImageID {
- t.Errorf("ImageId got %s; want %s", *got.ImageId, tc.wantImageID)
- }
- if *got.InstanceType != tc.wantInstanceType {
- t.Errorf("InstanceType got %s; want %s", *got.InstanceType, tc.wantInstanceType)
- }
-
- if *got.MinCount != 1 {
- t.Errorf("MinCount got %d; want %d", *got.MinCount, 1)
- }
- if *got.MaxCount != 1 {
- t.Errorf("MaxCount got %d; want %d", *got.MaxCount, 1)
- }
- if *got.Placement.AvailabilityZone != tc.wantZone {
- t.Errorf("AvailabilityZone got %s; want %s", *got.Placement.AvailabilityZone, tc.wantZone)
- }
- if *got.InstanceInitiatedShutdownBehavior != "terminate" {
- t.Errorf("InstanceType got %s; want %s", *got.InstanceInitiatedShutdownBehavior, "terminate")
- }
- if *got.TagSpecifications[0].ResourceType != "instance" {
- t.Errorf("Tag Resource Type got %s; want %s", *got.TagSpecifications[0].ResourceType, "instance")
- }
- if *got.TagSpecifications[0].Tags[0].Key != "Name" {
- t.Errorf("First Tag Key got %s; want %s", *got.TagSpecifications[0].Tags[0].Key, "Name")
- }
- if *got.TagSpecifications[0].Tags[0].Value != tc.wantName {
- t.Errorf("First Tag Value got %s; want %s", *got.TagSpecifications[0].Tags[0].Value, tc.wantName)
- }
- if *got.TagSpecifications[0].Tags[1].Key != "Description" {
- t.Errorf("Second Tag Key got %s; want %s", *got.TagSpecifications[0].Tags[1].Key, "Description")
- }
- if *got.TagSpecifications[0].Tags[1].Value != tc.wantDesc {
- t.Errorf("Second Tag Value got %s; want %s", *got.TagSpecifications[0].Tags[1].Value, tc.wantDesc)
- }
- gotUD := &EC2UserData{}
- gotUDJson, err := base64.StdEncoding.DecodeString(*got.UserData)
- if err != nil {
- t.Fatalf("unable to base64 decode string %q: %s", *got.UserData, err)
- }
- err = json.Unmarshal([]byte(gotUDJson), gotUD)
- if err != nil {
- t.Errorf("unable to unmarshal user data: %v", err)
- }
- if gotUD.BuildletBinaryURL != tc.hconf.BuildletBinaryURL(tc.buildEnv) {
- t.Errorf("buildletBinaryURL got %s; want %s", gotUD.BuildletBinaryURL, tc.hconf.BuildletBinaryURL(tc.buildEnv))
- }
- if gotUD.BuildletHostType != tc.hostType {
- t.Errorf("buildletHostType got %s; want %s", gotUD.BuildletHostType, tc.hostType)
- }
- if gotUD.BuildletName != tc.wantBuildletName {
- t.Errorf("buildletName got %s; want %s", gotUD.BuildletName, tc.wantBuildletName)
- }
- if gotUD.BuildletImageURL != tc.wantBuildletImage {
- t.Errorf("buildletImageURL got %s; want %s", gotUD.BuildletImageURL, tc.wantBuildletImage)
- }
-
- if gotUD.TLSCert != tc.opts.TLS.CertPEM {
- t.Errorf("TLSCert got %s; want %s", gotUD.TLSCert, tc.opts.TLS.CertPEM)
- }
- if gotUD.TLSKey != tc.opts.TLS.KeyPEM {
- t.Errorf("TLSKey got %s; want %s", gotUD.TLSKey, tc.opts.TLS.KeyPEM)
- }
- if gotUD.TLSPassword != tc.opts.TLS.Password() {
- t.Errorf("TLSPassword got %s; want %s", gotUD.TLSPassword, tc.opts.TLS.Password())
- }
- })
- }
-}
-
-func TestEC2Instance(t *testing.T) {
- instSample1 := &ec2.Instance{
- InstanceId: aws.String("id1"),
- }
- instSample2 := &ec2.Instance{
- InstanceId: aws.String("id2"),
- }
- resSample1 := &ec2.Reservation{
- Instances: []*ec2.Instance{
- instSample1,
- },
- RequesterId: aws.String("user1"),
- ReservationId: aws.String("reservation12"),
- }
- resSample2 := &ec2.Reservation{
- Instances: []*ec2.Instance{
- instSample2,
- },
- RequesterId: aws.String("user2"),
- ReservationId: aws.String("reservation22"),
- }
-
- testCases := []struct {
- desc string
- dio *ec2.DescribeInstancesOutput
- wantInst *ec2.Instance
- }{
- {
- desc: "single reservation",
- dio: &ec2.DescribeInstancesOutput{
- Reservations: []*ec2.Reservation{
- resSample1,
- },
- },
- wantInst: instSample1,
- },
- {
- desc: "multiple reservations",
- dio: &ec2.DescribeInstancesOutput{
- Reservations: []*ec2.Reservation{
- resSample2,
- resSample1,
- },
- },
- wantInst: instSample2,
- },
- }
- for _, tc := range testCases {
- t.Run(tc.desc, func(t *testing.T) {
- gotInst, gotErr := ec2Instance(tc.dio)
- if gotErr != nil {
- t.Errorf("ec2Instance(%v) failed: %v",
- tc.dio, gotErr)
- }
- if !cmp.Equal(gotInst, tc.wantInst) {
- t.Errorf("ec2Instance(%v) = %s; want %s",
- tc.dio, gotInst, tc.wantInst)
- }
- })
- }
-}
-
-func TestEC2InstanceError(t *testing.T) {
- testCases := []struct {
- desc string
- dio *ec2.DescribeInstancesOutput
- }{
- {
- desc: "nil input",
- dio: nil,
- },
- {
- desc: "nil reservation",
- dio: &ec2.DescribeInstancesOutput{
- Reservations: nil,
- },
- },
- {
- desc: "nil instances",
- dio: &ec2.DescribeInstancesOutput{
- Reservations: []*ec2.Reservation{
- &ec2.Reservation{
- Instances: nil,
- RequesterId: aws.String("user1"),
- ReservationId: aws.String("reservation12"),
- },
- },
- },
- },
- }
- for _, tc := range testCases {
- t.Run(tc.desc, func(t *testing.T) {
- _, gotErr := ec2Instance(tc.dio)
- if gotErr == nil {
- t.Errorf("ec2Instance(%v) did not fail", tc.dio)
- }
- })
- }
-}
-
-func TestEC2InstanceIPs(t *testing.T) {
- testCases := []struct {
- desc string
- inst *ec2.Instance
- wantIntIP string
- wantExtIP string
- }{
- {
- desc: "base case",
- inst: &ec2.Instance{
- PrivateIpAddress: aws.String("1.1.1.1"),
- PublicIpAddress: aws.String("8.8.8.8"),
- },
- wantIntIP: "1.1.1.1",
- wantExtIP: "8.8.8.8",
- },
- }
- for _, tc := range testCases {
- t.Run(tc.desc, func(t *testing.T) {
- gotIntIP, gotExtIP, gotErr := ec2InstanceIPs(tc.inst)
- if gotErr != nil {
- t.Errorf("ec2InstanceIPs(%v) failed: %v",
- tc.inst, gotErr)
- }
- if gotIntIP != tc.wantIntIP || gotExtIP != tc.wantExtIP {
- t.Errorf("ec2InstanceIPs(%v) = %s, %s, %v; want %s, %s, nil",
- tc.inst, gotIntIP, gotExtIP, gotErr, tc.wantIntIP, tc.wantExtIP)
- }
- })
- }
-}
-
-func TestEC2InstanceIPsErrors(t *testing.T) {
- testCases := []struct {
- desc string
- inst *ec2.Instance
- }{
- {
- desc: "default vallues",
- inst: &ec2.Instance{},
- },
- {
- desc: "missing public ip",
- inst: &ec2.Instance{
- PrivateIpAddress: aws.String("1.1.1.1"),
- },
- },
- {
- desc: "missing private ip",
- inst: &ec2.Instance{
- PublicIpAddress: aws.String("8.8.8.8"),
- },
- },
- {
- desc: "empty public ip",
- inst: &ec2.Instance{
- PrivateIpAddress: aws.String("1.1.1.1"),
- PublicIpAddress: aws.String(""),
- },
- },
- {
- desc: "empty private ip",
- inst: &ec2.Instance{
- PrivateIpAddress: aws.String(""),
- PublicIpAddress: aws.String("8.8.8.8"),
- },
- },
- }
- for _, tc := range testCases {
- t.Run(tc.desc, func(t *testing.T) {
- _, _, gotErr := ec2InstanceIPs(tc.inst)
- if gotErr == nil {
- t.Errorf("ec2InstanceIPs(%v) = nil: want error", tc.inst)
- }
- })
- }
-}
diff --git a/buildlet/buildlet.go b/buildlet/buildlet.go
index 2e6e911..9a58a90 100644
--- a/buildlet/buildlet.go
+++ b/buildlet/buildlet.go
@@ -13,7 +13,7 @@
"net/http"
"time"
- "github.com/aws/aws-sdk-go/service/ec2"
+ "golang.org/x/build/internal/cloud"
"google.golang.org/api/compute/v1"
)
@@ -61,7 +61,7 @@
// OnInstanceCreated optionally specifies a hook to run synchronously
// after the EC2 instance information is retrieved.
// Only valid for EC2 resources.
- OnGotEC2InstanceInfo func(*ec2.Instance)
+ OnGotEC2InstanceInfo func(*cloud.Instance)
// OnBeginBuildletProbe optionally specifies a hook to run synchronously
// before StartNewVM tries to hit buildletURL to see if it's up yet.
diff --git a/buildlet/ec2.go b/buildlet/ec2.go
new file mode 100644
index 0000000..4a52b20
--- /dev/null
+++ b/buildlet/ec2.go
@@ -0,0 +1,159 @@
+// 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 durring 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 enviornment")
+ }
+ 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.Zone == "" {
+ opts.Zone = buildEnv.RandomEC2VMZone()
+ }
+ if opts.DeleteIn == 0 {
+ 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
+}
diff --git a/buildlet/ec2_test.go b/buildlet/ec2_test.go
new file mode 100644
index 0000000..8fe1ae6
--- /dev/null
+++ b/buildlet/ec2_test.go
@@ -0,0 +1,451 @@
+// 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"
+ "encoding/base64"
+ "encoding/json"
+ "testing"
+ "time"
+
+ "golang.org/x/build/buildenv"
+ "golang.org/x/build/dashboard"
+ "golang.org/x/build/internal/cloud"
+)
+
+func TestStartNewVM(t *testing.T) {
+ kp, err := NewKeyPair()
+ if err != nil {
+ t.Fatalf("unable to generate key pair: %s", err)
+ }
+ buildEnv := &buildenv.Environment{}
+ hconf := &dashboard.HostConfig{
+ VMImage: "image-x",
+ }
+ vmName := "sample-vm"
+ hostType := "host-sample-os"
+ opts := &VMOpts{
+ Zone: "us-west",
+ ProjectID: "project1",
+ TLS: kp,
+ Description: "Golang builder for sample",
+ Meta: map[string]string{
+ "Owner": "george",
+ },
+ DeleteIn: 45 * time.Second,
+ SkipEndpointVerification: true,
+ }
+ c := &EC2Client{
+ client: cloud.NewFakeAWSClient(),
+ }
+ gotClient, gotErr := c.StartNewVM(context.Background(), buildEnv, hconf, vmName, hostType, opts)
+ if gotErr != nil {
+ t.Fatalf("error is not nil: %v", gotErr)
+ }
+ if gotClient == nil {
+ t.Fatalf("response is nil")
+ }
+}
+
+func TestStartNewVMError(t *testing.T) {
+ kp, err := NewKeyPair()
+ if err != nil {
+ t.Fatalf("unable to generate key pair: %s", err)
+ }
+
+ testCases := []struct {
+ desc string
+ buildEnv *buildenv.Environment
+ hconf *dashboard.HostConfig
+ vmName string
+ hostType string
+ opts *VMOpts
+ }{
+ {
+ desc: "nil-buildenv",
+ hconf: &dashboard.HostConfig{},
+ vmName: "sample-vm",
+ hostType: "host-sample-os",
+ opts: &VMOpts{
+ Zone: "us-west",
+ ProjectID: "project1",
+ TLS: kp,
+ Description: "Golang builder for sample",
+ Meta: map[string]string{
+ "Owner": "george",
+ },
+ DeleteIn: 45 * time.Second,
+ },
+ },
+ {
+ desc: "nil-hconf",
+ buildEnv: &buildenv.Environment{},
+ vmName: "sample-vm",
+ hostType: "host-sample-os",
+ opts: &VMOpts{
+ Zone: "us-west",
+ ProjectID: "project1",
+ TLS: kp,
+ Description: "Golang builder for sample",
+ Meta: map[string]string{
+ "Owner": "george",
+ },
+ DeleteIn: 45 * time.Second,
+ },
+ },
+ {
+ desc: "empty-vnName",
+ buildEnv: &buildenv.Environment{},
+ hconf: &dashboard.HostConfig{},
+ vmName: "",
+ hostType: "host-sample-os",
+ opts: &VMOpts{
+ Zone: "us-west",
+ ProjectID: "project1",
+ TLS: kp,
+ Description: "Golang builder for sample",
+ Meta: map[string]string{
+ "Owner": "george",
+ },
+ DeleteIn: 45 * time.Second,
+ },
+ },
+ {
+ desc: "empty-hostType",
+ buildEnv: &buildenv.Environment{},
+ hconf: &dashboard.HostConfig{},
+ vmName: "sample-vm",
+ hostType: "",
+ opts: &VMOpts{
+ Zone: "us-west",
+ ProjectID: "project1",
+ TLS: kp,
+ Description: "Golang builder for sample",
+ Meta: map[string]string{
+ "Owner": "george",
+ },
+ DeleteIn: 45 * time.Second,
+ },
+ },
+ {
+ desc: "missing-certs",
+ buildEnv: &buildenv.Environment{},
+ hconf: &dashboard.HostConfig{},
+ vmName: "sample-vm",
+ hostType: "host-sample-os",
+ opts: &VMOpts{
+ Zone: "us-west",
+ ProjectID: "project1",
+ Description: "Golang builder for sample",
+ Meta: map[string]string{
+ "Owner": "george",
+ },
+ DeleteIn: 45 * time.Second,
+ },
+ },
+ {
+ desc: "nil-opts",
+ buildEnv: &buildenv.Environment{},
+ hconf: &dashboard.HostConfig{},
+ vmName: "sample-vm",
+ hostType: "host-sample-os",
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ c := &EC2Client{
+ client: cloud.NewFakeAWSClient(),
+ }
+ gotClient, gotErr := c.StartNewVM(context.Background(), tc.buildEnv, tc.hconf, tc.vmName, tc.hostType, tc.opts)
+ if gotErr == nil {
+ t.Errorf("StartNewVM(ctx, %+v, %+v, %s, %s, %+v) = %+v, nil; want error", tc.buildEnv, tc.hconf, tc.vmName, tc.hostType, tc.opts, gotClient)
+ }
+ if gotClient != nil {
+ t.Errorf("got %+v; expected nil", gotClient)
+ }
+ })
+ }
+}
+
+func TestWaitUntilInstanceExists(t *testing.T) {
+ vmConfig := &cloud.EC2VMConfiguration{
+ ImageID: "foo",
+ Type: "type-a",
+ Zone: "eu-15",
+ }
+ invoked := false
+ opts := &VMOpts{
+ OnInstanceCreated: func() {
+ invoked = true
+ },
+ }
+ ctx := context.Background()
+ c := &EC2Client{
+ client: cloud.NewFakeAWSClient(),
+ }
+ gotVM, gotErr := c.createVM(ctx, vmConfig, opts)
+ if gotErr != nil {
+ t.Fatalf("createVM(ctx, %v, %v) failed with %s", vmConfig, opts, gotErr)
+ }
+ gotErr = c.WaitUntilVMExists(ctx, gotVM.ID, opts)
+ if gotErr != nil {
+ t.Fatalf("WaitUntilVMExists(%v, %v, %v) failed with error %s", ctx, gotVM.ID, opts, gotErr)
+ }
+ if !invoked {
+ t.Errorf("OnInstanceCreated() was not invoked")
+ }
+}
+
+func TestCreateVM(t *testing.T) {
+ vmConfig := &cloud.EC2VMConfiguration{
+ ImageID: "foo",
+ Type: "type-a",
+ Zone: "eu-15",
+ }
+ invoked := false
+ opts := &VMOpts{
+ OnInstanceRequested: func() {
+ invoked = true
+ },
+ }
+ c := &EC2Client{
+ client: cloud.NewFakeAWSClient(),
+ }
+ gotVM, gotErr := c.createVM(context.Background(), vmConfig, opts)
+ if gotErr != nil {
+ t.Fatalf("createVM(ctx, %v, %v) failed with %s", vmConfig, opts, gotErr)
+ }
+ if gotVM.ImageID != vmConfig.ImageID || gotVM.Type != vmConfig.Type || gotVM.Zone != vmConfig.Zone {
+ t.Errorf("createVM(ctx, %+v, %+v) = %+v, nil; want vm to match config", vmConfig, opts, gotVM)
+ }
+ if !invoked {
+ t.Errorf("OnInstanceRequested() was not invoked")
+ }
+}
+
+func TestCreateVMError(t *testing.T) {
+ testCases := []struct {
+ desc string
+ vmConfig *cloud.EC2VMConfiguration
+ opts *VMOpts
+ }{
+ {
+ desc: "missing-vmConfig",
+ },
+ {
+ desc: "missing-image-id",
+ vmConfig: &cloud.EC2VMConfiguration{
+ Type: "type-a",
+ Zone: "eu-15",
+ },
+ opts: &VMOpts{
+ OnInstanceRequested: func() {},
+ },
+ },
+ {
+ desc: "missing-instance-id",
+ vmConfig: &cloud.EC2VMConfiguration{
+ ImageID: "foo",
+ Zone: "eu-15",
+ },
+ opts: &VMOpts{
+ OnInstanceRequested: func() {},
+ },
+ },
+ {
+ desc: "missing-placement",
+ vmConfig: &cloud.EC2VMConfiguration{
+ Name: "foo",
+ Type: "type-a",
+ },
+ opts: &VMOpts{
+ OnInstanceRequested: func() {},
+ },
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ c := &EC2Client{
+ client: cloud.NewFakeAWSClient(),
+ //client: &fakeAWSClient{},
+ }
+ gotVM, gotErr := c.createVM(context.Background(), tc.vmConfig, tc.opts)
+ if gotErr == nil {
+ t.Errorf("createVM(ctx, %v, %v) = %s, %v; want error", tc.vmConfig, tc.opts, gotVM.ID, gotErr)
+ }
+ if gotVM != nil {
+ t.Errorf("createVM(ctx, %v, %v) = %s, %v; %q, error", tc.vmConfig, tc.opts, gotVM.ID, gotErr, "")
+ }
+ })
+ }
+}
+
+func TestEC2BuildletParams(t *testing.T) {
+ testCases := []struct {
+ desc string
+ inst *cloud.Instance
+ opts *VMOpts
+ wantURL string
+ wantPort string
+ wantCalled bool
+ wantErr bool
+ }{
+ {
+ desc: "base-case",
+ inst: &cloud.Instance{
+ IPAddressExternal: "8.8.8.8",
+ IPAddressInternal: "3.3.3.3",
+ },
+ opts: &VMOpts{},
+ wantCalled: true,
+ wantURL: "https://8.8.8.8",
+ wantPort: "8.8.8.8:443",
+ wantErr: false,
+ },
+ {
+ desc: "missing-int-ip",
+ inst: &cloud.Instance{
+ IPAddressExternal: "8.8.8.8",
+ },
+ opts: &VMOpts{},
+ wantCalled: true,
+ wantURL: "https://8.8.8.8",
+ wantPort: "8.8.8.8:443",
+ wantErr: false,
+ },
+ {
+ desc: "missing-ext-ip",
+ inst: &cloud.Instance{
+ IPAddressInternal: "3.3.3.3",
+ },
+ opts: &VMOpts{},
+ wantCalled: true,
+ wantURL: "",
+ wantPort: "",
+ wantErr: true,
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ gotURL, gotPort, gotErr := ec2BuildletParams(tc.inst, tc.opts)
+ if gotURL != tc.wantURL || gotPort != tc.wantPort || tc.wantErr != (gotErr != nil) {
+ t.Errorf("ec2BuildletParams(%v, %v) = %q, %q, nil; want %q, %q, nil", tc.inst, tc.opts, gotURL, gotPort, tc.wantURL, tc.wantPort)
+ }
+ })
+ }
+}
+
+func TestConfigureVM(t *testing.T) {
+ testCases := []struct {
+ desc string
+ buildEnv *buildenv.Environment
+ hconf *dashboard.HostConfig
+ hostType string
+ opts *VMOpts
+ vmName string
+ wantDesc string
+ wantImageID string
+ wantInstanceType string
+ wantName string
+ wantZone string
+ wantBuildletName string
+ wantBuildletImage string
+ }{
+ {
+ desc: "default-values",
+ buildEnv: &buildenv.Environment{},
+ hconf: &dashboard.HostConfig{
+ KonletVMImage: "gcr.io/symbolic-datum-552/gobuilder-arm64-aws",
+ },
+ vmName: "base_vm",
+ hostType: "host-foo-bar",
+ opts: &VMOpts{},
+ wantInstanceType: "n1-highcpu-2",
+ wantName: "base_vm",
+ wantBuildletName: "base_vm",
+ wantBuildletImage: "gcr.io/symbolic-datum-552/gobuilder-arm64-aws",
+ },
+ {
+ desc: "full-configuration",
+ buildEnv: &buildenv.Environment{},
+ hconf: &dashboard.HostConfig{
+ VMImage: "awesome_image",
+ KonletVMImage: "gcr.io/symbolic-datum-552/gobuilder-arm64-aws",
+ },
+ vmName: "base-vm",
+ hostType: "host-foo-bar",
+ opts: &VMOpts{
+ Zone: "sa-west",
+ TLS: KeyPair{
+ CertPEM: "abc",
+ KeyPEM: "xyz",
+ },
+ Description: "test description",
+ Meta: map[string]string{
+ "sample": "value",
+ },
+ },
+ wantDesc: "test description",
+ wantImageID: "awesome_image",
+ wantInstanceType: "n1-highcpu-2",
+ wantName: "base-vm",
+ wantZone: "sa-west",
+ wantBuildletName: "base-vm",
+ wantBuildletImage: "gcr.io/symbolic-datum-552/gobuilder-arm64-aws",
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ got := configureVM(tc.buildEnv, tc.hconf, tc.vmName, tc.hostType, tc.opts)
+ if got.ImageID != tc.wantImageID {
+ t.Errorf("ImageId got %s; want %s", got.ImageID, tc.wantImageID)
+ }
+ if got.Type != tc.wantInstanceType {
+ t.Errorf("Type got %s; want %s", got.Type, tc.wantInstanceType)
+ }
+ if got.Zone != tc.wantZone {
+ t.Errorf("Zone got %s; want %s", got.Zone, tc.wantZone)
+ }
+ if got.Name != tc.wantName {
+ t.Errorf("Name got %s; want %s", got.Name, tc.wantName)
+ }
+ if got.Description != tc.wantDesc {
+ t.Errorf("Description got %s; want %s", got.Description, tc.wantDesc)
+ }
+ gotUDJson, err := base64.StdEncoding.DecodeString(got.UserData)
+ if err != nil {
+ t.Fatalf("unable to base64 decode string %q: %s", got.UserData, err)
+ }
+ gotUD := &cloud.EC2UserData{}
+ err = json.Unmarshal([]byte(gotUDJson), gotUD)
+ if err != nil {
+ t.Errorf("unable to unmarshal user data: %v", err)
+ }
+ if gotUD.BuildletBinaryURL != tc.hconf.BuildletBinaryURL(tc.buildEnv) {
+ t.Errorf("buildletBinaryURL got %s; want %s", gotUD.BuildletBinaryURL, tc.hconf.BuildletBinaryURL(tc.buildEnv))
+ }
+ if gotUD.BuildletHostType != tc.hostType {
+ t.Errorf("buildletHostType got %s; want %s", gotUD.BuildletHostType, tc.hostType)
+ }
+ if gotUD.BuildletName != tc.wantBuildletName {
+ t.Errorf("buildletName got %s; want %s", gotUD.BuildletName, tc.wantBuildletName)
+ }
+ if gotUD.BuildletImageURL != tc.wantBuildletImage {
+ t.Errorf("buildletImageURL got %s; want %s", gotUD.BuildletImageURL, tc.wantBuildletImage)
+ }
+
+ if gotUD.TLSCert != tc.opts.TLS.CertPEM {
+ t.Errorf("TLSCert got %s; want %s", gotUD.TLSCert, tc.opts.TLS.CertPEM)
+ }
+ if gotUD.TLSKey != tc.opts.TLS.KeyPEM {
+ t.Errorf("TLSKey got %s; want %s", gotUD.TLSKey, tc.opts.TLS.KeyPEM)
+ }
+ if gotUD.TLSPassword != tc.opts.TLS.Password() {
+ t.Errorf("TLSPassword got %s; want %s", gotUD.TLSPassword, tc.opts.TLS.Password())
+ }
+ })
+ }
+}
diff --git a/cmd/buildlet/buildlet.go b/cmd/buildlet/buildlet.go
index a48f884..2aa8c79 100644
--- a/cmd/buildlet/buildlet.go
+++ b/cmd/buildlet/buildlet.go
@@ -44,6 +44,7 @@
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"golang.org/x/build/buildlet"
+ "golang.org/x/build/internal/cloud"
"golang.org/x/build/pargzip"
)
@@ -321,7 +322,7 @@
var (
// ec2UD contains a copy of the EC2 vm user data retrieved from the metadata.
- ec2UD *buildlet.EC2UserData
+ ec2UD *cloud.EC2UserData
// ec2MdC is an EC2 metadata client.
ec2MdC *ec2metadata.EC2Metadata
)
@@ -342,7 +343,7 @@
// mdValueFromUserData maps a metadata key value into the corresponding
// EC2UserData value. If a mapping is not found, an empty string is returned.
-func mdValueFromUserData(ud *buildlet.EC2UserData, key string) string {
+func mdValueFromUserData(ud *cloud.EC2UserData, key string) string {
switch key {
case metaKeyTLSCert:
return ud.TLSCert
@@ -385,7 +386,7 @@
if err != nil {
log.Fatalf("unable to retrieve EC2 user data: %v", err)
}
- ec2UD = &buildlet.EC2UserData{}
+ ec2UD = &cloud.EC2UserData{}
err = json.Unmarshal([]byte(ec2MetaJson), ec2UD)
if err != nil {
log.Fatalf("unable to unmarshal user data json: %v", err)
diff --git a/cmd/rundockerbuildlet/rundockerbuildlet.go b/cmd/rundockerbuildlet/rundockerbuildlet.go
index 59805e3..1ad6c00 100644
--- a/cmd/rundockerbuildlet/rundockerbuildlet.go
+++ b/cmd/rundockerbuildlet/rundockerbuildlet.go
@@ -27,7 +27,7 @@
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"golang.org/x/build/buildenv"
- "golang.org/x/build/buildlet"
+ "golang.org/x/build/internal/cloud"
)
var (
@@ -47,7 +47,7 @@
isReverse = true
isSingleRun = false
// ec2UD contains a copy of the EC2 vm user data retrieved from the metadata.
- ec2UD *buildlet.EC2UserData
+ ec2UD *cloud.EC2UserData
// ec2MetaClient is an EC2 metadata client.
ec2MetaClient *ec2metadata.EC2Metadata
)
@@ -301,7 +301,7 @@
if err != nil {
log.Fatalf("unable to retrieve EC2 user data: %v", err)
}
- ec2UD = &buildlet.EC2UserData{}
+ ec2UD = &cloud.EC2UserData{}
err = json.Unmarshal([]byte(ec2MetaJson), ec2UD)
if err != nil {
log.Fatalf("unable to unmarshal user data json: %v", err)