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, ®isterOpts{ 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 := ®isterRun{} 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 }