cmd/rundockerbuildlet, buildlet: enable running arm EC2 instances

This enables rundockerbuildlet to run non-reverse buildlet
image on EC2. It will only run a single instance of rundockerbuildlet
once. It exposes port 443 for the coordinator to authenticate with the running
buildlet.

This also adds the buildlet name and buildlet container URL to
the EC2 user data struct retrieved by rundockerbuildlet.

Updates golang/go#36841

Change-Id: I31de754e2ac8970c6f18993104de0e0baea5dc31
Reviewed-on: https://go-review.googlesource.com/c/build/+/234114
Run-TryBot: Carlos Amedee <carlos@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
diff --git a/buildlet/aws.go b/buildlet/aws.go
index a2c3e55..1d96c66 100644
--- a/buildlet/aws.go
+++ b/buildlet/aws.go
@@ -29,6 +29,8 @@
 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"`
@@ -184,12 +186,14 @@
 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(),
-		Metadata:          make(map[string]string),
 	}
 	for k, v := range opts.Meta {
 		ud.Metadata[k] = v
diff --git a/buildlet/aws_test.go b/buildlet/aws_test.go
index 1815f02..d13871b 100644
--- a/buildlet/aws_test.go
+++ b/buildlet/aws_test.go
@@ -432,33 +432,40 @@
 
 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
+		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{},
-			vmName:           "base_vm",
-			hostType:         "host-foo-bar",
-			opts:             &VMOpts{},
-			wantInstanceType: "n1-highcpu-2",
-			wantName:         "base_vm",
+			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",
+				VMImage:       "awesome_image",
+				KonletVMImage: "gcr.io/symbolic-datum-552/gobuilder-arm64-aws",
 			},
 			vmName:   "base-vm",
 			hostType: "host-foo-bar",
@@ -473,11 +480,13 @@
 					"sample": "value",
 				},
 			},
-			wantDesc:         "test description",
-			wantImageID:      "awesome_image",
-			wantInstanceType: "n1-highcpu-2",
-			wantName:         "base-vm",
-			wantZone:         "sa-west",
+			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 {
@@ -533,6 +542,13 @@
 			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)
 			}
diff --git a/cmd/rundockerbuildlet/rundockerbuildlet.go b/cmd/rundockerbuildlet/rundockerbuildlet.go
index c437b08..59805e3 100644
--- a/cmd/rundockerbuildlet/rundockerbuildlet.go
+++ b/cmd/rundockerbuildlet/rundockerbuildlet.go
@@ -10,6 +10,7 @@
 
 import (
 	"bytes"
+	"context"
 	"encoding/json"
 	"flag"
 	"fmt"
@@ -23,7 +24,10 @@
 	"strings"
 	"time"
 
+	"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"
 )
 
 var (
@@ -40,6 +44,12 @@
 var (
 	buildKey     []byte
 	scalewayMeta = new(scalewayMetadata)
+	isReverse    = true
+	isSingleRun  = false
+	// ec2UD contains a copy of the EC2 vm user data retrieved from the metadata.
+	ec2UD *buildlet.EC2UserData
+	// ec2MetaClient is an EC2 metadata client.
+	ec2MetaClient *ec2metadata.EC2Metadata
 )
 
 func main() {
@@ -52,9 +62,19 @@
 		*numInst = 1
 		*basename = "scaleway"
 		initScalewayMeta()
+	} else if onEC2() {
+		initEC2Meta()
+		*memory = ""
+		*image = ec2UD.BuildletImageURL
+		*pull = true
+		*numInst = 1
+		isReverse = false
+		isSingleRun = true
 	}
 
-	buildKey = getBuildKey()
+	if isReverse {
+		buildKey = getBuildKey()
+	}
 
 	if *image == "" {
 		log.Fatalf("docker --image is required")
@@ -65,10 +85,35 @@
 		if err := checkFix(); err != nil {
 			log.Print(err)
 		}
+		if isSingleRun {
+			log.Printf("Configured to run a single instance. Exiting")
+			os.Exit(0)
+		}
 		time.Sleep(time.Second) // TODO: docker wait on the running containers?
 	}
 }
 
+func ec2MdClient() *ec2metadata.EC2Metadata {
+	if ec2MetaClient != nil {
+		return ec2MetaClient
+	}
+	ses, err := session.NewSession()
+	if err != nil {
+		return nil
+	}
+	ec2MetaClient = ec2metadata.New(ses)
+	return ec2MetaClient
+}
+
+func onEC2() bool {
+	if ec2MdClient() == nil {
+		return false
+	}
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+	return ec2MdClient().AvailableWithContext(ctx)
+}
+
 func onScaleway() bool {
 	if *builderEnv == "host-linux-arm-scaleway" {
 		return true
@@ -136,6 +181,8 @@
 			// c1 instance hostname for debugability.
 			// There should only be one running container per c1 instance.
 			name = scalewayMeta.Hostname
+		} else if onEC2() {
+			name = ec2UD.BuildletName
 		} else {
 			name = fmt.Sprintf("%s%02d", *basename, num)
 		}
@@ -185,13 +232,17 @@
 		cmd := exec.Command("docker", "run",
 			"-d",
 			"--name="+name,
-			"-v", filepath.Dir(keyFile)+":/buildkey/",
 			"-e", "HOSTNAME="+name,
 			"--security-opt=seccomp=unconfined", // Issue 35547
 			"--tmpfs=/workdir:rw,exec")
 		if *memory != "" {
 			cmd.Args = append(cmd.Args, "--memory="+*memory)
 		}
+		if isReverse {
+			cmd.Args = append(cmd.Args, "-v", filepath.Dir(keyFile)+":/buildkey/")
+		} else {
+			cmd.Args = append(cmd.Args, "-p", "443:443")
+		}
 		if *cpu > 0 {
 			cmd.Args = append(cmd.Args, fmt.Sprintf("--cpuset-cpus=%d-%d", *cpu*(num-1), *cpu*num-1))
 		}
@@ -234,6 +285,29 @@
 	return false
 }
 
+func initEC2Meta() {
+	if !onEC2() {
+		log.Fatal("attempt to initialize metadata on non-EC2 instance")
+	}
+	if ec2UD != nil {
+		return
+	}
+	if ec2MdClient() == nil {
+		log.Fatalf("unable to retrieve EC2 metadata client")
+	}
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+	ec2MetaJson, err := ec2MdClient().GetUserDataWithContext(ctx)
+	if err != nil {
+		log.Fatalf("unable to retrieve EC2 user data: %v", err)
+	}
+	ec2UD = &buildlet.EC2UserData{}
+	err = json.Unmarshal([]byte(ec2MetaJson), ec2UD)
+	if err != nil {
+		log.Fatalf("unable to unmarshal user data json: %v", err)
+	}
+}
+
 func initScalewayMeta() {
 	const metaURL = "http://169.254.42.42/conf?format=json"
 	res, err := http.Get(metaURL)