buildlet: modify the request logic and parameters for EC2 instances

This change makes multiple changes to the creation of
EC2 instances via the AWS buildlet:
- Adds a tag resource type.
- Adds an ssh key.
- Instead of waiting for an instance to exist, it now
  waits for the instance to be running.
- Renames the user data type.
- It base64 encodes the user data.

Updates golang/go#36841

Change-Id: I90e98dfa11f3a14478580ba4ca4a79724c085be9
Reviewed-on: https://go-review.googlesource.com/c/build/+/233798
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
index a24aca5..8842fc3 100644
--- a/buildlet/aws.go
+++ b/buildlet/aws.go
@@ -6,6 +6,7 @@
 
 import (
 	"context"
+	"encoding/base64"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -13,20 +14,19 @@
 	"net"
 	"time"
 
-	"golang.org/x/build/buildenv"
-	"golang.org/x/build/dashboard"
-
 	"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"
 )
 
-// AWSUserData is stored in the user data for each EC2 instance. This is
+// 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 AWSUserData struct {
+type EC2UserData struct {
 	BuildletBinaryURL string            `json:"buildlet_binary_url,omitempty"`
 	BuildletHostType  string            `json:"buildlet_host_type,omitempty"`
 	Metadata          map[string]string `json:"metadata,omitempty"`
@@ -41,7 +41,7 @@
 	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)
-	WaitUntilInstanceExistsWithContext(context.Context, *ec2.DescribeInstancesInput, ...request.WaiterOption) error
+	WaitUntilInstanceRunningWithContext(context.Context, *ec2.DescribeInstancesInput, ...request.WaiterOption) error
 }
 
 // AWSClient is the client used to create and destroy buildlets on AWS.
@@ -92,6 +92,7 @@
 	}
 
 	vmConfig := c.configureVM(buildEnv, hconf, vmName, hostType, opts)
+
 	vmID, err := c.createVM(ctx, vmConfig, opts)
 	if err != nil {
 		return nil, err
@@ -122,7 +123,7 @@
 
 // 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.WaitUntilInstanceExistsWithContext(ctx, &ec2.DescribeInstancesInput{
+	err := c.client.WaitUntilInstanceRunningWithContext(ctx, &ec2.DescribeInstancesInput{
 		InstanceIds: []*string{aws.String(instID)},
 	})
 	if err != nil {
@@ -158,9 +159,11 @@
 		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"),
@@ -174,9 +177,13 @@
 			},
 		},
 	}
+	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 := AWSUserData{
+	ud := EC2UserData{
 		BuildletBinaryURL: hconf.BuildletBinaryURL(buildEnv),
 		BuildletHostType:  hostType,
 		TLSCert:           opts.TLS.CertPEM,
@@ -191,7 +198,9 @@
 	if err != nil {
 		log.Printf("unable to marshal user data: %v", err)
 	}
-	return vmConfig.SetUserData(string(jsonUserData))
+	// user data must be base64 encoded
+	jud := base64.StdEncoding.EncodeToString([]byte(jsonUserData))
+	vmConfig.SetUserData(jud)
 }
 
 // DestroyVM submits a request to destroy a VM.
diff --git a/buildlet/aws_test.go b/buildlet/aws_test.go
index 26d3537..1815f02 100644
--- a/buildlet/aws_test.go
+++ b/buildlet/aws_test.go
@@ -6,6 +6,7 @@
 
 import (
 	"context"
+	"encoding/base64"
 	"encoding/json"
 	"errors"
 	"testing"
@@ -78,7 +79,7 @@
 	}, nil
 }
 
-func (f *fakeEC2Client) WaitUntilInstanceExistsWithContext(ctx context.Context, input *ec2.DescribeInstancesInput, opt ...request.WaiterOption) error {
+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{}
 	}
@@ -502,6 +503,9 @@
 			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")
 			}
@@ -514,8 +518,12 @@
 			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 := &AWSUserData{}
-			err := json.Unmarshal([]byte(*got.UserData), &gotUD)
+			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)
 			}
diff --git a/buildlet/buildlet_test.go b/buildlet/buildlet_test.go
index eae9317..e270055 100644
--- a/buildlet/buildlet_test.go
+++ b/buildlet/buildlet_test.go
@@ -92,10 +92,10 @@
 		t.Error("http endpoint called")
 	}
 	if OnBeginBuildletProbeCalled {
-		t.Error("OnBeginBuildletProbe() was not called")
+		t.Error("OnBeginBuildletProbe() was called")
 	}
 	if OnEndBuildletProbeCalled {
-		t.Error("OnEndBuildletProbe() was not called")
+		t.Error("OnEndBuildletProbe() was called")
 	}
 }