go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/led/ledcli/edit.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 "net/http" 20 21 "github.com/maruel/subcommands" 22 23 "go.chromium.org/luci/auth" 24 "go.chromium.org/luci/common/flag/stringlistflag" 25 "go.chromium.org/luci/common/flag/stringmapflag" 26 "go.chromium.org/luci/led/job" 27 swarmingpb "go.chromium.org/luci/swarming/proto/api_v2" 28 ) 29 30 func editCmd(opts cmdBaseOptions) *subcommands.Command { 31 return &subcommands.Command{ 32 UsageLine: "edit [options]", 33 ShortDesc: "edits the userland of a JobDescription", 34 LongDesc: `Allows common manipulations to a JobDescription. 35 36 Example: 37 38 led get-builder ... | 39 led edit -d os=Linux -p something=[100] | 40 led launch 41 `, 42 43 CommandRun: func() subcommands.CommandRun { 44 ret := &cmdEdit{} 45 ret.initFlags(opts) 46 return ret 47 }, 48 } 49 } 50 51 type cmdEdit struct { 52 cmdBase 53 54 dimensions stringlistflag.Flag 55 properties stringmapflag.Value 56 propertiesAuto stringmapflag.Value 57 recipeName string 58 experimental string 59 experiments stringmapflag.Value 60 61 recipeIsolate string 62 recipeCIPDPkg string 63 recipeCIPDVer string 64 65 processedDimensions job.DimensionEditCommands 66 processedExperiments map[string]bool 67 68 swarmingHost string 69 taskName string 70 } 71 72 func (c *cmdEdit) initFlags(opts cmdBaseOptions) { 73 c.Flags.Var(&c.dimensions, "d", 74 "(repeatable) edit a dimension. "+ 75 "This takes a parameter of `dimension{=,-=,+=}[value[@expiration_secs]]`. "+ 76 "Specifying '=[value[@expiration_secs]]' will Reset the dimension to the"+ 77 " Set of values specified with = (repeating this adds to the Set."+ 78 " To clear the dimension, specify `dimension=`). "+ 79 "Specifying '-=value' will Delete the value from the dimension. "+ 80 "Specifying '+=value[@expiration_secs]' will Add that value to the dimension (expiration). "+ 81 "Operations are applied as Resets, Deletions, Additions. "+ 82 "If expiration_secs are omitted, all slices will have the dimension.") 83 84 c.Flags.Var(&c.properties, "p", 85 "(repeatable) override a recipe property. This takes a parameter of `property_name=json_value`. "+ 86 "Providing an empty json_value will remove that property.") 87 88 c.Flags.Var(&c.propertiesAuto, "pa", 89 "(repeatable) override a recipe property, using the recipe engine autoconvert rule. "+ 90 "This takes a parameter of `property_name=json_value_or_string`. If json_value_or_string "+ 91 "cannot be decoded as JSON, it will be used verbatim as the property value. "+ 92 "Providing an empty json_value will remove that property.") 93 94 c.Flags.StringVar(&c.recipeName, "r", "", 95 "override the `recipe` to run.") 96 97 // These three are used by the 'recipe_engine/led' module to pin the user 98 // task across nested led invocations. 99 c.Flags.StringVar(&c.recipeIsolate, "rbh", "", 100 "DEPRECATED: use `led edit-recipe-bundle` instead."+ 101 "override the recipe bundle `hash` (if not using CIPD or git). These should be prepared with"+ 102 " `recipes.py bundle` from the repo containing your desired recipe and then isolating the"+ 103 " resulting folder contents. The `led edit-recipe-bundle` subcommand does all this"+ 104 " automatically.") 105 c.Flags.StringVar(&c.recipeCIPDPkg, "rpkg", "", 106 "DEPRECATED: use `led edit-payload` instead."+ 107 "override the recipe CIPD `package` (if not using isolated).") 108 c.Flags.StringVar(&c.recipeCIPDVer, "rver", "", 109 "DEPRECATED: use `led edit-payload` instead."+ 110 "override the recipe CIPD `version` (if not using isolated).") 111 112 c.Flags.StringVar(&c.swarmingHost, "S", "", 113 "override the swarming `host` to launch the task on (i.e. chromium-swarm.appspot.com).") 114 115 c.Flags.StringVar(&c.taskName, "name", "", 116 "set the task name of the led job as it will show on swarming.") 117 118 c.Flags.StringVar(&c.experimental, "exp", "", 119 "set to `true` or `false` to change the Build.Input.Experimental value. `led` jobs, "+ 120 "by default, always start as experimental.") 121 c.Flags.Var(&c.experiments, "experiment", 122 "DEPRECATED: use `led get-build|get-builder -experiment` instead."+ 123 "(repeatable) enable or disable an experiment. This takes a parameter of `experiment_name=true|false` and "+ 124 "adds/removes the corresponding experiment. Already enabled experiments are left as is unless they "+ 125 "are explicitly disabled.") 126 127 c.cmdBase.initFlags(opts) 128 } 129 130 func (c *cmdEdit) positionalRange() (min, max int) { return 0, 0 } 131 func (c *cmdEdit) jobInput() bool { return true } 132 133 func (c *cmdEdit) validateFlags(ctx context.Context, _ []string, _ subcommands.Env) (err error) { 134 c.processedDimensions, err = job.MakeDimensionEditCommands(c.dimensions) 135 if err != nil { 136 return err 137 } 138 139 if c.processedExperiments, err = processExperiments(c.experiments); err != nil { 140 return err 141 } 142 143 return 144 } 145 146 func (c *cmdEdit) execute(ctx context.Context, _ *http.Client, _ auth.Options, inJob *job.Definition) (out any, err error) { 147 err = inJob.Edit(func(je job.Editor) { 148 je.EditDimensions(c.processedDimensions) 149 if host := c.swarmingHost; host != "" { 150 je.SwarmingHostname(c.swarmingHost) 151 } 152 if c.taskName != "" { 153 je.TaskName(c.taskName) 154 } 155 }) 156 if err == nil { 157 err = inJob.HighLevelEdit(func(je job.HighLevelEditor) { 158 je.Properties(c.properties, false) 159 je.Properties(c.propertiesAuto, true) 160 if c.recipeName != "" { 161 je.Properties(map[string]string{"recipe": c.recipeName}, true) 162 } 163 if c.recipeIsolate != "" || c.recipeCIPDPkg != "" || c.recipeCIPDVer != "" { 164 pkg, ver := inJob.HighLevelInfo().TaskPayloadSource() 165 if c.recipeIsolate == "" { 166 if c.recipeCIPDPkg != "" { 167 pkg = c.recipeCIPDPkg 168 } 169 if c.recipeCIPDVer != "" { 170 ver = c.recipeCIPDVer 171 } 172 } else { 173 pkg = "" 174 ver = "" 175 var digest *swarmingpb.Digest 176 if digest, err = job.ToCasDigest(c.recipeIsolate); err != nil { 177 return 178 } 179 je.CASTaskPayload("", &swarmingpb.CASReference{Digest: digest}) 180 } 181 je.TaskPayloadSource(pkg, ver) 182 } 183 if c.experimental != "" { 184 je.Experimental(c.experimental == "true") 185 } 186 je.Experiments(c.processedExperiments) 187 }) 188 } 189 return inJob, err 190 } 191 192 func (c *cmdEdit) Run(a subcommands.Application, args []string, env subcommands.Env) int { 193 return c.doContextExecute(a, c, args, env) 194 }