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  }