blob: 2e005e4655e6d857af79337fe6a3cc1a5f6af590 [file] [log] [blame]
// Copyright 2020 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ledcli
import (
"context"
"net/http"
"strconv"
"strings"
"github.com/maruel/subcommands"
"go.chromium.org/luci/auth"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/flag/stringlistflag"
"go.chromium.org/luci/common/flag/stringmapflag"
"go.chromium.org/luci/led/job"
apipb "go.chromium.org/luci/swarming/proto/api"
)
func editCmd(opts cmdBaseOptions) *subcommands.Command {
return &subcommands.Command{
UsageLine: "edit [options]",
ShortDesc: "edits the userland of a JobDescription",
LongDesc: `Allows common manipulations to a JobDescription.
Example:
led get-builder ... |
led edit -d os=Linux -p something=[100] |
led launch
`,
CommandRun: func() subcommands.CommandRun {
ret := &cmdEdit{}
ret.initFlags(opts)
return ret
},
}
}
type cmdEdit struct {
cmdBase
dimensions stringlistflag.Flag
properties stringmapflag.Value
propertiesAuto stringmapflag.Value
recipeName string
experimental string
experiments stringmapflag.Value
recipeIsolate string
recipeCIPDPkg string
recipeCIPDVer string
processedDimensions job.DimensionEditCommands
processedExperiments map[string]bool
swarmingHost string
taskName string
}
func (c *cmdEdit) initFlags(opts cmdBaseOptions) {
c.Flags.Var(&c.dimensions, "d",
"(repeatable) edit a dimension. "+
"This takes a parameter of `dimension{=,-=,+=}[value[@expiration_secs]]`. "+
"Specifying '=[value[@expiration_secs]]' will Reset the dimension to the"+
" Set of values specified with = (repeating this adds to the Set."+
" To clear the dimension, specify `dimension=`). "+
"Specifying '-=value' will Delete the value from the dimension. "+
"Specifying '+=value[@expiration_secs]' will Add that value to the dimension (expiration). "+
"Operations are applied as Resets, Deletions, Additions. "+
"If expiration_secs are omitted, all slices will have the dimension.")
c.Flags.Var(&c.properties, "p",
"(repeatable) override a recipe property. This takes a parameter of `property_name=json_value`. "+
"Providing an empty json_value will remove that property.")
c.Flags.Var(&c.propertiesAuto, "pa",
"(repeatable) override a recipe property, using the recipe engine autoconvert rule. "+
"This takes a parameter of `property_name=json_value_or_string`. If json_value_or_string "+
"cannot be decoded as JSON, it will be used verbatim as the property value. "+
"Providing an empty json_value will remove that property.")
c.Flags.StringVar(&c.recipeName, "r", "",
"override the `recipe` to run.")
// These three are used by the 'recipe_engine/led' module to pin the user
// task across nested led invocations.
c.Flags.StringVar(&c.recipeIsolate, "rbh", "",
"override the recipe bundle `hash` (if not using CIPD or git). These should be prepared with"+
" `recipes.py bundle` from the repo containing your desired recipe and then isolating the"+
" resulting folder contents. The `led edit-recipe-bundle` subcommand does all this"+
" automatically.")
c.Flags.StringVar(&c.recipeCIPDPkg, "rpkg", "",
"override the recipe CIPD `package` (if not using isolated).")
c.Flags.StringVar(&c.recipeCIPDVer, "rver", "",
"override the recipe CIPD `version` (if not using isolated).")
c.Flags.StringVar(&c.swarmingHost, "S", "",
"override the swarming `host` to launch the task on (i.e. chromium-swarm.appspot.com).")
c.Flags.StringVar(&c.taskName, "name", "",
"set the task name of the led job as it will show on swarming.")
c.Flags.StringVar(&c.experimental, "exp", "",
"set to `true` or `false` to change the Build.Input.Experimental value. `led` jobs, "+
"by default, always start as experimental.")
c.Flags.Var(&c.experiments, "experiment",
"(repeatable) enable or disable an experiment. This takes a parameter of `experiment_name=true|false` and "+
"adds/removes the corresponding experiment. Already enabled experiments are left as is unless they "+
"are explicitly disabled.")
c.cmdBase.initFlags(opts)
}
func (c *cmdEdit) positionalRange() (min, max int) { return 0, 0 }
func (c *cmdEdit) jobInput() bool { return true }
func (c *cmdEdit) validateFlags(ctx context.Context, _ []string, _ subcommands.Env) (err error) {
c.processedDimensions, err = job.MakeDimensionEditCommands(c.dimensions)
if err != nil {
return err
}
c.processedExperiments = make(map[string]bool, len(c.experiments))
for k, v := range c.experiments {
if v != "true" && v != "false" {
return errors.Reason("bad -experiment %s=...: the value should be `true` or `false`, got %q", k, v).Err()
}
c.processedExperiments[k] = v == "true"
}
return
}
func (c *cmdEdit) execute(ctx context.Context, _ *http.Client, _ auth.Options, inJob *job.Definition) (out interface{}, err error) {
err = inJob.Edit(func(je job.Editor) {
je.EditDimensions(c.processedDimensions)
if host := c.swarmingHost; host != "" {
je.SwarmingHostname(c.swarmingHost)
}
if c.taskName != "" {
je.TaskName(c.taskName)
}
})
if err == nil {
err = inJob.HighLevelEdit(func(je job.HighLevelEditor) {
je.Properties(c.properties, false)
je.Properties(c.propertiesAuto, true)
if c.recipeName != "" {
je.Properties(map[string]string{"recipe": c.recipeName}, true)
}
if c.recipeIsolate != "" || c.recipeCIPDPkg != "" || c.recipeCIPDVer != "" {
pkg, ver := inJob.HighLevelInfo().TaskPayloadSource()
if c.recipeIsolate == "" {
if c.recipeCIPDPkg != "" {
pkg = c.recipeCIPDPkg
}
if c.recipeCIPDVer != "" {
ver = c.recipeCIPDVer
}
} else {
pkg = ""
ver = ""
switch strs := strings.Split(c.recipeIsolate, "/"); {
case len(strs) == 1:
inJob.UserPayload.Digest = c.recipeIsolate
case len(strs) == 2:
inJob.CasUserPayload.Digest = &apipb.Digest{Hash: strs[0]}
if inJob.CasUserPayload.Digest.SizeBytes, err = strconv.ParseInt(strs[1], 10, 64); err != nil {
return
}
default:
err = errors.Reason("Invalid recipe bundle hash(rbh) %s", c.recipeIsolate).Err()
return
}
}
je.TaskPayloadSource(pkg, ver)
}
if c.experimental != "" {
je.Experimental(c.experimental == "true")
}
je.Experiments(c.processedExperiments)
})
}
return inJob, err
}
func (c *cmdEdit) Run(a subcommands.Application, args []string, env subcommands.Env) int {
return c.doContextExecute(a, c, args, env)
}