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 }