| // 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" |
| "fmt" |
| "net/http" |
| "os" |
| "path/filepath" |
| "time" |
| |
| "github.com/maruel/subcommands" |
| |
| "go.chromium.org/luci/auth" |
| "go.chromium.org/luci/common/errors" |
| "go.chromium.org/luci/common/flag/stringmapflag" |
| |
| "go.chromium.org/luci/led/job" |
| "go.chromium.org/luci/led/ledcmd" |
| ) |
| |
| func editRecipeBundleCmd(opts cmdBaseOptions) *subcommands.Command { |
| return &subcommands.Command{ |
| UsageLine: "edit-recipe-bundle [-O project_id=/path/to/local/repo]*", |
| ShortDesc: "isolates recipes and adds them to a JobDefinition", |
| LongDesc: `Takes recipes from the current repo (based on cwd), along with |
| any supplied overrides, and pushes them to the isolate service. The CAS digest |
| for the recipes will be added to the JobDefinition. If the -property-only flag |
| is passed or the builder has the "led_builder_is_bootstrapped" property set to |
| true, the "led_cas_recipe_bundle" property will be set with the CAS digest so |
| that the build's bootstrapper executable can launch the bundled recipes. |
| |
| Isolating recipes takes a bit of time, so you may want to save the result |
| of this command (stdout) to an intermediate file for quick edits. |
| `, |
| |
| CommandRun: func() subcommands.CommandRun { |
| ret := &cmdEditRecipeBundle{} |
| ret.initFlags(opts) |
| return ret |
| }, |
| } |
| } |
| |
| type cmdEditRecipeBundle struct { |
| cmdBase |
| |
| debugSleep time.Duration |
| |
| propertyOnly bool |
| |
| overrides stringmapflag.Value |
| } |
| |
| func (c *cmdEditRecipeBundle) initFlags(opts cmdBaseOptions) { |
| c.Flags.Var(&c.overrides, "O", |
| "(repeatable) override a repo dependency. Takes a parameter of `project_id=/path/to/local/repo`.") |
| |
| c.Flags.DurationVar(&c.debugSleep, "debug-sleep", 0, |
| "Injects an extra 'sleep' time into the recipe shim which will sleep for the "+ |
| "designated amount of time after the recipe completes to allow SSH "+ |
| "debugging of failed recipe state. This accepts a duration like `2h`. "+ |
| "Valid units are 's', 'm', or 'h'.") |
| |
| c.Flags.BoolVar(&c.propertyOnly, "property-only", false, |
| fmt.Sprintf("Pass the CAS blob information as JSON via the %q property and "+ |
| "preserve the executable of the input job rather than overwriting it. This "+ |
| "is useful for when `exe` is actually a bootstrap program that you don't "+ |
| "want to change. The same behavior can be enabled for a build without this "+ |
| "flag by setting the \"led_builder_is_bootstrapped\" property to true.", |
| ledcmd.CASRecipeBundleProperty)) |
| |
| c.cmdBase.initFlags(opts) |
| } |
| |
| func (c *cmdEditRecipeBundle) jobInput() bool { return true } |
| func (c *cmdEditRecipeBundle) positionalRange() (min, max int) { return 0, 0 } |
| |
| func (c *cmdEditRecipeBundle) validateFlags(ctx context.Context, _ []string, _ subcommands.Env) (err error) { |
| for k, v := range c.overrides { |
| if k == "" { |
| return errors.New("override has empty project_id") |
| } |
| if v == "" { |
| return errors.Reason("override %q has empty repo path", k).Err() |
| } |
| v, err = filepath.Abs(v) |
| if err != nil { |
| return errors.Annotate(err, "override %q", k).Err() |
| } |
| c.overrides[k] = v |
| |
| var fi os.FileInfo |
| switch fi, err = os.Stat(v); { |
| case err != nil: |
| return errors.Annotate(err, "override %q", k).Err() |
| case !fi.IsDir(): |
| return errors.Reason("override %q: not a directory", k).Err() |
| } |
| } |
| |
| switch { |
| case c.debugSleep == 0: |
| // OK |
| |
| case c.debugSleep < 0: |
| return errors.Reason( |
| "-debug-sleep %q: duration may not be negative", c.debugSleep).Err() |
| |
| case c.debugSleep < 10*time.Minute: |
| return errors.Reason( |
| "-debug-sleep %q: duration is less than 10 minutes... are you sure you want that?", |
| c.debugSleep).Err() |
| } |
| |
| return |
| } |
| |
| func (c *cmdEditRecipeBundle) execute(ctx context.Context, _ *http.Client, authOpts auth.Options, inJob *job.Definition) (out interface{}, err error) { |
| return inJob, ledcmd.EditRecipeBundle(ctx, authOpts, inJob, &ledcmd.EditRecipeBundleOpts{ |
| Overrides: c.overrides, |
| DebugSleep: c.debugSleep, |
| PropertyOnly: c.propertyOnly, |
| }) |
| } |
| |
| func (c *cmdEditRecipeBundle) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| return c.doContextExecute(a, c, args, env) |
| } |