internal/gomote: create gomote swarming server implementation

This change creates a new gomote GRPC server implementation which only
supports swarming bots.

For golang/go#61912

Change-Id: Ib813b5190e0826bcb008c67cacf11b95b9250823
Reviewed-on: https://go-review.googlesource.com/c/build/+/526259
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Auto-Submit: Carlos Amedee <carlos@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/internal/gomote/swarming.go b/internal/gomote/swarming.go
new file mode 100644
index 0000000..d26104a
--- /dev/null
+++ b/internal/gomote/swarming.go
@@ -0,0 +1,72 @@
+// Copyright 2023 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.
+
+//go:build linux || darwin
+// +build linux darwin
+
+package gomote
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"strings"
+
+	"cloud.google.com/go/storage"
+	"golang.org/x/build/internal/access"
+	"golang.org/x/build/internal/coordinator/remote"
+	"golang.org/x/build/internal/gomote/protos"
+	"golang.org/x/build/internal/swarmclient"
+	"golang.org/x/crypto/ssh"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
+)
+
+// SwarmingServer is a gomote server implementation which supports LUCI swarming bots.
+type SwarmingServer struct {
+	// embed the unimplemented server.
+	protos.UnimplementedGomoteServiceServer
+
+	bucket                  bucketHandle
+	buildlets               *remote.SessionPool
+	gceBucketName           string
+	luciConfigClient        *swarmclient.ConfigClient
+	sshCertificateAuthority ssh.Signer
+}
+
+// NewSwarming creates a gomote server. If the rawCAPriKey is invalid, the program will exit.
+func NewSwarming(rsp *remote.SessionPool, rawCAPriKey []byte, gomoteGCSBucket string, storageClient *storage.Client, configClient *swarmclient.ConfigClient) (*SwarmingServer, error) {
+	signer, err := ssh.ParsePrivateKey(rawCAPriKey)
+	if err != nil {
+		return nil, fmt.Errorf("unable to parse raw certificate authority private key into signer=%w", err)
+	}
+	return &SwarmingServer{
+		bucket:                  storageClient.Bucket(gomoteGCSBucket),
+		buildlets:               rsp,
+		gceBucketName:           gomoteGCSBucket,
+		luciConfigClient:        configClient,
+		sshCertificateAuthority: signer,
+	}, 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")
+	}
+	bots, err := ss.luciConfigClient.ListSwarmingBots(ctx)
+	if err != nil {
+		log.Printf("luciConfigClient.ListSwarmingBots(ctx) = %s", err)
+		return nil, status.Errorf(codes.Internal, "unable to query for bots")
+	}
+	var builders []string
+	for _, bot := range bots {
+		if bot.BucketName == "ci" && strings.HasPrefix(bot.Name, "gotip") {
+			builders = append(builders, bot.Name)
+		}
+	}
+	return &protos.ListSwarmingBuildersResponse{Builders: builders}, nil
+}
diff --git a/internal/gomote/swarming_test.go b/internal/gomote/swarming_test.go
new file mode 100644
index 0000000..cf06cf4
--- /dev/null
+++ b/internal/gomote/swarming_test.go
@@ -0,0 +1,104 @@
+// Copyright 2023 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.
+
+//go:build linux || darwin
+// +build linux darwin
+
+package gomote
+
+import (
+	"context"
+	"os"
+	"testing"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+	"golang.org/x/build/internal/access"
+	"golang.org/x/build/internal/coordinator/remote"
+	"golang.org/x/build/internal/gomote/protos"
+	"golang.org/x/build/internal/swarmclient"
+	"golang.org/x/crypto/ssh"
+	"golang.org/x/net/nettest"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
+)
+
+const testSwarmingBucketName = "unit-testing-bucket-swarming"
+
+func fakeGomoteSwarmingServer(t *testing.T, ctx context.Context, configClient *swarmclient.ConfigClient) protos.GomoteServiceServer {
+	signer, err := ssh.ParsePrivateKey([]byte(devCertCAPrivate))
+	if err != nil {
+		t.Fatalf("unable to parse raw certificate authority private key into signer=%s", err)
+	}
+	return &SwarmingServer{
+		bucket:                  &fakeBucketHandler{bucketName: testSwarmingBucketName},
+		buildlets:               remote.NewSessionPool(ctx),
+		gceBucketName:           testSwarmingBucketName,
+		sshCertificateAuthority: signer,
+		luciConfigClient:        configClient,
+	}
+}
+
+func setupGomoteSwarmingTest(t *testing.T, ctx context.Context) protos.GomoteServiceClient {
+	contents, err := os.ReadFile("../swarmclient/testdata/bb-sample.cfg")
+	if err != nil {
+		t.Fatalf("unable to read test buildbucket config: %s", err)
+	}
+	configClient := swarmclient.NewMemoryConfigClient(ctx, []*swarmclient.ConfigEntry{
+		&swarmclient.ConfigEntry{"cr-buildbucket.cfg", contents},
+	})
+	lis, err := nettest.NewLocalListener("tcp")
+	if err != nil {
+		t.Fatalf("unable to create net listener: %s", err)
+	}
+	sopts := access.FakeIAPAuthInterceptorOptions()
+	s := grpc.NewServer(sopts...)
+	protos.RegisterGomoteServiceServer(s, fakeGomoteSwarmingServer(t, ctx, configClient))
+	go s.Serve(lis)
+
+	// create GRPC client
+	copts := []grpc.DialOption{
+		grpc.WithInsecure(),
+		grpc.WithBlock(),
+		grpc.WithTimeout(5 * time.Second),
+	}
+	conn, err := grpc.Dial(lis.Addr().String(), copts...)
+	if err != nil {
+		lis.Close()
+		t.Fatalf("unable to create GRPC client: %s", err)
+	}
+	gc := protos.NewGomoteServiceClient(conn)
+	t.Cleanup(func() {
+		conn.Close()
+		s.Stop()
+		lis.Close()
+	})
+	return gc
+}
+
+func TestSwarmingListSwarmingBuilders(t *testing.T) {
+	client := setupGomoteSwarmingTest(t, context.Background())
+	ctx := access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP())
+	response, err := client.ListSwarmingBuilders(ctx, &protos.ListSwarmingBuildersRequest{})
+	if err != nil {
+		t.Fatalf("client.ListSwarmingBuilders = nil, %s; want no error", err)
+	}
+	got := response.GetBuilders()
+	if diff := cmp.Diff([]string{"gotip-linux-amd64-boringcrypto"}, got); diff != "" {
+		t.Errorf("ListBuilders() mismatch (-want, +got):\n%s", diff)
+	}
+}
+
+func TestSwarmingListSwarmingBuildersError(t *testing.T) {
+	client := setupGomoteSwarmingTest(t, context.Background())
+	req := &protos.ListSwarmingBuildersRequest{}
+	got, err := client.ListSwarmingBuilders(context.Background(), req)
+	if err != nil && status.Code(err) != codes.Unauthenticated {
+		t.Fatalf("unexpected error: %s; want %s", err, codes.Unauthenticated)
+	}
+	if err == nil {
+		t.Fatalf("client.ListSwarmingBuilder(ctx, %v) = %v, nil; want error", req, got)
+	}
+}