go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/led/ledcli/edit_recipe_bundle.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 "fmt" 20 "net/http" 21 "os" 22 "path/filepath" 23 "time" 24 25 "github.com/maruel/subcommands" 26 27 "go.chromium.org/luci/auth" 28 "go.chromium.org/luci/common/errors" 29 "go.chromium.org/luci/common/flag/stringmapflag" 30 31 "go.chromium.org/luci/led/job" 32 "go.chromium.org/luci/led/ledcmd" 33 ) 34 35 func editRecipeBundleCmd(opts cmdBaseOptions) *subcommands.Command { 36 return &subcommands.Command{ 37 UsageLine: "edit-recipe-bundle [-O project_id=/path/to/local/repo]*", 38 ShortDesc: "isolates recipes and adds them to a JobDefinition", 39 LongDesc: `Takes recipes from the current repo (based on cwd), along with 40 any supplied overrides, and pushes them to the isolate service. The CAS digest 41 for the recipes will be added to the JobDefinition. If the -property-only flag 42 is passed or the builder has the "led_builder_is_bootstrapped" property set to 43 true, the "led_cas_recipe_bundle" property will be set with the CAS digest so 44 that the build's bootstrapper executable can launch the bundled recipes. 45 46 Isolating recipes takes a bit of time, so you may want to save the result 47 of this command (stdout) to an intermediate file for quick edits. 48 `, 49 50 CommandRun: func() subcommands.CommandRun { 51 ret := &cmdEditRecipeBundle{} 52 ret.initFlags(opts) 53 return ret 54 }, 55 } 56 } 57 58 type cmdEditRecipeBundle struct { 59 cmdBase 60 61 debugSleep time.Duration 62 63 propertyOnly bool 64 65 overrides stringmapflag.Value 66 } 67 68 func (c *cmdEditRecipeBundle) initFlags(opts cmdBaseOptions) { 69 c.Flags.Var(&c.overrides, "O", 70 "(repeatable) override a repo dependency. Takes a parameter of `project_id=/path/to/local/repo`.") 71 72 c.Flags.DurationVar(&c.debugSleep, "debug-sleep", 0, 73 "Injects an extra 'sleep' time into the recipe shim which will sleep for the "+ 74 "designated amount of time after the recipe completes to allow SSH "+ 75 "debugging of failed recipe state. This accepts a duration like `2h`. "+ 76 "Valid units are 's', 'm', or 'h'.") 77 78 c.Flags.BoolVar(&c.propertyOnly, "property-only", false, 79 fmt.Sprintf("Pass the CAS blob information as JSON via the %q property and "+ 80 "preserve the executable of the input job rather than overwriting it. This "+ 81 "is useful for when `exe` is actually a bootstrap program that you don't "+ 82 "want to change. The same behavior can be enabled for a build without this "+ 83 "flag by setting the \"led_builder_is_bootstrapped\" property to true.", 84 ledcmd.CASRecipeBundleProperty)) 85 86 c.cmdBase.initFlags(opts) 87 } 88 89 func (c *cmdEditRecipeBundle) jobInput() bool { return true } 90 func (c *cmdEditRecipeBundle) positionalRange() (min, max int) { return 0, 0 } 91 92 func (c *cmdEditRecipeBundle) validateFlags(ctx context.Context, _ []string, _ subcommands.Env) (err error) { 93 for k, v := range c.overrides { 94 if k == "" { 95 return errors.New("override has empty project_id") 96 } 97 if v == "" { 98 return errors.Reason("override %q has empty repo path", k).Err() 99 } 100 v, err = filepath.Abs(v) 101 if err != nil { 102 return errors.Annotate(err, "override %q", k).Err() 103 } 104 c.overrides[k] = v 105 106 var fi os.FileInfo 107 switch fi, err = os.Stat(v); { 108 case err != nil: 109 return errors.Annotate(err, "override %q", k).Err() 110 case !fi.IsDir(): 111 return errors.Reason("override %q: not a directory", k).Err() 112 } 113 } 114 115 switch { 116 case c.debugSleep == 0: 117 // OK 118 119 case c.debugSleep < 0: 120 return errors.Reason( 121 "-debug-sleep %q: duration may not be negative", c.debugSleep).Err() 122 123 case c.debugSleep < 10*time.Minute: 124 return errors.Reason( 125 "-debug-sleep %q: duration is less than 10 minutes... are you sure you want that?", 126 c.debugSleep).Err() 127 } 128 129 return 130 } 131 132 func (c *cmdEditRecipeBundle) execute(ctx context.Context, _ *http.Client, authOpts auth.Options, inJob *job.Definition) (out any, err error) { 133 return inJob, ledcmd.EditRecipeBundle(ctx, authOpts, inJob, &ledcmd.EditRecipeBundleOpts{ 134 Overrides: c.overrides, 135 DebugSleep: c.debugSleep, 136 PropertyOnly: c.propertyOnly, 137 }) 138 } 139 140 func (c *cmdEditRecipeBundle) Run(a subcommands.Application, args []string, env subcommands.Env) int { 141 return c.doContextExecute(a, c, args, env) 142 }