internal/gomote: add add bootstrap endpoint
This change adds the bootstrap endpoint for the swarming
implementation of the gomote GRPC server.
Fixes golang/go#64212
Change-Id: Ib7c488bdf5ab8f6d993b6e95a6237cf12d19ed92
Reviewed-on: https://go-review.googlesource.com/c/build/+/543095
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Auto-Submit: Carlos Amedee <carlos@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/internal/gomote/gomote_test.go b/internal/gomote/gomote_test.go
index ec63003..d84e720 100644
--- a/internal/gomote/gomote_test.go
+++ b/internal/gomote/gomote_test.go
@@ -94,7 +94,7 @@
}
}
-func TestAddBootstrapAlive(t *testing.T) {
+func TestAddBootstrap(t *testing.T) {
client := setupGomoteTest(t, context.Background())
gomoteID := mustCreateInstance(t, client, fakeIAP())
req := &protos.AddBootstrapRequest{
diff --git a/internal/gomote/swarming.go b/internal/gomote/swarming.go
index 11f435f..fe27418 100644
--- a/internal/gomote/swarming.go
+++ b/internal/gomote/swarming.go
@@ -8,6 +8,7 @@
import (
"context"
+ "encoding/json"
"errors"
"fmt"
"io"
@@ -89,6 +90,58 @@
return &protos.AuthenticateResponse{}, nil
}
+// AddBootstrap adds the bootstrap version of Go to an instance and returns the URL for the bootstrap version. If no
+// bootstrap version is defined then the returned version URL will be empty.
+func (ss *SwarmingServer) AddBootstrap(ctx context.Context, req *protos.AddBootstrapRequest) (*protos.AddBootstrapResponse, error) {
+ creds, err := access.IAPFromContext(ctx)
+ if err != nil {
+ log.Printf("AddBootstrap access.IAPFromContext(ctx) = nil, %s", err)
+ return nil, status.Errorf(codes.Unauthenticated, "request does not contain the required authentication")
+ }
+ ses, bc, err := ss.sessionAndClient(ctx, req.GetGomoteId(), creds.ID)
+ if err != nil {
+ // 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",
+ },
+ })
+ if err != nil {
+ return nil, status.Errorf(codes.InvalidArgument, "unknown builder type")
+ }
+ type ConfigProperties struct {
+ BootstrapVersion string `json:"bootstrap_version, omitempty"`
+ }
+ var cp ConfigProperties
+ if err := json.Unmarshal([]byte(builder.GetConfig().GetProperties()), &cp); err != nil {
+ return &protos.AddBootstrapResponse{}, nil
+ }
+ if cp.BootstrapVersion == "" {
+ return &protos.AddBootstrapResponse{}, nil
+ }
+ var cipdPlatform string
+ for _, bd := range builder.GetConfig().GetDimensions() {
+ if !strings.HasPrefix(bd, "cipd_platform:") {
+ continue
+ }
+ var ok bool
+ _, cipdPlatform, ok = strings.Cut(bd, ":")
+ if !ok {
+ return nil, status.Errorf(codes.Internal, "unknown builder type")
+ }
+ break
+ }
+ url := fmt.Sprintf("https://storage.googleapis.com/go-builder-data/gobootstrap-%s-go%s.tar.gz", cipdPlatform, cp.BootstrapVersion)
+ if err = bc.PutTarFromURL(ctx, url, cp.BootstrapVersion); err != nil {
+ return nil, status.Errorf(codes.Internal, "unable to download bootstrap Go")
+ }
+ return &protos.AddBootstrapResponse{BootstrapGoUrl: url}, nil
+}
+
// CreateInstance will create a gomote instance within a swarming task for the authenticated user.
func (ss *SwarmingServer) CreateInstance(req *protos.CreateInstanceRequest, stream protos.GomoteService_CreateInstanceServer) error {
creds, err := access.IAPFromContext(stream.Context())
diff --git a/internal/gomote/swarming_test.go b/internal/gomote/swarming_test.go
index a493350..5302af6 100644
--- a/internal/gomote/swarming_test.go
+++ b/internal/gomote/swarming_test.go
@@ -104,6 +104,75 @@
}
}
+func TestSwarmingAddBootstrap(t *testing.T) {
+ ctx := access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP())
+ client := setupGomoteSwarmingTest(t, context.Background(), mockSwarmClientSimple())
+ gomoteID := mustCreateSwarmingInstance(t, client, fakeIAP())
+ req := &protos.AddBootstrapRequest{
+ GomoteId: gomoteID,
+ }
+ got, err := client.AddBootstrap(ctx, req)
+ if err != nil {
+ t.Fatalf("client.AddBootstrap(ctx, %v) = %v, %s; want no error", req, got, err)
+ }
+}
+
+func TestSwarmingAddBootstrapError(t *testing.T) {
+ // This test will create a gomote instance and attempt to call AddBootstrap.
+ // If overrideID is set to true, the test will use a different gomoteID than
+ // the one created for the test.
+ testCases := []struct {
+ desc string
+ ctx context.Context
+ overrideID bool
+ gomoteID string // Used iff overrideID is true.
+ wantCode codes.Code
+ }{
+ {
+ desc: "unauthenticated request",
+ ctx: context.Background(),
+ wantCode: codes.Unauthenticated,
+ },
+ {
+ desc: "missing gomote id",
+ ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()),
+ overrideID: true,
+ wantCode: codes.NotFound,
+ },
+ {
+ desc: "gomote does not exist",
+ ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()),
+ overrideID: true,
+ gomoteID: "xyz",
+ wantCode: codes.NotFound,
+ },
+ {
+ desc: "gomote is not owned by caller",
+ ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAPWithUser("user-x", "email-y")),
+ wantCode: codes.PermissionDenied,
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ client := setupGomoteSwarmingTest(t, context.Background(), mockSwarmClientSimple())
+ gomoteID := mustCreateSwarmingInstance(t, client, fakeIAP())
+ if tc.overrideID {
+ gomoteID = tc.gomoteID
+ }
+ req := &protos.AddBootstrapRequest{
+ GomoteId: gomoteID,
+ }
+ got, err := client.AddBootstrap(tc.ctx, req)
+ if err != nil && status.Code(err) != tc.wantCode {
+ t.Fatalf("unexpected error: %s; want %s", err, tc.wantCode)
+ }
+ if err == nil {
+ t.Fatalf("client.AddBootstrap(ctx, %v) = %v, nil; want error", req, got)
+ }
+ })
+ }
+}
+
func TestSwarmingListSwarmingBuilders(t *testing.T) {
log.SetOutput(io.Discard)
defer log.SetOutput(os.Stdout)