go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cipd/client/cli/main.go (about)

     1  // Copyright 2014 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 cli
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"encoding/json"
    21  	"flag"
    22  	"fmt"
    23  	"io"
    24  	"io/ioutil"
    25  	"net/http"
    26  	"os"
    27  	"path/filepath"
    28  	"sort"
    29  	"strings"
    30  	"sync"
    31  	"time"
    32  
    33  	"github.com/maruel/subcommands"
    34  	"golang.org/x/exp/slices"
    35  
    36  	"go.chromium.org/luci/auth"
    37  	"go.chromium.org/luci/client/versioncli"
    38  	"go.chromium.org/luci/common/cli"
    39  	"go.chromium.org/luci/common/data/stringset"
    40  	"go.chromium.org/luci/common/errors"
    41  	"go.chromium.org/luci/common/flag/fixflagpos"
    42  	"go.chromium.org/luci/common/logging"
    43  	"go.chromium.org/luci/common/logging/gologger"
    44  	"go.chromium.org/luci/common/retry/transient"
    45  	"go.chromium.org/luci/common/system/environ"
    46  	"go.chromium.org/luci/common/system/signals"
    47  	"go.chromium.org/luci/common/system/terminal"
    48  
    49  	"go.chromium.org/luci/auth/client/authcli"
    50  
    51  	api "go.chromium.org/luci/cipd/api/cipd/v1"
    52  	"go.chromium.org/luci/cipd/client/cipd"
    53  	"go.chromium.org/luci/cipd/client/cipd/builder"
    54  	"go.chromium.org/luci/cipd/client/cipd/deployer"
    55  	"go.chromium.org/luci/cipd/client/cipd/digests"
    56  	"go.chromium.org/luci/cipd/client/cipd/ensure"
    57  	"go.chromium.org/luci/cipd/client/cipd/fs"
    58  	"go.chromium.org/luci/cipd/client/cipd/pkg"
    59  	"go.chromium.org/luci/cipd/client/cipd/reader"
    60  	"go.chromium.org/luci/cipd/client/cipd/template"
    61  	"go.chromium.org/luci/cipd/client/cipd/ui"
    62  	"go.chromium.org/luci/cipd/common"
    63  	"go.chromium.org/luci/cipd/common/cipderr"
    64  )
    65  
    66  // TODO(vadimsh): Add some tests.
    67  
    68  // This is a killswitch that disables the fancy terminal progress bar UI in case
    69  // it has some fatal bugs or a user has aversion towards it.
    70  //
    71  // Note that cipd.Client doesn't know anything about the UI implementation and
    72  // thus this env var is defined here rather than in cipd/client.go like other
    73  // env vars.
    74  const envSimpleTerminalUI = "CIPD_SIMPLE_TERMINAL_UI"
    75  
    76  func expandTemplate(tmpl string) (pkg string, err error) {
    77  	pkg, err = template.DefaultExpander().Expand(tmpl)
    78  	if err != nil {
    79  		err = cliErrorTag.Apply(err)
    80  	}
    81  	return
    82  }
    83  
    84  // Parameters carry default configuration values for a CIPD CLI client.
    85  type Parameters struct {
    86  	// DefaultAuthOptions provide default values for authentication related
    87  	// options (most notably SecretsDir: a directory with token cache).
    88  	DefaultAuthOptions auth.Options
    89  
    90  	// ServiceURL is a backend URL to use by default.
    91  	ServiceURL string
    92  }
    93  
    94  ////////////////////////////////////////////////////////////////////////////////
    95  // Common subcommand functions.
    96  
    97  // pinInfo contains information about single package pin inside some site root,
    98  // or an error related to it. It is passed through channels when running batch
    99  // operations and dumped to JSON results file in doneWithPins.
   100  type pinInfo struct {
   101  	// Pkg is package name. Always set.
   102  	Pkg string `json:"package"`
   103  	// Pin is not nil if pin related operation succeeded. It contains instanceID.
   104  	Pin *common.Pin `json:"pin,omitempty"`
   105  	// Platform is set by 'ensure-file-verify' to a platform for this pin.
   106  	Platform string `json:"platform,omitempty"`
   107  	// Tracking is what ref is being tracked by that package in the site root.
   108  	Tracking string `json:"tracking,omitempty"`
   109  	// Error is not empty if pin related operation failed. Pin is nil in that case.
   110  	Error string `json:"error,omitempty"`
   111  	// ErrorCode is an enumeration with possible error conditions.
   112  	ErrorCode cipderr.Code `json:"error_code,omitempty"`
   113  	// ErrorDetails are structured error details.
   114  	ErrorDetails *cipderr.Details `json:"error_details,omitempty"`
   115  
   116  	// The original annotated error.
   117  	err error `json:"-"`
   118  }
   119  
   120  type instanceInfoWithRefs struct {
   121  	cipd.InstanceInfo
   122  	Refs []string `json:"refs,omitempty"`
   123  }
   124  
   125  // instancesOutput defines JSON format of 'cipd instances' output.
   126  type instancesOutput struct {
   127  	Instances []instanceInfoWithRefs `json:"instances"`
   128  }
   129  
   130  // cipdSubcommand is a base of all CIPD subcommands. It defines some common
   131  // flags, such as logging and JSON output parameters.
   132  type cipdSubcommand struct {
   133  	subcommands.CommandRunBase
   134  
   135  	jsonOutput string
   136  	logConfig  logging.Config
   137  
   138  	// TODO(dnj): Remove "verbose" flag once all current invocations of it are
   139  	// cleaned up and rolled out, as it is now deprecated in favor of "logConfig".
   140  	verbose bool
   141  }
   142  
   143  // ModifyContext implements cli.ContextModificator.
   144  func (c *cipdSubcommand) ModifyContext(ctx context.Context) context.Context {
   145  	if c.verbose {
   146  		ctx = logging.SetLevel(ctx, logging.Debug)
   147  	} else {
   148  		ctx = c.logConfig.Set(ctx)
   149  	}
   150  
   151  	// Give a lever to turn off the fancy UI if necessary.
   152  	useSimpleUI := environ.FromCtx(ctx).Get(envSimpleTerminalUI) == "1"
   153  
   154  	// If writing to a real terminal (rather than redirecting to a file) and not
   155  	// running at a non-default logging level, use a fancy UI with progress bars.
   156  	// It is more human readable, but doesn't preserve details of all operations
   157  	// in the terminal output.
   158  	if !useSimpleUI && logging.GetLevel(ctx) == logging.Info && terminal.IsTerminal(int(os.Stderr.Fd())) {
   159  		ctx = ui.SetImplementation(ctx, &ui.FancyImplementation{Out: os.Stderr})
   160  	}
   161  
   162  	return ctx
   163  }
   164  
   165  // registerBaseFlags registers common flags used by all subcommands.
   166  func (c *cipdSubcommand) registerBaseFlags() {
   167  	// Minimum default logging level is Info. This accommodates subcommands that
   168  	// don't explicitly set the log level, resulting in the zero value (Debug).
   169  	if c.logConfig.Level < logging.Info {
   170  		c.logConfig.Level = logging.Info
   171  	}
   172  
   173  	c.Flags.StringVar(&c.jsonOutput, "json-output", "", "A `path` to write operation results to.")
   174  	c.Flags.BoolVar(&c.verbose, "verbose", false, "Enable more logging (deprecated, use -log-level=debug).")
   175  	c.logConfig.AddFlags(&c.Flags)
   176  }
   177  
   178  // checkArgs checks command line args.
   179  //
   180  // It ensures all required positional and flag-like parameters are set.
   181  // Returns true if they are, or false (and prints to stderr) if not.
   182  func (c *cipdSubcommand) checkArgs(args []string, minPosCount, maxPosCount int) bool {
   183  	// Check number of expected positional arguments.
   184  	if maxPosCount == 0 && len(args) != 0 {
   185  		c.printError(makeCLIError("unexpected arguments %v", args))
   186  		return false
   187  	}
   188  	if len(args) < minPosCount || (maxPosCount >= 0 && len(args) > maxPosCount) {
   189  		var err error
   190  		if minPosCount == maxPosCount {
   191  			err = makeCLIError("expecting %d positional argument, got %d instead", minPosCount, len(args))
   192  		} else {
   193  			if maxPosCount >= 0 {
   194  				err = makeCLIError(
   195  					"expecting from %d to %d positional arguments, got %d instead",
   196  					minPosCount, maxPosCount, len(args))
   197  			} else {
   198  				err = makeCLIError(
   199  					"expecting at least %d positional arguments, got %d instead",
   200  					minPosCount, len(args))
   201  			}
   202  		}
   203  		c.printError(err)
   204  		return false
   205  	}
   206  
   207  	// Check required unset flags.
   208  	unset := []*flag.Flag{}
   209  	c.Flags.VisitAll(func(f *flag.Flag) {
   210  		if strings.HasPrefix(f.DefValue, "<") && f.Value.String() == f.DefValue {
   211  			unset = append(unset, f)
   212  		}
   213  	})
   214  	if len(unset) != 0 {
   215  		missing := make([]string, len(unset))
   216  		for i, f := range unset {
   217  			missing[i] = f.Name
   218  		}
   219  		c.printError(makeCLIError("missing required flags: %v", missing))
   220  		return false
   221  	}
   222  
   223  	return true
   224  }
   225  
   226  // printError prints error to stderr (recognizing cliErrorTag).
   227  func (c *cipdSubcommand) printError(err error) {
   228  	if cliErrorTag.In(err) {
   229  		fmt.Fprintf(os.Stderr, "Bad command line: %s.\n\n", err)
   230  		c.Flags.Usage()
   231  		return
   232  	}
   233  
   234  	if merr, _ := err.(errors.MultiError); len(merr) != 0 {
   235  		fmt.Fprintln(os.Stderr, "Errors:")
   236  		for _, err := range merr {
   237  			fmt.Fprintf(os.Stderr, "  %s\n", err)
   238  		}
   239  		return
   240  	}
   241  
   242  	fmt.Fprintf(os.Stderr, "Error: %s.\n", err)
   243  }
   244  
   245  // writeJSONOutput writes result to JSON output file. It returns original error
   246  // if it is non-nil.
   247  func (c *cipdSubcommand) writeJSONOutput(result any, err error) error {
   248  	// -json-output flag wasn't specified.
   249  	if c.jsonOutput == "" {
   250  		return err
   251  	}
   252  
   253  	// Prepare the body of the output file.
   254  	var body struct {
   255  		Error        string           `json:"error,omitempty"`         // human-readable message
   256  		ErrorCode    cipderr.Code     `json:"error_code,omitempty"`    // error code enum, omitted on success
   257  		ErrorDetails *cipderr.Details `json:"error_details,omitempty"` // structured error details
   258  		Result       any              `json:"result,omitempty"`
   259  	}
   260  	if err != nil {
   261  		body.Error = err.Error()
   262  		body.ErrorCode = cipderr.ToCode(err)
   263  		body.ErrorDetails = cipderr.ToDetails(err)
   264  	}
   265  	body.Result = result
   266  	out, e := json.MarshalIndent(&body, "", "  ")
   267  	if e != nil {
   268  		if err == nil {
   269  			err = e
   270  		} else {
   271  			fmt.Fprintf(os.Stderr, "Failed to serialize JSON output: %s\n", e)
   272  		}
   273  		return err
   274  	}
   275  
   276  	e = os.WriteFile(c.jsonOutput, out, 0666)
   277  	if e != nil {
   278  		if err == nil {
   279  			err = e
   280  		} else {
   281  			fmt.Fprintf(os.Stderr, "Failed write JSON output to %s: %s\n", c.jsonOutput, e)
   282  		}
   283  		return err
   284  	}
   285  
   286  	return err
   287  }
   288  
   289  // done is called as a last step of processing a subcommand. It dumps command
   290  // result (or error) to JSON output file, prints error message and generates
   291  // process exit code.
   292  func (c *cipdSubcommand) done(result any, err error) int {
   293  	err = c.writeJSONOutput(result, err)
   294  	if err != nil {
   295  		c.printError(err)
   296  		return 1
   297  	}
   298  	return 0
   299  }
   300  
   301  // doneWithPins is a handy shortcut that prints a pinInfo slice and
   302  // deduces process exit code based on presence of errors there.
   303  //
   304  // This just calls through to doneWithPinMap.
   305  func (c *cipdSubcommand) doneWithPins(pins []pinInfo, err error) int {
   306  	return c.doneWithPinMap(map[string][]pinInfo{"": pins}, err)
   307  }
   308  
   309  // doneWithPinMap is a handy shortcut that prints the subdir->pinInfo map and
   310  // deduces process exit code based on presence of errors there.
   311  func (c *cipdSubcommand) doneWithPinMap(pins map[string][]pinInfo, err error) int {
   312  	// If have an overall (not pin-specific error), print it before pin errors.
   313  	if err != nil {
   314  		c.printError(err)
   315  	}
   316  	printPinsAndError(pins)
   317  
   318  	// If have no overall error, hoist all pin errors up top, so we have a final
   319  	// overall error for writeJSONOutput, otherwise it may look as if the call
   320  	// succeeded.
   321  	if err == nil {
   322  		var merr errors.MultiError
   323  		for _, pinSlice := range pins {
   324  			for _, pin := range pinSlice {
   325  				if pin.err != nil {
   326  					merr = append(merr, pin.err)
   327  				}
   328  			}
   329  		}
   330  		if len(merr) != 0 {
   331  			err = merr
   332  		}
   333  	}
   334  
   335  	// Dump all this to JSON. Note we don't use done(...) to avoid printing the
   336  	// error again.
   337  	if c.writeJSONOutput(pins, err) != nil {
   338  		return 1
   339  	}
   340  	return 0
   341  }
   342  
   343  // cliErrorTag is used to tag errors related to CLI.
   344  var cliErrorTag = errors.BoolTag{Key: errors.NewTagKey("CIPD CLI error")}
   345  
   346  // makeCLIError returns a new error tagged with cliErrorTag and BadArgument.
   347  func makeCLIError(msg string, args ...any) error {
   348  	return errors.Reason(msg, args...).Tag(cliErrorTag, cipderr.BadArgument).Err()
   349  }
   350  
   351  ////////////////////////////////////////////////////////////////////////////////
   352  // maxThreadsOption mixin.
   353  
   354  type maxThreadsOption struct {
   355  	maxThreads int
   356  }
   357  
   358  func (opts *maxThreadsOption) registerFlags(f *flag.FlagSet) {
   359  	f.IntVar(&opts.maxThreads, "max-threads", 0,
   360  		"Number of worker threads for extracting packages. If 0 or negative, uses CPU count.")
   361  }
   362  
   363  // loadMaxThreads should only be used by subcommands that do not instantiate
   364  // the full CIPD client.
   365  func (opts *maxThreadsOption) loadMaxThreads(ctx context.Context) (int, error) {
   366  	clientOpts := cipd.ClientOptions{MaxThreads: opts.maxThreads}
   367  	if err := clientOpts.LoadFromEnv(ctx); err != nil {
   368  		return 0, err
   369  	}
   370  	return clientOpts.MaxThreads, nil
   371  }
   372  
   373  ////////////////////////////////////////////////////////////////////////////////
   374  // clientOptions mixin.
   375  
   376  type rootDirFlag bool
   377  
   378  const (
   379  	withRootDir    rootDirFlag = true
   380  	withoutRootDir rootDirFlag = false
   381  )
   382  
   383  type maxThreadsFlag bool
   384  
   385  const (
   386  	withMaxThreads    maxThreadsFlag = true
   387  	withoutMaxThreads maxThreadsFlag = false
   388  )
   389  
   390  // clientOptions defines command line arguments related to CIPD client creation.
   391  // Subcommands that need a CIPD client embed it.
   392  type clientOptions struct {
   393  	hardcoded Parameters // whatever was passed to registerFlags(...)
   394  
   395  	serviceURL string // also mutated by loadEnsureFile
   396  	cacheDir   string
   397  	maxThreads maxThreadsOption
   398  	rootDir    string              // used only if registerFlags got withRootDir arg
   399  	versions   ensure.VersionsFile // mutated by loadEnsureFile
   400  
   401  	authFlags authcli.Flags
   402  }
   403  
   404  func (opts *clientOptions) resolvedServiceURL(ctx context.Context) string {
   405  	if opts.serviceURL != "" {
   406  		return opts.serviceURL
   407  	}
   408  	if v := environ.FromCtx(ctx).Get(cipd.EnvCIPDServiceURL); v != "" {
   409  		return v
   410  	}
   411  	return opts.hardcoded.ServiceURL
   412  }
   413  
   414  func (opts *clientOptions) registerFlags(f *flag.FlagSet, params Parameters, rootDir rootDirFlag, maxThreads maxThreadsFlag) {
   415  	opts.hardcoded = params
   416  
   417  	f.StringVar(&opts.serviceURL, "service-url", "",
   418  		fmt.Sprintf(`Backend URL. If provided via an "ensure" file, the URL in the file takes precedence. `+
   419  			`(default %s)`, params.ServiceURL))
   420  	f.StringVar(&opts.cacheDir, "cache-dir", "",
   421  		fmt.Sprintf("Directory for the shared cache (can also be set by %s env var).", cipd.EnvCacheDir))
   422  
   423  	if rootDir {
   424  		f.StringVar(&opts.rootDir, "root", "<path>", "Path to an installation site root directory.")
   425  	}
   426  	if maxThreads {
   427  		opts.maxThreads.registerFlags(f)
   428  	}
   429  
   430  	opts.authFlags.Register(f, params.DefaultAuthOptions)
   431  }
   432  
   433  func (opts *clientOptions) toCIPDClientOpts(ctx context.Context) (cipd.ClientOptions, error) {
   434  	authOpts, err := opts.authFlags.Options()
   435  	if err != nil {
   436  		return cipd.ClientOptions{}, errors.Annotate(err, "bad auth options").Tag(cipderr.BadArgument).Err()
   437  	}
   438  	client, err := auth.NewAuthenticator(ctx, auth.OptionalLogin, authOpts).Client()
   439  	if err != nil {
   440  		return cipd.ClientOptions{}, errors.Annotate(err, "initializing auth client").Tag(cipderr.Auth).Err()
   441  	}
   442  
   443  	realOpts := cipd.ClientOptions{
   444  		Root:                opts.rootDir,
   445  		CacheDir:            opts.cacheDir,
   446  		Versions:            opts.versions,
   447  		AuthenticatedClient: client,
   448  		MaxThreads:          opts.maxThreads.maxThreads,
   449  		AnonymousClient:     http.DefaultClient,
   450  		PluginsContext:      ctx,
   451  		LoginInstructions:   "run `cipd auth-login` to login or relogin",
   452  	}
   453  	if err := realOpts.LoadFromEnv(ctx); err != nil {
   454  		return cipd.ClientOptions{}, err
   455  	}
   456  	realOpts.ServiceURL = opts.resolvedServiceURL(ctx)
   457  	return realOpts, nil
   458  }
   459  
   460  func (opts *clientOptions) makeCIPDClient(ctx context.Context) (cipd.Client, error) {
   461  	cipdOpts, err := opts.toCIPDClientOpts(ctx)
   462  	if err != nil {
   463  		return nil, err
   464  	}
   465  	return cipd.NewClient(cipdOpts)
   466  }
   467  
   468  ////////////////////////////////////////////////////////////////////////////////
   469  // inputOptions mixin.
   470  
   471  // packageVars holds array of '-pkg-var' command line options.
   472  type packageVars map[string]string
   473  
   474  func (vars *packageVars) String() string {
   475  	return "key:value"
   476  }
   477  
   478  // Set is called by 'flag' package when parsing command line options.
   479  func (vars *packageVars) Set(value string) error {
   480  	// <key>:<value> pair.
   481  	chunks := strings.SplitN(value, ":", 2)
   482  	if len(chunks) != 2 {
   483  		return makeCLIError("expecting <key>:<value> pair, got %q", value)
   484  	}
   485  	(*vars)[chunks[0]] = chunks[1]
   486  	return nil
   487  }
   488  
   489  // inputOptions defines command line arguments that specify where to get data
   490  // for a new package and how to build it.
   491  //
   492  // Subcommands that build packages embed it.
   493  type inputOptions struct {
   494  	// Path to *.yaml file with package definition.
   495  	packageDef string
   496  	vars       packageVars
   497  
   498  	// Alternative to 'pkg-def'.
   499  	packageName      string
   500  	inputDir         string
   501  	installMode      pkg.InstallMode
   502  	preserveModTime  bool
   503  	preserveWritable bool
   504  
   505  	// Deflate compression level (if [1-9]) or 0 to disable compression.
   506  	//
   507  	// Default is 5.
   508  	compressionLevel int
   509  }
   510  
   511  func (opts *inputOptions) registerFlags(f *flag.FlagSet) {
   512  	// Set default vars (e.g. ${platform}). They may be overridden through flags.
   513  	defVars := template.DefaultExpander()
   514  	opts.vars = make(packageVars, len(defVars))
   515  	for k, v := range defVars {
   516  		opts.vars[k] = v
   517  	}
   518  
   519  	// Interface to accept package definition file.
   520  	f.StringVar(&opts.packageDef, "pkg-def", "", "A *.yaml file `path` that defines what to put into the package.")
   521  	f.Var(&opts.vars, "pkg-var", "A `key:value` with a variable accessible from package definition file (can be used multiple times).")
   522  
   523  	// Interface to accept a single directory (alternative to -pkg-def).
   524  	f.StringVar(&opts.packageName, "name", "", "Package `name` (unused with -pkg-def).")
   525  	f.StringVar(&opts.inputDir, "in", "", "A `path` to a directory with files to package (unused with -pkg-def).")
   526  	f.Var(&opts.installMode, "install-mode",
   527  		"How the package should be installed: \"copy\" or \"symlink\" (unused with -pkg-def).")
   528  	f.BoolVar(&opts.preserveModTime, "preserve-mtime", false,
   529  		"Preserve file's modification time (unused with -pkg-def).")
   530  	f.BoolVar(&opts.preserveWritable, "preserve-writable", false,
   531  		"Preserve file's writable permission bit (unused with -pkg-def).")
   532  
   533  	// Options for the builder.
   534  	f.IntVar(&opts.compressionLevel, "compression-level", 5,
   535  		"Deflate compression level [0-9]: 0 - disable, 1 - best speed, 9 - best compression.")
   536  }
   537  
   538  // prepareInput processes inputOptions by collecting all files to be added to
   539  // a package and populating builder.Options. Caller is still responsible to fill
   540  // out Output field of Options.
   541  func (opts *inputOptions) prepareInput() (builder.Options, error) {
   542  	empty := builder.Options{}
   543  
   544  	if opts.compressionLevel < 0 || opts.compressionLevel > 9 {
   545  		return empty, makeCLIError("invalid -compression-level: must be in [0-9] set")
   546  	}
   547  
   548  	// Handle -name and -in if defined. Do not allow -pkg-def in that case, since
   549  	// it provides same information as -name and -in. Note that -pkg-var are
   550  	// ignored, even if defined. There's nothing to apply them to.
   551  	if opts.inputDir != "" {
   552  		if opts.packageName == "" {
   553  			return empty, makeCLIError("missing required flag: -name")
   554  		}
   555  		if opts.packageDef != "" {
   556  			return empty, makeCLIError("-pkg-def and -in can not be used together")
   557  		}
   558  
   559  		packageName, err := expandTemplate(opts.packageName)
   560  		if err != nil {
   561  			return empty, err
   562  		}
   563  
   564  		// Simply enumerate files in the directory.
   565  		files, err := fs.ScanFileSystem(opts.inputDir, opts.inputDir, nil, fs.ScanOptions{
   566  			PreserveModTime:  opts.preserveModTime,
   567  			PreserveWritable: opts.preserveWritable,
   568  		})
   569  		if err != nil {
   570  			return empty, err
   571  		}
   572  		return builder.Options{
   573  			Input:            files,
   574  			PackageName:      packageName,
   575  			InstallMode:      opts.installMode,
   576  			CompressionLevel: opts.compressionLevel,
   577  		}, nil
   578  	}
   579  
   580  	// Handle -pkg-def case. -in is "" (already checked), reject -name.
   581  	if opts.packageDef != "" {
   582  		if opts.packageName != "" {
   583  			return empty, makeCLIError("-pkg-def and -name can not be used together")
   584  		}
   585  		if opts.installMode != "" {
   586  			return empty, makeCLIError("-install-mode is ignored if -pkg-def is used")
   587  		}
   588  		if opts.preserveModTime {
   589  			return empty, makeCLIError("-preserve-mtime is ignored if -pkg-def is used")
   590  		}
   591  		if opts.preserveWritable {
   592  			return empty, makeCLIError("-preserve-writable is ignored if -pkg-def is used")
   593  		}
   594  
   595  		// Parse the file, perform variable substitution.
   596  		f, err := os.Open(opts.packageDef)
   597  		if err != nil {
   598  			if os.IsNotExist(err) {
   599  				return empty, errors.Annotate(err, "package definition file is missing").Tag(cipderr.BadArgument).Err()
   600  			}
   601  			return empty, errors.Annotate(err, "opening package definition file").Tag(cipderr.IO).Err()
   602  		}
   603  		defer f.Close()
   604  		pkgDef, err := builder.LoadPackageDef(f, opts.vars)
   605  		if err != nil {
   606  			return empty, err
   607  		}
   608  
   609  		// Scan the file system. Package definition may use path relative to the
   610  		// package definition file itself, so pass its location.
   611  		fmt.Println("Enumerating files to zip...")
   612  		files, err := pkgDef.FindFiles(filepath.Dir(opts.packageDef))
   613  		if err != nil {
   614  			return empty, err
   615  		}
   616  		return builder.Options{
   617  			Input:            files,
   618  			PackageName:      pkgDef.Package,
   619  			VersionFile:      pkgDef.VersionFile(),
   620  			InstallMode:      pkgDef.InstallMode,
   621  			CompressionLevel: opts.compressionLevel,
   622  		}, nil
   623  	}
   624  
   625  	// All command line options are missing.
   626  	return empty, makeCLIError("-pkg-def or -name/-in are required")
   627  }
   628  
   629  ////////////////////////////////////////////////////////////////////////////////
   630  // refsOptions mixin.
   631  
   632  // refList holds an array of '-ref' command line options.
   633  type refList []string
   634  
   635  func (refs *refList) String() string {
   636  	return "ref"
   637  }
   638  
   639  // Set is called by 'flag' package when parsing command line options.
   640  func (refs *refList) Set(value string) error {
   641  	err := common.ValidatePackageRef(value)
   642  	if err != nil {
   643  		return cliErrorTag.Apply(err)
   644  	}
   645  	*refs = append(*refs, value)
   646  	return nil
   647  }
   648  
   649  // refsOptions defines command line arguments for commands that accept a set
   650  // of refs.
   651  type refsOptions struct {
   652  	refs refList
   653  }
   654  
   655  func (opts *refsOptions) registerFlags(f *flag.FlagSet) {
   656  	f.Var(&opts.refs, "ref", "A `ref` to point to the package instance (can be used multiple times).")
   657  }
   658  
   659  ////////////////////////////////////////////////////////////////////////////////
   660  // tagsOptions mixin.
   661  
   662  // tagList holds an array of '-tag' command line options.
   663  type tagList []string
   664  
   665  func (tags *tagList) String() string {
   666  	return "key:value"
   667  }
   668  
   669  // Set is called by 'flag' package when parsing command line options.
   670  func (tags *tagList) Set(value string) error {
   671  	err := common.ValidateInstanceTag(value)
   672  	if err != nil {
   673  		return cliErrorTag.Apply(err)
   674  	}
   675  	*tags = append(*tags, value)
   676  	return nil
   677  }
   678  
   679  // tagsOptions defines command line arguments for commands that accept a set
   680  // of tags.
   681  type tagsOptions struct {
   682  	tags tagList
   683  }
   684  
   685  func (opts *tagsOptions) registerFlags(f *flag.FlagSet) {
   686  	f.Var(&opts.tags, "tag", "A `key:value` tag to attach to the package instance (can be used multiple times).")
   687  }
   688  
   689  ////////////////////////////////////////////////////////////////////////////////
   690  // metadataOptions mixin.
   691  
   692  type metadataFlagValue struct {
   693  	key         string
   694  	value       string // either a literal value or a path to read it from
   695  	contentType string
   696  }
   697  
   698  type metadataList struct {
   699  	entries   []metadataFlagValue
   700  	valueKind string // "value" or "path"
   701  }
   702  
   703  func (md *metadataList) String() string {
   704  	return "key:" + md.valueKind
   705  }
   706  
   707  // Set is called by 'flag' package when parsing command line options.
   708  func (md *metadataList) Set(value string) error {
   709  	// Should have form key_with_possible_content_type:value.
   710  	chunks := strings.SplitN(value, ":", 2)
   711  	if len(chunks) != 2 {
   712  		return md.badFormatError()
   713  	}
   714  
   715  	// Extract content-type from within trailing '(...)', if present.
   716  	key, contentType, value := chunks[0], "", chunks[1]
   717  	switch l, r := strings.Index(key, "("), strings.LastIndex(key, ")"); {
   718  	case l == -1 && r == -1:
   719  		// no content type, this is fine
   720  	case l != -1 && r != -1 && l < r:
   721  		// The closing ')' should be the last character.
   722  		if !strings.HasSuffix(key, ")") {
   723  			return md.badFormatError()
   724  		}
   725  		key, contentType = key[:l], key[l+1:r]
   726  	default:
   727  		return md.badFormatError()
   728  	}
   729  
   730  	// Validate everything we can.
   731  	if err := common.ValidateInstanceMetadataKey(key); err != nil {
   732  		return cliErrorTag.Apply(err)
   733  	}
   734  	if err := common.ValidateContentType(contentType); err != nil {
   735  		return cliErrorTag.Apply(err)
   736  	}
   737  	if md.valueKind == "value" {
   738  		if err := common.ValidateInstanceMetadataLen(len(value)); err != nil {
   739  			return cliErrorTag.Apply(err)
   740  		}
   741  	}
   742  
   743  	md.entries = append(md.entries, metadataFlagValue{
   744  		key:         key,
   745  		value:       value,
   746  		contentType: contentType,
   747  	})
   748  	return nil
   749  }
   750  
   751  func (md *metadataList) badFormatError() error {
   752  	return makeCLIError("should have form key:%s or key(content-type):%s", md.valueKind, md.valueKind)
   753  }
   754  
   755  // metadataOptions defines command line arguments for commands that accept a set
   756  // of metadata entries.
   757  type metadataOptions struct {
   758  	metadata         metadataList
   759  	metadataFromFile metadataList
   760  }
   761  
   762  func (opts *metadataOptions) registerFlags(f *flag.FlagSet) {
   763  	opts.metadata.valueKind = "value"
   764  	f.Var(&opts.metadata, "metadata",
   765  		"A metadata entry (`key:value` or key(content-type):value) to attach to the package instance (can be used multiple times).")
   766  
   767  	opts.metadataFromFile.valueKind = "path"
   768  	f.Var(&opts.metadataFromFile, "metadata-from-file",
   769  		"A metadata entry (`key:path` or key(content-type):path) to attach to the package instance (can be used multiple times). The path can be \"-\" to read from stdin.")
   770  }
   771  
   772  func (opts *metadataOptions) load(ctx context.Context) ([]cipd.Metadata, error) {
   773  	out := make([]cipd.Metadata, 0, len(opts.metadata.entries)+len(opts.metadataFromFile.entries))
   774  
   775  	// Convert -metadata to cipd.Metadata entries.
   776  	for _, md := range opts.metadata.entries {
   777  		entry := cipd.Metadata{
   778  			Key:         md.key,
   779  			Value:       []byte(md.value),
   780  			ContentType: md.contentType,
   781  		}
   782  		// The default content type for -metadata is text/plain (since values are
   783  		// supplied directly via the command line).
   784  		if entry.ContentType == "" {
   785  			entry.ContentType = "text/plain"
   786  		}
   787  		out = append(out, entry)
   788  	}
   789  
   790  	// Load -metadata-from-file entries. At most one `-metadata-from-file key:-`
   791  	// is allowed, we have only one stdin.
   792  	keyWithStdin := false
   793  	for _, md := range opts.metadataFromFile.entries {
   794  		if md.value == "-" {
   795  			if keyWithStdin {
   796  				return nil, makeCLIError("at most one -metadata-from-file can use \"-\" as a value")
   797  			}
   798  			keyWithStdin = true
   799  		}
   800  		entry := cipd.Metadata{
   801  			Key:         md.key,
   802  			ContentType: md.contentType,
   803  		}
   804  		var err error
   805  		if entry.Value, err = loadMetadataFromFile(ctx, md.key, md.value); err != nil {
   806  			return nil, err
   807  		}
   808  		// Guess the content type from the file extension and its body.
   809  		if entry.ContentType == "" {
   810  			entry.ContentType = guessMetadataContentType(md.value, entry.Value)
   811  		}
   812  		out = append(out, entry)
   813  	}
   814  
   815  	return out, nil
   816  }
   817  
   818  func loadMetadataFromFile(ctx context.Context, key, path string) ([]byte, error) {
   819  	var file *os.File
   820  	if path == "-" {
   821  		logging.Infof(ctx, "Reading metadata %q from the stdin...", key)
   822  		file = os.Stdin
   823  	} else {
   824  		logging.Infof(ctx, "Reading metadata %q from %q...", key, path)
   825  		var err error
   826  		file, err = os.Open(path)
   827  		if err != nil {
   828  			if os.IsNotExist(err) {
   829  				return nil, errors.Annotate(err, "missing metadata file").Tag(cipderr.BadArgument).Err()
   830  			}
   831  			return nil, errors.Annotate(err, "reading metadata file").Tag(cipderr.IO).Err()
   832  		}
   833  		defer file.Close()
   834  	}
   835  	// Read at most MetadataMaxLen plus one more byte to detect true EOF.
   836  	buf := bytes.Buffer{}
   837  	switch _, err := io.CopyN(&buf, file, common.MetadataMaxLen+1); {
   838  	case err == nil:
   839  		// Successfully read more than needed => the file size is too large.
   840  		return nil, errors.Reason("the metadata value in %q is too long, should be <=%d bytes", path, common.MetadataMaxLen).Tag(cipderr.BadArgument).Err()
   841  	case err != io.EOF:
   842  		// Failed with some unexpected read error.
   843  		return nil, errors.Annotate(err, "error reading metadata from %q", path).Tag(cipderr.IO).Err()
   844  	default:
   845  		return buf.Bytes(), nil
   846  	}
   847  }
   848  
   849  func guessMetadataContentType(path string, val []byte) string {
   850  	switch strings.ToLower(filepath.Ext(path)) {
   851  	case ".json":
   852  		return "application/json"
   853  	case ".jwt":
   854  		return "application/jwt"
   855  	default:
   856  		return http.DetectContentType(val)
   857  	}
   858  }
   859  
   860  ////////////////////////////////////////////////////////////////////////////////
   861  // uploadOptions mixin.
   862  
   863  // uploadOptions defines command line options for commands that upload packages.
   864  type uploadOptions struct {
   865  	verificationTimeout time.Duration
   866  }
   867  
   868  func (opts *uploadOptions) registerFlags(f *flag.FlagSet) {
   869  	f.DurationVar(
   870  		&opts.verificationTimeout, "verification-timeout",
   871  		cipd.CASFinalizationTimeout, "Maximum time to wait for backend-side package hash verification.")
   872  }
   873  
   874  ////////////////////////////////////////////////////////////////////////////////
   875  // hashOptions mixin.
   876  
   877  // allAlgos is used in the flag help text, it is "sha256, sha1, ...".
   878  var allAlgos string
   879  
   880  func init() {
   881  	algos := make([]string, 0, len(api.HashAlgo_name)-1)
   882  	for i := len(api.HashAlgo_name) - 1; i > 0; i-- {
   883  		algos = append(algos, strings.ToLower(api.HashAlgo_name[int32(i)]))
   884  	}
   885  	allAlgos = strings.Join(algos, ", ")
   886  }
   887  
   888  // hashAlgoFlag adapts api.HashAlgo to flag.Value interface.
   889  type hashAlgoFlag api.HashAlgo
   890  
   891  // String is called by 'flag' package when displaying default value of a flag.
   892  func (ha *hashAlgoFlag) String() string {
   893  	return strings.ToLower(api.HashAlgo(*ha).String())
   894  }
   895  
   896  // Set is called by 'flag' package when parsing command line options.
   897  func (ha *hashAlgoFlag) Set(value string) error {
   898  	val := api.HashAlgo_value[strings.ToUpper(value)]
   899  	if val == 0 {
   900  		return makeCLIError("unknown hash algo %q, should be one of: %s", value, allAlgos)
   901  	}
   902  	*ha = hashAlgoFlag(val)
   903  	return nil
   904  }
   905  
   906  // hashOptions defines -hash-algo flag that specifies hash algo to use for
   907  // constructing instance IDs.
   908  //
   909  // Default value is given by common.DefaultHashAlgo.
   910  //
   911  // Not all algos may be accepted by the server.
   912  type hashOptions struct {
   913  	algo hashAlgoFlag
   914  }
   915  
   916  func (opts *hashOptions) registerFlags(f *flag.FlagSet) {
   917  	opts.algo = hashAlgoFlag(common.DefaultHashAlgo)
   918  	f.Var(&opts.algo, "hash-algo", fmt.Sprintf("Algorithm to use for deriving package instance ID, one of: %s", allAlgos))
   919  }
   920  
   921  func (opts *hashOptions) hashAlgo() api.HashAlgo {
   922  	return api.HashAlgo(opts.algo)
   923  }
   924  
   925  ////////////////////////////////////////////////////////////////////////////////
   926  // ensureFileOptions mixin.
   927  
   928  type legacyListFlag bool
   929  
   930  const (
   931  	withLegacyListFlag    legacyListFlag = true
   932  	withoutLegacyListFlag legacyListFlag = false
   933  )
   934  
   935  type ensureOutFlag bool
   936  
   937  const (
   938  	withEnsureOutFlag    ensureOutFlag = true
   939  	withoutEnsureOutFlag ensureOutFlag = false
   940  )
   941  
   942  type verifyingEnsureFile bool
   943  
   944  const (
   945  	requireVerifyPlatforms verifyingEnsureFile = true
   946  	ignoreVerifyPlatforms  verifyingEnsureFile = false
   947  )
   948  
   949  type versionFileOpt bool
   950  
   951  const (
   952  	parseVersionsFile  versionFileOpt = true
   953  	ignoreVersionsFile versionFileOpt = false
   954  )
   955  
   956  // ensureFileOptions defines -ensure-file flag that specifies a location of the
   957  // "ensure file", which is a manifest that describes what should be installed
   958  // into a site root.
   959  type ensureFileOptions struct {
   960  	ensureFile    string
   961  	ensureFileOut string // used only if registerFlags got withEnsureOutFlag arg
   962  }
   963  
   964  func (opts *ensureFileOptions) registerFlags(f *flag.FlagSet, out ensureOutFlag, list legacyListFlag) {
   965  	f.StringVar(&opts.ensureFile, "ensure-file", "<path>",
   966  		`An "ensure" file. See syntax described here: `+
   967  			`https://godoc.org/go.chromium.org/luci/cipd/client/cipd/ensure.`+
   968  			` Providing '-' will read from stdin.`)
   969  	if out {
   970  		f.StringVar(&opts.ensureFileOut, "ensure-file-output", "",
   971  			`A path to write an "ensure" file which is the fully-resolved version `+
   972  				`of the input ensure file for the current platform. This output will `+
   973  				`not contain any ${params} or $Settings other than $ServiceURL.`)
   974  	}
   975  	if list {
   976  		f.StringVar(&opts.ensureFile, "list", "<path>", "(DEPRECATED) A synonym for -ensure-file.")
   977  	}
   978  }
   979  
   980  // loadEnsureFile parses the ensure file and mutates clientOpts to point to a
   981  // service URL specified in the ensure file.
   982  func (opts *ensureFileOptions) loadEnsureFile(ctx context.Context, clientOpts *clientOptions, verifying verifyingEnsureFile, parseVers versionFileOpt) (*ensure.File, error) {
   983  	parsedFile, err := ensure.LoadEnsureFile(opts.ensureFile)
   984  	if err != nil {
   985  		return nil, err
   986  	}
   987  
   988  	// Prefer the ServiceURL from the file (if set), and log a warning if the user
   989  	// provided one on the command line that doesn't match the one in the file.
   990  	if parsedFile.ServiceURL != "" {
   991  		if clientOpts.serviceURL != "" && clientOpts.serviceURL != parsedFile.ServiceURL {
   992  			logging.Warningf(ctx, "serviceURL in ensure file != serviceURL on CLI (%q v %q). Using %q from file.",
   993  				parsedFile.ServiceURL, clientOpts.serviceURL, parsedFile.ServiceURL)
   994  		}
   995  		clientOpts.serviceURL = parsedFile.ServiceURL
   996  	}
   997  
   998  	if verifying && len(parsedFile.VerifyPlatforms) == 0 {
   999  		defaultVerifiedPlatform := template.DefaultTemplate()
  1000  		parsedFile.VerifyPlatforms = append(parsedFile.VerifyPlatforms, defaultVerifiedPlatform)
  1001  
  1002  		logging.Infof(ctx, "$VerifiedPlatform directive required but not included in"+
  1003  			" ensure file, using '$VerifiedPlatform %s' as default.", defaultVerifiedPlatform)
  1004  	}
  1005  
  1006  	if parseVers && parsedFile.ResolvedVersions != "" {
  1007  		clientOpts.versions, err = loadVersionsFile(parsedFile.ResolvedVersions, opts.ensureFile)
  1008  		if err != nil {
  1009  			return nil, err
  1010  		}
  1011  		logging.Debugf(ctx, "Using the resolved version file %q", filepath.Base(parsedFile.ResolvedVersions))
  1012  	}
  1013  
  1014  	return parsedFile, nil
  1015  }
  1016  
  1017  ////////////////////////////////////////////////////////////////////////////////
  1018  // Support for running operations concurrently.
  1019  
  1020  // batchOperation defines what to do with a packages matching a prefix.
  1021  type batchOperation struct {
  1022  	client        cipd.Client
  1023  	packagePrefix string   // a package name or a prefix
  1024  	packages      []string // packages to operate on, overrides packagePrefix
  1025  	callback      func(pkg string) (common.Pin, error)
  1026  }
  1027  
  1028  // expandPkgDir takes a package name or '<prefix>/' and returns a list
  1029  // of matching packages (asking backend if necessary). Doesn't recurse, returns
  1030  // only direct children.
  1031  func expandPkgDir(ctx context.Context, c cipd.Client, packagePrefix string) ([]string, error) {
  1032  	if !strings.HasSuffix(packagePrefix, "/") {
  1033  		return []string{packagePrefix}, nil
  1034  	}
  1035  	pkgs, err := c.ListPackages(ctx, packagePrefix, false, false)
  1036  	if err != nil {
  1037  		return nil, err
  1038  	}
  1039  	// Skip directories.
  1040  	var out []string
  1041  	for _, p := range pkgs {
  1042  		if !strings.HasSuffix(p, "/") {
  1043  			out = append(out, p)
  1044  		}
  1045  	}
  1046  	if len(out) == 0 {
  1047  		return nil, errors.Reason("no packages under %s", packagePrefix).
  1048  			Tag(cipderr.RPC.WithDetails(cipderr.Details{
  1049  				Package: packagePrefix,
  1050  			})).Err()
  1051  	}
  1052  	return out, nil
  1053  }
  1054  
  1055  // performBatchOperation expands a package prefix into a list of packages and
  1056  // calls callback for each of them (concurrently) gathering the results.
  1057  //
  1058  // Returns an error only if the prefix expansion fails. Errors from individual
  1059  // operations are returned through []pinInfo, use hasErrors to check them.
  1060  func performBatchOperation(ctx context.Context, op batchOperation) ([]pinInfo, error) {
  1061  	op.client.BeginBatch(ctx)
  1062  	defer op.client.EndBatch(ctx)
  1063  
  1064  	pkgs := op.packages
  1065  	if len(pkgs) == 0 {
  1066  		var err error
  1067  		pkgs, err = expandPkgDir(ctx, op.client, op.packagePrefix)
  1068  		if err != nil {
  1069  			return nil, err
  1070  		}
  1071  	}
  1072  	return callConcurrently(pkgs, func(pkg string) pinInfo {
  1073  		pin, err := op.callback(pkg)
  1074  		if err != nil {
  1075  			return pinInfo{
  1076  				Pkg:          pkg,
  1077  				Error:        err.Error(),
  1078  				ErrorCode:    cipderr.ToCode(err),
  1079  				ErrorDetails: cipderr.ToDetails(err),
  1080  				err:          err,
  1081  			}
  1082  		}
  1083  		return pinInfo{Pkg: pkg, Pin: &pin}
  1084  	}), nil
  1085  }
  1086  
  1087  func callConcurrently(pkgs []string, callback func(pkg string) pinInfo) []pinInfo {
  1088  	// Push index through channel to make results ordered as 'pkgs'.
  1089  	ch := make(chan struct {
  1090  		int
  1091  		pinInfo
  1092  	})
  1093  	for idx, pkg := range pkgs {
  1094  		go func(idx int, pkg string) {
  1095  			ch <- struct {
  1096  				int
  1097  				pinInfo
  1098  			}{idx, callback(pkg)}
  1099  		}(idx, pkg)
  1100  	}
  1101  	pins := make([]pinInfo, len(pkgs))
  1102  	for i := 0; i < len(pkgs); i++ {
  1103  		res := <-ch
  1104  		pins[res.int] = res.pinInfo
  1105  	}
  1106  	return pins
  1107  }
  1108  
  1109  func printPinsAndError(pinMap map[string][]pinInfo) {
  1110  	for subdir, pins := range pinMap {
  1111  		hasPins := false
  1112  		hasErrors := false
  1113  		for _, p := range pins {
  1114  			if p.Error != "" {
  1115  				hasErrors = true
  1116  			} else if p.Pin != nil {
  1117  				hasPins = true
  1118  			}
  1119  		}
  1120  		subdirString := ""
  1121  		if (hasPins || hasErrors) && (len(pinMap) > 1 || subdir != "") {
  1122  			// only print this if it's not the root subdir, or there's more than one
  1123  			// subdir in pinMap.
  1124  			subdirString = fmt.Sprintf(" (subdir %q)", subdir)
  1125  		}
  1126  		if hasPins {
  1127  			fmt.Printf("Packages%s:\n", subdirString)
  1128  			for _, p := range pins {
  1129  				if p.Error != "" || p.Pin == nil {
  1130  					continue
  1131  				}
  1132  				plat := ""
  1133  				if p.Platform != "" {
  1134  					plat = fmt.Sprintf(" (for %s)", p.Platform)
  1135  				}
  1136  				tracking := ""
  1137  				if p.Tracking != "" {
  1138  					tracking = fmt.Sprintf(" (tracking %q)", p.Tracking)
  1139  				}
  1140  				fmt.Printf("  %s%s%s\n", p.Pin, plat, tracking)
  1141  			}
  1142  		}
  1143  		if hasErrors {
  1144  			fmt.Fprintf(os.Stderr, "Errors%s:\n", subdirString)
  1145  			for _, p := range pins {
  1146  				if p.Error != "" {
  1147  					fmt.Fprintf(os.Stderr, "  %s: %s.\n", p.Pkg, p.Error)
  1148  				}
  1149  			}
  1150  		}
  1151  	}
  1152  }
  1153  
  1154  func hasErrors(pins []pinInfo) bool {
  1155  	for _, p := range pins {
  1156  		if p.Error != "" {
  1157  			return true
  1158  		}
  1159  	}
  1160  	return false
  1161  }
  1162  
  1163  ////////////////////////////////////////////////////////////////////////////////
  1164  // Ensure-file related helpers.
  1165  
  1166  func resolveEnsureFile(ctx context.Context, f *ensure.File, clientOpts clientOptions) (map[string][]pinInfo, ensure.VersionsFile, error) {
  1167  	client, err := clientOpts.makeCIPDClient(ctx)
  1168  	if err != nil {
  1169  		return nil, nil, err
  1170  	}
  1171  	defer client.Close(ctx)
  1172  
  1173  	out := ensure.VersionsFile{}
  1174  	mu := sync.Mutex{}
  1175  
  1176  	resolver := cipd.Resolver{
  1177  		Client:         client,
  1178  		VerifyPresence: true,
  1179  		Visitor: func(pkg, ver, iid string) {
  1180  			mu.Lock()
  1181  			out.AddVersion(pkg, ver, iid)
  1182  			mu.Unlock()
  1183  		},
  1184  	}
  1185  	results, err := resolver.ResolveAllPlatforms(ctx, f)
  1186  	if err != nil {
  1187  		return nil, nil, err
  1188  	}
  1189  	return resolvedFilesToPinMap(results), out, nil
  1190  }
  1191  
  1192  func resolvedFilesToPinMap(res map[template.Platform]*ensure.ResolvedFile) map[string][]pinInfo {
  1193  	pinMap := map[string][]pinInfo{}
  1194  	for plat, resolved := range res {
  1195  		for subdir, resolvedPins := range resolved.PackagesBySubdir {
  1196  			pins := pinMap[subdir]
  1197  			for _, pin := range resolvedPins {
  1198  				// Put a copy into 'pins', otherwise they all end up pointing to the
  1199  				// same variable living in the outer scope.
  1200  				pin := pin
  1201  				pins = append(pins, pinInfo{
  1202  					Pkg:      pin.PackageName,
  1203  					Pin:      &pin,
  1204  					Platform: plat.String(),
  1205  				})
  1206  			}
  1207  			pinMap[subdir] = pins
  1208  		}
  1209  	}
  1210  
  1211  	// Sort pins by (package name, platform) for deterministic output.
  1212  	for _, v := range pinMap {
  1213  		sort.Slice(v, func(i, j int) bool {
  1214  			if v[i].Pkg == v[j].Pkg {
  1215  				return v[i].Platform < v[j].Platform
  1216  			}
  1217  			return v[i].Pkg < v[j].Pkg
  1218  		})
  1219  	}
  1220  	return pinMap
  1221  }
  1222  
  1223  func loadVersionsFile(path, ensureFile string) (ensure.VersionsFile, error) {
  1224  	switch f, err := os.Open(path); {
  1225  	case os.IsNotExist(err):
  1226  		return nil, errors.Reason("the resolved versions file doesn't exist, "+
  1227  			"use 'cipd ensure-file-resolve -ensure-file %q' to generate it", ensureFile).Tag(cipderr.BadArgument).Err()
  1228  	case err != nil:
  1229  		return nil, errors.Annotate(err, "reading resolved versions file").Tag(cipderr.IO).Err()
  1230  	default:
  1231  		defer f.Close()
  1232  		return ensure.ParseVersionsFile(f)
  1233  	}
  1234  }
  1235  
  1236  func saveVersionsFile(path string, v ensure.VersionsFile) error {
  1237  	buf := bytes.Buffer{}
  1238  	if err := v.Serialize(&buf); err != nil {
  1239  		return err
  1240  	}
  1241  	if err := os.WriteFile(path, buf.Bytes(), 0666); err != nil {
  1242  		return errors.Annotate(err, "writing versions file").Tag(cipderr.IO).Err()
  1243  	}
  1244  	return nil
  1245  }
  1246  
  1247  ////////////////////////////////////////////////////////////////////////////////
  1248  // 'create' subcommand.
  1249  
  1250  func cmdCreate(params Parameters) *subcommands.Command {
  1251  	return &subcommands.Command{
  1252  		UsageLine: "create [options]",
  1253  		ShortDesc: "builds and uploads a package instance file",
  1254  		LongDesc:  "Builds and uploads a package instance file.",
  1255  		CommandRun: func() subcommands.CommandRun {
  1256  			c := &createRun{}
  1257  			c.registerBaseFlags()
  1258  			c.Opts.inputOptions.registerFlags(&c.Flags)
  1259  			c.Opts.refsOptions.registerFlags(&c.Flags)
  1260  			c.Opts.tagsOptions.registerFlags(&c.Flags)
  1261  			c.Opts.metadataOptions.registerFlags(&c.Flags)
  1262  			c.Opts.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
  1263  			c.Opts.uploadOptions.registerFlags(&c.Flags)
  1264  			c.Opts.hashOptions.registerFlags(&c.Flags)
  1265  			return c
  1266  		},
  1267  	}
  1268  }
  1269  
  1270  type createOpts struct {
  1271  	inputOptions
  1272  	refsOptions
  1273  	tagsOptions
  1274  	metadataOptions
  1275  	clientOptions
  1276  	uploadOptions
  1277  	hashOptions
  1278  }
  1279  
  1280  type createRun struct {
  1281  	cipdSubcommand
  1282  
  1283  	Opts createOpts
  1284  }
  1285  
  1286  func (c *createRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  1287  	if !c.checkArgs(args, 0, 0) {
  1288  		return 1
  1289  	}
  1290  	ctx := cli.GetContext(a, c, env)
  1291  	return c.done(buildAndUploadInstance(ctx, &c.Opts))
  1292  }
  1293  
  1294  func buildAndUploadInstance(ctx context.Context, opts *createOpts) (common.Pin, error) {
  1295  	f, err := ioutil.TempFile("", "cipd_pkg")
  1296  	if err != nil {
  1297  		return common.Pin{}, errors.Annotate(err, "creating temp instance file").Tag(cipderr.IO).Err()
  1298  	}
  1299  	defer func() {
  1300  		// Note: we don't care about errors here since this file is used only as
  1301  		// a temporary buffer between buildInstanceFile and registerInstanceFile.
  1302  		// These functions check that everything was uploaded correctly using
  1303  		// hashes.
  1304  		_ = f.Close()
  1305  		_ = os.Remove(f.Name())
  1306  	}()
  1307  	pin, err := buildInstanceFile(ctx, f.Name(), opts.inputOptions, opts.hashAlgo())
  1308  	if err != nil {
  1309  		return common.Pin{}, err
  1310  	}
  1311  	return registerInstanceFile(ctx, f.Name(), &pin, &registerOpts{
  1312  		refsOptions:     opts.refsOptions,
  1313  		tagsOptions:     opts.tagsOptions,
  1314  		metadataOptions: opts.metadataOptions,
  1315  		clientOptions:   opts.clientOptions,
  1316  		uploadOptions:   opts.uploadOptions,
  1317  		hashOptions:     opts.hashOptions,
  1318  	})
  1319  }
  1320  
  1321  ////////////////////////////////////////////////////////////////////////////////
  1322  // 'attach' subcommand.
  1323  
  1324  func cmdAttach(params Parameters) *subcommands.Command {
  1325  	return &subcommands.Command{
  1326  		UsageLine: "attach <package or package prefix> -metadata key:value -metadata-from-file key:path -tag key:value -ref name [options]",
  1327  		ShortDesc: "attaches tags, metadata and points refs to an instance",
  1328  		LongDesc: `Attaches tags, metadata and points refs to an instance.
  1329  
  1330  Note that this operation is not atomic. It attaches metadata first, then tags,
  1331  then moves refs one by one. Reattaching already attached data is not an error
  1332  though, so a failed operation can be safely retried.
  1333  `,
  1334  		CommandRun: func() subcommands.CommandRun {
  1335  			c := &attachRun{}
  1336  			c.registerBaseFlags()
  1337  			c.refsOptions.registerFlags(&c.Flags)
  1338  			c.tagsOptions.registerFlags(&c.Flags)
  1339  			c.metadataOptions.registerFlags(&c.Flags)
  1340  			c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
  1341  			c.Flags.StringVar(&c.version, "version", "<version>",
  1342  				"Package version to resolve. Could also be a tag or a ref.")
  1343  			return c
  1344  		},
  1345  	}
  1346  }
  1347  
  1348  type attachRun struct {
  1349  	cipdSubcommand
  1350  	refsOptions
  1351  	tagsOptions
  1352  	metadataOptions
  1353  	clientOptions
  1354  
  1355  	version string
  1356  }
  1357  
  1358  func (c *attachRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  1359  	if !c.checkArgs(args, 1, 1) {
  1360  		return 1
  1361  	}
  1362  
  1363  	ctx := cli.GetContext(a, c, env)
  1364  
  1365  	md, err := c.metadataOptions.load(ctx)
  1366  	if err != nil {
  1367  		return c.done(nil, err)
  1368  	}
  1369  	if len(c.refs) == 0 && len(c.tags) == 0 && len(md) == 0 {
  1370  		return c.done(nil, makeCLIError("no -tags, -refs or -metadata is provided"))
  1371  	}
  1372  
  1373  	pkgPrefix, err := expandTemplate(args[0])
  1374  	if err != nil {
  1375  		return c.done(nil, err)
  1376  	}
  1377  
  1378  	return c.doneWithPins(visitPins(ctx, &visitPinsArgs{
  1379  		clientOptions: c.clientOptions,
  1380  		packagePrefix: pkgPrefix,
  1381  		version:       c.version,
  1382  		updatePin: func(client cipd.Client, pin common.Pin) error {
  1383  			return attachAndMove(ctx, client, pin, md, c.tags, c.refs)
  1384  		},
  1385  	}))
  1386  }
  1387  
  1388  ////////////////////////////////////////////////////////////////////////////////
  1389  // 'ensure' subcommand.
  1390  
  1391  func cmdEnsure(params Parameters) *subcommands.Command {
  1392  	return &subcommands.Command{
  1393  		UsageLine: "ensure [options]",
  1394  		ShortDesc: "installs, removes and updates packages in one go",
  1395  		LongDesc: `Installs, removes and updates packages in one go.
  1396  
  1397  Prepare an 'ensure file' by listing packages and their versions, each on their
  1398  own line, e.g.:
  1399  
  1400      some/package/name/${platform}  version:1.2.3
  1401      other/package                  some_ref
  1402  
  1403  Then use the ensure command to read this ensure file and 'ensure' that a given
  1404  folder has the packages at the versions specified:
  1405  
  1406      cipd ensure -root a/directory -ensure-file ensure_file
  1407  
  1408  For the full syntax of the ensure file, see:
  1409  
  1410     https://go.chromium.org/luci/cipd/client/cipd/ensure
  1411  `,
  1412  		CommandRun: func() subcommands.CommandRun {
  1413  			c := &ensureRun{}
  1414  			c.registerBaseFlags()
  1415  			c.clientOptions.registerFlags(&c.Flags, params, withRootDir, withMaxThreads)
  1416  			c.ensureFileOptions.registerFlags(&c.Flags, withEnsureOutFlag, withLegacyListFlag)
  1417  			return c
  1418  		},
  1419  	}
  1420  }
  1421  
  1422  type ensureRun struct {
  1423  	cipdSubcommand
  1424  	clientOptions
  1425  	ensureFileOptions
  1426  }
  1427  
  1428  func (c *ensureRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  1429  	if !c.checkArgs(args, 0, 0) {
  1430  		return 1
  1431  	}
  1432  	ctx := cli.GetContext(a, c, env)
  1433  
  1434  	ef, err := c.loadEnsureFile(ctx, &c.clientOptions, ignoreVerifyPlatforms, parseVersionsFile)
  1435  	if err != nil {
  1436  		return c.done(nil, err)
  1437  	}
  1438  
  1439  	pins, _, err := ensurePackages(ctx, ef, c.ensureFileOut, false, c.clientOptions)
  1440  	return c.done(pins, err)
  1441  }
  1442  
  1443  func ensurePackages(ctx context.Context, ef *ensure.File, ensureFileOut string, dryRun bool, clientOpts clientOptions) (common.PinSliceBySubdir, cipd.ActionMap, error) {
  1444  	client, err := clientOpts.makeCIPDClient(ctx)
  1445  	if err != nil {
  1446  		return nil, nil, err
  1447  	}
  1448  	defer client.Close(ctx)
  1449  
  1450  	client.BeginBatch(ctx)
  1451  	defer client.EndBatch(ctx)
  1452  
  1453  	resolver := cipd.Resolver{Client: client}
  1454  	resolved, err := resolver.Resolve(ctx, ef, template.DefaultExpander())
  1455  	if err != nil {
  1456  		return nil, nil, err
  1457  	}
  1458  
  1459  	actions, err := client.EnsurePackages(ctx, resolved.PackagesBySubdir, &cipd.EnsureOptions{
  1460  		Paranoia:            resolved.ParanoidMode,
  1461  		DryRun:              dryRun,
  1462  		OverrideInstallMode: resolved.OverrideInstallMode,
  1463  	})
  1464  	if err != nil {
  1465  		return nil, actions, err
  1466  	}
  1467  
  1468  	if ensureFileOut != "" {
  1469  		buf := bytes.Buffer{}
  1470  		resolved.ServiceURL = clientOpts.resolvedServiceURL(ctx)
  1471  		resolved.ParanoidMode = ""
  1472  		if err = resolved.Serialize(&buf); err == nil {
  1473  			err = os.WriteFile(ensureFileOut, buf.Bytes(), 0666)
  1474  			if err != nil {
  1475  				err = errors.Annotate(err, "writing resolved ensure file").Tag(cipderr.IO).Err()
  1476  			}
  1477  		}
  1478  
  1479  	}
  1480  
  1481  	return resolved.PackagesBySubdir, actions, err
  1482  }
  1483  
  1484  ////////////////////////////////////////////////////////////////////////////////
  1485  // 'ensure-file-verify' subcommand.
  1486  
  1487  func cmdEnsureFileVerify(params Parameters) *subcommands.Command {
  1488  	return &subcommands.Command{
  1489  		UsageLine: "ensure-file-verify [options]",
  1490  		ShortDesc: "verifies packages in a manifest exist for all platforms",
  1491  		LongDesc: "Verifies that the packages in the \"ensure\" file exist for all platforms.\n\n" +
  1492  			"Additionally if the ensure file uses $ResolvedVersions directive, checks that " +
  1493  			"all versions there are up-to-date. Returns non-zero if some version can't be " +
  1494  			"resolved or $ResolvedVersions file is outdated.",
  1495  		Advanced: true,
  1496  		CommandRun: func() subcommands.CommandRun {
  1497  			c := &ensureFileVerifyRun{}
  1498  			c.registerBaseFlags()
  1499  			c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
  1500  			c.ensureFileOptions.registerFlags(&c.Flags, withoutEnsureOutFlag, withoutLegacyListFlag)
  1501  			return c
  1502  		},
  1503  	}
  1504  }
  1505  
  1506  type ensureFileVerifyRun struct {
  1507  	cipdSubcommand
  1508  	clientOptions
  1509  	ensureFileOptions
  1510  }
  1511  
  1512  func (c *ensureFileVerifyRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  1513  	if !c.checkArgs(args, 0, 0) {
  1514  		return 1
  1515  	}
  1516  	ctx := cli.GetContext(a, c, env)
  1517  
  1518  	ef, err := c.loadEnsureFile(ctx, &c.clientOptions, requireVerifyPlatforms, ignoreVersionsFile)
  1519  	if err != nil {
  1520  		return c.done(nil, err)
  1521  	}
  1522  
  1523  	// Resolving all versions in the ensure file also naturally verifies all
  1524  	// versions exist.
  1525  	pinMap, versions, err := resolveEnsureFile(ctx, ef, c.clientOptions)
  1526  	if err != nil || ef.ResolvedVersions == "" {
  1527  		return c.doneWithPinMap(pinMap, err)
  1528  	}
  1529  
  1530  	// Verify $ResolvedVersions file is up-to-date too.
  1531  	switch existing, err := loadVersionsFile(ef.ResolvedVersions, c.ensureFile); {
  1532  	case err != nil:
  1533  		return c.done(nil, err)
  1534  	case !existing.Equal(versions):
  1535  		return c.done(nil, errors.Reason("the resolved versions file %s is stale, "+
  1536  			"use 'cipd ensure-file-resolve -ensure-file %q' to update it",
  1537  			filepath.Base(ef.ResolvedVersions), c.ensureFile).Tag(cipderr.Stale).Err())
  1538  	default:
  1539  		return c.doneWithPinMap(pinMap, err)
  1540  	}
  1541  }
  1542  
  1543  ////////////////////////////////////////////////////////////////////////////////
  1544  // 'ensure-file-resolve' subcommand.
  1545  
  1546  func cmdEnsureFileResolve(params Parameters) *subcommands.Command {
  1547  	return &subcommands.Command{
  1548  		UsageLine: "ensure-file-resolve [options]",
  1549  		ShortDesc: "resolves versions of all packages and writes them into $ResolvedVersions file",
  1550  		LongDesc: "Resolves versions of all packages for all verified platforms in the \"ensure\" file.\n\n" +
  1551  			`Writes them to a file specified by $ResolvedVersions directive in the ensure file, ` +
  1552  			`to be used for version resolution during "cipd ensure ..." instead of calling the backend.`,
  1553  		Advanced: true,
  1554  		CommandRun: func() subcommands.CommandRun {
  1555  			c := &ensureFileResolveRun{}
  1556  			c.registerBaseFlags()
  1557  			c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
  1558  			c.ensureFileOptions.registerFlags(&c.Flags, withoutEnsureOutFlag, withoutLegacyListFlag)
  1559  			return c
  1560  		},
  1561  	}
  1562  }
  1563  
  1564  type ensureFileResolveRun struct {
  1565  	cipdSubcommand
  1566  	clientOptions
  1567  	ensureFileOptions
  1568  }
  1569  
  1570  func (c *ensureFileResolveRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  1571  	if !c.checkArgs(args, 0, 0) {
  1572  		return 1
  1573  	}
  1574  	ctx := cli.GetContext(a, c, env)
  1575  
  1576  	ef, err := c.loadEnsureFile(ctx, &c.clientOptions, requireVerifyPlatforms, ignoreVersionsFile)
  1577  	switch {
  1578  	case err != nil:
  1579  		return c.done(nil, err)
  1580  	case ef.ResolvedVersions == "":
  1581  		logging.Errorf(ctx,
  1582  			"The ensure file doesn't have $ResolvedVersion directive that specifies "+
  1583  				"where to put the resolved package versions, so it can't be resolved.")
  1584  		return c.done(nil, errors.Reason("no resolved versions file configured").Tag(cipderr.BadArgument).Err())
  1585  	}
  1586  
  1587  	pinMap, versions, err := resolveEnsureFile(ctx, ef, c.clientOptions)
  1588  	if err != nil {
  1589  		return c.doneWithPinMap(pinMap, err)
  1590  	}
  1591  
  1592  	if err := saveVersionsFile(ef.ResolvedVersions, versions); err != nil {
  1593  		return c.done(nil, err)
  1594  	}
  1595  
  1596  	fmt.Printf("The resolved versions have been written to %s.\n\n", filepath.Base(ef.ResolvedVersions))
  1597  	return c.doneWithPinMap(pinMap, nil)
  1598  }
  1599  
  1600  ////////////////////////////////////////////////////////////////////////////////
  1601  // 'puppet-check-updates' subcommand.
  1602  
  1603  func cmdPuppetCheckUpdates(params Parameters) *subcommands.Command {
  1604  	return &subcommands.Command{
  1605  		Advanced:  true,
  1606  		UsageLine: "puppet-check-updates [options]",
  1607  		ShortDesc: "returns 0 exit code iff 'ensure' will do some actions",
  1608  		LongDesc: "Returns 0 exit code iff 'ensure' will do some actions.\n\n" +
  1609  			"Exists to be used from Puppet's Exec 'onlyif' option to trigger " +
  1610  			"'ensure' only if something is out of date. If puppet-check-updates " +
  1611  			"fails with a transient error, it returns non-zero exit code (as usual), " +
  1612  			"so that Puppet doesn't trigger notification chain (that can result in " +
  1613  			"service restarts). On fatal errors it returns 0 to let Puppet run " +
  1614  			"'ensure' for real and catch an error.",
  1615  		CommandRun: func() subcommands.CommandRun {
  1616  			c := &checkUpdatesRun{}
  1617  			c.registerBaseFlags()
  1618  			c.clientOptions.registerFlags(&c.Flags, params, withRootDir, withMaxThreads)
  1619  			c.ensureFileOptions.registerFlags(&c.Flags, withoutEnsureOutFlag, withLegacyListFlag)
  1620  			return c
  1621  		},
  1622  	}
  1623  }
  1624  
  1625  type checkUpdatesRun struct {
  1626  	cipdSubcommand
  1627  	clientOptions
  1628  	ensureFileOptions
  1629  }
  1630  
  1631  func (c *checkUpdatesRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  1632  	if !c.checkArgs(args, 0, 0) {
  1633  		return 1
  1634  	}
  1635  	ctx := cli.GetContext(a, c, env)
  1636  
  1637  	ef, err := c.loadEnsureFile(ctx, &c.clientOptions, ignoreVerifyPlatforms, parseVersionsFile)
  1638  	if err != nil {
  1639  		return 0 // on fatal errors ask puppet to run 'ensure' for real
  1640  	}
  1641  
  1642  	_, actions, err := ensurePackages(ctx, ef, "", true, c.clientOptions)
  1643  	if err != nil {
  1644  		ret := c.done(actions, err)
  1645  		if transient.Tag.In(err) {
  1646  			return ret // fail as usual
  1647  		}
  1648  		return 0 // on fatal errors ask puppet to run 'ensure' for real
  1649  	}
  1650  	c.done(actions, nil)
  1651  	if len(actions) == 0 {
  1652  		return 5 // some arbitrary non-zero number, unlikely to show up on errors
  1653  	}
  1654  	return 0
  1655  }
  1656  
  1657  ////////////////////////////////////////////////////////////////////////////////
  1658  // 'resolve' subcommand.
  1659  
  1660  func cmdResolve(params Parameters) *subcommands.Command {
  1661  	return &subcommands.Command{
  1662  		UsageLine: "resolve <package or package prefix> [options]",
  1663  		ShortDesc: "returns concrete package instance ID given a version",
  1664  		LongDesc:  "Returns concrete package instance ID given a version.",
  1665  		CommandRun: func() subcommands.CommandRun {
  1666  			c := &resolveRun{}
  1667  			c.registerBaseFlags()
  1668  			c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
  1669  			c.Flags.StringVar(&c.version, "version", "<version>", "Package version to resolve.")
  1670  			return c
  1671  		},
  1672  	}
  1673  }
  1674  
  1675  type resolveRun struct {
  1676  	cipdSubcommand
  1677  	clientOptions
  1678  
  1679  	version string
  1680  }
  1681  
  1682  func (c *resolveRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  1683  	if !c.checkArgs(args, 1, 1) {
  1684  		return 1
  1685  	}
  1686  	ctx := cli.GetContext(a, c, env)
  1687  	return c.doneWithPins(resolveVersion(ctx, args[0], c.version, c.clientOptions))
  1688  }
  1689  
  1690  func resolveVersion(ctx context.Context, packagePrefix, version string, clientOpts clientOptions) ([]pinInfo, error) {
  1691  	packagePrefix, err := expandTemplate(packagePrefix)
  1692  	if err != nil {
  1693  		return nil, err
  1694  	}
  1695  
  1696  	client, err := clientOpts.makeCIPDClient(ctx)
  1697  	if err != nil {
  1698  		return nil, err
  1699  	}
  1700  	defer client.Close(ctx)
  1701  
  1702  	return performBatchOperation(ctx, batchOperation{
  1703  		client:        client,
  1704  		packagePrefix: packagePrefix,
  1705  		callback: func(pkg string) (common.Pin, error) {
  1706  			return client.ResolveVersion(ctx, pkg, version)
  1707  		},
  1708  	})
  1709  }
  1710  
  1711  ////////////////////////////////////////////////////////////////////////////////
  1712  // 'export' subcommand.
  1713  
  1714  func cmdExport(params Parameters) *subcommands.Command {
  1715  	return &subcommands.Command{
  1716  		UsageLine: "export [options]",
  1717  		ShortDesc: "writes packages to disk as a 'one-off'",
  1718  		LongDesc: `Writes packages to disk as a 'one-off'.
  1719  
  1720  This writes packages to disk and discards any CIPD-related tracking data.  The
  1721  result is an installation of the specified packages on disk in a form you could
  1722  use to re-package them elsewhere (e.g. to create a tarball, zip, etc.), or for
  1723  some 'one time' use (where you don't need any of the guarantees provided by
  1724  "ensure").
  1725  
  1726  In particular:
  1727    * Export will blindly overwrite files in the target directory.
  1728    * If a package removes a file(s) in some newer version, using "export"
  1729      to write over an old version with the new version will not clean
  1730      up the file(s).
  1731  
  1732  Prepare an 'ensure file' as you would pass to the "ensure" subcommand.
  1733  This command implies "$OverrideInstallMode copy" in the ensure file.
  1734  
  1735      cipd export -root a/directory -ensure-file ensure_file
  1736  
  1737  For the full syntax of the ensure file, see:
  1738  
  1739     https://go.chromium.org/luci/cipd/client/cipd/ensure
  1740  `,
  1741  		CommandRun: func() subcommands.CommandRun {
  1742  			c := &exportRun{}
  1743  			c.registerBaseFlags()
  1744  			c.clientOptions.registerFlags(&c.Flags, params, withRootDir, withMaxThreads)
  1745  			c.ensureFileOptions.registerFlags(&c.Flags, withEnsureOutFlag, withLegacyListFlag)
  1746  
  1747  			return c
  1748  		},
  1749  	}
  1750  }
  1751  
  1752  type exportRun struct {
  1753  	cipdSubcommand
  1754  	clientOptions
  1755  	ensureFileOptions
  1756  }
  1757  
  1758  func (c *exportRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  1759  	if !c.checkArgs(args, 0, 0) {
  1760  		return 1
  1761  	}
  1762  	ctx := cli.GetContext(a, c, env)
  1763  
  1764  	ef, err := c.loadEnsureFile(ctx, &c.clientOptions, ignoreVerifyPlatforms, parseVersionsFile)
  1765  	if err != nil {
  1766  		return c.done(nil, err)
  1767  	}
  1768  	ef.OverrideInstallMode = pkg.InstallModeCopy
  1769  
  1770  	pins, _, err := ensurePackages(ctx, ef, c.ensureFileOut, false, c.clientOptions)
  1771  	if err != nil {
  1772  		return c.done(pins, err)
  1773  	}
  1774  
  1775  	logging.Infof(ctx, "Removing cipd metadata")
  1776  	fsystem := fs.NewFileSystem(c.rootDir, "")
  1777  	ssd, err := fsystem.RootRelToAbs(fs.SiteServiceDir)
  1778  	if err != nil {
  1779  		err = errors.Annotate(err, "unable to resolve service dir").Tag(cipderr.IO).Err()
  1780  	} else if err = fsystem.EnsureDirectoryGone(ctx, ssd); err != nil {
  1781  		err = errors.Annotate(err, "unable to purge service dir").Tag(cipderr.IO).Err()
  1782  	}
  1783  	return c.done(pins, err)
  1784  }
  1785  
  1786  ////////////////////////////////////////////////////////////////////////////////
  1787  // 'describe' subcommand.
  1788  
  1789  func cmdDescribe(params Parameters) *subcommands.Command {
  1790  	return &subcommands.Command{
  1791  		UsageLine: "describe <package> [options]",
  1792  		ShortDesc: "returns information about a package instance given its version",
  1793  		LongDesc: "Returns information about a package instance given its version: " +
  1794  			"who uploaded the instance and when and a list of attached tags.",
  1795  		CommandRun: func() subcommands.CommandRun {
  1796  			c := &describeRun{}
  1797  			c.registerBaseFlags()
  1798  			c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
  1799  			c.Flags.StringVar(&c.version, "version", "<version>", "Package version to describe.")
  1800  			return c
  1801  		},
  1802  	}
  1803  }
  1804  
  1805  type describeRun struct {
  1806  	cipdSubcommand
  1807  	clientOptions
  1808  
  1809  	version string
  1810  }
  1811  
  1812  func isPrintableContentType(contentType string) bool {
  1813  	if strings.HasPrefix(contentType, "text/") ||
  1814  		slices.Contains([]string{"application/json", "application/jwt"}, contentType) {
  1815  		return true
  1816  	}
  1817  	return false
  1818  }
  1819  
  1820  func (c *describeRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  1821  	if !c.checkArgs(args, 1, 1) {
  1822  		return 1
  1823  	}
  1824  	ctx := cli.GetContext(a, c, env)
  1825  	return c.done(describeInstance(ctx, args[0], c.version, c.clientOptions))
  1826  }
  1827  
  1828  func describeInstance(ctx context.Context, pkg, version string, clientOpts clientOptions) (*cipd.InstanceDescription, error) {
  1829  	pkg, err := expandTemplate(pkg)
  1830  	if err != nil {
  1831  		return nil, err
  1832  	}
  1833  
  1834  	client, err := clientOpts.makeCIPDClient(ctx)
  1835  	if err != nil {
  1836  		return nil, err
  1837  	}
  1838  	defer client.Close(ctx)
  1839  
  1840  	pin, err := client.ResolveVersion(ctx, pkg, version)
  1841  	if err != nil {
  1842  		return nil, err
  1843  	}
  1844  
  1845  	desc, err := client.DescribeInstance(ctx, pin, &cipd.DescribeInstanceOpts{
  1846  		DescribeRefs:     true,
  1847  		DescribeTags:     true,
  1848  		DescribeMetadata: true,
  1849  	})
  1850  	if err != nil {
  1851  		return nil, err
  1852  	}
  1853  
  1854  	fmt.Printf("Package:       %s\n", desc.Pin.PackageName)
  1855  	fmt.Printf("Instance ID:   %s\n", desc.Pin.InstanceID)
  1856  	fmt.Printf("Registered by: %s\n", desc.RegisteredBy)
  1857  	fmt.Printf("Registered at: %s\n", time.Time(desc.RegisteredTs).Local())
  1858  	if len(desc.Refs) != 0 {
  1859  		fmt.Printf("Refs:\n")
  1860  		for _, t := range desc.Refs {
  1861  			fmt.Printf("  %s\n", t.Ref)
  1862  		}
  1863  	} else {
  1864  		fmt.Printf("Refs:          none\n")
  1865  	}
  1866  	if len(desc.Tags) != 0 {
  1867  		fmt.Printf("Tags:\n")
  1868  		for _, t := range desc.Tags {
  1869  			fmt.Printf("  %s\n", t.Tag)
  1870  		}
  1871  	} else {
  1872  		fmt.Printf("Tags:          none\n")
  1873  	}
  1874  	if len(desc.Metadata) != 0 {
  1875  		fmt.Printf("Metadata:\n")
  1876  		for _, md := range desc.Metadata {
  1877  			printValue := string(md.Value)
  1878  			if !isPrintableContentType(md.ContentType) {
  1879  				// Content type is highly unlikely to be meaningfully printable.
  1880  				// Output something reasonable to the CLI (JSON will still have
  1881  				// the full payload).
  1882  				printValue = fmt.Sprintf("<%s binary, %d bytes>", md.ContentType, len(md.Value))
  1883  			}
  1884  			// Add indentation if the value contains newlines.
  1885  			if strings.Contains(printValue, "\n") {
  1886  				printValue = "\n" + printValue
  1887  				printValue = strings.ReplaceAll(printValue, "\n", "\n    ")
  1888  			}
  1889  			fmt.Printf("  %s:%s\n", md.Key, printValue)
  1890  		}
  1891  	} else {
  1892  		fmt.Printf("Metadata:      none\n")
  1893  	}
  1894  
  1895  	return desc, nil
  1896  }
  1897  
  1898  ////////////////////////////////////////////////////////////////////////////////
  1899  // 'instances' subcommand.
  1900  
  1901  func cmdInstances(params Parameters) *subcommands.Command {
  1902  	return &subcommands.Command{
  1903  		UsageLine: "instances <package> [-limit ...]",
  1904  		ShortDesc: "lists instances of a package",
  1905  		LongDesc:  "Lists instances of a package, most recently uploaded first.",
  1906  		CommandRun: func() subcommands.CommandRun {
  1907  			c := &instancesRun{}
  1908  			c.registerBaseFlags()
  1909  			c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
  1910  			c.Flags.IntVar(&c.limit, "limit", 20, "How many instances to return or 0 for all.")
  1911  			return c
  1912  		},
  1913  	}
  1914  }
  1915  
  1916  type instancesRun struct {
  1917  	cipdSubcommand
  1918  	clientOptions
  1919  
  1920  	limit int
  1921  }
  1922  
  1923  func (c *instancesRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  1924  	if !c.checkArgs(args, 1, 1) {
  1925  		return 1
  1926  	}
  1927  	ctx := cli.GetContext(a, c, env)
  1928  	return c.done(listInstances(ctx, args[0], c.limit, c.clientOptions))
  1929  }
  1930  
  1931  func listInstances(ctx context.Context, pkg string, limit int, clientOpts clientOptions) (*instancesOutput, error) {
  1932  	pkg, err := expandTemplate(pkg)
  1933  	if err != nil {
  1934  		return nil, err
  1935  	}
  1936  
  1937  	client, err := clientOpts.makeCIPDClient(ctx)
  1938  	if err != nil {
  1939  		return nil, err
  1940  	}
  1941  	defer client.Close(ctx)
  1942  
  1943  	// TODO(vadimsh): The backend currently doesn't support retrieving
  1944  	// per-instance refs when listing instances. Instead we fetch ALL refs in
  1945  	// parallel and then merge this information with the instance listing. This
  1946  	// works fine for packages with few refs (up to 50 maybe), but horribly
  1947  	// inefficient if the cardinality of the set of all refs is larger than a
  1948  	// typical size of instance listing (we spend time fetching data we don't
  1949  	// need). To support this case better, the backend should learn to maintain
  1950  	// {instance ID => ref} mapping (in addition to {ref => instance ID} mapping
  1951  	// it already has). This would require back filling all existing entities.
  1952  
  1953  	// Fetch the refs in parallel with the first page of results. We merge them
  1954  	// with the list of instances during the display.
  1955  	type refsMap map[string][]string // instance ID => list of refs
  1956  	type refsOrErr struct {
  1957  		refs refsMap
  1958  		err  error
  1959  	}
  1960  	refsChan := make(chan refsOrErr, 1)
  1961  	go func() {
  1962  		defer close(refsChan)
  1963  		asMap := refsMap{}
  1964  		refs, err := client.FetchPackageRefs(ctx, pkg)
  1965  		for _, info := range refs {
  1966  			asMap[info.InstanceID] = append(asMap[info.InstanceID], info.Ref)
  1967  		}
  1968  		refsChan <- refsOrErr{asMap, err}
  1969  	}()
  1970  
  1971  	enum, err := client.ListInstances(ctx, pkg)
  1972  	if err != nil {
  1973  		return nil, err
  1974  	}
  1975  
  1976  	formatRow := func(instanceID, when, who, refs string) string {
  1977  		if len(who) > 25 {
  1978  			who = who[:22] + "..."
  1979  		}
  1980  		return fmt.Sprintf("%-44s │ %-21s │ %-25s │ %-12s", instanceID, when, who, refs)
  1981  	}
  1982  
  1983  	var refs refsMap // populated on after fetching first page
  1984  
  1985  	out := []instanceInfoWithRefs{}
  1986  	for {
  1987  		pageSize := 200
  1988  		if limit != 0 && limit-len(out) < pageSize {
  1989  			pageSize = limit - len(out)
  1990  			if pageSize == 0 {
  1991  				// Fetched everything we wanted. There's likely more instances available
  1992  				// (unless '-limit' happens to exactly match number of instances on the
  1993  				// backend, which is not very probable). Hint this by printing '...'.
  1994  				fmt.Println(formatRow("...", "...", "...", "..."))
  1995  				break
  1996  			}
  1997  		}
  1998  		page, err := enum.Next(ctx, pageSize)
  1999  		if err != nil {
  2000  			return nil, err
  2001  		}
  2002  		if len(page) == 0 {
  2003  			if len(out) == 0 {
  2004  				fmt.Println("No instances found")
  2005  			}
  2006  			break // no more results to fetch
  2007  		}
  2008  
  2009  		if len(out) == 0 {
  2010  			// Need to wait for refs to be fetched, they are required to display
  2011  			// "Refs" column.
  2012  			refsOrErr := <-refsChan
  2013  			if refsOrErr.err != nil {
  2014  				return nil, refsOrErr.err
  2015  			}
  2016  			refs = refsOrErr.refs
  2017  
  2018  			// Draw the header now that we have some real results (i.e no errors).
  2019  			hdr := formatRow("Instance ID", "Timestamp", "Uploader", "Refs")
  2020  			fmt.Println(hdr)
  2021  			fmt.Println(strings.Repeat("─", len(hdr)))
  2022  		}
  2023  
  2024  		for _, info := range page {
  2025  			instanceRefs := refs[info.Pin.InstanceID]
  2026  			out = append(out, instanceInfoWithRefs{
  2027  				InstanceInfo: info,
  2028  				Refs:         instanceRefs,
  2029  			})
  2030  			fmt.Println(formatRow(
  2031  				info.Pin.InstanceID,
  2032  				time.Time(info.RegisteredTs).Local().Format("Jan 02 15:04 MST 2006"),
  2033  				strings.TrimPrefix(info.RegisteredBy, "user:"),
  2034  				strings.Join(instanceRefs, " ")))
  2035  		}
  2036  	}
  2037  
  2038  	return &instancesOutput{out}, nil
  2039  }
  2040  
  2041  ////////////////////////////////////////////////////////////////////////////////
  2042  // 'set-ref' subcommand.
  2043  
  2044  func cmdSetRef(params Parameters) *subcommands.Command {
  2045  	return &subcommands.Command{
  2046  		UsageLine: "set-ref <package or package prefix> [options]",
  2047  		ShortDesc: "moves a ref to point to a given version",
  2048  		LongDesc:  "Moves a ref to point to a given version.",
  2049  		CommandRun: func() subcommands.CommandRun {
  2050  			c := &setRefRun{}
  2051  			c.registerBaseFlags()
  2052  			c.refsOptions.registerFlags(&c.Flags)
  2053  			c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
  2054  			c.Flags.StringVar(&c.version, "version", "<version>", "Package version to point the ref to.")
  2055  			return c
  2056  		},
  2057  	}
  2058  }
  2059  
  2060  type setRefRun struct {
  2061  	cipdSubcommand
  2062  	refsOptions
  2063  	clientOptions
  2064  
  2065  	version string
  2066  }
  2067  
  2068  func (c *setRefRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  2069  	if !c.checkArgs(args, 1, 1) {
  2070  		return 1
  2071  	}
  2072  	if len(c.refs) == 0 {
  2073  		return c.done(nil, makeCLIError("at least one -ref must be provided"))
  2074  	}
  2075  	pkgPrefix, err := expandTemplate(args[0])
  2076  	if err != nil {
  2077  		return c.done(nil, err)
  2078  	}
  2079  
  2080  	ctx := cli.GetContext(a, c, env)
  2081  	return c.doneWithPins(visitPins(ctx, &visitPinsArgs{
  2082  		clientOptions: c.clientOptions,
  2083  		packagePrefix: pkgPrefix,
  2084  		version:       c.version,
  2085  		updatePin: func(client cipd.Client, pin common.Pin) error {
  2086  			for _, ref := range c.refs {
  2087  				if err := client.SetRefWhenReady(ctx, ref, pin); err != nil {
  2088  					return err
  2089  				}
  2090  			}
  2091  			return nil
  2092  		},
  2093  	}))
  2094  }
  2095  
  2096  type visitPinsArgs struct {
  2097  	clientOptions
  2098  
  2099  	packagePrefix string
  2100  	version       string
  2101  
  2102  	updatePin func(client cipd.Client, pin common.Pin) error
  2103  }
  2104  
  2105  func visitPins(ctx context.Context, args *visitPinsArgs) ([]pinInfo, error) {
  2106  	client, err := args.clientOptions.makeCIPDClient(ctx)
  2107  	if err != nil {
  2108  		return nil, err
  2109  	}
  2110  	defer client.Close(ctx)
  2111  
  2112  	client.BeginBatch(ctx)
  2113  	defer client.EndBatch(ctx)
  2114  
  2115  	// Do not touch anything if some packages do not have requested version. So
  2116  	// resolve versions first and only then move refs.
  2117  	pins, err := performBatchOperation(ctx, batchOperation{
  2118  		client:        client,
  2119  		packagePrefix: args.packagePrefix,
  2120  		callback: func(pkg string) (common.Pin, error) {
  2121  			return client.ResolveVersion(ctx, pkg, args.version)
  2122  		},
  2123  	})
  2124  	if err != nil {
  2125  		return nil, err
  2126  	}
  2127  	if hasErrors(pins) {
  2128  		printPinsAndError(map[string][]pinInfo{"": pins})
  2129  		return nil, errors.Reason("can't find %q version in all packages, aborting", args.version).
  2130  			Tag(cipderr.InvalidVersion.WithDetails(cipderr.Details{
  2131  				Version: args.version,
  2132  			})).Err()
  2133  	}
  2134  
  2135  	// Prepare for the next batch call.
  2136  	packages := make([]string, len(pins))
  2137  	pinsToUse := make(map[string]common.Pin, len(pins))
  2138  	for i, p := range pins {
  2139  		packages[i] = p.Pkg
  2140  		pinsToUse[p.Pkg] = *p.Pin
  2141  	}
  2142  
  2143  	// Update all refs or tags.
  2144  	return performBatchOperation(ctx, batchOperation{
  2145  		client:   client,
  2146  		packages: packages,
  2147  		callback: func(pkg string) (common.Pin, error) {
  2148  			pin := pinsToUse[pkg]
  2149  			if err := args.updatePin(client, pin); err != nil {
  2150  				return common.Pin{}, err
  2151  			}
  2152  			return pin, nil
  2153  		},
  2154  	})
  2155  }
  2156  
  2157  ////////////////////////////////////////////////////////////////////////////////
  2158  // 'set-tag' subcommand.
  2159  
  2160  func cmdSetTag(params Parameters) *subcommands.Command {
  2161  	return &subcommands.Command{
  2162  		UsageLine: "set-tag <package or package prefix> -tag key:value [options]",
  2163  		ShortDesc: "tags package of a specific version",
  2164  		LongDesc:  "Tags package of a specific version",
  2165  		CommandRun: func() subcommands.CommandRun {
  2166  			c := &setTagRun{}
  2167  			c.registerBaseFlags()
  2168  			c.tagsOptions.registerFlags(&c.Flags)
  2169  			c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
  2170  			c.Flags.StringVar(&c.version, "version", "<version>",
  2171  				"Package version to resolve. Could also be a tag or a ref.")
  2172  			return c
  2173  		},
  2174  	}
  2175  }
  2176  
  2177  type setTagRun struct {
  2178  	cipdSubcommand
  2179  	tagsOptions
  2180  	clientOptions
  2181  
  2182  	version string
  2183  }
  2184  
  2185  func (c *setTagRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  2186  	if !c.checkArgs(args, 1, 1) {
  2187  		return 1
  2188  	}
  2189  	if len(c.tags) == 0 {
  2190  		return c.done(nil, makeCLIError("at least one -tag must be provided"))
  2191  	}
  2192  	pkgPrefix, err := expandTemplate(args[0])
  2193  	if err != nil {
  2194  		return c.done(nil, err)
  2195  	}
  2196  
  2197  	ctx := cli.GetContext(a, c, env)
  2198  	return c.done(visitPins(ctx, &visitPinsArgs{
  2199  		clientOptions: c.clientOptions,
  2200  		packagePrefix: pkgPrefix,
  2201  		version:       c.version,
  2202  		updatePin: func(client cipd.Client, pin common.Pin) error {
  2203  			return client.AttachTagsWhenReady(ctx, pin, c.tags)
  2204  		},
  2205  	}))
  2206  }
  2207  
  2208  ////////////////////////////////////////////////////////////////////////////////
  2209  // 'set-metadata' subcommand.
  2210  
  2211  func cmdSetMetadata(params Parameters) *subcommands.Command {
  2212  	return &subcommands.Command{
  2213  		UsageLine: "set-metadata <package or package prefix> -metadata key:value -metadata-from-file key:path [options]",
  2214  		ShortDesc: "attaches metadata to an instance",
  2215  		LongDesc:  "Attaches metadata to an instance",
  2216  		CommandRun: func() subcommands.CommandRun {
  2217  			c := &setMetadataRun{}
  2218  			c.registerBaseFlags()
  2219  			c.metadataOptions.registerFlags(&c.Flags)
  2220  			c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
  2221  			c.Flags.StringVar(&c.version, "version", "<version>",
  2222  				"Package version to resolve. Could also be a tag or a ref.")
  2223  			return c
  2224  		},
  2225  	}
  2226  }
  2227  
  2228  type setMetadataRun struct {
  2229  	cipdSubcommand
  2230  	metadataOptions
  2231  	clientOptions
  2232  
  2233  	version string
  2234  }
  2235  
  2236  func (c *setMetadataRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  2237  	if !c.checkArgs(args, 1, 1) {
  2238  		return 1
  2239  	}
  2240  
  2241  	ctx := cli.GetContext(a, c, env)
  2242  
  2243  	md, err := c.metadataOptions.load(ctx)
  2244  	if err == nil && len(md) == 0 {
  2245  		err = makeCLIError("at least one -metadata or -metadata-from-file must be provided")
  2246  	}
  2247  	if err != nil {
  2248  		return c.done(nil, err)
  2249  	}
  2250  
  2251  	pkgPrefix, err := expandTemplate(args[0])
  2252  	if err != nil {
  2253  		return c.done(nil, err)
  2254  	}
  2255  
  2256  	return c.doneWithPins(visitPins(ctx, &visitPinsArgs{
  2257  		clientOptions: c.clientOptions,
  2258  		packagePrefix: pkgPrefix,
  2259  		version:       c.version,
  2260  		updatePin: func(client cipd.Client, pin common.Pin) error {
  2261  			return client.AttachMetadataWhenReady(ctx, pin, md)
  2262  		},
  2263  	}))
  2264  }
  2265  
  2266  ////////////////////////////////////////////////////////////////////////////////
  2267  // 'expand-package-name' subcommand.
  2268  
  2269  func cmdExpandPackageName(params Parameters) *subcommands.Command {
  2270  	return &subcommands.Command{
  2271  		UsageLine: "expand-package-name",
  2272  		ShortDesc: "replaces any placeholder variables in the given package name",
  2273  		LongDesc: "Replaces any placeholder variables in the given package " +
  2274  			"name.\n If supplying a name using the feature ${var=possible,values} " +
  2275  			"an empty string will be returned if the expansion does not match the " +
  2276  			"current variable state.",
  2277  		Advanced: true,
  2278  		CommandRun: func() subcommands.CommandRun {
  2279  			c := &expandPackageNameRun{}
  2280  			c.registerBaseFlags()
  2281  			c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
  2282  			return c
  2283  		},
  2284  	}
  2285  }
  2286  
  2287  type expandPackageNameRun struct {
  2288  	cipdSubcommand
  2289  	clientOptions
  2290  }
  2291  
  2292  func (c *expandPackageNameRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  2293  	if !c.checkArgs(args, 0, 1) {
  2294  		return 1
  2295  	}
  2296  
  2297  	if len(args) == 1 {
  2298  		path, err := template.DefaultExpander().Expand(args[0])
  2299  		if err != nil {
  2300  			if !errors.Is(err, template.ErrSkipTemplate) {
  2301  				return c.done(nil, err)
  2302  			}
  2303  			// return an empty string if the variable expansion does not
  2304  			// apply to current system.
  2305  			path = ""
  2306  		}
  2307  
  2308  		fmt.Println(path)
  2309  
  2310  		return c.done(path, nil)
  2311  	}
  2312  
  2313  	return c.done("", makeCLIError("one package name must be supplied: %v", args))
  2314  }
  2315  
  2316  ////////////////////////////////////////////////////////////////////////////////
  2317  // 'ls' subcommand.
  2318  
  2319  func cmdListPackages(params Parameters) *subcommands.Command {
  2320  	return &subcommands.Command{
  2321  		UsageLine: "ls [-r] [<prefix string>]",
  2322  		ShortDesc: "lists matching packages on the server",
  2323  		LongDesc: "Queries the backend for a list of packages in the given path to " +
  2324  			"which the user has access, optionally recursively.",
  2325  		CommandRun: func() subcommands.CommandRun {
  2326  			c := &listPackagesRun{}
  2327  			c.registerBaseFlags()
  2328  			c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
  2329  			c.Flags.BoolVar(&c.recursive, "r", false, "Whether to list packages in subdirectories.")
  2330  			c.Flags.BoolVar(&c.showHidden, "h", false, "Whether also to list hidden packages.")
  2331  			return c
  2332  		},
  2333  	}
  2334  }
  2335  
  2336  type listPackagesRun struct {
  2337  	cipdSubcommand
  2338  	clientOptions
  2339  
  2340  	recursive  bool
  2341  	showHidden bool
  2342  }
  2343  
  2344  func (c *listPackagesRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  2345  	if !c.checkArgs(args, 0, 1) {
  2346  		return 1
  2347  	}
  2348  	path, err := "", error(nil)
  2349  	if len(args) == 1 {
  2350  		path, err = expandTemplate(args[0])
  2351  		if err != nil {
  2352  			return c.done(nil, err)
  2353  		}
  2354  	}
  2355  	ctx := cli.GetContext(a, c, env)
  2356  	return c.done(listPackages(ctx, path, c.recursive, c.showHidden, c.clientOptions))
  2357  }
  2358  
  2359  func listPackages(ctx context.Context, path string, recursive, showHidden bool, clientOpts clientOptions) ([]string, error) {
  2360  	client, err := clientOpts.makeCIPDClient(ctx)
  2361  	if err != nil {
  2362  		return nil, err
  2363  	}
  2364  	defer client.Close(ctx)
  2365  
  2366  	packages, err := client.ListPackages(ctx, path, recursive, showHidden)
  2367  	if err != nil {
  2368  		return nil, err
  2369  	}
  2370  	if len(packages) == 0 {
  2371  		fmt.Println("No matching packages.")
  2372  	} else {
  2373  		for _, p := range packages {
  2374  			fmt.Println(p)
  2375  		}
  2376  	}
  2377  	return packages, nil
  2378  }
  2379  
  2380  ////////////////////////////////////////////////////////////////////////////////
  2381  // 'search' subcommand.
  2382  
  2383  func cmdSearch(params Parameters) *subcommands.Command {
  2384  	return &subcommands.Command{
  2385  		UsageLine: "search <package> -tag key:value [options]",
  2386  		ShortDesc: "searches for package instances by tag",
  2387  		LongDesc:  "Searches for instances of some package with all given tags.",
  2388  		CommandRun: func() subcommands.CommandRun {
  2389  			c := &searchRun{}
  2390  			c.registerBaseFlags()
  2391  			c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
  2392  			c.tagsOptions.registerFlags(&c.Flags)
  2393  			return c
  2394  		},
  2395  	}
  2396  }
  2397  
  2398  type searchRun struct {
  2399  	cipdSubcommand
  2400  	clientOptions
  2401  	tagsOptions
  2402  }
  2403  
  2404  func (c *searchRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  2405  	if !c.checkArgs(args, 1, 1) {
  2406  		return 1
  2407  	}
  2408  	if len(c.tags) == 0 {
  2409  		return c.done(nil, makeCLIError("at least one -tag must be provided"))
  2410  	}
  2411  	packageName, err := expandTemplate(args[0])
  2412  	if err != nil {
  2413  		return c.done(nil, err)
  2414  	}
  2415  	ctx := cli.GetContext(a, c, env)
  2416  	return c.done(searchInstances(ctx, packageName, c.tags, c.clientOptions))
  2417  }
  2418  
  2419  func searchInstances(ctx context.Context, packageName string, tags []string, clientOpts clientOptions) ([]common.Pin, error) {
  2420  	client, err := clientOpts.makeCIPDClient(ctx)
  2421  	if err != nil {
  2422  		return nil, err
  2423  	}
  2424  	defer client.Close(ctx)
  2425  
  2426  	pins, err := client.SearchInstances(ctx, packageName, tags)
  2427  	if err != nil {
  2428  		return nil, err
  2429  	}
  2430  	if len(pins) == 0 {
  2431  		fmt.Println("No matching instances.")
  2432  	} else {
  2433  		fmt.Println("Instances:")
  2434  		for _, pin := range pins {
  2435  			fmt.Printf("  %s\n", pin)
  2436  		}
  2437  	}
  2438  	return pins, nil
  2439  }
  2440  
  2441  ////////////////////////////////////////////////////////////////////////////////
  2442  // 'acl-list' subcommand.
  2443  
  2444  func cmdListACL(params Parameters) *subcommands.Command {
  2445  	return &subcommands.Command{
  2446  		Advanced:  true,
  2447  		UsageLine: "acl-list <package subpath>",
  2448  		ShortDesc: "lists package path Access Control List",
  2449  		LongDesc:  "Lists package path Access Control List.",
  2450  		CommandRun: func() subcommands.CommandRun {
  2451  			c := &listACLRun{}
  2452  			c.registerBaseFlags()
  2453  			c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
  2454  			return c
  2455  		},
  2456  	}
  2457  }
  2458  
  2459  type listACLRun struct {
  2460  	cipdSubcommand
  2461  	clientOptions
  2462  }
  2463  
  2464  func (c *listACLRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  2465  	if !c.checkArgs(args, 1, 1) {
  2466  		return 1
  2467  	}
  2468  	pkg, err := expandTemplate(args[0])
  2469  	if err != nil {
  2470  		return c.done(nil, err)
  2471  	}
  2472  
  2473  	ctx := cli.GetContext(a, c, env)
  2474  	return c.done(listACL(ctx, pkg, c.clientOptions))
  2475  }
  2476  
  2477  func listACL(ctx context.Context, packagePath string, clientOpts clientOptions) (map[string][]cipd.PackageACL, error) {
  2478  	client, err := clientOpts.makeCIPDClient(ctx)
  2479  	if err != nil {
  2480  		return nil, err
  2481  	}
  2482  	defer client.Close(ctx)
  2483  
  2484  	acls, err := client.FetchACL(ctx, packagePath)
  2485  	if err != nil {
  2486  		return nil, err
  2487  	}
  2488  
  2489  	// Split by role, drop empty ACLs.
  2490  	byRole := map[string][]cipd.PackageACL{}
  2491  	for _, a := range acls {
  2492  		if len(a.Principals) != 0 {
  2493  			byRole[a.Role] = append(byRole[a.Role], a)
  2494  		}
  2495  	}
  2496  
  2497  	listRoleACL := func(title string, acls []cipd.PackageACL) {
  2498  		fmt.Printf("%s:\n", title)
  2499  		if len(acls) == 0 {
  2500  			fmt.Printf("  none\n")
  2501  			return
  2502  		}
  2503  		for _, a := range acls {
  2504  			fmt.Printf("  via %q:\n", a.PackagePath)
  2505  			for _, u := range a.Principals {
  2506  				fmt.Printf("    %s\n", u)
  2507  			}
  2508  		}
  2509  	}
  2510  
  2511  	listRoleACL("Owners", byRole["OWNER"])
  2512  	listRoleACL("Writers", byRole["WRITER"])
  2513  	listRoleACL("Readers", byRole["READER"])
  2514  
  2515  	return byRole, nil
  2516  }
  2517  
  2518  ////////////////////////////////////////////////////////////////////////////////
  2519  // 'acl-edit' subcommand.
  2520  
  2521  // principalsList is used as custom flag value. It implements flag.Value.
  2522  type principalsList []string
  2523  
  2524  func (l *principalsList) String() string {
  2525  	return fmt.Sprintf("%v", *l)
  2526  }
  2527  
  2528  func (l *principalsList) Set(value string) error {
  2529  	// Ensure <type>:<id> syntax is used. Let the backend to validate the rest.
  2530  	chunks := strings.Split(value, ":")
  2531  	if len(chunks) != 2 {
  2532  		return makeCLIError("%q doesn't look like principal id (<type>:<id>)", value)
  2533  	}
  2534  	*l = append(*l, value)
  2535  	return nil
  2536  }
  2537  
  2538  func cmdEditACL(params Parameters) *subcommands.Command {
  2539  	return &subcommands.Command{
  2540  		Advanced:  true,
  2541  		UsageLine: "acl-edit <package subpath> [options]",
  2542  		ShortDesc: "modifies package path Access Control List",
  2543  		LongDesc:  "Modifies package path Access Control List.",
  2544  		CommandRun: func() subcommands.CommandRun {
  2545  			c := &editACLRun{}
  2546  			c.registerBaseFlags()
  2547  			c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
  2548  			c.Flags.Var(&c.owner, "owner", "Users (user:email) or groups (`group:name`) to grant OWNER role.")
  2549  			c.Flags.Var(&c.writer, "writer", "Users (user:email) or groups (`group:name`) to grant WRITER role.")
  2550  			c.Flags.Var(&c.reader, "reader", "Users (user:email) or groups (`group:name`) to grant READER role.")
  2551  			c.Flags.Var(&c.revoke, "revoke", "Users (user:email) or groups (`group:name`) to remove from all roles.")
  2552  			return c
  2553  		},
  2554  	}
  2555  }
  2556  
  2557  type editACLRun struct {
  2558  	cipdSubcommand
  2559  	clientOptions
  2560  
  2561  	owner  principalsList
  2562  	writer principalsList
  2563  	reader principalsList
  2564  	revoke principalsList
  2565  }
  2566  
  2567  func (c *editACLRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  2568  	if !c.checkArgs(args, 1, 1) {
  2569  		return 1
  2570  	}
  2571  	pkg, err := expandTemplate(args[0])
  2572  	if err != nil {
  2573  		return c.done(nil, err)
  2574  	}
  2575  
  2576  	ctx := cli.GetContext(a, c, env)
  2577  	return c.done(nil, editACL(ctx, pkg, c.owner, c.writer, c.reader, c.revoke, c.clientOptions))
  2578  }
  2579  
  2580  func editACL(ctx context.Context, packagePath string, owners, writers, readers, revoke principalsList, clientOpts clientOptions) error {
  2581  	changes := []cipd.PackageACLChange{}
  2582  
  2583  	makeChanges := func(action cipd.PackageACLChangeAction, role string, list principalsList) {
  2584  		for _, p := range list {
  2585  			changes = append(changes, cipd.PackageACLChange{
  2586  				Action:    action,
  2587  				Role:      role,
  2588  				Principal: p,
  2589  			})
  2590  		}
  2591  	}
  2592  
  2593  	makeChanges(cipd.GrantRole, "OWNER", owners)
  2594  	makeChanges(cipd.GrantRole, "WRITER", writers)
  2595  	makeChanges(cipd.GrantRole, "READER", readers)
  2596  
  2597  	makeChanges(cipd.RevokeRole, "OWNER", revoke)
  2598  	makeChanges(cipd.RevokeRole, "WRITER", revoke)
  2599  	makeChanges(cipd.RevokeRole, "READER", revoke)
  2600  
  2601  	if len(changes) == 0 {
  2602  		return nil
  2603  	}
  2604  
  2605  	client, err := clientOpts.makeCIPDClient(ctx)
  2606  	if err != nil {
  2607  		return err
  2608  	}
  2609  	defer client.Close(ctx)
  2610  
  2611  	err = client.ModifyACL(ctx, packagePath, changes)
  2612  	if err != nil {
  2613  		return err
  2614  	}
  2615  	fmt.Println("ACL changes applied.")
  2616  	return nil
  2617  }
  2618  
  2619  ////////////////////////////////////////////////////////////////////////////////
  2620  // 'acl-check' subcommand.
  2621  
  2622  func cmdCheckACL(params Parameters) *subcommands.Command {
  2623  	return &subcommands.Command{
  2624  		Advanced:  true,
  2625  		UsageLine: "acl-check <package subpath> [options]",
  2626  		ShortDesc: "checks whether the caller has given roles in a package",
  2627  		LongDesc:  "Checks whether the caller has given roles in a package.",
  2628  		CommandRun: func() subcommands.CommandRun {
  2629  			c := &checkACLRun{}
  2630  			c.registerBaseFlags()
  2631  			c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
  2632  			c.Flags.BoolVar(&c.owner, "owner", false, "Check for OWNER role.")
  2633  			c.Flags.BoolVar(&c.writer, "writer", false, "Check for WRITER role.")
  2634  			c.Flags.BoolVar(&c.reader, "reader", false, "Check for READER role.")
  2635  			return c
  2636  		},
  2637  	}
  2638  }
  2639  
  2640  type checkACLRun struct {
  2641  	cipdSubcommand
  2642  	clientOptions
  2643  
  2644  	owner  bool
  2645  	writer bool
  2646  	reader bool
  2647  }
  2648  
  2649  func (c *checkACLRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  2650  	if !c.checkArgs(args, 1, 1) {
  2651  		return 1
  2652  	}
  2653  
  2654  	var roles []string
  2655  	if c.owner {
  2656  		roles = append(roles, "OWNER")
  2657  	}
  2658  	if c.writer {
  2659  		roles = append(roles, "WRITER")
  2660  	}
  2661  	if c.reader {
  2662  		roles = append(roles, "READER")
  2663  	}
  2664  
  2665  	// By default, check for READER access.
  2666  	if len(roles) == 0 {
  2667  		roles = append(roles, "READER")
  2668  	}
  2669  
  2670  	pkg, err := expandTemplate(args[0])
  2671  	if err != nil {
  2672  		return c.done(nil, err)
  2673  	}
  2674  
  2675  	ctx := cli.GetContext(a, c, env)
  2676  	return c.done(checkACL(ctx, pkg, roles, c.clientOptions))
  2677  }
  2678  
  2679  func checkACL(ctx context.Context, packagePath string, roles []string, clientOpts clientOptions) (bool, error) {
  2680  	client, err := clientOpts.makeCIPDClient(ctx)
  2681  	if err != nil {
  2682  		return false, err
  2683  	}
  2684  	defer client.Close(ctx)
  2685  
  2686  	actualRoles, err := client.FetchRoles(ctx, packagePath)
  2687  	if err != nil {
  2688  		return false, err
  2689  	}
  2690  	roleSet := stringset.NewFromSlice(actualRoles...)
  2691  
  2692  	var missing []string
  2693  	for _, r := range roles {
  2694  		if !roleSet.Has(r) {
  2695  			missing = append(missing, r)
  2696  		}
  2697  	}
  2698  
  2699  	if len(missing) == 0 {
  2700  		fmt.Printf("The caller has all requested role(s): %s\n", strings.Join(roles, ", "))
  2701  		return true, nil
  2702  	}
  2703  
  2704  	fmt.Printf("The caller doesn't have following role(s): %s\n", strings.Join(missing, ", "))
  2705  	return false, nil
  2706  }
  2707  
  2708  ////////////////////////////////////////////////////////////////////////////////
  2709  // 'pkg-build' subcommand.
  2710  
  2711  func cmdBuild() *subcommands.Command {
  2712  	return &subcommands.Command{
  2713  		Advanced:  true,
  2714  		UsageLine: "pkg-build [options]",
  2715  		ShortDesc: "builds a package instance file",
  2716  		LongDesc:  "Builds a package instance producing *.cipd file.",
  2717  		CommandRun: func() subcommands.CommandRun {
  2718  			c := &buildRun{}
  2719  			c.registerBaseFlags()
  2720  			c.inputOptions.registerFlags(&c.Flags)
  2721  			c.hashOptions.registerFlags(&c.Flags)
  2722  			c.Flags.StringVar(&c.outputFile, "out", "<path>", "Path to a file to write the final package to.")
  2723  			return c
  2724  		},
  2725  	}
  2726  }
  2727  
  2728  type buildRun struct {
  2729  	cipdSubcommand
  2730  	inputOptions
  2731  	hashOptions
  2732  
  2733  	outputFile string
  2734  }
  2735  
  2736  func (c *buildRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  2737  	if !c.checkArgs(args, 0, 0) {
  2738  		return 1
  2739  	}
  2740  	ctx := cli.GetContext(a, c, env)
  2741  	_, err := buildInstanceFile(ctx, c.outputFile, c.inputOptions, c.hashAlgo())
  2742  	if err != nil {
  2743  		return c.done(nil, err)
  2744  	}
  2745  	return c.done(inspectInstanceFile(ctx, c.outputFile, c.hashAlgo(), false))
  2746  }
  2747  
  2748  func buildInstanceFile(ctx context.Context, instanceFile string, inputOpts inputOptions, algo api.HashAlgo) (common.Pin, error) {
  2749  	// Read the list of files to add to the package.
  2750  	buildOpts, err := inputOpts.prepareInput()
  2751  	if err != nil {
  2752  		return common.Pin{}, err
  2753  	}
  2754  
  2755  	// Prepare the destination, update build options with io.Writer to it.
  2756  	out, err := os.OpenFile(instanceFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
  2757  	if err != nil {
  2758  		return common.Pin{}, errors.Annotate(err, "opening instance file for writing").Tag(cipderr.IO).Err()
  2759  	}
  2760  	buildOpts.Output = out
  2761  	buildOpts.HashAlgo = algo
  2762  
  2763  	// Build the package.
  2764  	pin, err := builder.BuildInstance(ctx, buildOpts)
  2765  	if err != nil {
  2766  		out.Close()
  2767  		os.Remove(instanceFile)
  2768  		return common.Pin{}, err
  2769  	}
  2770  
  2771  	// Make sure it is flushed properly by ensuring Close succeeds.
  2772  	if err := out.Close(); err != nil {
  2773  		return common.Pin{}, errors.Annotate(err, "flushing built instance file").Tag(cipderr.IO).Err()
  2774  	}
  2775  
  2776  	return pin, nil
  2777  }
  2778  
  2779  ////////////////////////////////////////////////////////////////////////////////
  2780  // 'pkg-deploy' subcommand.
  2781  
  2782  func cmdDeploy() *subcommands.Command {
  2783  	return &subcommands.Command{
  2784  		Advanced:  true,
  2785  		UsageLine: "pkg-deploy <package instance file> [options]",
  2786  		ShortDesc: "deploys a package instance file",
  2787  		LongDesc:  "Deploys a *.cipd package instance into a site root.",
  2788  		CommandRun: func() subcommands.CommandRun {
  2789  			c := &deployRun{}
  2790  			c.registerBaseFlags()
  2791  			c.hashOptions.registerFlags(&c.Flags)
  2792  			c.maxThreadsOption.registerFlags(&c.Flags)
  2793  			c.Flags.StringVar(&c.rootDir, "root", "<path>", "Path to an installation site root directory.")
  2794  			c.Flags.Var(&c.overrideInstallMode, "install-mode",
  2795  				"Deploy using this mode instead, even if the instance manifest specifies otherwise.")
  2796  			return c
  2797  		},
  2798  	}
  2799  }
  2800  
  2801  type deployRun struct {
  2802  	cipdSubcommand
  2803  	hashOptions
  2804  	maxThreadsOption
  2805  
  2806  	rootDir             string
  2807  	overrideInstallMode pkg.InstallMode
  2808  }
  2809  
  2810  func (c *deployRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  2811  	if !c.checkArgs(args, 1, 1) {
  2812  		return 1
  2813  	}
  2814  	ctx := cli.GetContext(a, c, env)
  2815  	maxThreads, err := c.loadMaxThreads(ctx)
  2816  	if err != nil {
  2817  		return c.done(nil, err)
  2818  	}
  2819  	return c.done(deployInstanceFile(ctx, c.rootDir, args[0], c.hashAlgo(), maxThreads, c.overrideInstallMode))
  2820  }
  2821  
  2822  func deployInstanceFile(ctx context.Context, root, instanceFile string, hashAlgo api.HashAlgo, maxThreads int, overrideInstallMode pkg.InstallMode) (common.Pin, error) {
  2823  	inst, err := reader.OpenInstanceFile(ctx, instanceFile, reader.OpenInstanceOpts{
  2824  		VerificationMode: reader.CalculateHash,
  2825  		HashAlgo:         hashAlgo,
  2826  	})
  2827  	if err != nil {
  2828  		return common.Pin{}, err
  2829  	}
  2830  	defer inst.Close(ctx, false)
  2831  
  2832  	inspectInstance(ctx, inst, false)
  2833  
  2834  	d := deployer.New(root)
  2835  	defer d.FS().CleanupTrash(ctx)
  2836  
  2837  	// TODO(iannucci): add subdir arg to deployRun
  2838  
  2839  	return d.DeployInstance(ctx, "", inst, overrideInstallMode, maxThreads)
  2840  }
  2841  
  2842  ////////////////////////////////////////////////////////////////////////////////
  2843  // 'pkg-fetch' subcommand.
  2844  
  2845  func cmdFetch(params Parameters) *subcommands.Command {
  2846  	return &subcommands.Command{
  2847  		Advanced:  true,
  2848  		UsageLine: "pkg-fetch <package> [options]",
  2849  		ShortDesc: "fetches a package instance file from the repository",
  2850  		LongDesc:  "Fetches a package instance file from the repository.",
  2851  		CommandRun: func() subcommands.CommandRun {
  2852  			c := &fetchRun{}
  2853  			c.registerBaseFlags()
  2854  			c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
  2855  			c.Flags.StringVar(&c.version, "version", "<version>", "Package version to fetch.")
  2856  			c.Flags.StringVar(&c.outputPath, "out", "<path>", "Path to a file to write fetch to.")
  2857  			return c
  2858  		},
  2859  	}
  2860  }
  2861  
  2862  type fetchRun struct {
  2863  	cipdSubcommand
  2864  	clientOptions
  2865  
  2866  	version    string
  2867  	outputPath string
  2868  }
  2869  
  2870  func (c *fetchRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  2871  	if !c.checkArgs(args, 1, 1) {
  2872  		return 1
  2873  	}
  2874  	pkg, err := expandTemplate(args[0])
  2875  	if err != nil {
  2876  		return c.done(nil, err)
  2877  	}
  2878  
  2879  	ctx := cli.GetContext(a, c, env)
  2880  	return c.done(fetchInstanceFile(ctx, pkg, c.version, c.outputPath, c.clientOptions))
  2881  }
  2882  
  2883  func fetchInstanceFile(ctx context.Context, packageName, version, instanceFile string, clientOpts clientOptions) (pin common.Pin, err error) {
  2884  	client, err := clientOpts.makeCIPDClient(ctx)
  2885  	if err != nil {
  2886  		return common.Pin{}, err
  2887  	}
  2888  	defer client.Close(ctx)
  2889  
  2890  	defer func() {
  2891  		cipderr.AttachDetails(&err, cipderr.Details{
  2892  			Package: packageName,
  2893  			Version: version,
  2894  		})
  2895  	}()
  2896  
  2897  	pin, err = client.ResolveVersion(ctx, packageName, version)
  2898  	if err != nil {
  2899  		return common.Pin{}, err
  2900  	}
  2901  
  2902  	out, err := os.OpenFile(instanceFile, os.O_CREATE|os.O_WRONLY, 0666)
  2903  	if err != nil {
  2904  		return common.Pin{}, errors.Annotate(err, "opening the instance file for writing").Tag(cipderr.IO).Err()
  2905  	}
  2906  	ok := false
  2907  	defer func() {
  2908  		if !ok {
  2909  			out.Close()
  2910  			os.Remove(instanceFile)
  2911  		}
  2912  	}()
  2913  
  2914  	err = client.FetchInstanceTo(ctx, pin, out)
  2915  	if err != nil {
  2916  		return common.Pin{}, err
  2917  	}
  2918  
  2919  	if err := out.Close(); err != nil {
  2920  		return common.Pin{}, errors.Annotate(err, "flushing fetched instance file").Tag(cipderr.IO).Err()
  2921  	}
  2922  	ok = true
  2923  
  2924  	// Print information about the instance. 'FetchInstanceTo' already verified
  2925  	// the hash.
  2926  	inst, err := reader.OpenInstanceFile(ctx, instanceFile, reader.OpenInstanceOpts{
  2927  		VerificationMode: reader.SkipHashVerification,
  2928  		InstanceID:       pin.InstanceID,
  2929  	})
  2930  	if err != nil {
  2931  		os.Remove(instanceFile)
  2932  		return common.Pin{}, err
  2933  	}
  2934  	defer inst.Close(ctx, false)
  2935  	inspectInstance(ctx, inst, false)
  2936  	return inst.Pin(), nil
  2937  }
  2938  
  2939  ////////////////////////////////////////////////////////////////////////////////
  2940  // 'pkg-inspect' subcommand.
  2941  
  2942  func cmdInspect() *subcommands.Command {
  2943  	return &subcommands.Command{
  2944  		Advanced:  true,
  2945  		UsageLine: "pkg-inspect <package instance file>",
  2946  		ShortDesc: "inspects contents of a package instance file",
  2947  		LongDesc:  "Reads contents *.cipd file and prints information about it.",
  2948  		CommandRun: func() subcommands.CommandRun {
  2949  			c := &inspectRun{}
  2950  			c.registerBaseFlags()
  2951  			c.hashOptions.registerFlags(&c.Flags)
  2952  			return c
  2953  		},
  2954  	}
  2955  }
  2956  
  2957  type inspectRun struct {
  2958  	cipdSubcommand
  2959  	hashOptions
  2960  }
  2961  
  2962  func (c *inspectRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  2963  	if !c.checkArgs(args, 1, 1) {
  2964  		return 1
  2965  	}
  2966  	ctx := cli.GetContext(a, c, env)
  2967  	return c.done(inspectInstanceFile(ctx, args[0], c.hashAlgo(), true))
  2968  }
  2969  
  2970  func inspectInstanceFile(ctx context.Context, instanceFile string, hashAlgo api.HashAlgo, listFiles bool) (common.Pin, error) {
  2971  	inst, err := reader.OpenInstanceFile(ctx, instanceFile, reader.OpenInstanceOpts{
  2972  		VerificationMode: reader.CalculateHash,
  2973  		HashAlgo:         hashAlgo,
  2974  	})
  2975  	if err != nil {
  2976  		return common.Pin{}, err
  2977  	}
  2978  	defer inst.Close(ctx, false)
  2979  	inspectInstance(ctx, inst, listFiles)
  2980  	return inst.Pin(), nil
  2981  }
  2982  
  2983  func inspectPin(ctx context.Context, pin common.Pin) {
  2984  	fmt.Printf("Instance: %s\n", pin)
  2985  }
  2986  
  2987  func inspectInstance(ctx context.Context, inst pkg.Instance, listFiles bool) {
  2988  	inspectPin(ctx, inst.Pin())
  2989  	if listFiles {
  2990  		fmt.Println("Package files:")
  2991  		for _, f := range inst.Files() {
  2992  			if f.Symlink() {
  2993  				target, err := f.SymlinkTarget()
  2994  				if err != nil {
  2995  					fmt.Printf(" E %s (%s)\n", f.Name(), err)
  2996  				} else {
  2997  					fmt.Printf(" S %s -> %s\n", f.Name(), target)
  2998  				}
  2999  			} else {
  3000  				flags := make([]string, 0, 3)
  3001  				if f.Executable() {
  3002  					flags = append(flags, "+x")
  3003  				}
  3004  				if f.WinAttrs()&fs.WinAttrHidden != 0 {
  3005  					flags = append(flags, "+H")
  3006  				}
  3007  				if f.WinAttrs()&fs.WinAttrSystem != 0 {
  3008  					flags = append(flags, "+S")
  3009  				}
  3010  				flagText := ""
  3011  				if len(flags) > 0 {
  3012  					flagText = fmt.Sprintf(" (%s)", strings.Join(flags, ""))
  3013  				}
  3014  				fmt.Printf(" F %s%s\n", f.Name(), flagText)
  3015  			}
  3016  		}
  3017  	}
  3018  }
  3019  
  3020  ////////////////////////////////////////////////////////////////////////////////
  3021  // 'pkg-register' subcommand.
  3022  
  3023  func cmdRegister(params Parameters) *subcommands.Command {
  3024  	return &subcommands.Command{
  3025  		Advanced:  true,
  3026  		UsageLine: "pkg-register <package instance file>",
  3027  		ShortDesc: "uploads and registers package instance in the package repository",
  3028  		LongDesc:  "Uploads and registers package instance in the package repository.",
  3029  		CommandRun: func() subcommands.CommandRun {
  3030  			c := &registerRun{}
  3031  			c.registerBaseFlags()
  3032  			c.Opts.refsOptions.registerFlags(&c.Flags)
  3033  			c.Opts.tagsOptions.registerFlags(&c.Flags)
  3034  			c.Opts.metadataOptions.registerFlags(&c.Flags)
  3035  			c.Opts.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
  3036  			c.Opts.uploadOptions.registerFlags(&c.Flags)
  3037  			c.Opts.hashOptions.registerFlags(&c.Flags)
  3038  			return c
  3039  		},
  3040  	}
  3041  }
  3042  
  3043  type registerOpts struct {
  3044  	refsOptions
  3045  	tagsOptions
  3046  	metadataOptions
  3047  	clientOptions
  3048  	uploadOptions
  3049  	hashOptions
  3050  }
  3051  
  3052  type registerRun struct {
  3053  	cipdSubcommand
  3054  
  3055  	Opts registerOpts
  3056  }
  3057  
  3058  func (c *registerRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  3059  	if !c.checkArgs(args, 1, 1) {
  3060  		return 1
  3061  	}
  3062  	ctx := cli.GetContext(a, c, env)
  3063  	return c.done(registerInstanceFile(ctx, args[0], nil, &c.Opts))
  3064  }
  3065  
  3066  func registerInstanceFile(ctx context.Context, instanceFile string, knownPin *common.Pin, opts *registerOpts) (common.Pin, error) {
  3067  	// Load metadata, in particular process -metadata-from-file, which may fail.
  3068  	metadata, err := opts.metadataOptions.load(ctx)
  3069  	if err != nil {
  3070  		return common.Pin{}, err
  3071  	}
  3072  
  3073  	src, err := pkg.NewFileSource(instanceFile)
  3074  	if err != nil {
  3075  		if os.IsNotExist(err) {
  3076  			return common.Pin{}, errors.Annotate(err, "missing input instance file").Tag(cipderr.BadArgument).Err()
  3077  		}
  3078  		return common.Pin{}, errors.Annotate(err, "opening input instance file").Tag(cipderr.IO).Err()
  3079  	}
  3080  	defer src.Close(ctx, false)
  3081  
  3082  	// Calculate the pin if not yet known.
  3083  	var pin common.Pin
  3084  	if knownPin != nil {
  3085  		pin = *knownPin
  3086  	} else {
  3087  		pin, err = reader.CalculatePin(ctx, src, opts.hashAlgo())
  3088  		if err != nil {
  3089  			return common.Pin{}, err
  3090  		}
  3091  	}
  3092  	inspectPin(ctx, pin)
  3093  
  3094  	client, err := opts.clientOptions.makeCIPDClient(ctx)
  3095  	if err != nil {
  3096  		return common.Pin{}, err
  3097  	}
  3098  	defer client.Close(ctx)
  3099  
  3100  	err = client.RegisterInstance(ctx, pin, src, opts.uploadOptions.verificationTimeout)
  3101  	if err != nil {
  3102  		return common.Pin{}, err
  3103  	}
  3104  	err = attachAndMove(ctx, client, pin, metadata, opts.tags, opts.refs)
  3105  	if err != nil {
  3106  		return common.Pin{}, err
  3107  	}
  3108  	return pin, nil
  3109  }
  3110  
  3111  func attachAndMove(ctx context.Context, client cipd.Client, pin common.Pin, md []cipd.Metadata, tags tagList, refs refList) error {
  3112  	if err := client.AttachMetadataWhenReady(ctx, pin, md); err != nil {
  3113  		return err
  3114  	}
  3115  	if err := client.AttachTagsWhenReady(ctx, pin, tags); err != nil {
  3116  		return err
  3117  	}
  3118  	for _, ref := range refs {
  3119  		if err := client.SetRefWhenReady(ctx, ref, pin); err != nil {
  3120  			return err
  3121  		}
  3122  	}
  3123  	return nil
  3124  }
  3125  
  3126  ////////////////////////////////////////////////////////////////////////////////
  3127  // 'selfupdate' subcommand.
  3128  
  3129  func cmdSelfUpdate(params Parameters) *subcommands.Command {
  3130  	return &subcommands.Command{
  3131  		UsageLine: "selfupdate -version <version> | -version-file <path>",
  3132  		ShortDesc: "updates the current CIPD client binary",
  3133  		LongDesc: "Does an in-place upgrade to the current CIPD binary.\n\n" +
  3134  			"Reads the version either from the command line (when using -version) or " +
  3135  			"from a file (when using -version-file). When using -version-file, also " +
  3136  			"loads special *.digests file (from <version-file>.digests path) with " +
  3137  			"pinned hashes of the client binary for all platforms. When selfupdating, " +
  3138  			"the client will verify the new downloaded binary has a hash specified in " +
  3139  			"the *.digests file.",
  3140  		CommandRun: func() subcommands.CommandRun {
  3141  			c := &selfupdateRun{}
  3142  
  3143  			// By default, show a reduced number of logs unless something goes wrong.
  3144  			c.logConfig.Level = logging.Warning
  3145  
  3146  			c.registerBaseFlags()
  3147  			c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
  3148  			c.Flags.StringVar(&c.version, "version", "", "Version of the client to update to (incompatible with -version-file).")
  3149  			c.Flags.StringVar(&c.versionFile, "version-file", "",
  3150  				"Indicates the path to read the new version from (<version-file> itself) and "+
  3151  					"the path to the file with pinned hashes of the CIPD binary (<version-file>.digests file).")
  3152  			return c
  3153  		},
  3154  	}
  3155  }
  3156  
  3157  type selfupdateRun struct {
  3158  	cipdSubcommand
  3159  	clientOptions
  3160  
  3161  	version     string
  3162  	versionFile string
  3163  }
  3164  
  3165  func (c *selfupdateRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  3166  	if !c.checkArgs(args, 0, 0) {
  3167  		return 1
  3168  	}
  3169  
  3170  	ctx := cli.GetContext(a, c, env)
  3171  
  3172  	switch {
  3173  	case c.version != "" && c.versionFile != "":
  3174  		return c.done(nil, makeCLIError("-version and -version-file are mutually exclusive, use only one"))
  3175  	case c.version == "" && c.versionFile == "":
  3176  		return c.done(nil, makeCLIError("either -version or -version-file are required"))
  3177  	}
  3178  
  3179  	var version = c.version
  3180  	var digests *digests.ClientDigestsFile
  3181  
  3182  	if version == "" { // using -version-file instead? load *.digests
  3183  		var err error
  3184  		version, err = loadClientVersion(c.versionFile)
  3185  		if err != nil {
  3186  			return c.done(nil, err)
  3187  		}
  3188  		digests, err = loadClientDigests(c.versionFile + digestsSfx)
  3189  		if err != nil {
  3190  			return c.done(nil, err)
  3191  		}
  3192  	}
  3193  
  3194  	return c.done(func() (common.Pin, error) {
  3195  		exePath, err := os.Executable()
  3196  		if err != nil {
  3197  			return common.Pin{}, err
  3198  		}
  3199  		opts, err := c.clientOptions.toCIPDClientOpts(ctx)
  3200  		if err != nil {
  3201  			return common.Pin{}, err
  3202  		}
  3203  		return cipd.MaybeUpdateClient(ctx, opts, version, exePath, digests)
  3204  	}())
  3205  }
  3206  
  3207  ////////////////////////////////////////////////////////////////////////////////
  3208  // 'selfupdate-roll' subcommand.
  3209  
  3210  func cmdSelfUpdateRoll(params Parameters) *subcommands.Command {
  3211  	return &subcommands.Command{
  3212  		Advanced:  true,
  3213  		UsageLine: "selfupdate-roll -version-file <path> (-version <version> | -check)",
  3214  		ShortDesc: "generates or checks the client version and *.digests files",
  3215  		LongDesc: "Generates or checks the client version and *.digests files.\n\n" +
  3216  			"When -version is specified, takes its value as CIPD client version, " +
  3217  			"resolves it into a list of hashes of the client binary at this version " +
  3218  			"for all known platforms, and (on success) puts the version into a file " +
  3219  			"specified by -version-file (referred to as <version-file> below), and " +
  3220  			"all hashes into <version-file>.digests file. They are later used by " +
  3221  			"'selfupdate -version-file <version-file>' to verify validity of the " +
  3222  			"fetched binary.\n\n" +
  3223  			"If -version is not specified, reads it from <version-file> and generates " +
  3224  			"<version-file>.digests file based on it.\n\n" +
  3225  			"When using -check, just verifies hashes in the <version-file>.digests " +
  3226  			"file match the version recorded in the <version-file>.",
  3227  		CommandRun: func() subcommands.CommandRun {
  3228  			c := &selfupdateRollRun{}
  3229  
  3230  			c.registerBaseFlags()
  3231  			c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads)
  3232  			c.Flags.StringVar(&c.version, "version", "", "Version of the client to roll to.")
  3233  			c.Flags.StringVar(&c.versionFile, "version-file", "<version-file>",
  3234  				"Indicates the path to a file with the version (<version-file> itself) and "+
  3235  					"the path to the file with pinned hashes of the CIPD binary (<version-file>.digests file).")
  3236  			c.Flags.BoolVar(&c.check, "check", false, "If set, checks that the file with "+
  3237  				"pinned hashes of the CIPD binary (<version-file>.digests file) is up-to-date.")
  3238  			return c
  3239  		},
  3240  	}
  3241  }
  3242  
  3243  type selfupdateRollRun struct {
  3244  	cipdSubcommand
  3245  	clientOptions
  3246  
  3247  	version     string
  3248  	versionFile string
  3249  	check       bool
  3250  }
  3251  
  3252  func (c *selfupdateRollRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  3253  	if !c.checkArgs(args, 0, 0) {
  3254  		return 1
  3255  	}
  3256  
  3257  	ctx := cli.GetContext(a, c, env)
  3258  	client, err := c.clientOptions.makeCIPDClient(ctx)
  3259  	if err != nil {
  3260  		return c.done(nil, err)
  3261  	}
  3262  	defer client.Close(ctx)
  3263  
  3264  	if c.check {
  3265  		if c.version != "" {
  3266  			return c.done(nil, makeCLIError("-version should not be used in -check mode"))
  3267  		}
  3268  		version, err := loadClientVersion(c.versionFile)
  3269  		if err != nil {
  3270  			return c.done(nil, err)
  3271  		}
  3272  		return c.doneWithPins(checkClientDigests(ctx, client, c.versionFile+digestsSfx, version))
  3273  	}
  3274  
  3275  	// Grab the version from the command line and fallback to the -version-file
  3276  	// otherwise. The fallback is useful when we just want to regenerate *.digests
  3277  	// without touching the version file itself.
  3278  	version := c.version
  3279  	if version == "" {
  3280  		var err error
  3281  		version, err = loadClientVersion(c.versionFile)
  3282  		if err != nil {
  3283  			return c.done(nil, err)
  3284  		}
  3285  	}
  3286  
  3287  	// It really makes sense to pin only tags. Warn about that. Still proceed,
  3288  	// maybe users are using refs and do not move them by convention.
  3289  	switch {
  3290  	case common.ValidateInstanceID(version, common.AnyHash) == nil:
  3291  		return c.done(nil, errors.Reason("expecting a version identifier that can be "+
  3292  			"resolved for all per-platform CIPD client packages, not a concrete instance ID").Tag(cipderr.BadArgument).Err())
  3293  	case common.ValidateInstanceTag(version) != nil:
  3294  		fmt.Printf(
  3295  			"WARNING! Version %q is not a tag. The hash pinning in *.digests file is "+
  3296  				"only useful for unmovable version identifiers. Proceeding, assuming "+
  3297  				"the immutability of %q is maintained manually. If it moves, selfupdate "+
  3298  				"will break due to *.digests file no longer matching the packages!\n\n",
  3299  			version, version)
  3300  	}
  3301  
  3302  	pins, err := generateClientDigests(ctx, client, c.versionFile+digestsSfx, version)
  3303  	if err != nil {
  3304  		return c.doneWithPins(pins, err)
  3305  	}
  3306  
  3307  	if c.version != "" {
  3308  		if err := os.WriteFile(c.versionFile, []byte(c.version+"\n"), 0666); err != nil {
  3309  			return c.done(nil, errors.Annotate(err, "writing the client version file").Tag(cipderr.IO).Err())
  3310  		}
  3311  	}
  3312  
  3313  	return c.doneWithPins(pins, nil)
  3314  }
  3315  
  3316  ////////////////////////////////////////////////////////////////////////////////
  3317  
  3318  const digestsSfx = ".digests"
  3319  
  3320  func generateClientDigests(ctx context.Context, client cipd.Client, path, version string) ([]pinInfo, error) {
  3321  	digests, pins, err := assembleClientDigests(ctx, client, version)
  3322  	if err != nil {
  3323  		return pins, err
  3324  	}
  3325  
  3326  	buf := bytes.Buffer{}
  3327  	versionFileName := strings.TrimSuffix(filepath.Base(path), digestsSfx)
  3328  	if err := digests.Serialize(&buf, version, versionFileName); err != nil {
  3329  		return nil, err
  3330  	}
  3331  	if err := os.WriteFile(path, buf.Bytes(), 0666); err != nil {
  3332  		return nil, errors.Annotate(err, "writing client digests file").Tag(cipderr.IO).Err()
  3333  	}
  3334  
  3335  	fmt.Printf("The pinned client hashes have been written to %s.\n\n", filepath.Base(path))
  3336  	return pins, nil
  3337  }
  3338  
  3339  func checkClientDigests(ctx context.Context, client cipd.Client, path, version string) ([]pinInfo, error) {
  3340  	existing, err := loadClientDigests(path)
  3341  	if err != nil {
  3342  		return nil, err
  3343  	}
  3344  	digests, pins, err := assembleClientDigests(ctx, client, version)
  3345  	if err != nil {
  3346  		return pins, err
  3347  	}
  3348  	if !digests.Equal(existing) {
  3349  		base := filepath.Base(path)
  3350  		return nil, errors.Reason("the file with pinned client hashes (%s) is stale, "+
  3351  			"use 'cipd selfupdate-roll -version-file %s' to update it",
  3352  			base, strings.TrimSuffix(base, digestsSfx)).Tag(cipderr.Stale).Err()
  3353  	}
  3354  	fmt.Printf("The file with pinned client hashes (%s) is up-to-date.\n\n", filepath.Base(path))
  3355  	return pins, nil
  3356  }
  3357  
  3358  // loadClientVersion reads a version string from a file.
  3359  func loadClientVersion(path string) (string, error) {
  3360  	blob, err := os.ReadFile(path)
  3361  	if err != nil {
  3362  		return "", errors.Annotate(err, "reading client version file").Tag(cipderr.IO).Err()
  3363  	}
  3364  	version := strings.TrimSpace(string(blob))
  3365  	if err := common.ValidateInstanceVersion(version); err != nil {
  3366  		return "", err
  3367  	}
  3368  	return version, nil
  3369  }
  3370  
  3371  // loadClientDigests loads the *.digests file with client binary hashes.
  3372  func loadClientDigests(path string) (*digests.ClientDigestsFile, error) {
  3373  	switch f, err := os.Open(path); {
  3374  	case os.IsNotExist(err):
  3375  		base := filepath.Base(path)
  3376  		return nil, errors.Reason("the file with pinned client hashes (%s) doesn't exist, "+
  3377  			"use 'cipd selfupdate-roll -version-file %s' to generate it",
  3378  			base, strings.TrimSuffix(base, digestsSfx)).Tag(cipderr.Stale).Err()
  3379  	case err != nil:
  3380  		return nil, errors.Annotate(err, "error reading client digests file").Tag(cipderr.IO).Err()
  3381  	default:
  3382  		defer f.Close()
  3383  		return digests.ParseClientDigestsFile(f)
  3384  	}
  3385  }
  3386  
  3387  // assembleClientDigests produces the digests file by making backend RPCs.
  3388  func assembleClientDigests(ctx context.Context, c cipd.Client, version string) (*digests.ClientDigestsFile, []pinInfo, error) {
  3389  	if !strings.HasSuffix(cipd.ClientPackage, "/${platform}") {
  3390  		panic(fmt.Sprintf("client package template (%q) is expected to end with '${platform}'", cipd.ClientPackage))
  3391  	}
  3392  
  3393  	out := &digests.ClientDigestsFile{}
  3394  	mu := sync.Mutex{}
  3395  
  3396  	// Ask the backend to give us hashes of the client binary for all platforms.
  3397  	pins, err := performBatchOperation(ctx, batchOperation{
  3398  		client:        c,
  3399  		packagePrefix: strings.TrimSuffix(cipd.ClientPackage, "${platform}"),
  3400  		callback: func(pkg string) (common.Pin, error) {
  3401  			pin, err := c.ResolveVersion(ctx, pkg, version)
  3402  			if err != nil {
  3403  				return common.Pin{}, err
  3404  			}
  3405  			desc, err := c.DescribeClient(ctx, pin)
  3406  			if err != nil {
  3407  				return common.Pin{}, err
  3408  			}
  3409  			mu.Lock()
  3410  			defer mu.Unlock()
  3411  			plat := pkg[strings.LastIndex(pkg, "/")+1:]
  3412  			if err := out.AddClientRef(plat, desc.Digest); err != nil {
  3413  				return common.Pin{}, err
  3414  			}
  3415  			return pin, nil
  3416  		},
  3417  	})
  3418  	switch {
  3419  	case err != nil:
  3420  		return nil, pins, err
  3421  	case hasErrors(pins):
  3422  		return nil, pins, errors.Reason("failed to obtain the client binary digest for all platforms").Tag(cipderr.RPC).Err()
  3423  	}
  3424  
  3425  	out.Sort()
  3426  	return out, pins, nil
  3427  }
  3428  
  3429  ////////////////////////////////////////////////////////////////////////////////
  3430  // 'deployment-check' subcommand.
  3431  
  3432  func cmdCheckDeployment(params Parameters) *subcommands.Command {
  3433  	return &subcommands.Command{
  3434  		Advanced:  true,
  3435  		UsageLine: "deployment-check [options]",
  3436  		ShortDesc: "verifies all files that are supposed to be installed are present",
  3437  		LongDesc: "Compares CIPD package manifests stored in .cipd/* with what's on disk.\n\n" +
  3438  			"Useful when debugging issues with broken installations.",
  3439  		CommandRun: func() subcommands.CommandRun {
  3440  			c := &checkDeploymentRun{}
  3441  			c.registerBaseFlags()
  3442  			c.clientOptions.registerFlags(&c.Flags, params, withRootDir, withoutMaxThreads)
  3443  			return c
  3444  		},
  3445  	}
  3446  }
  3447  
  3448  type checkDeploymentRun struct {
  3449  	cipdSubcommand
  3450  	clientOptions
  3451  }
  3452  
  3453  func (c *checkDeploymentRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  3454  	if !c.checkArgs(args, 0, 0) {
  3455  		return 1
  3456  	}
  3457  	ctx := cli.GetContext(a, c, env)
  3458  	return c.done(checkDeployment(ctx, c.clientOptions))
  3459  }
  3460  
  3461  func checkDeployment(ctx context.Context, clientOpts clientOptions) (cipd.ActionMap, error) {
  3462  	client, err := clientOpts.makeCIPDClient(ctx)
  3463  	if err != nil {
  3464  		return nil, err
  3465  	}
  3466  	defer client.Close(ctx)
  3467  
  3468  	currentDeployment, err := client.FindDeployed(ctx)
  3469  	if err != nil {
  3470  		return nil, err
  3471  	}
  3472  
  3473  	actions, err := client.EnsurePackages(ctx, currentDeployment, &cipd.EnsureOptions{
  3474  		Paranoia: cipd.CheckIntegrity,
  3475  		DryRun:   true,
  3476  		Silent:   true,
  3477  	})
  3478  	if err != nil {
  3479  		return nil, err
  3480  	}
  3481  	actions.Log(ctx, true)
  3482  	if len(actions) != 0 {
  3483  		err = errors.Reason("the deployment needs a repair").Tag(cipderr.Stale).Err()
  3484  	}
  3485  	return actions, err
  3486  }
  3487  
  3488  ////////////////////////////////////////////////////////////////////////////////
  3489  // 'deployment-repair' subcommand.
  3490  
  3491  func cmdRepairDeployment(params Parameters) *subcommands.Command {
  3492  	return &subcommands.Command{
  3493  		Advanced:  true,
  3494  		UsageLine: "deployment-repair [options]",
  3495  		ShortDesc: "attempts to repair a deployment if it is broken",
  3496  		LongDesc:  "This is equivalent of running 'ensure' in paranoia.",
  3497  		CommandRun: func() subcommands.CommandRun {
  3498  			c := &repairDeploymentRun{}
  3499  			c.registerBaseFlags()
  3500  			c.clientOptions.registerFlags(&c.Flags, params, withRootDir, withMaxThreads)
  3501  			return c
  3502  		},
  3503  	}
  3504  }
  3505  
  3506  type repairDeploymentRun struct {
  3507  	cipdSubcommand
  3508  	clientOptions
  3509  }
  3510  
  3511  func (c *repairDeploymentRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
  3512  	if !c.checkArgs(args, 0, 0) {
  3513  		return 1
  3514  	}
  3515  	ctx := cli.GetContext(a, c, env)
  3516  	return c.done(repairDeployment(ctx, c.clientOptions))
  3517  }
  3518  
  3519  func repairDeployment(ctx context.Context, clientOpts clientOptions) (cipd.ActionMap, error) {
  3520  	client, err := clientOpts.makeCIPDClient(ctx)
  3521  	if err != nil {
  3522  		return nil, err
  3523  	}
  3524  	defer client.Close(ctx)
  3525  
  3526  	currentDeployment, err := client.FindDeployed(ctx)
  3527  	if err != nil {
  3528  		return nil, err
  3529  	}
  3530  
  3531  	return client.EnsurePackages(ctx, currentDeployment, &cipd.EnsureOptions{
  3532  		Paranoia: cipd.CheckIntegrity,
  3533  	})
  3534  }
  3535  
  3536  ////////////////////////////////////////////////////////////////////////////////
  3537  // Main.
  3538  
  3539  // GetApplication returns cli.Application.
  3540  //
  3541  // It can be used directly by subcommands.Run(...), or nested into another
  3542  // application.
  3543  func GetApplication(params Parameters) *cli.Application {
  3544  	return &cli.Application{
  3545  		Name:  "cipd",
  3546  		Title: "Chrome Infra Package Deployer (" + cipd.UserAgent + ")",
  3547  
  3548  		Context: func(ctx context.Context) context.Context {
  3549  			loggerConfig := gologger.LoggerConfig{
  3550  				Format: `[P%{pid} %{time:15:04:05.000} %{shortfile} %{level:.1s}] %{message}`,
  3551  				Out:    os.Stderr,
  3552  			}
  3553  			ctx, cancel := context.WithCancel(loggerConfig.Use(ctx))
  3554  			signals.HandleInterrupt(cancel)
  3555  			return ctx
  3556  		},
  3557  
  3558  		EnvVars: map[string]subcommands.EnvVarDefinition{
  3559  			cipd.EnvConfigFile: {
  3560  				Advanced: true,
  3561  				ShortDesc: fmt.Sprintf(
  3562  					"Path to a config file to load instead of the default %q. If `-`, just ignore the default config file.",
  3563  					cipd.DefaultConfigFilePath(),
  3564  				),
  3565  			},
  3566  			cipd.EnvHTTPUserAgentPrefix: {
  3567  				Advanced:  true,
  3568  				ShortDesc: "Optional http User-Agent prefix.",
  3569  			},
  3570  			cipd.EnvCacheDir: {
  3571  				ShortDesc: "Directory with shared instance and tags cache " +
  3572  					"(-cache-dir, if given, takes precedence).",
  3573  			},
  3574  			cipd.EnvMaxThreads: {
  3575  				Advanced: true,
  3576  				ShortDesc: "Number of worker threads for extracting packages. " +
  3577  					"If 0 or negative, uses CPU count. (-max-threads, if given and not 0, takes precedence.)",
  3578  			},
  3579  			cipd.EnvParallelDownloads: {
  3580  				Advanced: true,
  3581  				ShortDesc: fmt.Sprintf("How many packages are allowed to be fetched concurrently. "+
  3582  					"If <=1, packages will be fetched sequentially. Default is %d.", cipd.DefaultParallelDownloads),
  3583  			},
  3584  			cipd.EnvAdmissionPlugin: {
  3585  				Advanced:  true,
  3586  				ShortDesc: "JSON-encoded list with a command line of a deployment admission plugin.",
  3587  			},
  3588  			cipd.EnvCIPDServiceURL: {
  3589  				Advanced:  true,
  3590  				ShortDesc: "Override CIPD service URL.",
  3591  			},
  3592  			envSimpleTerminalUI: {
  3593  				Advanced:  true,
  3594  				ShortDesc: "If set disables the fancy terminal UI with progress bars in favor of a simpler one that just logs to stderr.",
  3595  			},
  3596  		},
  3597  
  3598  		Commands: []*subcommands.Command{
  3599  			subcommands.CmdHelp,
  3600  			versioncli.CmdVersion(cipd.UserAgent),
  3601  
  3602  			// Authentication related commands.
  3603  			{}, // These are spacers so that the commands appear in groups.
  3604  			authcli.SubcommandInfo(params.DefaultAuthOptions, "auth-info", true),
  3605  			authcli.SubcommandLogin(params.DefaultAuthOptions, "auth-login", false),
  3606  			authcli.SubcommandLogout(params.DefaultAuthOptions, "auth-logout", false),
  3607  
  3608  			// High level read commands.
  3609  			{},
  3610  			cmdListPackages(params),
  3611  			cmdSearch(params),
  3612  			cmdResolve(params),
  3613  			cmdDescribe(params),
  3614  			cmdInstances(params),
  3615  
  3616  			// High level remote write commands.
  3617  			{},
  3618  			cmdCreate(params),
  3619  			cmdAttach(params),
  3620  			cmdSetRef(params),
  3621  			cmdSetTag(params),
  3622  			cmdSetMetadata(params),
  3623  
  3624  			// High level local write commands.
  3625  			{},
  3626  			cmdEnsure(params),
  3627  			cmdExport(params),
  3628  			cmdSelfUpdate(params),
  3629  			cmdSelfUpdateRoll(params),
  3630  
  3631  			// Advanced ensure file operations.
  3632  			{Advanced: true},
  3633  			cmdEnsureFileVerify(params),
  3634  			cmdEnsureFileResolve(params),
  3635  
  3636  			// User friendly subcommands that operates within a site root. Implemented
  3637  			// in friendly.go. These are advanced because they're half-baked.
  3638  			{Advanced: true},
  3639  			cmdInit(params),
  3640  			cmdInstall(params),
  3641  			cmdInstalled(params),
  3642  
  3643  			// ACLs.
  3644  			{Advanced: true},
  3645  			cmdListACL(params),
  3646  			cmdEditACL(params),
  3647  			cmdCheckACL(params),
  3648  
  3649  			// Low level pkg-* commands.
  3650  			{Advanced: true},
  3651  			cmdBuild(),
  3652  			cmdDeploy(),
  3653  			cmdFetch(params),
  3654  			cmdInspect(),
  3655  			cmdRegister(params),
  3656  
  3657  			// Low level deployment-* commands.
  3658  			{Advanced: true},
  3659  			cmdCheckDeployment(params),
  3660  			cmdRepairDeployment(params),
  3661  
  3662  			// Low level misc commands.
  3663  			{Advanced: true},
  3664  			cmdExpandPackageName(params),
  3665  			cmdPuppetCheckUpdates(params),
  3666  		},
  3667  	}
  3668  }
  3669  
  3670  // Main runs the CIPD CLI.
  3671  func Main(params Parameters, args []string) int {
  3672  	return subcommands.Run(GetApplication(params), fixflagpos.FixSubcommands(args))
  3673  }