go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/led/ledcli/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 ledcli 16 17 import ( 18 "context" 19 "errors" 20 "net/http" 21 "os" 22 "strings" 23 24 "github.com/maruel/subcommands" 25 26 "go.chromium.org/luci/auth" 27 "go.chromium.org/luci/common/data/text" 28 "go.chromium.org/luci/common/logging" 29 "go.chromium.org/luci/common/system/terminal" 30 "go.chromium.org/luci/led/job" 31 "go.chromium.org/luci/led/ledcmd" 32 "go.chromium.org/luci/swarming/client/swarming" 33 ) 34 35 func launchCmd(opts cmdBaseOptions) *subcommands.Command { 36 return &subcommands.Command{ 37 UsageLine: "launch", 38 ShortDesc: "launches a JobDefinition on swarming", 39 LongDesc: `Launches a given JobDefinition on swarming. 40 41 Example: 42 43 led get-builder ... | 44 led edit ... | 45 led launch 46 47 If stdout is not a tty (e.g. a file), this command writes a JSON object 48 containing information about the launched task to stdout. 49 `, 50 51 CommandRun: func() subcommands.CommandRun { 52 ret := &cmdLaunch{} 53 ret.initFlags(opts) 54 return ret 55 }, 56 } 57 } 58 59 type cmdLaunch struct { 60 cmdBase 61 62 modernize bool 63 dump bool 64 noLEDTag bool 65 resultdb job.RDBEnablement 66 realBuild bool 67 boundToParent bool 68 } 69 70 func (c *cmdLaunch) initFlags(opts cmdBaseOptions) { 71 c.Flags.BoolVar(&c.modernize, "modernize", false, "Update the launched task to modern LUCI standards.") 72 c.Flags.BoolVar(&c.noLEDTag, "no-led-tag", false, "Don't add user_agent:led tag") 73 c.Flags.BoolVar(&c.dump, "dump", false, "Dump swarming task to stdout instead of running it.") 74 c.resultdb = "" 75 c.Flags.Var(&c.resultdb, "resultdb", text.Doc(` 76 Flag for Swarming/ResultDB integration on the launched task. Can be "on" or "off". 77 If "on", resultdb will be forcefully enabled. 78 If "off", resultdb will be forcefully disabled. 79 If unspecified, resultdb will be enabled if the original build had resultdb enabled.`)) 80 c.Flags.BoolVar(&c.realBuild, "real-build", false, text.Doc(` 81 DEPRECATED: Launch a real Buildbucket build instead of a raw swarming task. 82 If the job definition is for a real build, led will launch a real build regardless of this flag. 83 If the job definition is for a raw swarming task but this flag is set, led launch will fail.`)) 84 c.Flags.BoolVar(&c.boundToParent, "bound-to-parent", false, text.Doc(` 85 If the launched job is bound to its parent or not. 86 If true, the launched job CANNOT outlive its parent. 87 This flag only has effect if the launched job is a real Buildbucket build.`)) 88 c.cmdBase.initFlags(opts) 89 } 90 91 func (c *cmdLaunch) jobInput() bool { return true } 92 func (c *cmdLaunch) positionalRange() (min, max int) { return 0, 0 } 93 94 func (c *cmdLaunch) validateFlags(ctx context.Context, _ []string, _ subcommands.Env) (err error) { 95 return 96 } 97 98 func (c *cmdLaunch) execute(ctx context.Context, authClient *http.Client, _ auth.Options, inJob *job.Definition) (out any, err error) { 99 uid, err := ledcmd.GetUID(ctx, c.authenticator) 100 if err != nil { 101 return nil, err 102 } 103 104 switch { 105 case c.realBuild == inJob.GetBuildbucket().GetRealBuild(): 106 case !c.realBuild && inJob.GetBuildbucket().GetRealBuild(): 107 // Likely for `led get-* -real-build | led launch`. 108 // We should allow it and treat it as 109 // `led get-* -real-build | led launch -real-build` 110 logging.Infof(ctx, "Launching the led job as a real build") 111 c.realBuild = true 112 case c.realBuild && !inJob.GetBuildbucket().GetRealBuild(): 113 // Likely for `led get-* | led launch -real-build`. 114 // Fail it. 115 return nil, errors.New("cannot launch a led real build from a legacy job definition") 116 } 117 118 opts := ledcmd.LaunchSwarmingOpts{ 119 DryRun: c.dump, 120 UserID: uid, 121 FinalBuildProto: "build.proto.json", 122 KitchenSupport: c.kitchenSupport, 123 ParentTaskId: os.Getenv(swarming.TaskIDEnvVar), 124 ResultDB: c.resultdb, 125 NoLEDTag: c.noLEDTag, 126 CanOutliveParent: !c.boundToParent, 127 } 128 129 buildbucketHostname := inJob.GetBuildbucket().GetBbagentArgs().GetBuild().GetInfra().GetBuildbucket().GetHostname() 130 swarmingHostname := inJob.Info().SwarmingHostname() 131 miloHost := "ci.chromium.org" 132 if strings.Contains(buildbucketHostname, "-dev") || strings.Contains(swarmingHostname, "-dev") { 133 miloHost = "luci-milo-dev.appspot.com" 134 } 135 136 // Currently modernize only means 'upgrade to bbagent from kitchen'. 137 if bb := inJob.GetBuildbucket(); bb != nil { 138 if c.modernize { 139 bb.LegacyKitchen = false 140 } 141 if c.realBuild { 142 build, err := ledcmd.LaunchBuild(ctx, authClient, inJob, opts) 143 if err != nil { 144 return nil, err 145 } 146 if c.dump { 147 return build, nil 148 } 149 logging.Infof(ctx, "LUCI UI: https://%s/b/%d", miloHost, build.Id) 150 if !terminal.IsTerminal(int(os.Stdout.Fd())) { 151 ret := &struct { 152 Buildbucket struct { 153 // The id of the launched build. 154 BuildID int64 `json:"build_id"` 155 156 // The hostname of the buildbucket server 157 Hostname string `json:"host_name"` 158 } `json:"buildbucket"` 159 }{} 160 161 ret.Buildbucket.BuildID = build.Id 162 ret.Buildbucket.Hostname = buildbucketHostname 163 return ret, nil 164 } 165 return nil, nil 166 } 167 } 168 169 task, meta, err := ledcmd.LaunchSwarming(ctx, authClient, inJob, opts) 170 if err != nil { 171 return nil, err 172 } 173 if c.dump { 174 return task, nil 175 } 176 177 logging.Infof(ctx, "Launched swarming task: https://%s/task?id=%s", 178 swarmingHostname, meta.TaskId) 179 logging.Infof(ctx, "LUCI UI: https://%s/swarming/task/%s?server=%s", 180 miloHost, meta.TaskId, swarmingHostname) 181 182 ret := &struct { 183 Swarming struct { 184 // The swarming task ID of the launched task. 185 TaskID string `json:"task_id"` 186 187 // The hostname of the swarming server 188 Hostname string `json:"host_name"` 189 } `json:"swarming"` 190 }{} 191 192 if !terminal.IsTerminal(int(os.Stdout.Fd())) { 193 ret.Swarming.TaskID = meta.TaskId 194 ret.Swarming.Hostname = swarmingHostname 195 } else { 196 ret = nil 197 } 198 199 return ret, nil 200 } 201 202 func (c *cmdLaunch) Run(a subcommands.Application, args []string, env subcommands.Env) int { 203 return c.doContextExecute(a, c, args, env) 204 }