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)
+ }
+}