golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/gomote/swarming.go (about)

     1  // Copyright 2023 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build linux || darwin
     6  
     7  package gomote
     8  
     9  import (
    10  	"context"
    11  	"encoding/json"
    12  	"errors"
    13  	"fmt"
    14  	"io"
    15  	"io/fs"
    16  	"log"
    17  	"net/http"
    18  	"slices"
    19  	"sort"
    20  	"strings"
    21  	"time"
    22  
    23  	"cloud.google.com/go/storage"
    24  	"github.com/google/uuid"
    25  	buildbucketpb "go.chromium.org/luci/buildbucket/proto"
    26  	"go.chromium.org/luci/swarming/client/swarming"
    27  	swarmpb "go.chromium.org/luci/swarming/proto/api_v2"
    28  	"golang.org/x/build/buildlet"
    29  	"golang.org/x/build/internal"
    30  	"golang.org/x/build/internal/access"
    31  	"golang.org/x/build/internal/coordinator/remote"
    32  	"golang.org/x/build/internal/gomote/protos"
    33  	"golang.org/x/build/internal/rendezvous"
    34  	"golang.org/x/crypto/ssh"
    35  	"google.golang.org/grpc"
    36  	"google.golang.org/grpc/codes"
    37  	"google.golang.org/grpc/status"
    38  )
    39  
    40  // expDisableGolangbuild disables the use of golangbuild during the swarming task gomote bootstrap process.
    41  const expDisableGolangbuild = "disable-golang-build"
    42  
    43  type rendezvousClient interface {
    44  	DeregisterInstance(ctx context.Context, id string)
    45  	HandleReverse(w http.ResponseWriter, r *http.Request)
    46  	RegisterInstance(ctx context.Context, id string, wait time.Duration)
    47  	WaitForInstance(ctx context.Context, id string) (buildlet.Client, error)
    48  }
    49  
    50  // BuildersClient is a partial interface of the buildbuicketpb.BuildersClient interface.
    51  type BuildersClient interface {
    52  	GetBuilder(ctx context.Context, in *buildbucketpb.GetBuilderRequest, opts ...grpc.CallOption) (*buildbucketpb.BuilderItem, error)
    53  	ListBuilders(ctx context.Context, in *buildbucketpb.ListBuildersRequest, opts ...grpc.CallOption) (*buildbucketpb.ListBuildersResponse, error)
    54  }
    55  
    56  // SwarmingServer is a gomote server implementation which supports LUCI swarming bots.
    57  type SwarmingServer struct {
    58  	// embed the unimplemented server.
    59  	protos.UnimplementedGomoteServiceServer
    60  
    61  	bucket                  bucketHandle
    62  	buildersClient          BuildersClient
    63  	buildlets               *remote.SessionPool
    64  	gceBucketName           string
    65  	rendezvous              rendezvousClient
    66  	sshCertificateAuthority ssh.Signer
    67  	swarmingClient          swarming.Client
    68  }
    69  
    70  // NewSwarming creates a gomote server. If the rawCAPriKey is invalid, the program will exit.
    71  func NewSwarming(rsp *remote.SessionPool, rawCAPriKey []byte, gomoteGCSBucket string, storageClient *storage.Client, rdv *rendezvous.Rendezvous, swarmClient swarming.Client, buildersClient buildbucketpb.BuildersClient) (*SwarmingServer, error) {
    72  	signer, err := ssh.ParsePrivateKey(rawCAPriKey)
    73  	if err != nil {
    74  		return nil, fmt.Errorf("unable to parse raw certificate authority private key into signer=%w", err)
    75  	}
    76  	return &SwarmingServer{
    77  		bucket:                  storageClient.Bucket(gomoteGCSBucket),
    78  		buildersClient:          buildersClient,
    79  		buildlets:               rsp,
    80  		gceBucketName:           gomoteGCSBucket,
    81  		rendezvous:              rdv,
    82  		sshCertificateAuthority: signer,
    83  		swarmingClient:          swarmClient,
    84  	}, nil
    85  }
    86  
    87  // Authenticate will allow the caller to verify that they are properly authenticated and authorized to interact with the
    88  // Service.
    89  func (ss *SwarmingServer) Authenticate(ctx context.Context, req *protos.AuthenticateRequest) (*protos.AuthenticateResponse, error) {
    90  	_, err := access.IAPFromContext(ctx)
    91  	if err != nil {
    92  		log.Printf("Authenticate access.IAPFromContext(ctx) = nil, %s", err)
    93  		return nil, status.Errorf(codes.Unauthenticated, "request does not contain the required authentication")
    94  	}
    95  	return &protos.AuthenticateResponse{}, nil
    96  }
    97  
    98  // AddBootstrap adds the bootstrap version of Go to an instance and returns the URL for the bootstrap version. If no
    99  // bootstrap version is defined then the returned version URL will be empty.
   100  func (ss *SwarmingServer) AddBootstrap(ctx context.Context, req *protos.AddBootstrapRequest) (*protos.AddBootstrapResponse, error) {
   101  	creds, err := access.IAPFromContext(ctx)
   102  	if err != nil {
   103  		log.Printf("AddBootstrap access.IAPFromContext(ctx) = nil, %s", err)
   104  		return nil, status.Errorf(codes.Unauthenticated, "request does not contain the required authentication")
   105  	}
   106  	ses, bc, err := ss.sessionAndClient(ctx, req.GetGomoteId(), creds.ID)
   107  	if err != nil {
   108  		// the helper function returns meaningful GRPC error.
   109  		return nil, err
   110  	}
   111  	bs, err := ss.validBuilders(ctx)
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  	builder, ok := bs[ses.BuilderType]
   116  	if !ok {
   117  		return nil, status.Errorf(codes.Internal, "unable to determine builder definition")
   118  	}
   119  	cp, err := builderProperties(builder)
   120  	if err != nil {
   121  		log.Printf("AddBootstrap: bootstrap version not found for %s: %s", builder.GetId().GetBuilder(), err)
   122  		return &protos.AddBootstrapResponse{}, nil
   123  	}
   124  	if cp.BootstrapVersion == "latest" {
   125  		return &protos.AddBootstrapResponse{}, nil
   126  	}
   127  	var cipdPlatform string
   128  	for _, bd := range builder.GetConfig().GetDimensions() {
   129  		if !strings.HasPrefix(bd, "cipd_platform:") {
   130  			continue
   131  		}
   132  		var ok bool
   133  		_, cipdPlatform, ok = strings.Cut(bd, ":")
   134  		if !ok {
   135  			return nil, status.Errorf(codes.Internal, "unknown builder type")
   136  		}
   137  		break
   138  	}
   139  	goos, goarch, err := platformToGoValues(cipdPlatform)
   140  	if err != nil {
   141  		return nil, status.Errorf(codes.Internal, "unknown platform type")
   142  	}
   143  	url := fmt.Sprintf("https://storage.googleapis.com/go-builder-data/gobootstrap-%s-%s-go%s.tar.gz", goos, goarch, cp.BootstrapVersion)
   144  	if err = bc.PutTarFromURL(ctx, url, cp.BootstrapVersion); err != nil {
   145  		return nil, status.Errorf(codes.Internal, "unable to download bootstrap Go")
   146  	}
   147  	return &protos.AddBootstrapResponse{BootstrapGoUrl: url}, nil
   148  }
   149  
   150  // CreateInstance will create a gomote instance within a swarming task for the authenticated user.
   151  func (ss *SwarmingServer) CreateInstance(req *protos.CreateInstanceRequest, stream protos.GomoteService_CreateInstanceServer) error {
   152  	creds, err := access.IAPFromContext(stream.Context())
   153  	if err != nil {
   154  		log.Printf("CreateInstance access.IAPFromContext(ctx) = nil, %s", err)
   155  		return status.Errorf(codes.Unauthenticated, "request does not contain the required authentication")
   156  	}
   157  	if req.GetBuilderType() == "" {
   158  		return status.Errorf(codes.InvalidArgument, "invalid builder type")
   159  	}
   160  	bs, err := ss.validBuilders(stream.Context())
   161  	if err != nil {
   162  		return err
   163  	}
   164  	builder, ok := bs[req.GetBuilderType()]
   165  	if !ok {
   166  		return status.Errorf(codes.InvalidArgument, "unknown builder type")
   167  	}
   168  	userName, err := emailToUser(creds.Email)
   169  	if err != nil {
   170  		return status.Errorf(codes.Internal, "invalid user email format")
   171  	}
   172  	type result struct {
   173  		buildletClient buildlet.Client
   174  		err            error
   175  	}
   176  	rc := make(chan result, 1)
   177  	dimensions := make(map[string]string)
   178  
   179  	for _, bd := range builder.GetConfig().GetDimensions() {
   180  		k, v, ok := strings.Cut(bd, ":")
   181  		if ok {
   182  			dimensions[k] = v
   183  		} else {
   184  			log.Printf("failed dimension cut: %s", bd)
   185  		}
   186  	}
   187  	name := fmt.Sprintf("gomote-%s-%s", userName, uuid.NewString())
   188  	cp, err := builderProperties(builder)
   189  	if err != nil {
   190  		log.Printf("CreateInstance: builder configuration not found for %s: %s", builder.GetId().GetBuilder(), err)
   191  		return status.Errorf(codes.Internal, "invalid builder configuration")
   192  	}
   193  	useGolangbuild := !slices.Contains(req.GetExperimentOption(), expDisableGolangbuild)
   194  	go func() {
   195  		bc, err := ss.startNewSwarmingTask(stream.Context(), name, dimensions, cp, &SwarmOpts{}, useGolangbuild)
   196  		if err != nil {
   197  			log.Printf("startNewSwarmingTask() = %s", err)
   198  		}
   199  		rc <- result{bc, err}
   200  	}()
   201  	ticker := time.NewTicker(5 * time.Second)
   202  	defer ticker.Stop()
   203  	for {
   204  		select {
   205  		case <-stream.Context().Done():
   206  			return status.Errorf(codes.DeadlineExceeded, "timed out waiting for gomote instance to be created")
   207  		case <-ticker.C:
   208  			err := stream.Send(&protos.CreateInstanceResponse{
   209  				Status:       protos.CreateInstanceResponse_WAITING,
   210  				WaitersAhead: int64(0), // Not convinced querying for pending jobs is useful
   211  			})
   212  			if err != nil {
   213  				return status.Errorf(codes.Internal, "unable to stream result: %s", err)
   214  			}
   215  		case r := <-rc:
   216  			if r.err != nil {
   217  				log.Printf("error creating gomote buildlet instance=%s: %s", name, r.err)
   218  				return status.Errorf(codes.Internal, "gomote creation failed instance=%s", name)
   219  			}
   220  			gomoteID := ss.buildlets.AddSession(creds.ID, userName, req.GetBuilderType(), req.GetBuilderType(), r.buildletClient)
   221  			log.Printf("created buildlet %s for %s (%s)", gomoteID, userName, r.buildletClient.String())
   222  			session, err := ss.buildlets.Session(gomoteID)
   223  			if err != nil {
   224  				return status.Errorf(codes.Internal, "unable to query for gomote timeout") // this should never happen
   225  			}
   226  			wd, err := r.buildletClient.WorkDir(stream.Context())
   227  			if err != nil {
   228  				return status.Errorf(codes.Internal, "could not read working dir: %v", err)
   229  			}
   230  			err = stream.Send(&protos.CreateInstanceResponse{
   231  				Instance: &protos.Instance{
   232  					GomoteId:    gomoteID,
   233  					BuilderType: req.GetBuilderType(),
   234  					HostType:    "swarming task",
   235  					Expires:     session.Expires.Unix(),
   236  					WorkingDir:  wd,
   237  				},
   238  				Status:       protos.CreateInstanceResponse_COMPLETE,
   239  				WaitersAhead: 0,
   240  			})
   241  			if err != nil {
   242  				return status.Errorf(codes.Internal, "unable to stream result: %s", err)
   243  			}
   244  			return nil
   245  		}
   246  	}
   247  }
   248  
   249  // DestroyInstance will destroy a gomote instance. It will ensure that the caller is authenticated and is the owner of the instance
   250  // before it destroys the instance.
   251  func (ss *SwarmingServer) DestroyInstance(ctx context.Context, req *protos.DestroyInstanceRequest) (*protos.DestroyInstanceResponse, error) {
   252  	creds, err := access.IAPFromContext(ctx)
   253  	if err != nil {
   254  		log.Printf("DestroyInstance access.IAPFromContext(ctx) = nil, %s", err)
   255  		return nil, status.Errorf(codes.Unauthenticated, "request does not contain the required authentication")
   256  	}
   257  	if req.GetGomoteId() == "" {
   258  		return nil, status.Errorf(codes.InvalidArgument, "invalid gomote ID")
   259  	}
   260  	_, err = ss.session(req.GetGomoteId(), creds.ID)
   261  	if err != nil {
   262  		// the helper function returns a meaningful GRPC error.
   263  		return nil, err
   264  	}
   265  	if err := ss.buildlets.DestroySession(req.GetGomoteId()); err != nil {
   266  		log.Printf("DestroyInstance remote.DestroySession(%s) = %s", req.GetGomoteId(), err)
   267  		return nil, status.Errorf(codes.Internal, "unable to destroy gomote instance")
   268  	}
   269  	// TODO(go.dev/issue/63819) consider destroying the bot after the task has ended.
   270  	return &protos.DestroyInstanceResponse{}, nil
   271  }
   272  
   273  // ExecuteCommand will execute a command on a gomote instance. The output from the command will be streamed back to the caller if the output is set.
   274  func (ss *SwarmingServer) ExecuteCommand(req *protos.ExecuteCommandRequest, stream protos.GomoteService_ExecuteCommandServer) error {
   275  	creds, err := access.IAPFromContext(stream.Context())
   276  	if err != nil {
   277  		return status.Errorf(codes.Unauthenticated, "request does not contain the required authentication")
   278  	}
   279  	ses, bc, err := ss.sessionAndClient(stream.Context(), req.GetGomoteId(), creds.ID)
   280  	if err != nil {
   281  		// the helper function returns meaningful GRPC error.
   282  		return err
   283  	}
   284  	builderType := req.GetImitateHostType()
   285  	if builderType == "" {
   286  		builderType = ses.BuilderType
   287  	}
   288  	remoteErr, execErr := bc.Exec(stream.Context(), req.GetCommand(), buildlet.ExecOpts{
   289  		Dir:         req.GetDirectory(),
   290  		SystemLevel: req.GetSystemLevel(),
   291  		Output: &streamWriter{writeFunc: func(p []byte) (int, error) {
   292  			err := stream.Send(&protos.ExecuteCommandResponse{
   293  				Output: p,
   294  			})
   295  			if err != nil {
   296  				return 0, fmt.Errorf("unable to send data=%w", err)
   297  			}
   298  			return len(p), nil
   299  		}},
   300  		Args:     req.GetArgs(),
   301  		ExtraEnv: req.GetAppendEnvironment(),
   302  		Debug:    req.GetDebug(),
   303  		Path:     req.GetPath(),
   304  	})
   305  	if execErr != nil {
   306  		// there were system errors preventing the command from being started or seen to completion.
   307  		return status.Errorf(codes.Aborted, "unable to execute command: %s", execErr)
   308  	}
   309  	if remoteErr != nil {
   310  		// the command failed remotely
   311  		return status.Errorf(codes.Unknown, "command execution failed: %s", remoteErr)
   312  	}
   313  	return nil
   314  }
   315  
   316  // InstanceAlive will ensure that the gomote instance is still alive and will extend the timeout. The requester must be authenticated.
   317  func (ss *SwarmingServer) InstanceAlive(ctx context.Context, req *protos.InstanceAliveRequest) (*protos.InstanceAliveResponse, error) {
   318  	creds, err := access.IAPFromContext(ctx)
   319  	if err != nil {
   320  		log.Printf("InstanceAlive access.IAPFromContext(ctx) = nil, %s", err)
   321  		return nil, status.Errorf(codes.Unauthenticated, "request does not contain the required authentication")
   322  	}
   323  	if req.GetGomoteId() == "" {
   324  		return nil, status.Errorf(codes.InvalidArgument, "invalid gomote ID")
   325  	}
   326  	_, err = ss.session(req.GetGomoteId(), creds.ID)
   327  	if err != nil {
   328  		// the helper function returns meaningful GRPC error.
   329  		return nil, err
   330  	}
   331  	if err := ss.buildlets.RenewTimeout(req.GetGomoteId()); err != nil {
   332  		return nil, status.Errorf(codes.Internal, "unable to renew timeout")
   333  	}
   334  	return &protos.InstanceAliveResponse{}, nil
   335  }
   336  
   337  // ListDirectory lists the contents of the directory on a gomote instance.
   338  func (ss *SwarmingServer) ListDirectory(ctx context.Context, req *protos.ListDirectoryRequest) (*protos.ListDirectoryResponse, error) {
   339  	creds, err := access.IAPFromContext(ctx)
   340  	if err != nil {
   341  		return nil, status.Errorf(codes.Unauthenticated, "request does not contain the required authentication")
   342  	}
   343  	if req.GetGomoteId() == "" || req.GetDirectory() == "" {
   344  		return nil, status.Errorf(codes.InvalidArgument, "invalid arguments")
   345  	}
   346  	_, bc, err := ss.sessionAndClient(ctx, req.GetGomoteId(), creds.ID)
   347  	if err != nil {
   348  		// the helper function returns meaningful GRPC error.
   349  		return nil, err
   350  	}
   351  	opt := buildlet.ListDirOpts{
   352  		Recursive: req.GetRecursive(),
   353  		Digest:    req.GetDigest(),
   354  		Skip:      req.GetSkipFiles(),
   355  	}
   356  	var entries []string
   357  	if err = bc.ListDir(context.Background(), req.GetDirectory(), opt, func(bi buildlet.DirEntry) {
   358  		entries = append(entries, bi.String())
   359  	}); err != nil {
   360  		return nil, status.Errorf(codes.Unimplemented, "method ListDirectory not implemented")
   361  	}
   362  	return &protos.ListDirectoryResponse{
   363  		Entries: entries,
   364  	}, nil
   365  }
   366  
   367  const (
   368  	// golangbuildModeAll is golangbuild's MODE_ALL mode that
   369  	// builds and tests the project all within the same build.
   370  	//
   371  	// See https://source.chromium.org/chromium/infra/infra/+/main:go/src/infra/experimental/golangbuild/golangbuildpb/params.proto;l=148-149;drc=4e874bfb4ff7ff0620940712983ca82e8ea81028.
   372  	golangbuildModeAll = 0
   373  	// golangbuildPerfMode is golangbuild's MODE_PERF that
   374  	// runs performance tests.
   375  	//
   376  	// See https://source.chromium.org/chromium/infra/infra/+/main:go/src/infra/experimental/golangbuild/golangbuildpb/params.proto;l=174-177;drc=fdea4abccf8447808d4e702c8d09fdd20fd81acb.
   377  	golangbuildPerfMode = 4
   378  )
   379  
   380  func (ss *SwarmingServer) validBuilders(ctx context.Context) (map[string]*buildbucketpb.BuilderItem, error) {
   381  	listBuilders := func(bucket string) ([]*buildbucketpb.BuilderItem, error) {
   382  		var builders []*buildbucketpb.BuilderItem
   383  		var nextToken string
   384  		for {
   385  			buildersResp, err := ss.buildersClient.ListBuilders(ctx, &buildbucketpb.ListBuildersRequest{
   386  				Project:   "golang",
   387  				Bucket:    bucket,
   388  				PageSize:  1000,
   389  				PageToken: nextToken,
   390  			})
   391  			if err != nil {
   392  				return nil, err
   393  			}
   394  			builders = append(builders, buildersResp.GetBuilders()...)
   395  			if tok := buildersResp.GetNextPageToken(); tok != "" {
   396  				nextToken = tok
   397  				continue
   398  			}
   399  			return builders, nil
   400  		}
   401  	}
   402  	// list all the valid builders in ci-workers
   403  	builderBucket := "ci-workers"
   404  	builderResponse, err := listBuilders(builderBucket)
   405  	if err != nil {
   406  		log.Printf("buildersClient.ListBuilders(ctx, %s) = nil, %s", builderBucket, err)
   407  		return nil, status.Errorf(codes.Internal, "unable to query for builders")
   408  	}
   409  	builders := make(map[string]*buildbucketpb.BuilderItem)
   410  	for _, builder := range builderResponse {
   411  		bID := builder.GetId()
   412  		if bID == nil {
   413  			continue
   414  		}
   415  		name := bID.GetBuilder()
   416  		if !strings.HasPrefix(name, "go") {
   417  			continue
   418  		}
   419  		if !strings.HasSuffix(name, "-test_only") {
   420  			continue
   421  		}
   422  		builders[strings.TrimSuffix(name, "-test_only")] = builder
   423  	}
   424  	// list all the valid builders in ci
   425  	builderBucket = "ci"
   426  	builderResponse, err = listBuilders(builderBucket)
   427  	if err != nil {
   428  		log.Printf("buildersClient.ListBuilders(ctx, %s) = nil, %s", builderBucket, err)
   429  		return nil, status.Errorf(codes.Internal, "unable to query for builders")
   430  	}
   431  	for _, builder := range builderResponse {
   432  		bID := builder.GetId()
   433  		if bID == nil {
   434  			continue
   435  		}
   436  		name := bID.GetBuilder()
   437  		if !strings.HasPrefix(name, "go") {
   438  			continue
   439  		}
   440  		if _, ok := builders[name]; ok {
   441  			// should not happen
   442  			continue
   443  		}
   444  		config, err := builderProperties(builder)
   445  		if err != nil || !slices.Contains([]int{golangbuildModeAll, golangbuildPerfMode}, config.Mode) {
   446  			continue
   447  		}
   448  		builders[name] = builder
   449  	}
   450  	return builders, nil
   451  }
   452  
   453  // ListSwarmingBuilders lists all of the swarming builders which run for the Go master or release branches. The requester must be authenticated.
   454  func (ss *SwarmingServer) ListSwarmingBuilders(ctx context.Context, req *protos.ListSwarmingBuildersRequest) (*protos.ListSwarmingBuildersResponse, error) {
   455  	_, err := access.IAPFromContext(ctx)
   456  	if err != nil {
   457  		log.Printf("ListSwarmingInstances access.IAPFromContext(ctx) = nil, %s", err)
   458  		return nil, status.Errorf(codes.Unauthenticated, "request does not contain the required authentication")
   459  	}
   460  	bs, err := ss.validBuilders(ctx)
   461  	if err != nil {
   462  		return nil, err
   463  	}
   464  	var builders []string
   465  	for builder, _ := range bs {
   466  		builders = append(builders, builder)
   467  	}
   468  	sort.Strings(builders)
   469  	return &protos.ListSwarmingBuildersResponse{Builders: builders}, nil
   470  }
   471  
   472  // ListInstances will list the gomote instances owned by the requester. The requester must be authenticated.
   473  func (ss *SwarmingServer) ListInstances(ctx context.Context, req *protos.ListInstancesRequest) (*protos.ListInstancesResponse, error) {
   474  	creds, err := access.IAPFromContext(ctx)
   475  	if err != nil {
   476  		log.Printf("ListInstances access.IAPFromContext(ctx) = nil, %s", err)
   477  		return nil, status.Errorf(codes.Unauthenticated, "request does not contain the required authentication")
   478  	}
   479  	res := &protos.ListInstancesResponse{}
   480  	for _, s := range ss.buildlets.List() {
   481  		if s.OwnerID != creds.ID {
   482  			continue
   483  		}
   484  		res.Instances = append(res.Instances, &protos.Instance{
   485  			GomoteId:    s.ID,
   486  			BuilderType: s.BuilderType,
   487  			HostType:    s.HostType,
   488  			Expires:     s.Expires.Unix(),
   489  		})
   490  	}
   491  	return res, nil
   492  }
   493  
   494  // ReadTGZToURL retrieves a directory from the gomote instance and writes the file to GCS. It returns a signed URL which the caller uses
   495  // to read the file from GCS.
   496  func (ss *SwarmingServer) ReadTGZToURL(ctx context.Context, req *protos.ReadTGZToURLRequest) (*protos.ReadTGZToURLResponse, error) {
   497  	creds, err := access.IAPFromContext(ctx)
   498  	if err != nil {
   499  		return nil, status.Errorf(codes.Unauthenticated, "request does not contain the required authentication")
   500  	}
   501  	_, bc, err := ss.sessionAndClient(ctx, req.GetGomoteId(), creds.ID)
   502  	if err != nil {
   503  		// the helper function returns meaningful GRPC error.
   504  		return nil, err
   505  	}
   506  	tgz, err := bc.GetTar(ctx, req.GetDirectory())
   507  	if err != nil {
   508  		return nil, status.Errorf(codes.Aborted, "unable to retrieve tar from gomote instance: %s", err)
   509  	}
   510  	defer tgz.Close()
   511  	objectName := uuid.NewString()
   512  	objectHandle := ss.bucket.Object(objectName)
   513  	// A context for writes is used to ensure we can cancel the context if a
   514  	// problem is encountered while writing to the object store. The API documentation
   515  	// states that the context should be canceled to stop writing without saving the data.
   516  	writeCtx, cancel := context.WithCancel(ctx)
   517  	tgzWriter := objectHandle.NewWriter(writeCtx)
   518  	defer cancel()
   519  	if _, err = io.Copy(tgzWriter, tgz); err != nil {
   520  		return nil, status.Errorf(codes.Aborted, "unable to stream tar.gz: %s", err)
   521  	}
   522  	// when close is called, the object is stored in the bucket.
   523  	if err := tgzWriter.Close(); err != nil {
   524  		return nil, status.Errorf(codes.Aborted, "unable to store object: %s", err)
   525  	}
   526  	url, err := ss.signURLForDownload(objectName)
   527  	if err != nil {
   528  		return nil, status.Errorf(codes.Internal, "unable to create signed URL for download: %s", err)
   529  	}
   530  	return &protos.ReadTGZToURLResponse{
   531  		Url: url,
   532  	}, nil
   533  }
   534  
   535  // RemoveFiles removes files or directories from the gomote instance.
   536  func (ss *SwarmingServer) RemoveFiles(ctx context.Context, req *protos.RemoveFilesRequest) (*protos.RemoveFilesResponse, error) {
   537  	creds, err := access.IAPFromContext(ctx)
   538  	if err != nil {
   539  		log.Printf("RemoveFiles access.IAPFromContext(ctx) = nil, %s", err)
   540  		return nil, status.Errorf(codes.Unauthenticated, "request does not contain the required authentication")
   541  	}
   542  	// TODO(go.dev/issue/48742) consider what additional path validation should be implemented.
   543  	if req.GetGomoteId() == "" || len(req.GetPaths()) == 0 {
   544  		return nil, status.Errorf(codes.InvalidArgument, "invalid arguments")
   545  	}
   546  	_, bc, err := ss.sessionAndClient(ctx, req.GetGomoteId(), creds.ID)
   547  	if err != nil {
   548  		// the helper function returns meaningful GRPC error.
   549  		return nil, err
   550  	}
   551  	if err := bc.RemoveAll(ctx, req.GetPaths()...); err != nil {
   552  		log.Printf("RemoveFiles buildletClient.RemoveAll(ctx, %q) = %s", req.GetPaths(), err)
   553  		return nil, status.Errorf(codes.Unknown, "unable to remove files")
   554  	}
   555  	return &protos.RemoveFilesResponse{}, nil
   556  }
   557  
   558  // SignSSHKey signs the public SSH key with a certificate. The signed public SSH key is intended for use with the gomote service SSH
   559  // server. It will be signed by the certificate authority of the server and will restrict access to the gomote instance that it was
   560  // signed for.
   561  func (ss *SwarmingServer) SignSSHKey(ctx context.Context, req *protos.SignSSHKeyRequest) (*protos.SignSSHKeyResponse, error) {
   562  	creds, err := access.IAPFromContext(ctx)
   563  	if err != nil {
   564  		return nil, status.Errorf(codes.Unauthenticated, "request does not contain the required authentication")
   565  	}
   566  	session, err := ss.session(req.GetGomoteId(), creds.ID)
   567  	if err != nil {
   568  		// the helper function returns meaningful GRPC error.
   569  		return nil, err
   570  	}
   571  	signedPublicKey, err := remote.SignPublicSSHKey(ctx, ss.sshCertificateAuthority, req.GetPublicSshKey(), session.ID, session.OwnerID, 5*time.Minute)
   572  	if err != nil {
   573  		return nil, status.Errorf(codes.InvalidArgument, "unable to sign ssh key")
   574  	}
   575  	return &protos.SignSSHKeyResponse{
   576  		SignedPublicSshKey: signedPublicKey,
   577  	}, nil
   578  }
   579  
   580  // UploadFile creates a URL and a set of HTTP post fields which are used to upload a file to a staging GCS bucket. Uploaded files are made available to the
   581  // gomote instances via a subsequent call to one of the WriteFromURL endpoints.
   582  func (ss *SwarmingServer) UploadFile(ctx context.Context, req *protos.UploadFileRequest) (*protos.UploadFileResponse, error) {
   583  	_, err := access.IAPFromContext(ctx)
   584  	if err != nil {
   585  		return nil, status.Errorf(codes.Unauthenticated, "request does not contain the required authentication")
   586  	}
   587  	objectName := uuid.NewString()
   588  	url, fields, err := ss.signURLForUpload(objectName)
   589  	if err != nil {
   590  		log.Printf("unable to create signed URL: %s", err)
   591  		return nil, status.Errorf(codes.Internal, "unable to create signed url")
   592  	}
   593  	return &protos.UploadFileResponse{
   594  		Url:        url,
   595  		Fields:     fields,
   596  		ObjectName: objectName,
   597  	}, nil
   598  }
   599  
   600  // signURLForUpload generates a signed URL and a set of http Post fields to be used to upload an object to GCS without authenticating.
   601  func (ss *SwarmingServer) signURLForUpload(object string) (url string, fields map[string]string, err error) {
   602  	if object == "" {
   603  		return "", nil, errors.New("invalid object name")
   604  	}
   605  	pv4, err := ss.bucket.GenerateSignedPostPolicyV4(object, &storage.PostPolicyV4Options{
   606  		Expires:  time.Now().Add(10 * time.Minute),
   607  		Insecure: false,
   608  	})
   609  	if err != nil {
   610  		return "", nil, fmt.Errorf("unable to generate signed url: %w", err)
   611  	}
   612  	return pv4.URL, pv4.Fields, nil
   613  }
   614  
   615  // WriteFileFromURL initiates an HTTP request to the passed in URL and streams the contents of the request to the gomote instance.
   616  func (ss *SwarmingServer) WriteFileFromURL(ctx context.Context, req *protos.WriteFileFromURLRequest) (*protos.WriteFileFromURLResponse, error) {
   617  	creds, err := access.IAPFromContext(ctx)
   618  	if err != nil {
   619  		log.Printf("WriteTGZFromURL access.IAPFromContext(ctx) = nil, %s", err)
   620  		return nil, status.Errorf(codes.Unauthenticated, "request does not contain the required authentication")
   621  	}
   622  	_, bc, err := ss.sessionAndClient(ctx, req.GetGomoteId(), creds.ID)
   623  	if err != nil {
   624  		// the helper function returns meaningful GRPC error.
   625  		return nil, err
   626  	}
   627  	var rc io.ReadCloser
   628  	// objects stored in the gomote staging bucket are only accessible when you have been granted explicit permissions. A builder
   629  	// requires a signed URL in order to access objects stored in the gomote staging bucket.
   630  	if onObjectStore(ss.gceBucketName, req.GetUrl()) {
   631  		object, err := objectFromURL(ss.gceBucketName, req.GetUrl())
   632  		if err != nil {
   633  			return nil, status.Errorf(codes.InvalidArgument, "invalid object URL")
   634  		}
   635  		rc, err = ss.bucket.Object(object).NewReader(ctx)
   636  		if err != nil {
   637  			return nil, status.Errorf(codes.Internal, "unable to create object reader: %s", err)
   638  		}
   639  	} else {
   640  		httpRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, req.GetUrl(), nil)
   641  		if err != nil {
   642  			log.Printf("gomote: unable to create HTTP request: %s", err)
   643  			return nil, status.Errorf(codes.Internal, "unable to create HTTP request")
   644  		}
   645  		// TODO(amedee) find sane client defaults, possibly rely on context timeout in request.
   646  		client := &http.Client{
   647  			Timeout: 30 * time.Second,
   648  			Transport: &http.Transport{
   649  				TLSHandshakeTimeout: 5 * time.Second,
   650  			},
   651  		}
   652  		resp, err := client.Do(httpRequest)
   653  		if err != nil {
   654  			return nil, status.Errorf(codes.Aborted, "failed to get file from URL: %s", err)
   655  		}
   656  		if resp.StatusCode != http.StatusOK {
   657  			return nil, status.Errorf(codes.Aborted, "unable to get file from %q: response code: %d", req.GetUrl(), resp.StatusCode)
   658  		}
   659  		rc = resp.Body
   660  	}
   661  	defer rc.Close()
   662  	if err := bc.Put(ctx, rc, req.GetFilename(), fs.FileMode(req.GetMode())); err != nil {
   663  		return nil, status.Errorf(codes.Aborted, "failed to send the file to the gomote instance: %s", err)
   664  	}
   665  	return &protos.WriteFileFromURLResponse{}, nil
   666  }
   667  
   668  // WriteTGZFromURL will instruct the gomote instance to download the tar.gz from the provided URL. The tar.gz file will be unpacked in the work directory
   669  // relative to the directory provided.
   670  func (ss *SwarmingServer) WriteTGZFromURL(ctx context.Context, req *protos.WriteTGZFromURLRequest) (*protos.WriteTGZFromURLResponse, error) {
   671  	creds, err := access.IAPFromContext(ctx)
   672  	if err != nil {
   673  		log.Printf("WriteTGZFromURL access.IAPFromContext(ctx) = nil, %s", err)
   674  		return nil, status.Errorf(codes.Unauthenticated, "request does not contain the required authentication")
   675  	}
   676  	if req.GetGomoteId() == "" {
   677  		return nil, status.Errorf(codes.InvalidArgument, "invalid gomote ID")
   678  	}
   679  	if req.GetUrl() == "" {
   680  		return nil, status.Errorf(codes.InvalidArgument, "missing URL")
   681  	}
   682  	_, bc, err := ss.sessionAndClient(ctx, req.GetGomoteId(), creds.ID)
   683  	if err != nil {
   684  		// the helper function returns meaningful GRPC error.
   685  		return nil, err
   686  	}
   687  	url := req.GetUrl()
   688  	if onObjectStore(ss.gceBucketName, url) {
   689  		object, err := objectFromURL(ss.gceBucketName, url)
   690  		if err != nil {
   691  			return nil, status.Errorf(codes.InvalidArgument, "invalid URL")
   692  		}
   693  		url, err = ss.signURLForDownload(object)
   694  		if err != nil {
   695  			return nil, status.Errorf(codes.Aborted, "unable to sign url for download: %s", err)
   696  		}
   697  	}
   698  	if err := bc.PutTarFromURL(ctx, url, req.GetDirectory()); err != nil {
   699  		return nil, status.Errorf(codes.FailedPrecondition, "unable to write tar.gz: %s", err)
   700  	}
   701  	return &protos.WriteTGZFromURLResponse{}, nil
   702  }
   703  
   704  // session is a helper function that retrieves a session associated with the gomoteID and ownerID.
   705  func (ss *SwarmingServer) session(gomoteID, ownerID string) (*remote.Session, error) {
   706  	session, err := ss.buildlets.Session(gomoteID)
   707  	if err != nil {
   708  		return nil, status.Errorf(codes.NotFound, "specified gomote instance does not exist")
   709  	}
   710  	if session.OwnerID != ownerID {
   711  		return nil, status.Errorf(codes.PermissionDenied, "not allowed to modify this gomote session")
   712  	}
   713  	return session, nil
   714  }
   715  
   716  // sessionAndClient is a helper function that retrieves a session and buildlet client for the
   717  // associated gomoteID and ownerID. The gomote instance timeout is renewed if the gomote id and owner id
   718  // are valid.
   719  func (ss *SwarmingServer) sessionAndClient(ctx context.Context, gomoteID, ownerID string) (*remote.Session, buildlet.Client, error) {
   720  	session, err := ss.session(gomoteID, ownerID)
   721  	if err != nil {
   722  		return nil, nil, err
   723  	}
   724  	bc, err := ss.buildlets.BuildletClient(gomoteID)
   725  	if err != nil {
   726  		return nil, nil, status.Errorf(codes.NotFound, "specified gomote instance does not exist")
   727  	}
   728  	if err := ss.buildlets.KeepAlive(ctx, gomoteID); err != nil {
   729  		log.Printf("gomote: unable to keep alive %s: %s", gomoteID, err)
   730  	}
   731  	return session, bc, nil
   732  }
   733  
   734  // signURLForDownload generates a signed URL and fields to be used to upload an object to GCS without authenticating.
   735  func (ss *SwarmingServer) signURLForDownload(object string) (url string, err error) {
   736  	url, err = ss.bucket.SignedURL(object, &storage.SignedURLOptions{
   737  		Expires: time.Now().Add(10 * time.Minute),
   738  		Method:  http.MethodGet,
   739  		Scheme:  storage.SigningSchemeV4,
   740  	})
   741  	if err != nil {
   742  		return "", fmt.Errorf("unable to generate signed url: %w", err)
   743  	}
   744  	return url, err
   745  }
   746  
   747  // SwarmOpts provides additional options for swarming task creation.
   748  type SwarmOpts struct {
   749  	// OnInstanceRequested optionally specifies a hook to run synchronously
   750  	// after the computeService.Instances.Insert call, but before
   751  	// waiting for its operation to proceed.
   752  	OnInstanceRequested func()
   753  
   754  	// OnInstanceCreated optionally specifies a hook to run synchronously
   755  	// after the instance operation succeeds.
   756  	OnInstanceCreated func()
   757  
   758  	// OnInstanceRegistration optionally specifies a hook to run synchronously
   759  	// after the instance has been registered in rendezvous.
   760  	OnInstanceRegistration func()
   761  }
   762  
   763  // startNewSwarmingTask starts a new swarming task on a bot with the buildlet
   764  // running on it. It returns a buildlet client configured to speak to it.
   765  // The request will last as long as the lifetime of the context. The dimensions
   766  // are a set of key value pairs used to describe what instance type to create.
   767  func (ss *SwarmingServer) startNewSwarmingTask(ctx context.Context, name string, dimensions map[string]string, properties *configProperties, opts *SwarmOpts, useGolangbuild bool) (buildlet.Client, error) {
   768  	ss.rendezvous.RegisterInstance(ctx, name, 10*time.Minute)
   769  	condRun(opts.OnInstanceRegistration)
   770  
   771  	taskID, err := ss.newSwarmingTask(ctx, name, dimensions, properties, opts, useGolangbuild)
   772  	if err != nil {
   773  		ss.rendezvous.DeregisterInstance(ctx, name)
   774  		return nil, err
   775  	}
   776  	log.Printf("gomote: swarming task requested name=%s taskID=%s", name, taskID)
   777  	condRun(opts.OnInstanceRequested)
   778  
   779  	queryCtx, cancel := context.WithTimeout(ctx, 20*time.Minute)
   780  	defer cancel()
   781  	var taskUp bool
   782  	tryThenPeriodicallyDo(queryCtx, 5*time.Second, func(ctx context.Context, _ time.Time) {
   783  		resp, err := ss.swarmingClient.TaskResult(ctx, taskID, &swarming.TaskResultFields{WithPerf: false})
   784  		if err != nil {
   785  			log.Printf("gomote: unable to query for swarming task state: name=%s taskID=%s %s", name, taskID, err)
   786  			return
   787  		}
   788  		switch taskState := resp.GetState(); taskState {
   789  		case swarmpb.TaskState_COMPLETED, swarmpb.TaskState_RUNNING:
   790  			taskUp = true
   791  			cancel()
   792  		case swarmpb.TaskState_EXPIRED, swarmpb.TaskState_INVALID, swarmpb.TaskState_BOT_DIED, swarmpb.TaskState_CANCELED,
   793  			swarmpb.TaskState_CLIENT_ERROR, swarmpb.TaskState_KILLED, swarmpb.TaskState_NO_RESOURCE, swarmpb.TaskState_TIMED_OUT:
   794  			log.Printf("gomote: swarming task creation failed name=%s state=%s", name, taskState)
   795  			cancel()
   796  		case swarmpb.TaskState_PENDING:
   797  			// continue waiting
   798  		default:
   799  			log.Printf("gomote: unexpected swarming task state for %s: %s", name, taskState)
   800  		}
   801  	})
   802  	if !taskUp {
   803  		ss.rendezvous.DeregisterInstance(ctx, name)
   804  		return nil, fmt.Errorf("unable to create swarming task name=%s taskID=%s", name, taskID)
   805  	}
   806  	condRun(opts.OnInstanceCreated)
   807  
   808  	bc, err := ss.waitForInstanceOrFailure(ctx, taskID, name)
   809  	if err != nil {
   810  		ss.rendezvous.DeregisterInstance(ctx, name)
   811  		return nil, err
   812  	}
   813  	return bc, nil
   814  }
   815  
   816  // waitForInstanceOrFailure waits for either the swarming task to enter a failed state or the successful connection from
   817  // a buildlet client.
   818  func (ss *SwarmingServer) waitForInstanceOrFailure(ctx context.Context, taskID, name string) (buildlet.Client, error) {
   819  	queryCtx, cancel := context.WithTimeout(ctx, 25*time.Minute)
   820  
   821  	checkForTaskFailure := func(pollCtx context.Context) <-chan error {
   822  		errCh := make(chan error, 1)
   823  		go func() {
   824  			internal.PeriodicallyDo(pollCtx, 10*time.Second, func(ctx context.Context, _ time.Time) {
   825  				resp, err := ss.swarmingClient.TaskResult(ctx, taskID, &swarming.TaskResultFields{WithPerf: false})
   826  				if err != nil {
   827  					log.Printf("gomote: unable to query for swarming task state: name=%s taskID=%s %s", name, taskID, err)
   828  					return
   829  				}
   830  				switch taskState := resp.GetState(); taskState {
   831  				case swarmpb.TaskState_RUNNING:
   832  					// expected
   833  				case swarmpb.TaskState_EXPIRED, swarmpb.TaskState_INVALID, swarmpb.TaskState_BOT_DIED, swarmpb.TaskState_CANCELED,
   834  					swarmpb.TaskState_CLIENT_ERROR, swarmpb.TaskState_KILLED, swarmpb.TaskState_NO_RESOURCE, swarmpb.TaskState_TIMED_OUT, swarmpb.TaskState_COMPLETED:
   835  					errCh <- fmt.Errorf("swarming task creation failed name=%s state=%s", name, taskState)
   836  				default:
   837  					log.Printf("gomote: unexpected swarming task state for %s: %s", name, taskState)
   838  				}
   839  			})
   840  		}()
   841  		return errCh
   842  	}
   843  
   844  	type result struct {
   845  		err error
   846  		bc  buildlet.Client
   847  	}
   848  
   849  	getConn := func(waitCtx context.Context) <-chan *result {
   850  		ch := make(chan *result, 1)
   851  		go func() {
   852  			bc, err := ss.rendezvous.WaitForInstance(waitCtx, name)
   853  			if err != nil {
   854  				ss.rendezvous.DeregisterInstance(ctx, name)
   855  			}
   856  			ch <- &result{err: err, bc: bc}
   857  		}()
   858  		return ch
   859  	}
   860  
   861  	statusChan := checkForTaskFailure(queryCtx)
   862  	resChan := getConn(queryCtx)
   863  
   864  	select {
   865  	case err := <-statusChan:
   866  		cancel()
   867  		ss.rendezvous.DeregisterInstance(ctx, name)
   868  		log.Printf("gomote: failed waiting for task to run %q: %s", name, err)
   869  		return nil, err
   870  	case r := <-resChan:
   871  		cancel()
   872  		if r.err != nil {
   873  			log.Printf("gomote: failed to establish connection %q: %s", name, r.err)
   874  			return nil, r.err
   875  		}
   876  		return r.bc, r.err
   877  	}
   878  }
   879  
   880  func buildletStartup(goos, goarch string) string {
   881  	cmd := `import urllib.request
   882  import sys
   883  import platform
   884  import subprocess
   885  import os
   886  import stat
   887  
   888  def add_os_file_ext(filename):
   889      if sys.platform == "win32":
   890          return filename+".exe"
   891      return filename
   892  
   893  def sep():
   894      if sys.platform == "win32":
   895          return "\\"
   896      else:
   897          return "/"
   898  
   899  def delete_if_exists(file_path):
   900      if os.path.exists(file_path):
   901          os.remove(file_path)
   902  
   903  def make_executable(file_path):
   904      if sys.platform != "win32":
   905          st = os.stat(file_path)
   906          os.chmod(file_path, st.st_mode | stat.S_IEXEC)
   907  
   908  if __name__ == "__main__":
   909      buildlet_name = add_os_file_ext("buildlet")
   910      delete_if_exists(buildlet_name)
   911      urllib.request.urlretrieve("https://storage.googleapis.com/go-builder-data/buildlet.%s-%s", buildlet_name)
   912      make_executable(os.getcwd() + sep() + buildlet_name)
   913      buildlet_name = "."+sep()+buildlet_name
   914      subprocess.run([buildlet_name, "--coordinator=gomotessh.golang.org:443", "--reverse-type=swarming-task", "-swarming-bot", "-halt=false"], shell=False, env=os.environ.copy())
   915  `
   916  	return fmt.Sprintf(cmd, goos, goarch)
   917  }
   918  
   919  func createStringPairs(m map[string]string) []*swarmpb.StringPair {
   920  	dims := make([]*swarmpb.StringPair, 0, len(m))
   921  	for k, v := range m {
   922  		dims = append(dims, &swarmpb.StringPair{
   923  			Key:   k,
   924  			Value: v,
   925  		})
   926  	}
   927  	return dims
   928  }
   929  
   930  func platformToGoValues(platform string) (goos string, goarch string, err error) {
   931  	goos, goarch, ok := strings.Cut(platform, "-")
   932  	if !ok {
   933  		return "", "", fmt.Errorf("cipd_platform not in proper format=%s", platform)
   934  	}
   935  	if goos == "Mac" || goos == "mac" {
   936  		goos = "darwin"
   937  	}
   938  	if goarch == "armv6l" {
   939  		goarch = "arm"
   940  	}
   941  	return goos, goarch, nil
   942  }
   943  
   944  func (ss *SwarmingServer) newSwarmingTask(ctx context.Context, name string, dimensions map[string]string, properties *configProperties, opts *SwarmOpts, useGolangbuild bool) (string, error) {
   945  	if useGolangbuild {
   946  
   947  		return ss.newSwarmingTaskWithGolangbuild(ctx, name, dimensions, properties, opts)
   948  	}
   949  	cipdPlatform, ok := dimensions["cipd_platform"]
   950  	if !ok {
   951  		return "", fmt.Errorf("dimensions require cipd_platform: instance=%s", name)
   952  	}
   953  	goos, goarch, err := platformToGoValues(cipdPlatform)
   954  	if err != nil {
   955  		return "", err
   956  	}
   957  	packages := []*swarmpb.CipdPackage{
   958  		{Path: "tools/bin", PackageName: "infra/tools/luci-auth/" + cipdPlatform, Version: "latest"},
   959  		{Path: "tools/bootstrap-go", PackageName: "golang/bootstrap-go/" + cipdPlatform, Version: properties.BootstrapVersion},
   960  	}
   961  	pythonBin := "python3"
   962  	switch goos {
   963  	case "darwin":
   964  		pythonBin = `tools/bin/python3`
   965  		packages = append(packages,
   966  			&swarmpb.CipdPackage{Path: "tools/bin", PackageName: "infra/tools/mac_toolchain/" + cipdPlatform, Version: "latest"},
   967  			&swarmpb.CipdPackage{Path: "tools", PackageName: "infra/3pp/tools/cpython3/" + cipdPlatform, Version: "latest"})
   968  	case "windows":
   969  		pythonBin = `tools\bin\python3.exe`
   970  		packages = append(packages, &swarmpb.CipdPackage{Path: "tools", PackageName: "infra/3pp/tools/cpython3/" + cipdPlatform, Version: "latest"})
   971  	}
   972  
   973  	req := &swarmpb.NewTaskRequest{
   974  		Name:           name,
   975  		Priority:       20, // 30 is the priority for builds
   976  		ServiceAccount: "coordinator-builder@golang-ci-luci.iam.gserviceaccount.com",
   977  		TaskSlices: []*swarmpb.TaskSlice{
   978  			&swarmpb.TaskSlice{
   979  				Properties: &swarmpb.TaskProperties{
   980  					CipdInput: &swarmpb.CipdInput{
   981  						Packages: packages,
   982  					},
   983  					EnvPrefixes: []*swarmpb.StringListPair{
   984  						{Key: "PATH", Value: []string{"tools/bin", "go/bin"}},
   985  						{Key: "GOROOT_BOOTSTRAP", Value: []string{"tools/bootstrap-go"}},
   986  						{Key: "GOPATH", Value: []string{"gopath"}},
   987  					},
   988  					Command:    []string{pythonBin, "-c", buildletStartup(goos, goarch)},
   989  					Dimensions: createStringPairs(dimensions),
   990  					Env: []*swarmpb.StringPair{
   991  						&swarmpb.StringPair{
   992  							Key:   "GOMOTEID",
   993  							Value: name,
   994  						},
   995  					},
   996  					ExecutionTimeoutSecs: 86400,
   997  				},
   998  				ExpirationSecs:  86400,
   999  				WaitForCapacity: false,
  1000  			},
  1001  		},
  1002  		Tags:  []string{"golang_mode:gomote"},
  1003  		Realm: "golang:ci",
  1004  	}
  1005  	taskMD, err := ss.swarmingClient.NewTask(ctx, req)
  1006  	if err != nil {
  1007  		log.Printf("gomote: swarming task creation failed name=%s: %s", name, err)
  1008  		return "", fmt.Errorf("unable to start task: %w", err)
  1009  	}
  1010  	log.Printf("gomote: task created: id=%s https://chromium-swarm.appspot.com/task?id=%s", taskMD.TaskId, taskMD.TaskId)
  1011  	return taskMD.TaskId, nil
  1012  }
  1013  
  1014  func golangbuildStartup(goos, goarch, golangbuildBin string) string {
  1015  	cmd := `import urllib.request
  1016  import sys
  1017  import platform
  1018  import subprocess
  1019  import os
  1020  import stat
  1021  
  1022  def make_executable(file_path):
  1023      if sys.platform != "win32":
  1024          st = os.stat(file_path)
  1025          os.chmod(file_path, st.st_mode | stat.S_IEXEC)
  1026  
  1027  if __name__ == "__main__":
  1028      ext = ""
  1029      if sys.platform == "win32":
  1030          ext = ".exe"
  1031      sep = "/"
  1032      if sys.platform == "win32":
  1033          sep = "\\\\"
  1034      buildlet_file = "buildlet" + ext
  1035      buildlet_path = "." + sep + buildlet_file
  1036      if os.path.exists(buildlet_path):
  1037          os.remove(buildlet_path)
  1038      urllib.request.urlretrieve("https://storage.googleapis.com/go-builder-data/buildlet.%s-%s", buildlet_path)
  1039      make_executable(os.getcwd() + sep + buildlet_file)
  1040      subprocess.run(["%s", buildlet_path, "--coordinator=gomotessh.golang.org:443", "--reverse-type=swarming-task", "-swarming-bot", "-halt=false"], shell=False, env=os.environ.copy())
  1041  `
  1042  	return fmt.Sprintf(cmd, goos, goarch, golangbuildBin)
  1043  }
  1044  
  1045  func (ss *SwarmingServer) newSwarmingTaskWithGolangbuild(ctx context.Context, name string, dimensions map[string]string, properties *configProperties, opts *SwarmOpts) (string, error) {
  1046  	log.Printf("gomote: swarming task creation using golangbuild name=%s", name)
  1047  	cipdPlatform, ok := dimensions["cipd_platform"]
  1048  	if !ok {
  1049  		return "", fmt.Errorf("dimensions require cipd_platform: instance=%s", name)
  1050  	}
  1051  	goos, goarch, err := platformToGoValues(cipdPlatform)
  1052  	if err != nil {
  1053  		return "", err
  1054  	}
  1055  	packages := []*swarmpb.CipdPackage{
  1056  		{Path: "tools/bin", PackageName: "infra/experimental/golangbuild/" + cipdPlatform, Version: "latest"},
  1057  		{Path: "tools/bin", PackageName: "infra/tools/cipd/" + cipdPlatform, Version: "latest"},
  1058  		{Path: "tools/bin", PackageName: "infra/tools/luci-auth/" + cipdPlatform, Version: "latest"},
  1059  	}
  1060  	pythonBin := "python3"
  1061  	golangbuildBin := "golangbuild"
  1062  	switch goos {
  1063  	case "darwin":
  1064  		golangbuildBin = `tools/bin/golangbuild`
  1065  		pythonBin = `tools/bin/python3`
  1066  		packages = append(packages,
  1067  			&swarmpb.CipdPackage{Path: "tools", PackageName: "infra/3pp/tools/cpython3/" + cipdPlatform, Version: "latest"})
  1068  	case "windows":
  1069  		golangbuildBin = `tools\\bin\\golangbuild.exe`
  1070  		pythonBin = `tools\bin\python3.exe`
  1071  		packages = append(packages, &swarmpb.CipdPackage{Path: "tools", PackageName: "infra/3pp/tools/cpython3/" + cipdPlatform, Version: "latest"})
  1072  	}
  1073  
  1074  	req := &swarmpb.NewTaskRequest{
  1075  		Name:           name,
  1076  		Priority:       20, // 30 is the priority for builds
  1077  		ServiceAccount: "coordinator-builder@golang-ci-luci.iam.gserviceaccount.com",
  1078  		TaskSlices: []*swarmpb.TaskSlice{
  1079  			&swarmpb.TaskSlice{
  1080  				Properties: &swarmpb.TaskProperties{
  1081  					CipdInput: &swarmpb.CipdInput{
  1082  						Packages: packages,
  1083  					},
  1084  					EnvPrefixes: []*swarmpb.StringListPair{
  1085  						{Key: "PATH", Value: []string{"tools/bin"}},
  1086  					},
  1087  					Command:    []string{pythonBin, "-c", golangbuildStartup(goos, goarch, golangbuildBin)},
  1088  					Dimensions: createStringPairs(dimensions),
  1089  					Env: []*swarmpb.StringPair{
  1090  						&swarmpb.StringPair{
  1091  							Key:   "GOMOTEID",
  1092  							Value: name,
  1093  						},
  1094  						&swarmpb.StringPair{
  1095  							Key:   "GOMOTE_SETUP",
  1096  							Value: properties.BuilderId,
  1097  						},
  1098  					},
  1099  					ExecutionTimeoutSecs: 86400,
  1100  				},
  1101  				ExpirationSecs:  86400,
  1102  				WaitForCapacity: false,
  1103  			},
  1104  		},
  1105  		Tags:  []string{"golang_mode:gomote"},
  1106  		Realm: "golang:ci",
  1107  	}
  1108  	taskMD, err := ss.swarmingClient.NewTask(ctx, req)
  1109  	if err != nil {
  1110  		log.Printf("gomote: swarming task creation failed name=%s: %s", name, err)
  1111  		return "", fmt.Errorf("unable to start task: %w", err)
  1112  	}
  1113  	log.Printf("gomote: task created: id=%s https://chromium-swarm.appspot.com/task?id=%s", taskMD.TaskId, taskMD.TaskId)
  1114  	return taskMD.TaskId, nil
  1115  }
  1116  
  1117  func condRun(fn func()) {
  1118  	if fn != nil {
  1119  		fn()
  1120  	}
  1121  }
  1122  
  1123  // tryThenPeriodicallyDo calls f and then calls f every period until the provided context is cancelled.
  1124  func tryThenPeriodicallyDo(ctx context.Context, period time.Duration, f func(context.Context, time.Time)) {
  1125  	f(ctx, time.Now())
  1126  	internal.PeriodicallyDo(ctx, period, f)
  1127  }
  1128  
  1129  type configProperties struct {
  1130  	BootstrapVersion string `json:"bootstrap_version"`
  1131  	Mode             int    `json:"mode"`
  1132  	BuilderId        string // <project>/<bucket>/<builder>
  1133  }
  1134  
  1135  func builderProperties(builder *buildbucketpb.BuilderItem) (*configProperties, error) {
  1136  	cp := new(configProperties)
  1137  	if err := json.Unmarshal([]byte(builder.GetConfig().GetProperties()), cp); err != nil {
  1138  		return nil, fmt.Errorf("builder property unmarshal error: %s", err)
  1139  	}
  1140  	if cp.BootstrapVersion == "" {
  1141  		cp.BootstrapVersion = "latest"
  1142  	}
  1143  	id := builder.GetId()
  1144  	cp.BuilderId = fmt.Sprintf("%s/%s/%s", id.GetProject(), id.GetBucket(), id.GetBuilder())
  1145  	return cp, nil
  1146  }