go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/led/ledcmd/launch.go (about)

     1  // Copyright 2020 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package ledcmd
    16  
    17  import (
    18  	"context"
    19  	"net/http"
    20  	"sort"
    21  	"time"
    22  
    23  	"google.golang.org/grpc/metadata"
    24  
    25  	"go.chromium.org/luci/auth"
    26  	"go.chromium.org/luci/buildbucket"
    27  	bbpb "go.chromium.org/luci/buildbucket/proto"
    28  	"go.chromium.org/luci/common/errors"
    29  	"go.chromium.org/luci/common/gcloud/googleoauth"
    30  	"go.chromium.org/luci/common/logging"
    31  	"go.chromium.org/luci/led/job"
    32  	swarmingpb "go.chromium.org/luci/swarming/proto/api_v2"
    33  
    34  	"go.chromium.org/luci/led/job/jobexport"
    35  	"go.chromium.org/luci/lucictx"
    36  )
    37  
    38  // UserAgentTag is added by default to all Swarming tasks launched by LED.
    39  const UserAgentTag = "user_agent:led"
    40  
    41  // LaunchSwarmingOpts are the options for LaunchSwarming.
    42  type LaunchSwarmingOpts struct {
    43  	// If true, just generates the NewTaskRequest but does not send it to swarming
    44  	// (SwarmingRpcsTaskRequestMetadata will be nil).
    45  	DryRun bool
    46  
    47  	// Must be a unique user identity string and must not be empty.
    48  	//
    49  	// Picking a bad value here means that generated logdog prefixes will
    50  	// possibly collide, and the swarming task's User field will be misreported.
    51  	//
    52  	// See GetUID to obtain a standardized value here.
    53  	UserID string
    54  
    55  	// If launched from within a swarming task, this will be the current swarming
    56  	// task's task id to be attached as the parent of the launched task.
    57  	ParentTaskId string
    58  
    59  	// A path, relative to ${ISOLATED_OUTDIR} of where to place the final
    60  	// build.proto from this build. If omitted, the build.proto will not be
    61  	// dumped.
    62  	FinalBuildProto string
    63  
    64  	KitchenSupport job.KitchenSupport
    65  
    66  	// A flag for swarming/ResultDB integration on the launched task.
    67  	ResultDB job.RDBEnablement
    68  
    69  	// If true, `user_agent:led` tag will not be added to the launched task tags,
    70  	// which is otherwise added by default.
    71  	NoLEDTag bool
    72  
    73  	// If the launched real Buildbucket build can outlive its parent or not.
    74  	// Only works in the real build mode.
    75  	CanOutliveParent bool
    76  }
    77  
    78  // GetUID derives a user id string from the Authenticator for use with
    79  // LaunchSwarming.
    80  //
    81  // If the given authenticator has the userinfo.email scope, this will be the
    82  // email associated with the Authenticator. Otherwise, this will be
    83  // 'uid:<opaque user id>'.
    84  func GetUID(ctx context.Context, authenticator *auth.Authenticator) (string, error) {
    85  	tok, err := authenticator.GetAccessToken(time.Minute)
    86  	if err != nil {
    87  		return "", errors.Annotate(err, "getting access token").Err()
    88  	}
    89  	info, err := googleoauth.GetTokenInfo(ctx, googleoauth.TokenInfoParams{
    90  		AccessToken: tok.AccessToken,
    91  	})
    92  	if err != nil {
    93  		return "", errors.Annotate(err, "getting access token info").Err()
    94  	}
    95  	if info.Email != "" {
    96  		return info.Email, nil
    97  	}
    98  	return "uid:" + info.Sub, nil
    99  }
   100  
   101  // LaunchSwarming launches the given job Definition on swarming, returning the
   102  // NewTaskRequest launched, as well as the launch metadata.
   103  func LaunchSwarming(ctx context.Context, authClient *http.Client, jd *job.Definition, opts LaunchSwarmingOpts) (*swarmingpb.NewTaskRequest, *swarmingpb.TaskRequestMetadataResponse, error) {
   104  	if opts.KitchenSupport == nil {
   105  		opts.KitchenSupport = job.NoKitchenSupport()
   106  	}
   107  	if opts.UserID == "" {
   108  		return nil, nil, errors.New("opts.UserID is empty")
   109  	}
   110  
   111  	logging.Infof(ctx, "building swarming task")
   112  	if err := jd.FlattenToSwarming(ctx, opts.UserID, opts.ParentTaskId, opts.KitchenSupport, opts.ResultDB); err != nil {
   113  		return nil, nil, errors.Annotate(err, "failed to flatten job definition to swarming").Err()
   114  	}
   115  
   116  	st, err := jobexport.ToSwarmingNewTask(jd.GetSwarming())
   117  	if err != nil {
   118  		return nil, nil, err
   119  	}
   120  	if !opts.NoLEDTag {
   121  		addUserAgentTag(st)
   122  	}
   123  	logging.Infof(ctx, "building swarming task: done")
   124  
   125  	if opts.DryRun {
   126  		return st, nil, nil
   127  	}
   128  
   129  	swarmTasksClient := newSwarmTasksClient(authClient, jd.Info().SwarmingHostname())
   130  
   131  	logging.Infof(ctx, "launching swarming task")
   132  	resp, err := swarmTasksClient.NewTask(ctx, st)
   133  	if err != nil {
   134  		return nil, nil, err
   135  	}
   136  	logging.Infof(ctx, "launching swarming task: done")
   137  
   138  	return st, resp, nil
   139  }
   140  
   141  func addUserAgentTag(req *swarmingpb.NewTaskRequest) {
   142  	for _, t := range req.Tags {
   143  		if t == UserAgentTag {
   144  			return
   145  		}
   146  	}
   147  	req.Tags = append(req.Tags, UserAgentTag)
   148  }
   149  
   150  // LaunchBuild creates a real Buildbucket build based on the given job Definition.
   151  func LaunchBuild(ctx context.Context, authClient *http.Client, jd *job.Definition, opts LaunchSwarmingOpts) (*bbpb.Build, error) {
   152  	if jd.GetBuildbucket() == nil {
   153  		return nil, nil
   154  	}
   155  	bb := jd.GetBuildbucket()
   156  	build := bb.GetBbagentArgs().GetBuild()
   157  	build.CanOutliveParent = opts.CanOutliveParent
   158  	err := bb.UpdateLedProperties()
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  
   163  	// Attach user tag.
   164  	tags := build.Tags
   165  	tags = append(tags, &bbpb.StringPair{
   166  		Key:   "user",
   167  		Value: opts.UserID,
   168  	})
   169  	sort.Slice(tags, func(i, j int) bool { return tags[i].Key < tags[j].Key })
   170  	build.Tags = tags
   171  
   172  	if opts.DryRun {
   173  		return build, nil
   174  	}
   175  
   176  	bbClient := newBuildbucketClient(authClient, build.GetInfra().GetBuildbucket().GetHostname())
   177  
   178  	bbCtx := lucictx.GetBuildbucket(ctx)
   179  	if bbCtx != nil && bbCtx.GetScheduleBuildToken() != "" && bbCtx.GetScheduleBuildToken() != buildbucket.DummyBuildbucketToken {
   180  		ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs(buildbucket.BuildbucketTokenHeader, bbCtx.ScheduleBuildToken))
   181  	} else {
   182  		build.CanOutliveParent = false
   183  	}
   184  	return bbClient.CreateBuild(ctx, &bbpb.CreateBuildRequest{
   185  		Build: build,
   186  	})
   187  }