internal/gomote: change the query for valid builders

This change modifies the query used to determine the list of valid
builders. Builders reside in two different buildbucket buckets. Each action that
requires a builder definition has been updated to query for all valid
builders. In the future, we should consider caching the results for queries
since they don't change often.

The bootstrap version of Go will now be installed on the builder via
CIPD packages.

Fixes golang/go#64743
Fixes golang/go#64745

Change-Id: I85d7b1aa8255a35e0ffb03d95534a20506953d66
Reviewed-on: https://go-review.googlesource.com/c/build/+/550356
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Auto-Submit: Carlos Amedee <carlos@golang.org>
diff --git a/internal/gomote/swarming.go b/internal/gomote/swarming.go
index fe27418..dda5da9 100644
--- a/internal/gomote/swarming.go
+++ b/internal/gomote/swarming.go
@@ -15,6 +15,7 @@
 	"io/fs"
 	"log"
 	"net/http"
+	"sort"
 	"strings"
 	"time"
 
@@ -103,24 +104,20 @@
 		// the helper function returns meaningful GRPC error.
 		return nil, err
 	}
-	builder, err := ss.buildersClient.GetBuilder(ctx, &buildbucketpb.GetBuilderRequest{
-		Id: &buildbucketpb.BuilderID{
-			Project: "golang",
-			Bucket:  "ci-workers",
-			Builder: ses.BuilderType + "-test_only",
-		},
-	})
+	bs, err := ss.validBuilders(ctx)
 	if err != nil {
-		return nil, status.Errorf(codes.InvalidArgument, "unknown builder type")
+		return nil, err
 	}
-	type ConfigProperties struct {
-		BootstrapVersion string `json:"bootstrap_version, omitempty"`
+	builder, ok := bs[ses.BuilderType]
+	if !ok {
+		return nil, status.Errorf(codes.Internal, "unable to determine builder definition")
 	}
-	var cp ConfigProperties
-	if err := json.Unmarshal([]byte(builder.GetConfig().GetProperties()), &cp); err != nil {
+	cp, err := builderProperties(builder)
+	if err != nil {
+		log.Printf("AddBootstrap: bootstrap version not found for %s: %s", builder.GetId().GetBuilder(), err)
 		return &protos.AddBootstrapResponse{}, nil
 	}
-	if cp.BootstrapVersion == "" {
+	if cp.BootstrapVersion == "latest" {
 		return &protos.AddBootstrapResponse{}, nil
 	}
 	var cipdPlatform string
@@ -135,7 +132,11 @@
 		}
 		break
 	}
-	url := fmt.Sprintf("https://storage.googleapis.com/go-builder-data/gobootstrap-%s-go%s.tar.gz", cipdPlatform, cp.BootstrapVersion)
+	goos, goarch, err := platformToGoValues(cipdPlatform)
+	if err != nil {
+		return nil, status.Errorf(codes.Internal, "unknown platform type")
+	}
+	url := fmt.Sprintf("https://storage.googleapis.com/go-builder-data/gobootstrap-%s-%s-go%s.tar.gz", goos, goarch, cp.BootstrapVersion)
 	if err = bc.PutTarFromURL(ctx, url, cp.BootstrapVersion); err != nil {
 		return nil, status.Errorf(codes.Internal, "unable to download bootstrap Go")
 	}
@@ -152,14 +153,12 @@
 	if req.GetBuilderType() == "" {
 		return status.Errorf(codes.InvalidArgument, "invalid builder type")
 	}
-	builder, err := ss.buildersClient.GetBuilder(stream.Context(), &buildbucketpb.GetBuilderRequest{
-		Id: &buildbucketpb.BuilderID{
-			Project: "golang",
-			Bucket:  "ci-workers",
-			Builder: req.GetBuilderType() + "-test_only",
-		},
-	})
+	bs, err := ss.validBuilders(stream.Context())
 	if err != nil {
+		return err
+	}
+	builder, ok := bs[req.GetBuilderType()]
+	if !ok {
 		return status.Errorf(codes.InvalidArgument, "unknown builder type")
 	}
 	userName, err := emailToUser(creds.Email)
@@ -182,8 +181,13 @@
 		}
 	}
 	name := fmt.Sprintf("gomote-%s-%s", userName, uuid.NewString())
+	cp, err := builderProperties(builder)
+	if err != nil {
+		log.Printf("CreateInstance: builder configuration not found for %s: %s", builder.GetId().GetBuilder(), err)
+		return status.Errorf(codes.Internal, "invalid builder configuration")
+	}
 	go func() {
-		bc, err := ss.startNewSwarmingTask(stream.Context(), name, dimensions, &SwarmOpts{})
+		bc, err := ss.startNewSwarmingTask(stream.Context(), name, dimensions, cp, &SwarmOpts{})
 		if err != nil {
 			log.Printf("startNewSwarmingTask() = %s", err)
 		}
@@ -354,20 +358,20 @@
 	}, nil
 }
 
-// ListSwarmingBuilders lists all of the swarming builders which run for gotip. The requester must be authenticated.
-func (ss *SwarmingServer) ListSwarmingBuilders(ctx context.Context, req *protos.ListSwarmingBuildersRequest) (*protos.ListSwarmingBuildersResponse, error) {
-	_, err := access.IAPFromContext(ctx)
-	if err != nil {
-		log.Printf("ListSwarmingInstances access.IAPFromContext(ctx) = nil, %s", err)
-		return nil, status.Errorf(codes.Unauthenticated, "request does not contain the required authentication")
-	}
-	listBuilders := func() ([]*buildbucketpb.BuilderItem, error) {
+// golangbuildModeAll is golangbuild's MODE_ALL mode that
+// builds and tests the project all within the same build.
+//
+// See https://source.chromium.org/chromium/infra/infra/+/main:go/src/infra/experimental/golangbuild/golangbuildpb/params.proto;l=148-149;drc=4e874bfb4ff7ff0620940712983ca82e8ea81028.
+const golangbuildModeAll = 0
+
+func (ss *SwarmingServer) validBuilders(ctx context.Context) (map[string]*buildbucketpb.BuilderItem, error) {
+	listBuilders := func(bucket string) ([]*buildbucketpb.BuilderItem, error) {
 		var builders []*buildbucketpb.BuilderItem
 		var nextToken string
 		for {
 			buildersResp, err := ss.buildersClient.ListBuilders(ctx, &buildbucketpb.ListBuildersRequest{
 				Project:   "golang",
-				Bucket:    "ci-workers",
+				Bucket:    bucket,
 				PageSize:  1000,
 				PageToken: nextToken,
 			})
@@ -382,26 +386,73 @@
 			return builders, nil
 		}
 	}
-	builderResponse, err := listBuilders()
+	// list all the valid builders in ci-workers
+	builderBucket := "ci-workers"
+	builderResponse, err := listBuilders(builderBucket)
 	if err != nil {
-		log.Printf("buildersClient.ListBuilders(ctx) = nil, %s", err)
-		return nil, status.Errorf(codes.Internal, "unable to query for bots")
+		log.Printf("buildersClient.ListBuilders(ctx, %s) = nil, %s", builderBucket, err)
+		return nil, status.Errorf(codes.Internal, "unable to query for builders")
 	}
-	var builders []string
+	builders := make(map[string]*buildbucketpb.BuilderItem)
 	for _, builder := range builderResponse {
 		bID := builder.GetId()
 		if bID == nil {
 			continue
 		}
 		name := bID.GetBuilder()
-		if !strings.HasPrefix(name, "gotip") {
+		if !strings.HasPrefix(name, "go") {
 			continue
 		}
 		if !strings.HasSuffix(name, "-test_only") {
 			continue
 		}
-		builders = append(builders, strings.TrimSuffix(name, "-test_only"))
+		builders[strings.TrimSuffix(name, "-test_only")] = builder
 	}
+	// list all the valid builders in ci
+	builderBucket = "ci"
+	builderResponse, err = listBuilders(builderBucket)
+	if err != nil {
+		log.Printf("buildersClient.ListBuilders(ctx, %s) = nil, %s", builderBucket, err)
+		return nil, status.Errorf(codes.Internal, "unable to query for builders")
+	}
+	for _, builder := range builderResponse {
+		bID := builder.GetId()
+		if bID == nil {
+			continue
+		}
+		name := bID.GetBuilder()
+		if !strings.HasPrefix(name, "go") {
+			continue
+		}
+		if _, ok := builders[name]; ok {
+			// should not happen
+			continue
+		}
+		config, err := builderProperties(builder)
+		if err != nil || config.Mode != golangbuildModeAll {
+			continue
+		}
+		builders[name] = builder
+	}
+	return builders, nil
+}
+
+// ListSwarmingBuilders lists all of the swarming builders which run for the Go master or release branches. The requester must be authenticated.
+func (ss *SwarmingServer) ListSwarmingBuilders(ctx context.Context, req *protos.ListSwarmingBuildersRequest) (*protos.ListSwarmingBuildersResponse, error) {
+	_, err := access.IAPFromContext(ctx)
+	if err != nil {
+		log.Printf("ListSwarmingInstances access.IAPFromContext(ctx) = nil, %s", err)
+		return nil, status.Errorf(codes.Unauthenticated, "request does not contain the required authentication")
+	}
+	bs, err := ss.validBuilders(ctx)
+	if err != nil {
+		return nil, err
+	}
+	var builders []string
+	for builder, _ := range bs {
+		builders = append(builders, builder)
+	}
+	sort.Strings(builders)
 	return &protos.ListSwarmingBuildersResponse{Builders: builders}, nil
 }
 
@@ -700,11 +751,11 @@
 // running on it. It returns a buildlet client configured to speak to it.
 // The request will last as long as the lifetime of the context. The dimensions
 // are a set of key value pairs used to describe what instance type to create.
-func (ss *SwarmingServer) startNewSwarmingTask(ctx context.Context, name string, dimensions map[string]string, opts *SwarmOpts) (buildlet.Client, error) {
+func (ss *SwarmingServer) startNewSwarmingTask(ctx context.Context, name string, dimensions map[string]string, properties *configProperties, opts *SwarmOpts) (buildlet.Client, error) {
 	ss.rendezvous.RegisterInstance(ctx, name, 10*time.Minute)
 	condRun(opts.OnInstanceRegistration)
 
-	taskID, err := ss.newSwarmingTask(ctx, name, dimensions, opts)
+	taskID, err := ss.newSwarmingTask(ctx, name, dimensions, properties, opts)
 	if err != nil {
 		ss.rendezvous.DeregisterInstance(ctx, name)
 		return nil, err
@@ -874,7 +925,7 @@
 	return goos, goarch, nil
 }
 
-func (ss *SwarmingServer) newSwarmingTask(ctx context.Context, name string, dimensions map[string]string, opts *SwarmOpts) (string, error) {
+func (ss *SwarmingServer) newSwarmingTask(ctx context.Context, name string, dimensions map[string]string, properties *configProperties, opts *SwarmOpts) (string, error) {
 	cipdPlatform, ok := dimensions["cipd_platform"]
 	if !ok {
 		return "", fmt.Errorf("dimensions require cipd_platform: instance=%s", name)
@@ -885,7 +936,7 @@
 	}
 	packages := []*swarmpb.CipdPackage{
 		{Path: "tools/bin", PackageName: "infra/tools/luci-auth/" + cipdPlatform, Version: "latest"},
-		{Path: "tools", PackageName: "golang/bootstrap-go/" + cipdPlatform, Version: "latest"},
+		{Path: "tools", PackageName: "golang/bootstrap-go/" + cipdPlatform, Version: properties.BootstrapVersion},
 	}
 	pythonBin := "python3"
 	switch goos {
@@ -948,3 +999,19 @@
 	f(ctx, time.Now())
 	internal.PeriodicallyDo(ctx, period, f)
 }
+
+type configProperties struct {
+	BootstrapVersion string `json:"bootstrap_version"`
+	Mode             int    `json:"mode"`
+}
+
+func builderProperties(builder *buildbucketpb.BuilderItem) (*configProperties, error) {
+	cp := new(configProperties)
+	if err := json.Unmarshal([]byte(builder.GetConfig().GetProperties()), cp); err != nil {
+		return nil, fmt.Errorf("builder property unmarshal error: %s", err)
+	}
+	if cp.BootstrapVersion == "" {
+		cp.BootstrapVersion = "latest"
+	}
+	return cp, nil
+}
diff --git a/internal/gomote/swarming_test.go b/internal/gomote/swarming_test.go
index 5302af6..baa4b5c 100644
--- a/internal/gomote/swarming_test.go
+++ b/internal/gomote/swarming_test.go
@@ -184,7 +184,7 @@
 		t.Fatalf("client.ListSwarmingBuilders = nil, %s; want no error", err)
 	}
 	got := response.GetBuilders()
-	if diff := cmp.Diff([]string{"gotip-linux-amd64-boringcrypto"}, got); diff != "" {
+	if diff := cmp.Diff([]string{"gotip-linux-amd64", "gotip-linux-amd64-boringcrypto", "gotip-linux-arm"}, got); diff != "" {
 		t.Errorf("ListBuilders() mismatch (-want, +got):\n%s", diff)
 	}
 }
@@ -1137,7 +1137,7 @@
 	}
 	id := "task-123"
 	errCh := make(chan error, 2)
-	if _, err := ss.startNewSwarmingTask(ctx, id, map[string]string{"cipd_platform": "linux-amd64"}, &SwarmOpts{
+	if _, err := ss.startNewSwarmingTask(ctx, id, map[string]string{"cipd_platform": "linux-amd64"}, &configProperties{}, &SwarmOpts{
 		OnInstanceRegistration: func() {
 			client := ts.Client()
 			req, err := http.NewRequest("GET", ts.URL, nil)
@@ -1288,31 +1288,37 @@
 }
 
 func (fbc *FakeBuildersClient) ListBuilders(ctx context.Context, in *buildbucketpb.ListBuildersRequest, opts ...grpc.CallOption) (*buildbucketpb.ListBuildersResponse, error) {
+	makeBuilderItem := func(bucket string, builders ...string) []*buildbucketpb.BuilderItem {
+		out := make([]*buildbucketpb.BuilderItem, 0, len(builders))
+		for _, b := range builders {
+			out = append(out, &buildbucketpb.BuilderItem{
+				Id: &buildbucketpb.BuilderID{
+					Project: "golang",
+					Bucket:  bucket,
+					Builder: b,
+				},
+				Config: &buildbucketpb.BuilderConfig{
+					Name: b,
+					Dimensions: []string{
+						"cipd_platform:linux-amd64",
+					},
+					Properties: `{"mode": 0, "bootstrap_version":"latest"}`,
+				},
+			})
+		}
+		return out
+	}
+	var builders []*buildbucketpb.BuilderItem
+	switch bucket := in.GetBucket(); bucket {
+	case "ci-workers":
+		builders = makeBuilderItem(bucket, "gotip-linux-amd64-boringcrypto", "gotip-linux-amd64-boringcrypto-test_only")
+	case "ci":
+		builders = makeBuilderItem(bucket, "gotip-linux-arm", "gotip-linux-amd64")
+	default:
+		builders = []*buildbucketpb.BuilderItem{}
+	}
 	out := &buildbucketpb.ListBuildersResponse{
-		Builders: []*buildbucketpb.BuilderItem{
-			&buildbucketpb.BuilderItem{
-				Id: &buildbucketpb.BuilderID{
-					Project: "golang",
-					Bucket:  "ci-workers",
-					Builder: "gotip-linux-amd64-boringcrypto",
-				},
-				Config: &buildbucketpb.BuilderConfig{
-					Name:       "gotip-linux-amd64-boringcrypto",
-					Dimensions: []string{},
-				},
-			},
-			&buildbucketpb.BuilderItem{
-				Id: &buildbucketpb.BuilderID{
-					Project: "golang",
-					Bucket:  "ci-workers",
-					Builder: "gotip-linux-amd64-boringcrypto-test_only",
-				},
-				Config: &buildbucketpb.BuilderConfig{
-					Name:       "gotip-linux-amd64-boringcrypto-test_only",
-					Dimensions: []string{},
-				},
-			},
-		},
+		Builders:      builders,
 		NextPageToken: "",
 	}
 	return out, nil