| // Copyright 2014 The LUCI Authors. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package cli |
| |
| import ( |
| "bytes" |
| "context" |
| "encoding/json" |
| "flag" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "net/http" |
| "os" |
| "path/filepath" |
| "sort" |
| "strings" |
| "sync" |
| "time" |
| |
| "github.com/maruel/subcommands" |
| |
| "go.chromium.org/luci/auth" |
| "go.chromium.org/luci/client/versioncli" |
| "go.chromium.org/luci/common/cli" |
| "go.chromium.org/luci/common/data/stringset" |
| "go.chromium.org/luci/common/errors" |
| "go.chromium.org/luci/common/flag/fixflagpos" |
| "go.chromium.org/luci/common/logging" |
| "go.chromium.org/luci/common/logging/gologger" |
| "go.chromium.org/luci/common/retry/transient" |
| "go.chromium.org/luci/common/system/signals" |
| "go.chromium.org/luci/common/system/terminal" |
| |
| "go.chromium.org/luci/auth/client/authcli" |
| |
| api "go.chromium.org/luci/cipd/api/cipd/v1" |
| "go.chromium.org/luci/cipd/client/cipd" |
| "go.chromium.org/luci/cipd/client/cipd/builder" |
| "go.chromium.org/luci/cipd/client/cipd/deployer" |
| "go.chromium.org/luci/cipd/client/cipd/digests" |
| "go.chromium.org/luci/cipd/client/cipd/ensure" |
| "go.chromium.org/luci/cipd/client/cipd/fs" |
| "go.chromium.org/luci/cipd/client/cipd/pkg" |
| "go.chromium.org/luci/cipd/client/cipd/plugin/host" |
| "go.chromium.org/luci/cipd/client/cipd/reader" |
| "go.chromium.org/luci/cipd/client/cipd/template" |
| "go.chromium.org/luci/cipd/client/cipd/ui" |
| "go.chromium.org/luci/cipd/common" |
| ) |
| |
| // TODO(vadimsh): Add some tests. |
| |
| // This is a killswitch that disables the fancy terminal progress bar UI in case |
| // it has some fatal bugs or a user has aversion towards it. |
| // |
| // Note that cipd.Client doesn't know anything about the UI implementation and |
| // thus this env var is defined here rather than in cipd/client.go like other |
| // env vars. |
| const envSimpleTerminalUI = "CIPD_SIMPLE_TERMINAL_UI" |
| |
| func expandTemplate(tmpl string) (pkg string, err error) { |
| pkg, err = template.DefaultExpander().Expand(tmpl) |
| if err != nil { |
| err = commandLineError{err} |
| } |
| return |
| } |
| |
| // Parameters carry default configuration values for a CIPD CLI client. |
| type Parameters struct { |
| // DefaultAuthOptions provide default values for authentication related |
| // options (most notably SecretsDir: a directory with token cache). |
| DefaultAuthOptions auth.Options |
| |
| // ServiceURL is a backend URL to use by default. |
| ServiceURL string |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Common subcommand functions. |
| |
| // pinInfo contains information about single package pin inside some site root, |
| // or an error related to it. It is passed through channels when running batch |
| // operations and dumped to JSON results file in doneWithPins. |
| type pinInfo struct { |
| // Pkg is package name. Always set. |
| Pkg string `json:"package"` |
| // Pin is not nil if pin related operation succeeded. It contains instanceID. |
| Pin *common.Pin `json:"pin,omitempty"` |
| // Platform is set by 'ensure-file-verify' to a platform for this pin. |
| Platform string `json:"platform,omitempty"` |
| // Tracking is what ref is being tracked by that package in the site root. |
| Tracking string `json:"tracking,omitempty"` |
| // Err is not empty if pin related operation failed. Pin is nil in that case. |
| Err string `json:"error,omitempty"` |
| } |
| |
| type instanceInfoWithRefs struct { |
| cipd.InstanceInfo |
| Refs []string `json:"refs,omitempty"` |
| } |
| |
| // instancesOutput defines JSON format of 'cipd instances' output. |
| type instancesOutput struct { |
| Instances []instanceInfoWithRefs `json:"instances"` |
| } |
| |
| // cipdSubcommand is a base of all CIPD subcommands. It defines some common |
| // flags, such as logging and JSON output parameters. |
| type cipdSubcommand struct { |
| subcommands.CommandRunBase |
| |
| jsonOutput string |
| logConfig logging.Config |
| |
| // TODO(dnj): Remove "verbose" flag once all current invocations of it are |
| // cleaned up and rolled out, as it is now deprecated in favor of "logConfig". |
| verbose bool |
| } |
| |
| // ModifyContext implements cli.ContextModificator. |
| func (c *cipdSubcommand) ModifyContext(ctx context.Context) context.Context { |
| if c.verbose { |
| ctx = logging.SetLevel(ctx, logging.Debug) |
| } else { |
| ctx = c.logConfig.Set(ctx) |
| } |
| |
| // Note: cli.Getenv is not effective here yet since the context is not fully |
| // initialized. |
| useSimpleUI := os.Getenv(envSimpleTerminalUI) == "1" |
| |
| // If writing to a real terminal (rather than redirecting to a file) and not |
| // running at a non-default logging level, use a fancy UI with progress bars. |
| // It is more human readable, but doesn't preserve details of all operations |
| // in the terminal output. |
| if !useSimpleUI && logging.GetLevel(ctx) == logging.Info && terminal.IsTerminal(int(os.Stderr.Fd())) { |
| ctx = ui.SetImplementation(ctx, &ui.FancyImplementation{Out: os.Stderr}) |
| } |
| |
| return ctx |
| } |
| |
| // registerBaseFlags registers common flags used by all subcommands. |
| func (c *cipdSubcommand) registerBaseFlags() { |
| // Minimum default logging level is Info. This accommodates subcommands that |
| // don't explicitly set the log level, resulting in the zero value (Debug). |
| if c.logConfig.Level < logging.Info { |
| c.logConfig.Level = logging.Info |
| } |
| |
| c.Flags.StringVar(&c.jsonOutput, "json-output", "", "A `path` to write operation results to.") |
| c.Flags.BoolVar(&c.verbose, "verbose", false, "Enable more logging (deprecated, use -log-level=debug).") |
| c.logConfig.AddFlags(&c.Flags) |
| } |
| |
| // checkArgs checks command line args. |
| // |
| // It ensures all required positional and flag-like parameters are set. |
| // Returns true if they are, or false (and prints to stderr) if not. |
| func (c *cipdSubcommand) checkArgs(args []string, minPosCount, maxPosCount int) bool { |
| // Check number of expected positional arguments. |
| if maxPosCount == 0 && len(args) != 0 { |
| c.printError(makeCLIError("unexpected arguments %v", args)) |
| return false |
| } |
| if len(args) < minPosCount || (maxPosCount >= 0 && len(args) > maxPosCount) { |
| var err error |
| if minPosCount == maxPosCount { |
| err = makeCLIError("expecting %d positional argument, got %d instead", minPosCount, len(args)) |
| } else { |
| if maxPosCount >= 0 { |
| err = makeCLIError( |
| "expecting from %d to %d positional arguments, got %d instead", |
| minPosCount, maxPosCount, len(args)) |
| } else { |
| err = makeCLIError( |
| "expecting at least %d positional arguments, got %d instead", |
| minPosCount, len(args)) |
| } |
| } |
| c.printError(err) |
| return false |
| } |
| |
| // Check required unset flags. |
| unset := []*flag.Flag{} |
| c.Flags.VisitAll(func(f *flag.Flag) { |
| if strings.HasPrefix(f.DefValue, "<") && f.Value.String() == f.DefValue { |
| unset = append(unset, f) |
| } |
| }) |
| if len(unset) != 0 { |
| missing := make([]string, len(unset)) |
| for i, f := range unset { |
| missing[i] = f.Name |
| } |
| c.printError(makeCLIError("missing required flags: %v", missing)) |
| return false |
| } |
| |
| return true |
| } |
| |
| // printError prints error to stderr (recognizing commandLineError). |
| func (c *cipdSubcommand) printError(err error) { |
| if _, ok := err.(commandLineError); ok { |
| fmt.Fprintf(os.Stderr, "Bad command line: %s.\n\n", err) |
| c.Flags.Usage() |
| return |
| } |
| |
| if merr, _ := err.(errors.MultiError); len(merr) != 0 { |
| fmt.Fprintln(os.Stderr, "Errors:") |
| for _, err := range merr { |
| fmt.Fprintf(os.Stderr, " %s\n", err) |
| } |
| return |
| } |
| |
| fmt.Fprintf(os.Stderr, "Error: %s.\n", err) |
| } |
| |
| // writeJSONOutput writes result to JSON output file. It returns original error |
| // if it is non-nil. |
| func (c *cipdSubcommand) writeJSONOutput(result interface{}, err error) error { |
| // -json-output flag wasn't specified. |
| if c.jsonOutput == "" { |
| return err |
| } |
| |
| // Prepare the body of the output file. |
| var body struct { |
| Error string `json:"error,omitempty"` |
| Result interface{} `json:"result,omitempty"` |
| } |
| if err != nil { |
| body.Error = err.Error() |
| } |
| body.Result = result |
| out, e := json.MarshalIndent(&body, "", " ") |
| if e != nil { |
| if err == nil { |
| err = e |
| } else { |
| fmt.Fprintf(os.Stderr, "Failed to serialize JSON output: %s\n", e) |
| } |
| return err |
| } |
| |
| e = ioutil.WriteFile(c.jsonOutput, out, 0666) |
| if e != nil { |
| if err == nil { |
| err = e |
| } else { |
| fmt.Fprintf(os.Stderr, "Failed write JSON output to %s: %s\n", c.jsonOutput, e) |
| } |
| return err |
| } |
| |
| return err |
| } |
| |
| // done is called as a last step of processing a subcommand. It dumps command |
| // result (or error) to JSON output file, prints error message and generates |
| // process exit code. |
| func (c *cipdSubcommand) done(result interface{}, err error) int { |
| err = c.writeJSONOutput(result, err) |
| if err != nil { |
| c.printError(err) |
| return 1 |
| } |
| return 0 |
| } |
| |
| // doneWithPins is a handy shortcut that prints a pinInfo slice and |
| // deduces process exit code based on presence of errors there. |
| // |
| // This just calls through to doneWithPinMap. |
| func (c *cipdSubcommand) doneWithPins(pins []pinInfo, err error) int { |
| return c.doneWithPinMap(map[string][]pinInfo{"": pins}, err) |
| } |
| |
| // doneWithPinMap is a handy shortcut that prints the subdir->pinInfo map and |
| // deduces process exit code based on presence of errors there. |
| func (c *cipdSubcommand) doneWithPinMap(pins map[string][]pinInfo, err error) int { |
| if len(pins) == 0 { |
| if err == nil { // this error will be printed in c.done(...) |
| fmt.Println("No packages.") |
| } |
| } else { |
| printPinsAndError(pins) |
| } |
| if ret := c.done(pins, err); ret != 0 { |
| return ret |
| } |
| for _, infos := range pins { |
| if hasErrors(infos) { |
| return 1 |
| } |
| } |
| return 0 |
| } |
| |
| // commandLineError is used to tag errors related to CLI. |
| type commandLineError struct { |
| error |
| } |
| |
| // makeCLIError returns new commandLineError. |
| func makeCLIError(msg string, args ...interface{}) error { |
| return commandLineError{fmt.Errorf(msg, args...)} |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // maxThreadsOption mixin. |
| |
| type maxThreadsOption struct { |
| maxThreads int |
| } |
| |
| func (opts *maxThreadsOption) registerFlags(f *flag.FlagSet) { |
| f.IntVar(&opts.maxThreads, "max-threads", 0, |
| "Number of worker threads for extracting packages. If 0 or negative, uses CPU count.") |
| } |
| |
| // loadMaxThreads should only be used by subcommands that do not instantiate |
| // the full CIPD client. |
| func (opts *maxThreadsOption) loadMaxThreads(ctx context.Context) (int, error) { |
| clientOpts := cipd.ClientOptions{MaxThreads: opts.maxThreads} |
| if err := clientOpts.LoadFromEnv(cli.MakeGetEnv(ctx)); err != nil { |
| return 0, err |
| } |
| return clientOpts.MaxThreads, nil |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // clientOptions mixin. |
| |
| type rootDirFlag bool |
| |
| const ( |
| withRootDir rootDirFlag = true |
| withoutRootDir rootDirFlag = false |
| ) |
| |
| type maxThreadsFlag bool |
| |
| const ( |
| withMaxThreads maxThreadsFlag = true |
| withoutMaxThreads maxThreadsFlag = false |
| ) |
| |
| // clientOptions defines command line arguments related to CIPD client creation. |
| // Subcommands that need a CIPD client embed it. |
| type clientOptions struct { |
| hardcoded Parameters // whatever was passed to registerFlags(...) |
| |
| serviceURL string // also mutated by loadEnsureFile |
| cacheDir string |
| maxThreads maxThreadsOption |
| rootDir string // used only if registerFlags got withRootDir arg |
| versions ensure.VersionsFile // mutated by loadEnsureFile |
| |
| authFlags authcli.Flags |
| } |
| |
| func (opts *clientOptions) resolvedServiceURL(ctx context.Context) string { |
| if opts.serviceURL != "" { |
| return opts.serviceURL |
| } |
| if v := cli.Getenv(ctx, cipd.EnvCIPDServiceURL); v != "" { |
| return v |
| } |
| return opts.hardcoded.ServiceURL |
| } |
| |
| func (opts *clientOptions) registerFlags(f *flag.FlagSet, params Parameters, rootDir rootDirFlag, maxThreads maxThreadsFlag) { |
| opts.hardcoded = params |
| |
| f.StringVar(&opts.serviceURL, "service-url", "", |
| fmt.Sprintf(`Backend URL. If provided via an "ensure" file, the URL in the file takes precedence. `+ |
| `(default %s)`, params.ServiceURL)) |
| f.StringVar(&opts.cacheDir, "cache-dir", "", |
| fmt.Sprintf("Directory for the shared cache (can also be set by %s env var).", cipd.EnvCacheDir)) |
| |
| if rootDir { |
| f.StringVar(&opts.rootDir, "root", "<path>", "Path to an installation site root directory.") |
| } |
| if maxThreads { |
| opts.maxThreads.registerFlags(f) |
| } |
| |
| opts.authFlags.Register(f, params.DefaultAuthOptions) |
| } |
| |
| func (opts *clientOptions) toCIPDClientOpts(ctx context.Context) (cipd.ClientOptions, error) { |
| authOpts, err := opts.authFlags.Options() |
| if err != nil { |
| return cipd.ClientOptions{}, err |
| } |
| client, err := auth.NewAuthenticator(ctx, auth.OptionalLogin, authOpts).Client() |
| if err != nil { |
| return cipd.ClientOptions{}, err |
| } |
| |
| realOpts := cipd.ClientOptions{ |
| Root: opts.rootDir, |
| CacheDir: opts.cacheDir, |
| Versions: opts.versions, |
| AuthenticatedClient: client, |
| MaxThreads: opts.maxThreads.maxThreads, |
| AnonymousClient: http.DefaultClient, |
| PluginHost: &host.Host{PluginsContext: ctx}, |
| LoginInstructions: "run `cipd auth-login` to login or relogin", |
| } |
| if err := realOpts.LoadFromEnv(cli.MakeGetEnv(ctx)); err != nil { |
| return cipd.ClientOptions{}, err |
| } |
| realOpts.ServiceURL = opts.resolvedServiceURL(ctx) |
| return realOpts, nil |
| } |
| |
| func (opts *clientOptions) makeCIPDClient(ctx context.Context) (cipd.Client, error) { |
| cipdOpts, err := opts.toCIPDClientOpts(ctx) |
| if err != nil { |
| return nil, err |
| } |
| return cipd.NewClient(cipdOpts) |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // inputOptions mixin. |
| |
| // packageVars holds array of '-pkg-var' command line options. |
| type packageVars map[string]string |
| |
| func (vars *packageVars) String() string { |
| return "key:value" |
| } |
| |
| // Set is called by 'flag' package when parsing command line options. |
| func (vars *packageVars) Set(value string) error { |
| // <key>:<value> pair. |
| chunks := strings.SplitN(value, ":", 2) |
| if len(chunks) != 2 { |
| return makeCLIError("expecting <key>:<value> pair, got %q", value) |
| } |
| (*vars)[chunks[0]] = chunks[1] |
| return nil |
| } |
| |
| // inputOptions defines command line arguments that specify where to get data |
| // for a new package and how to build it. |
| // |
| // Subcommands that build packages embed it. |
| type inputOptions struct { |
| // Path to *.yaml file with package definition. |
| packageDef string |
| vars packageVars |
| |
| // Alternative to 'pkg-def'. |
| packageName string |
| inputDir string |
| installMode pkg.InstallMode |
| preserveModTime bool |
| preserveWritable bool |
| |
| // Deflate compression level (if [1-9]) or 0 to disable compression. |
| // |
| // Default is 5. |
| compressionLevel int |
| } |
| |
| func (opts *inputOptions) registerFlags(f *flag.FlagSet) { |
| // Set default vars (e.g. ${platform}). They may be overridden through flags. |
| defVars := template.DefaultExpander() |
| opts.vars = make(packageVars, len(defVars)) |
| for k, v := range defVars { |
| opts.vars[k] = v |
| } |
| |
| // Interface to accept package definition file. |
| f.StringVar(&opts.packageDef, "pkg-def", "", "A *.yaml file `path` that defines what to put into the package.") |
| f.Var(&opts.vars, "pkg-var", "A `key:value` with a variable accessible from package definition file (can be used multiple times).") |
| |
| // Interface to accept a single directory (alternative to -pkg-def). |
| f.StringVar(&opts.packageName, "name", "", "Package `name` (unused with -pkg-def).") |
| f.StringVar(&opts.inputDir, "in", "", "A `path` to a directory with files to package (unused with -pkg-def).") |
| f.Var(&opts.installMode, "install-mode", |
| "How the package should be installed: \"copy\" or \"symlink\" (unused with -pkg-def).") |
| f.BoolVar(&opts.preserveModTime, "preserve-mtime", false, |
| "Preserve file's modification time (unused with -pkg-def).") |
| f.BoolVar(&opts.preserveWritable, "preserve-writable", false, |
| "Preserve file's writable permission bit (unused with -pkg-def).") |
| |
| // Options for the builder. |
| f.IntVar(&opts.compressionLevel, "compression-level", 5, |
| "Deflate compression level [0-9]: 0 - disable, 1 - best speed, 9 - best compression.") |
| } |
| |
| // prepareInput processes inputOptions by collecting all files to be added to |
| // a package and populating builder.Options. Caller is still responsible to fill |
| // out Output field of Options. |
| func (opts *inputOptions) prepareInput() (builder.Options, error) { |
| empty := builder.Options{} |
| |
| if opts.compressionLevel < 0 || opts.compressionLevel > 9 { |
| return empty, makeCLIError("invalid -compression-level: must be in [0-9] set") |
| } |
| |
| // Handle -name and -in if defined. Do not allow -pkg-def in that case, since |
| // it provides same information as -name and -in. Note that -pkg-var are |
| // ignored, even if defined. There's nothing to apply them to. |
| if opts.inputDir != "" { |
| if opts.packageName == "" { |
| return empty, makeCLIError("missing required flag: -name") |
| } |
| if opts.packageDef != "" { |
| return empty, makeCLIError("-pkg-def and -in can not be used together") |
| } |
| |
| packageName, err := expandTemplate(opts.packageName) |
| if err != nil { |
| return empty, err |
| } |
| |
| // Simply enumerate files in the directory. |
| files, err := fs.ScanFileSystem(opts.inputDir, opts.inputDir, nil, fs.ScanOptions{ |
| PreserveModTime: opts.preserveModTime, |
| PreserveWritable: opts.preserveWritable, |
| }) |
| if err != nil { |
| return empty, err |
| } |
| return builder.Options{ |
| Input: files, |
| PackageName: packageName, |
| InstallMode: opts.installMode, |
| CompressionLevel: opts.compressionLevel, |
| }, nil |
| } |
| |
| // Handle -pkg-def case. -in is "" (already checked), reject -name. |
| if opts.packageDef != "" { |
| if opts.packageName != "" { |
| return empty, makeCLIError("-pkg-def and -name can not be used together") |
| } |
| if opts.installMode != "" { |
| return empty, makeCLIError("-install-mode is ignored if -pkg-def is used") |
| } |
| if opts.preserveModTime { |
| return empty, makeCLIError("-preserve-mtime is ignored if -pkg-def is used") |
| } |
| if opts.preserveWritable { |
| return empty, makeCLIError("-preserve-writable is ignored if -pkg-def is used") |
| } |
| |
| // Parse the file, perform variable substitution. |
| f, err := os.Open(opts.packageDef) |
| if err != nil { |
| return empty, err |
| } |
| defer f.Close() |
| pkgDef, err := builder.LoadPackageDef(f, opts.vars) |
| if err != nil { |
| return empty, err |
| } |
| |
| // Scan the file system. Package definition may use path relative to the |
| // package definition file itself, so pass its location. |
| fmt.Println("Enumerating files to zip...") |
| files, err := pkgDef.FindFiles(filepath.Dir(opts.packageDef)) |
| if err != nil { |
| return empty, err |
| } |
| return builder.Options{ |
| Input: files, |
| PackageName: pkgDef.Package, |
| VersionFile: pkgDef.VersionFile(), |
| InstallMode: pkgDef.InstallMode, |
| CompressionLevel: opts.compressionLevel, |
| }, nil |
| } |
| |
| // All command line options are missing. |
| return empty, makeCLIError("-pkg-def or -name/-in are required") |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // refsOptions mixin. |
| |
| // refList holds an array of '-ref' command line options. |
| type refList []string |
| |
| func (refs *refList) String() string { |
| return "ref" |
| } |
| |
| // Set is called by 'flag' package when parsing command line options. |
| func (refs *refList) Set(value string) error { |
| err := common.ValidatePackageRef(value) |
| if err != nil { |
| return commandLineError{err} |
| } |
| *refs = append(*refs, value) |
| return nil |
| } |
| |
| // refsOptions defines command line arguments for commands that accept a set |
| // of refs. |
| type refsOptions struct { |
| refs refList |
| } |
| |
| func (opts *refsOptions) registerFlags(f *flag.FlagSet) { |
| f.Var(&opts.refs, "ref", "A `ref` to point to the package instance (can be used multiple times).") |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // tagsOptions mixin. |
| |
| // tagList holds an array of '-tag' command line options. |
| type tagList []string |
| |
| func (tags *tagList) String() string { |
| return "key:value" |
| } |
| |
| // Set is called by 'flag' package when parsing command line options. |
| func (tags *tagList) Set(value string) error { |
| err := common.ValidateInstanceTag(value) |
| if err != nil { |
| return commandLineError{err} |
| } |
| *tags = append(*tags, value) |
| return nil |
| } |
| |
| // tagsOptions defines command line arguments for commands that accept a set |
| // of tags. |
| type tagsOptions struct { |
| tags tagList |
| } |
| |
| func (opts *tagsOptions) registerFlags(f *flag.FlagSet) { |
| f.Var(&opts.tags, "tag", "A `key:value` tag to attach to the package instance (can be used multiple times).") |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // metadataOptions mixin. |
| |
| type metadataFlagValue struct { |
| key string |
| value string // either a literal value or a path to read it from |
| contentType string |
| } |
| |
| type metadataList struct { |
| entries []metadataFlagValue |
| valueKind string // "value" or "path" |
| } |
| |
| func (md *metadataList) String() string { |
| return "key:" + md.valueKind |
| } |
| |
| // Set is called by 'flag' package when parsing command line options. |
| func (md *metadataList) Set(value string) error { |
| // Should have form key_with_possible_content_type:value. |
| chunks := strings.SplitN(value, ":", 2) |
| if len(chunks) != 2 { |
| return md.badFormatError() |
| } |
| |
| // Extract content-type from within trailing '(...)', if present. |
| key, contentType, value := chunks[0], "", chunks[1] |
| switch l, r := strings.Index(key, "("), strings.LastIndex(key, ")"); { |
| case l == -1 && r == -1: |
| // no content type, this is fine |
| case l != -1 && r != -1 && l < r: |
| // The closing ')' should be the last character. |
| if !strings.HasSuffix(key, ")") { |
| return md.badFormatError() |
| } |
| key, contentType = key[:l], key[l+1:r] |
| default: |
| return md.badFormatError() |
| } |
| |
| // Validate everything we can. |
| if err := common.ValidateInstanceMetadataKey(key); err != nil { |
| return commandLineError{err} |
| } |
| if err := common.ValidateContentType(contentType); err != nil { |
| return commandLineError{err} |
| } |
| if md.valueKind == "value" { |
| if err := common.ValidateInstanceMetadataLen(len(value)); err != nil { |
| return commandLineError{err} |
| } |
| } |
| |
| md.entries = append(md.entries, metadataFlagValue{ |
| key: key, |
| value: value, |
| contentType: contentType, |
| }) |
| return nil |
| } |
| |
| func (md *metadataList) badFormatError() error { |
| return makeCLIError("should have form key:%s or key(content-type):%s", md.valueKind, md.valueKind) |
| } |
| |
| // metadataOptions defines command line arguments for commands that accept a set |
| // of metadata entries. |
| type metadataOptions struct { |
| metadata metadataList |
| metadataFromFile metadataList |
| } |
| |
| func (opts *metadataOptions) registerFlags(f *flag.FlagSet) { |
| opts.metadata.valueKind = "value" |
| f.Var(&opts.metadata, "metadata", |
| "A metadata entry (`key:value` or key(content-type):value) to attach to the package instance (can be used multiple times).") |
| |
| opts.metadataFromFile.valueKind = "path" |
| f.Var(&opts.metadataFromFile, "metadata-from-file", |
| "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.") |
| } |
| |
| func (opts *metadataOptions) load(ctx context.Context) ([]cipd.Metadata, error) { |
| out := make([]cipd.Metadata, 0, len(opts.metadata.entries)+len(opts.metadataFromFile.entries)) |
| |
| // Convert -metadata to cipd.Metadata entries. |
| for _, md := range opts.metadata.entries { |
| entry := cipd.Metadata{ |
| Key: md.key, |
| Value: []byte(md.value), |
| ContentType: md.contentType, |
| } |
| // The default content type for -metadata is text/plain (since values are |
| // supplied directly via the command line). |
| if entry.ContentType == "" { |
| entry.ContentType = "text/plain" |
| } |
| out = append(out, entry) |
| } |
| |
| // Load -metadata-from-file entries. At most one `-metadata-from-file key:-` |
| // is allowed, we have only one stdin. |
| keyWithStdin := false |
| for _, md := range opts.metadataFromFile.entries { |
| if md.value == "-" { |
| if keyWithStdin { |
| return nil, makeCLIError("at most one -metadata-from-file can use \"-\" as a value") |
| } |
| keyWithStdin = true |
| } |
| entry := cipd.Metadata{ |
| Key: md.key, |
| ContentType: md.contentType, |
| } |
| var err error |
| if entry.Value, err = loadMetadataFromFile(ctx, md.key, md.value); err != nil { |
| return nil, makeCLIError("when loading metadata from %q: %s", md.value, err) |
| } |
| // Guess the content type from the file extension and its body. |
| if entry.ContentType == "" { |
| entry.ContentType = guessMetadataContentType(md.value, entry.Value) |
| } |
| out = append(out, entry) |
| } |
| |
| return out, nil |
| } |
| |
| func loadMetadataFromFile(ctx context.Context, key, path string) ([]byte, error) { |
| var file *os.File |
| if path == "-" { |
| logging.Infof(ctx, "Reading metadata %q from the stdin...", key) |
| file = os.Stdin |
| } else { |
| logging.Infof(ctx, "Reading metadata %q from %q...", key, path) |
| var err error |
| file, err = os.Open(path) |
| if err != nil { |
| return nil, err |
| } |
| defer file.Close() |
| } |
| // Read at most MetadataMaxLen plus one more byte to detect true EOF. |
| buf := bytes.Buffer{} |
| switch _, err := io.CopyN(&buf, file, common.MetadataMaxLen+1); { |
| case err == nil: |
| // Successfully read more than needed => the file size is too large. |
| return nil, fmt.Errorf("the metadata value is too long, should be <=%d bytes", common.MetadataMaxLen) |
| case err != io.EOF: |
| // Failed with some unexpected read error. |
| return nil, err |
| default: |
| return buf.Bytes(), nil |
| } |
| } |
| |
| func guessMetadataContentType(path string, val []byte) string { |
| switch strings.ToLower(filepath.Ext(path)) { |
| case ".json": |
| return "application/json" |
| case ".jwt": |
| return "application/jwt" |
| default: |
| return http.DetectContentType(val) |
| } |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // uploadOptions mixin. |
| |
| // uploadOptions defines command line options for commands that upload packages. |
| type uploadOptions struct { |
| verificationTimeout time.Duration |
| } |
| |
| func (opts *uploadOptions) registerFlags(f *flag.FlagSet) { |
| f.DurationVar( |
| &opts.verificationTimeout, "verification-timeout", |
| cipd.CASFinalizationTimeout, "Maximum time to wait for backend-side package hash verification.") |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // hashOptions mixin. |
| |
| // allAlgos is used in the flag help text, it is "sha256, sha1, ...". |
| var allAlgos string |
| |
| func init() { |
| algos := make([]string, 0, len(api.HashAlgo_name)-1) |
| for i := len(api.HashAlgo_name) - 1; i > 0; i-- { |
| algos = append(algos, strings.ToLower(api.HashAlgo_name[int32(i)])) |
| } |
| allAlgos = strings.Join(algos, ", ") |
| } |
| |
| // hashAlgoFlag adapts api.HashAlgo to flag.Value interface. |
| type hashAlgoFlag api.HashAlgo |
| |
| // String is called by 'flag' package when displaying default value of a flag. |
| func (ha *hashAlgoFlag) String() string { |
| return strings.ToLower(api.HashAlgo(*ha).String()) |
| } |
| |
| // Set is called by 'flag' package when parsing command line options. |
| func (ha *hashAlgoFlag) Set(value string) error { |
| val := api.HashAlgo_value[strings.ToUpper(value)] |
| if val == 0 { |
| return fmt.Errorf("unknown hash algo %q, should be one of: %s", value, allAlgos) |
| } |
| *ha = hashAlgoFlag(val) |
| return nil |
| } |
| |
| // hashOptions defines -hash-algo flag that specifies hash algo to use for |
| // constructing instance IDs. |
| // |
| // Default value is given by common.DefaultHashAlgo. |
| // |
| // Not all algos may be accepted by the server. |
| type hashOptions struct { |
| algo hashAlgoFlag |
| } |
| |
| func (opts *hashOptions) registerFlags(f *flag.FlagSet) { |
| opts.algo = hashAlgoFlag(common.DefaultHashAlgo) |
| f.Var(&opts.algo, "hash-algo", fmt.Sprintf("Algorithm to use for deriving package instance ID, one of: %s", allAlgos)) |
| } |
| |
| func (opts *hashOptions) hashAlgo() api.HashAlgo { |
| return api.HashAlgo(opts.algo) |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // ensureFileOptions mixin. |
| |
| type legacyListFlag bool |
| |
| const ( |
| withLegacyListFlag legacyListFlag = true |
| withoutLegacyListFlag legacyListFlag = false |
| ) |
| |
| type ensureOutFlag bool |
| |
| const ( |
| withEnsureOutFlag ensureOutFlag = true |
| withoutEnsureOutFlag ensureOutFlag = false |
| ) |
| |
| type verifyingEnsureFile bool |
| |
| const ( |
| requireVerifyPlatforms verifyingEnsureFile = true |
| ignoreVerifyPlatforms verifyingEnsureFile = false |
| ) |
| |
| type versionFileOpt bool |
| |
| const ( |
| parseVersionsFile versionFileOpt = true |
| ignoreVersionsFile versionFileOpt = false |
| ) |
| |
| // ensureFileOptions defines -ensure-file flag that specifies a location of the |
| // "ensure file", which is a manifest that describes what should be installed |
| // into a site root. |
| type ensureFileOptions struct { |
| ensureFile string |
| ensureFileOut string // used only if registerFlags got withEnsureOutFlag arg |
| } |
| |
| func (opts *ensureFileOptions) registerFlags(f *flag.FlagSet, out ensureOutFlag, list legacyListFlag) { |
| f.StringVar(&opts.ensureFile, "ensure-file", "<path>", |
| `An "ensure" file. See syntax described here: `+ |
| `https://godoc.org/go.chromium.org/luci/cipd/client/cipd/ensure.`+ |
| ` Providing '-' will read from stdin.`) |
| if out { |
| f.StringVar(&opts.ensureFileOut, "ensure-file-output", "", |
| `A path to write an "ensure" file which is the fully-resolved version `+ |
| `of the input ensure file for the current platform. This output will `+ |
| `not contain any ${params} or $Settings other than $ServiceURL.`) |
| } |
| if list { |
| f.StringVar(&opts.ensureFile, "list", "<path>", "(DEPRECATED) A synonym for -ensure-file.") |
| } |
| } |
| |
| // loadEnsureFile parses the ensure file and mutates clientOpts to point to a |
| // service URL specified in the ensure file. |
| func (opts *ensureFileOptions) loadEnsureFile(ctx context.Context, clientOpts *clientOptions, verifying verifyingEnsureFile, parseVers versionFileOpt) (*ensure.File, error) { |
| parsedFile, err := ensure.LoadEnsureFile(opts.ensureFile) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Prefer the ServiceURL from the file (if set), and log a warning if the user |
| // provided one on the command line that doesn't match the one in the file. |
| if parsedFile.ServiceURL != "" { |
| if clientOpts.serviceURL != "" && clientOpts.serviceURL != parsedFile.ServiceURL { |
| logging.Warningf(ctx, "serviceURL in ensure file != serviceURL on CLI (%q v %q). Using %q from file.", |
| parsedFile.ServiceURL, clientOpts.serviceURL, parsedFile.ServiceURL) |
| } |
| clientOpts.serviceURL = parsedFile.ServiceURL |
| } |
| |
| if verifying && len(parsedFile.VerifyPlatforms) == 0 { |
| logging.Errorf(ctx, |
| "For this feature to work, verification platforms must be specified in "+ |
| "the ensure file using one or more $VerifiedPlatform directives.") |
| return nil, errors.New("no verification platforms configured") |
| } |
| |
| if parseVers && parsedFile.ResolvedVersions != "" { |
| clientOpts.versions, err = loadVersionsFile(parsedFile.ResolvedVersions, opts.ensureFile) |
| if err != nil { |
| return nil, err |
| } |
| logging.Debugf(ctx, "Using the resolved version file %q", filepath.Base(parsedFile.ResolvedVersions)) |
| } |
| |
| return parsedFile, nil |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Support for running operations concurrently. |
| |
| // batchOperation defines what to do with a packages matching a prefix. |
| type batchOperation struct { |
| client cipd.Client |
| packagePrefix string // a package name or a prefix |
| packages []string // packages to operate on, overrides packagePrefix |
| callback func(pkg string) (common.Pin, error) |
| } |
| |
| // expandPkgDir takes a package name or '<prefix>/' and returns a list |
| // of matching packages (asking backend if necessary). Doesn't recurse, returns |
| // only direct children. |
| func expandPkgDir(ctx context.Context, c cipd.Client, packagePrefix string) ([]string, error) { |
| if !strings.HasSuffix(packagePrefix, "/") { |
| return []string{packagePrefix}, nil |
| } |
| pkgs, err := c.ListPackages(ctx, packagePrefix, false, false) |
| if err != nil { |
| return nil, err |
| } |
| // Skip directories. |
| var out []string |
| for _, p := range pkgs { |
| if !strings.HasSuffix(p, "/") { |
| out = append(out, p) |
| } |
| } |
| if len(out) == 0 { |
| return nil, fmt.Errorf("no packages under %s", packagePrefix) |
| } |
| return out, nil |
| } |
| |
| // performBatchOperation expands a package prefix into a list of packages and |
| // calls callback for each of them (concurrently) gathering the results. |
| // |
| // Returns an error only if the prefix expansion fails. Errors from individual |
| // operations are returned through []pinInfo, use hasErrors to check them. |
| func performBatchOperation(ctx context.Context, op batchOperation) ([]pinInfo, error) { |
| op.client.BeginBatch(ctx) |
| defer op.client.EndBatch(ctx) |
| |
| pkgs := op.packages |
| if len(pkgs) == 0 { |
| var err error |
| pkgs, err = expandPkgDir(ctx, op.client, op.packagePrefix) |
| if err != nil { |
| return nil, err |
| } |
| } |
| return callConcurrently(pkgs, func(pkg string) pinInfo { |
| pin, err := op.callback(pkg) |
| if err != nil { |
| return pinInfo{Pkg: pkg, Err: err.Error()} |
| } |
| return pinInfo{Pkg: pkg, Pin: &pin} |
| }), nil |
| } |
| |
| func callConcurrently(pkgs []string, callback func(pkg string) pinInfo) []pinInfo { |
| // Push index through channel to make results ordered as 'pkgs'. |
| ch := make(chan struct { |
| int |
| pinInfo |
| }) |
| for idx, pkg := range pkgs { |
| go func(idx int, pkg string) { |
| ch <- struct { |
| int |
| pinInfo |
| }{idx, callback(pkg)} |
| }(idx, pkg) |
| } |
| pins := make([]pinInfo, len(pkgs)) |
| for i := 0; i < len(pkgs); i++ { |
| res := <-ch |
| pins[res.int] = res.pinInfo |
| } |
| return pins |
| } |
| |
| func printPinsAndError(pinMap map[string][]pinInfo) { |
| for subdir, pins := range pinMap { |
| hasPins := false |
| hasErrors := false |
| for _, p := range pins { |
| if p.Err != "" { |
| hasErrors = true |
| } else if p.Pin != nil { |
| hasPins = true |
| } |
| } |
| subdirString := "" |
| if (hasPins || hasErrors) && (len(pinMap) > 1 || subdir != "") { |
| // only print this if it's not the root subdir, or there's more than one |
| // subdir in pinMap. |
| subdirString = fmt.Sprintf(" (subdir %q)", subdir) |
| } |
| if hasPins { |
| fmt.Printf("Packages%s:\n", subdirString) |
| for _, p := range pins { |
| if p.Err != "" || p.Pin == nil { |
| continue |
| } |
| plat := "" |
| if p.Platform != "" { |
| plat = fmt.Sprintf(" (for %s)", p.Platform) |
| } |
| tracking := "" |
| if p.Tracking != "" { |
| tracking = fmt.Sprintf(" (tracking %q)", p.Tracking) |
| } |
| fmt.Printf(" %s%s%s\n", p.Pin, plat, tracking) |
| } |
| } |
| if hasErrors { |
| fmt.Fprintf(os.Stderr, "Errors%s:\n", subdirString) |
| for _, p := range pins { |
| if p.Err != "" { |
| fmt.Fprintf(os.Stderr, " %s: %s.\n", p.Pkg, p.Err) |
| } |
| } |
| } |
| } |
| } |
| |
| func hasErrors(pins []pinInfo) bool { |
| for _, p := range pins { |
| if p.Err != "" { |
| return true |
| } |
| } |
| return false |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Ensure-file related helpers. |
| |
| func resolveEnsureFile(ctx context.Context, f *ensure.File, clientOpts clientOptions) (map[string][]pinInfo, ensure.VersionsFile, error) { |
| client, err := clientOpts.makeCIPDClient(ctx) |
| if err != nil { |
| return nil, nil, err |
| } |
| defer client.Close(ctx) |
| |
| out := ensure.VersionsFile{} |
| mu := sync.Mutex{} |
| |
| resolver := cipd.Resolver{ |
| Client: client, |
| VerifyPresence: true, |
| Visitor: func(pkg, ver, iid string) { |
| mu.Lock() |
| out.AddVersion(pkg, ver, iid) |
| mu.Unlock() |
| }, |
| } |
| results, err := resolver.ResolveAllPlatforms(ctx, f) |
| if err != nil { |
| return nil, nil, err |
| } |
| return resolvedFilesToPinMap(results), out, nil |
| } |
| |
| func resolvedFilesToPinMap(res map[template.Platform]*ensure.ResolvedFile) map[string][]pinInfo { |
| pinMap := map[string][]pinInfo{} |
| for plat, resolved := range res { |
| for subdir, resolvedPins := range resolved.PackagesBySubdir { |
| pins := pinMap[subdir] |
| for _, pin := range resolvedPins { |
| // Put a copy into 'pins', otherwise they all end up pointing to the |
| // same variable living in the outer scope. |
| pin := pin |
| pins = append(pins, pinInfo{ |
| Pkg: pin.PackageName, |
| Pin: &pin, |
| Platform: plat.String(), |
| }) |
| } |
| pinMap[subdir] = pins |
| } |
| } |
| |
| // Sort pins by (package name, platform) for deterministic output. |
| for _, v := range pinMap { |
| sort.Slice(v, func(i, j int) bool { |
| if v[i].Pkg == v[j].Pkg { |
| return v[i].Platform < v[j].Platform |
| } |
| return v[i].Pkg < v[j].Pkg |
| }) |
| } |
| return pinMap |
| } |
| |
| func loadVersionsFile(path, ensureFile string) (ensure.VersionsFile, error) { |
| switch f, err := os.Open(path); { |
| case os.IsNotExist(err): |
| return nil, fmt.Errorf("the resolved versions file doesn't exist, "+ |
| "use 'cipd ensure-file-resolve -ensure-file %q' to generate it", ensureFile) |
| case err != nil: |
| return nil, err |
| default: |
| defer f.Close() |
| return ensure.ParseVersionsFile(f) |
| } |
| } |
| |
| func saveVersionsFile(path string, v ensure.VersionsFile) error { |
| buf := bytes.Buffer{} |
| if err := v.Serialize(&buf); err != nil { |
| return err |
| } |
| return ioutil.WriteFile(path, buf.Bytes(), 0666) |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 'create' subcommand. |
| |
| func cmdCreate(params Parameters) *subcommands.Command { |
| return &subcommands.Command{ |
| UsageLine: "create [options]", |
| ShortDesc: "builds and uploads a package instance file", |
| LongDesc: "Builds and uploads a package instance file.", |
| CommandRun: func() subcommands.CommandRun { |
| c := &createRun{} |
| c.registerBaseFlags() |
| c.Opts.inputOptions.registerFlags(&c.Flags) |
| c.Opts.refsOptions.registerFlags(&c.Flags) |
| c.Opts.tagsOptions.registerFlags(&c.Flags) |
| c.Opts.metadataOptions.registerFlags(&c.Flags) |
| c.Opts.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads) |
| c.Opts.uploadOptions.registerFlags(&c.Flags) |
| c.Opts.hashOptions.registerFlags(&c.Flags) |
| return c |
| }, |
| } |
| } |
| |
| type createOpts struct { |
| inputOptions |
| refsOptions |
| tagsOptions |
| metadataOptions |
| clientOptions |
| uploadOptions |
| hashOptions |
| } |
| |
| type createRun struct { |
| cipdSubcommand |
| |
| Opts createOpts |
| } |
| |
| func (c *createRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if !c.checkArgs(args, 0, 0) { |
| return 1 |
| } |
| ctx := cli.GetContext(a, c, env) |
| return c.done(buildAndUploadInstance(ctx, &c.Opts)) |
| } |
| |
| func buildAndUploadInstance(ctx context.Context, opts *createOpts) (common.Pin, error) { |
| f, err := ioutil.TempFile("", "cipd_pkg") |
| if err != nil { |
| return common.Pin{}, err |
| } |
| defer func() { |
| f.Close() |
| os.Remove(f.Name()) |
| }() |
| pin, err := buildInstanceFile(ctx, f.Name(), opts.inputOptions, opts.hashAlgo()) |
| if err != nil { |
| return common.Pin{}, err |
| } |
| return registerInstanceFile(ctx, f.Name(), &pin, ®isterOpts{ |
| refsOptions: opts.refsOptions, |
| tagsOptions: opts.tagsOptions, |
| metadataOptions: opts.metadataOptions, |
| clientOptions: opts.clientOptions, |
| uploadOptions: opts.uploadOptions, |
| hashOptions: opts.hashOptions, |
| }) |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 'attach' subcommand. |
| |
| func cmdAttach(params Parameters) *subcommands.Command { |
| return &subcommands.Command{ |
| UsageLine: "attach <package or package prefix> -metadata key:value -metadata-from-file key:path -tag key:value -ref name [options]", |
| ShortDesc: "attaches tags, metadata and points refs to an instance", |
| LongDesc: `Attaches tags, metadata and points refs to an instance. |
| |
| Note that this operation is not atomic. It attaches metadata first, then tags, |
| then moves refs one by one. Reattaching already attached data is not an error |
| though, so a failed operation can be safely retried. |
| `, |
| CommandRun: func() subcommands.CommandRun { |
| c := &attachRun{} |
| c.registerBaseFlags() |
| c.refsOptions.registerFlags(&c.Flags) |
| c.tagsOptions.registerFlags(&c.Flags) |
| c.metadataOptions.registerFlags(&c.Flags) |
| c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads) |
| c.Flags.StringVar(&c.version, "version", "<version>", |
| "Package version to resolve. Could also be a tag or a ref.") |
| return c |
| }, |
| } |
| } |
| |
| type attachRun struct { |
| cipdSubcommand |
| refsOptions |
| tagsOptions |
| metadataOptions |
| clientOptions |
| |
| version string |
| } |
| |
| func (c *attachRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if !c.checkArgs(args, 1, 1) { |
| return 1 |
| } |
| |
| ctx := cli.GetContext(a, c, env) |
| |
| md, err := c.metadataOptions.load(ctx) |
| if err != nil { |
| return c.done(nil, err) |
| } |
| if len(c.refs) == 0 && len(c.tags) == 0 && len(md) == 0 { |
| return c.done(nil, makeCLIError("no -tags, -refs or -metadata is provided")) |
| } |
| |
| pkgPrefix, err := expandTemplate(args[0]) |
| if err != nil { |
| return c.done(nil, err) |
| } |
| |
| return c.doneWithPins(visitPins(ctx, &visitPinsArgs{ |
| clientOptions: c.clientOptions, |
| packagePrefix: pkgPrefix, |
| version: c.version, |
| updatePin: func(client cipd.Client, pin common.Pin) error { |
| return attachAndMove(ctx, client, pin, md, c.tags, c.refs) |
| }, |
| })) |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 'ensure' subcommand. |
| |
| func cmdEnsure(params Parameters) *subcommands.Command { |
| return &subcommands.Command{ |
| UsageLine: "ensure [options]", |
| ShortDesc: "installs, removes and updates packages in one go", |
| LongDesc: `Installs, removes and updates packages in one go. |
| |
| Prepare an 'ensure file' by listing packages and their versions, each on their |
| own line, e.g.: |
| |
| some/package/name/${platform} version:1.2.3 |
| other/package some_ref |
| |
| Then use the ensure command to read this ensure file and 'ensure' that a given |
| folder has the packages at the versions specified: |
| |
| cipd ensure -root a/directory -ensure-file ensure_file |
| |
| For the full syntax of the ensure file, see: |
| |
| https://go.chromium.org/luci/cipd/client/cipd/ensure |
| `, |
| CommandRun: func() subcommands.CommandRun { |
| c := &ensureRun{} |
| c.registerBaseFlags() |
| c.clientOptions.registerFlags(&c.Flags, params, withRootDir, withMaxThreads) |
| c.ensureFileOptions.registerFlags(&c.Flags, withEnsureOutFlag, withLegacyListFlag) |
| return c |
| }, |
| } |
| } |
| |
| type ensureRun struct { |
| cipdSubcommand |
| clientOptions |
| ensureFileOptions |
| } |
| |
| func (c *ensureRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if !c.checkArgs(args, 0, 0) { |
| return 1 |
| } |
| ctx := cli.GetContext(a, c, env) |
| |
| ef, err := c.loadEnsureFile(ctx, &c.clientOptions, ignoreVerifyPlatforms, parseVersionsFile) |
| if err != nil { |
| return c.done(nil, err) |
| } |
| |
| pins, _, err := ensurePackages(ctx, ef, c.ensureFileOut, false, c.clientOptions) |
| return c.done(pins, err) |
| } |
| |
| func ensurePackages(ctx context.Context, ef *ensure.File, ensureFileOut string, dryRun bool, clientOpts clientOptions) (common.PinSliceBySubdir, cipd.ActionMap, error) { |
| client, err := clientOpts.makeCIPDClient(ctx) |
| if err != nil { |
| return nil, nil, err |
| } |
| defer client.Close(ctx) |
| |
| client.BeginBatch(ctx) |
| defer client.EndBatch(ctx) |
| |
| resolver := cipd.Resolver{Client: client} |
| resolved, err := resolver.Resolve(ctx, ef, template.DefaultExpander()) |
| if err != nil { |
| return nil, nil, err |
| } |
| |
| actions, err := client.EnsurePackages(ctx, resolved.PackagesBySubdir, &cipd.EnsureOptions{ |
| Paranoia: resolved.ParanoidMode, |
| DryRun: dryRun, |
| }) |
| if err != nil { |
| return nil, actions, err |
| } |
| |
| if ensureFileOut != "" { |
| buf := bytes.Buffer{} |
| resolved.ServiceURL = clientOpts.resolvedServiceURL(ctx) |
| resolved.ParanoidMode = "" |
| if err = resolved.Serialize(&buf); err == nil { |
| err = ioutil.WriteFile(ensureFileOut, buf.Bytes(), 0666) |
| } |
| } |
| |
| return resolved.PackagesBySubdir, actions, err |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 'ensure-file-verify' subcommand. |
| |
| func cmdEnsureFileVerify(params Parameters) *subcommands.Command { |
| return &subcommands.Command{ |
| UsageLine: "ensure-file-verify [options]", |
| ShortDesc: "verifies packages in a manifest exist for all platforms", |
| LongDesc: "Verifies that the packages in the \"ensure\" file exist for all platforms.\n\n" + |
| "Additionally if the ensure file uses $ResolvedVersions directive, checks that " + |
| "all versions there are up-to-date. Returns non-zero if some version can't be " + |
| "resolved or $ResolvedVersions file is outdated.", |
| Advanced: true, |
| CommandRun: func() subcommands.CommandRun { |
| c := &ensureFileVerifyRun{} |
| c.registerBaseFlags() |
| c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads) |
| c.ensureFileOptions.registerFlags(&c.Flags, withoutEnsureOutFlag, withoutLegacyListFlag) |
| return c |
| }, |
| } |
| } |
| |
| type ensureFileVerifyRun struct { |
| cipdSubcommand |
| clientOptions |
| ensureFileOptions |
| } |
| |
| func (c *ensureFileVerifyRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if !c.checkArgs(args, 0, 0) { |
| return 1 |
| } |
| ctx := cli.GetContext(a, c, env) |
| |
| ef, err := c.loadEnsureFile(ctx, &c.clientOptions, requireVerifyPlatforms, ignoreVersionsFile) |
| if err != nil { |
| return c.done(nil, err) |
| } |
| |
| // Resolving all versions in the ensure file also naturally verifies all |
| // versions exist. |
| pinMap, versions, err := resolveEnsureFile(ctx, ef, c.clientOptions) |
| if err != nil || ef.ResolvedVersions == "" { |
| return c.doneWithPinMap(pinMap, err) |
| } |
| |
| // Verify $ResolvedVersions file is up-to-date too. |
| switch existing, err := loadVersionsFile(ef.ResolvedVersions, c.ensureFile); { |
| case err != nil: |
| return c.done(nil, err) |
| case !existing.Equal(versions): |
| return c.done(nil, fmt.Errorf("the resolved versions file %s is stale, "+ |
| "use 'cipd ensure-file-resolve -ensure-file %q' to update it", |
| filepath.Base(ef.ResolvedVersions), c.ensureFile)) |
| default: |
| return c.doneWithPinMap(pinMap, err) |
| } |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 'ensure-file-resolve' subcommand. |
| |
| func cmdEnsureFileResolve(params Parameters) *subcommands.Command { |
| return &subcommands.Command{ |
| UsageLine: "ensure-file-resolve [options]", |
| ShortDesc: "resolves versions of all packages and writes them into $ResolvedVersions file", |
| LongDesc: "Resolves versions of all packages for all verified platforms in the \"ensure\" file.\n\n" + |
| `Writes them to a file specified by $ResolvedVersions directive in the ensure file, ` + |
| `to be used for version resolution during "cipd ensure ..." instead of calling the backend.`, |
| Advanced: true, |
| CommandRun: func() subcommands.CommandRun { |
| c := &ensureFileResolveRun{} |
| c.registerBaseFlags() |
| c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads) |
| c.ensureFileOptions.registerFlags(&c.Flags, withoutEnsureOutFlag, withoutLegacyListFlag) |
| return c |
| }, |
| } |
| } |
| |
| type ensureFileResolveRun struct { |
| cipdSubcommand |
| clientOptions |
| ensureFileOptions |
| } |
| |
| func (c *ensureFileResolveRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if !c.checkArgs(args, 0, 0) { |
| return 1 |
| } |
| ctx := cli.GetContext(a, c, env) |
| |
| ef, err := c.loadEnsureFile(ctx, &c.clientOptions, requireVerifyPlatforms, ignoreVersionsFile) |
| switch { |
| case err != nil: |
| return c.done(nil, err) |
| case ef.ResolvedVersions == "": |
| logging.Errorf(ctx, |
| "The ensure file doesn't have $ResolvedVersion directive that specifies "+ |
| "where to put the resolved package versions, so it can't be resolved.") |
| return c.done(nil, errors.New("no resolved versions file configured")) |
| } |
| |
| pinMap, versions, err := resolveEnsureFile(ctx, ef, c.clientOptions) |
| if err != nil { |
| return c.doneWithPinMap(pinMap, err) |
| } |
| |
| if err := saveVersionsFile(ef.ResolvedVersions, versions); err != nil { |
| return c.done(nil, err) |
| } |
| |
| fmt.Printf("The resolved versions have been written to %s.\n\n", filepath.Base(ef.ResolvedVersions)) |
| return c.doneWithPinMap(pinMap, nil) |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 'puppet-check-updates' subcommand. |
| |
| func cmdPuppetCheckUpdates(params Parameters) *subcommands.Command { |
| return &subcommands.Command{ |
| Advanced: true, |
| UsageLine: "puppet-check-updates [options]", |
| ShortDesc: "returns 0 exit code iff 'ensure' will do some actions", |
| LongDesc: "Returns 0 exit code iff 'ensure' will do some actions.\n\n" + |
| "Exists to be used from Puppet's Exec 'onlyif' option to trigger " + |
| "'ensure' only if something is out of date. If puppet-check-updates " + |
| "fails with a transient error, it returns non-zero exit code (as usual), " + |
| "so that Puppet doesn't trigger notification chain (that can result in " + |
| "service restarts). On fatal errors it returns 0 to let Puppet run " + |
| "'ensure' for real and catch an error.", |
| CommandRun: func() subcommands.CommandRun { |
| c := &checkUpdatesRun{} |
| c.registerBaseFlags() |
| c.clientOptions.registerFlags(&c.Flags, params, withRootDir, withMaxThreads) |
| c.ensureFileOptions.registerFlags(&c.Flags, withoutEnsureOutFlag, withLegacyListFlag) |
| return c |
| }, |
| } |
| } |
| |
| type checkUpdatesRun struct { |
| cipdSubcommand |
| clientOptions |
| ensureFileOptions |
| } |
| |
| func (c *checkUpdatesRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if !c.checkArgs(args, 0, 0) { |
| return 1 |
| } |
| ctx := cli.GetContext(a, c, env) |
| |
| ef, err := c.loadEnsureFile(ctx, &c.clientOptions, ignoreVerifyPlatforms, parseVersionsFile) |
| if err != nil { |
| return 0 // on fatal errors ask puppet to run 'ensure' for real |
| } |
| |
| _, actions, err := ensurePackages(ctx, ef, "", true, c.clientOptions) |
| if err != nil { |
| ret := c.done(actions, err) |
| if transient.Tag.In(err) { |
| return ret // fail as usual |
| } |
| return 0 // on fatal errors ask puppet to run 'ensure' for real |
| } |
| c.done(actions, nil) |
| if len(actions) == 0 { |
| return 5 // some arbitrary non-zero number, unlikely to show up on errors |
| } |
| return 0 |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 'resolve' subcommand. |
| |
| func cmdResolve(params Parameters) *subcommands.Command { |
| return &subcommands.Command{ |
| UsageLine: "resolve <package or package prefix> [options]", |
| ShortDesc: "returns concrete package instance ID given a version", |
| LongDesc: "Returns concrete package instance ID given a version.", |
| CommandRun: func() subcommands.CommandRun { |
| c := &resolveRun{} |
| c.registerBaseFlags() |
| c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads) |
| c.Flags.StringVar(&c.version, "version", "<version>", "Package version to resolve.") |
| return c |
| }, |
| } |
| } |
| |
| type resolveRun struct { |
| cipdSubcommand |
| clientOptions |
| |
| version string |
| } |
| |
| func (c *resolveRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if !c.checkArgs(args, 1, 1) { |
| return 1 |
| } |
| ctx := cli.GetContext(a, c, env) |
| return c.doneWithPins(resolveVersion(ctx, args[0], c.version, c.clientOptions)) |
| } |
| |
| func resolveVersion(ctx context.Context, packagePrefix, version string, clientOpts clientOptions) ([]pinInfo, error) { |
| packagePrefix, err := expandTemplate(packagePrefix) |
| if err != nil { |
| return nil, err |
| } |
| |
| client, err := clientOpts.makeCIPDClient(ctx) |
| if err != nil { |
| return nil, err |
| } |
| defer client.Close(ctx) |
| |
| return performBatchOperation(ctx, batchOperation{ |
| client: client, |
| packagePrefix: packagePrefix, |
| callback: func(pkg string) (common.Pin, error) { |
| return client.ResolveVersion(ctx, pkg, version) |
| }, |
| }) |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 'describe' subcommand. |
| |
| func cmdDescribe(params Parameters) *subcommands.Command { |
| return &subcommands.Command{ |
| UsageLine: "describe <package> [options]", |
| ShortDesc: "returns information about a package instance given its version", |
| LongDesc: "Returns information about a package instance given its version: " + |
| "who uploaded the instance and when and a list of attached tags.", |
| CommandRun: func() subcommands.CommandRun { |
| c := &describeRun{} |
| c.registerBaseFlags() |
| c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads) |
| c.Flags.StringVar(&c.version, "version", "<version>", "Package version to describe.") |
| return c |
| }, |
| } |
| } |
| |
| type describeRun struct { |
| cipdSubcommand |
| clientOptions |
| |
| version string |
| } |
| |
| func (c *describeRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if !c.checkArgs(args, 1, 1) { |
| return 1 |
| } |
| ctx := cli.GetContext(a, c, env) |
| return c.done(describeInstance(ctx, args[0], c.version, c.clientOptions)) |
| } |
| |
| func describeInstance(ctx context.Context, pkg, version string, clientOpts clientOptions) (*cipd.InstanceDescription, error) { |
| pkg, err := expandTemplate(pkg) |
| if err != nil { |
| return nil, err |
| } |
| |
| client, err := clientOpts.makeCIPDClient(ctx) |
| if err != nil { |
| return nil, err |
| } |
| defer client.Close(ctx) |
| |
| pin, err := client.ResolveVersion(ctx, pkg, version) |
| if err != nil { |
| return nil, err |
| } |
| |
| desc, err := client.DescribeInstance(ctx, pin, &cipd.DescribeInstanceOpts{ |
| DescribeRefs: true, |
| DescribeTags: true, |
| }) |
| if err != nil { |
| return nil, err |
| } |
| |
| fmt.Printf("Package: %s\n", desc.Pin.PackageName) |
| fmt.Printf("Instance ID: %s\n", desc.Pin.InstanceID) |
| fmt.Printf("Registered by: %s\n", desc.RegisteredBy) |
| fmt.Printf("Registered at: %s\n", time.Time(desc.RegisteredTs).Local()) |
| if len(desc.Refs) != 0 { |
| fmt.Printf("Refs:\n") |
| for _, t := range desc.Refs { |
| fmt.Printf(" %s\n", t.Ref) |
| } |
| } else { |
| fmt.Printf("Refs: none\n") |
| } |
| if len(desc.Tags) != 0 { |
| fmt.Printf("Tags:\n") |
| for _, t := range desc.Tags { |
| fmt.Printf(" %s\n", t.Tag) |
| } |
| } else { |
| fmt.Printf("Tags: none\n") |
| } |
| |
| return desc, nil |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 'instances' subcommand. |
| |
| func cmdInstances(params Parameters) *subcommands.Command { |
| return &subcommands.Command{ |
| UsageLine: "instances <package> [-limit ...]", |
| ShortDesc: "lists instances of a package", |
| LongDesc: "Lists instances of a package, most recently uploaded first.", |
| CommandRun: func() subcommands.CommandRun { |
| c := &instancesRun{} |
| c.registerBaseFlags() |
| c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads) |
| c.Flags.IntVar(&c.limit, "limit", 20, "How many instances to return or 0 for all.") |
| return c |
| }, |
| } |
| } |
| |
| type instancesRun struct { |
| cipdSubcommand |
| clientOptions |
| |
| limit int |
| } |
| |
| func (c *instancesRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if !c.checkArgs(args, 1, 1) { |
| return 1 |
| } |
| ctx := cli.GetContext(a, c, env) |
| return c.done(listInstances(ctx, args[0], c.limit, c.clientOptions)) |
| } |
| |
| func listInstances(ctx context.Context, pkg string, limit int, clientOpts clientOptions) (*instancesOutput, error) { |
| pkg, err := expandTemplate(pkg) |
| if err != nil { |
| return nil, err |
| } |
| |
| client, err := clientOpts.makeCIPDClient(ctx) |
| if err != nil { |
| return nil, err |
| } |
| defer client.Close(ctx) |
| |
| // TODO(vadimsh): The backend currently doesn't support retrieving |
| // per-instance refs when listing instances. Instead we fetch ALL refs in |
| // parallel and then merge this information with the instance listing. This |
| // works fine for packages with few refs (up to 50 maybe), but horribly |
| // inefficient if the cardinality of the set of all refs is larger than a |
| // typical size of instance listing (we spend time fetching data we don't |
| // need). To support this case better, the backend should learn to maintain |
| // {instance ID => ref} mapping (in addition to {ref => instance ID} mapping |
| // it already has). This would require back filling all existing entities. |
| |
| // Fetch the refs in parallel with the first page of results. We merge them |
| // with the list of instances during the display. |
| type refsMap map[string][]string // instance ID => list of refs |
| type refsOrErr struct { |
| refs refsMap |
| err error |
| } |
| refsChan := make(chan refsOrErr, 1) |
| go func() { |
| defer close(refsChan) |
| asMap := refsMap{} |
| refs, err := client.FetchPackageRefs(ctx, pkg) |
| for _, info := range refs { |
| asMap[info.InstanceID] = append(asMap[info.InstanceID], info.Ref) |
| } |
| refsChan <- refsOrErr{asMap, err} |
| }() |
| |
| enum, err := client.ListInstances(ctx, pkg) |
| if err != nil { |
| return nil, err |
| } |
| |
| formatRow := func(instanceID, when, who, refs string) string { |
| if len(who) > 25 { |
| who = who[:22] + "..." |
| } |
| return fmt.Sprintf("%-44s │ %-21s │ %-25s │ %-12s", instanceID, when, who, refs) |
| } |
| |
| var refs refsMap // populated on after fetching first page |
| |
| out := []instanceInfoWithRefs{} |
| for { |
| pageSize := 200 |
| if limit != 0 && limit-len(out) < pageSize { |
| pageSize = limit - len(out) |
| if pageSize == 0 { |
| // Fetched everything we wanted. There's likely more instances available |
| // (unless '-limit' happens to exactly match number of instances on the |
| // backend, which is not very probable). Hint this by printing '...'. |
| fmt.Println(formatRow("...", "...", "...", "...")) |
| break |
| } |
| } |
| page, err := enum.Next(ctx, pageSize) |
| if err != nil { |
| return nil, err |
| } |
| if len(page) == 0 { |
| if len(out) == 0 { |
| fmt.Println("No instances found") |
| } |
| break // no more results to fetch |
| } |
| |
| if len(out) == 0 { |
| // Need to wait for refs to be fetched, they are required to display |
| // "Refs" column. |
| refsOrErr := <-refsChan |
| if refsOrErr.err != nil { |
| return nil, refsOrErr.err |
| } |
| refs = refsOrErr.refs |
| |
| // Draw the header now that we have some real results (i.e no errors). |
| hdr := formatRow("Instance ID", "Timestamp", "Uploader", "Refs") |
| fmt.Println(hdr) |
| fmt.Println(strings.Repeat("─", len(hdr))) |
| } |
| |
| for _, info := range page { |
| instanceRefs := refs[info.Pin.InstanceID] |
| out = append(out, instanceInfoWithRefs{ |
| InstanceInfo: info, |
| Refs: instanceRefs, |
| }) |
| fmt.Println(formatRow( |
| info.Pin.InstanceID, |
| time.Time(info.RegisteredTs).Local().Format("Jan 02 15:04 MST 2006"), |
| strings.TrimPrefix(info.RegisteredBy, "user:"), |
| strings.Join(instanceRefs, " "))) |
| } |
| } |
| |
| return &instancesOutput{out}, nil |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 'set-ref' subcommand. |
| |
| func cmdSetRef(params Parameters) *subcommands.Command { |
| return &subcommands.Command{ |
| UsageLine: "set-ref <package or package prefix> [options]", |
| ShortDesc: "moves a ref to point to a given version", |
| LongDesc: "Moves a ref to point to a given version.", |
| CommandRun: func() subcommands.CommandRun { |
| c := &setRefRun{} |
| c.registerBaseFlags() |
| c.refsOptions.registerFlags(&c.Flags) |
| c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads) |
| c.Flags.StringVar(&c.version, "version", "<version>", "Package version to point the ref to.") |
| return c |
| }, |
| } |
| } |
| |
| type setRefRun struct { |
| cipdSubcommand |
| refsOptions |
| clientOptions |
| |
| version string |
| } |
| |
| func (c *setRefRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if !c.checkArgs(args, 1, 1) { |
| return 1 |
| } |
| if len(c.refs) == 0 { |
| return c.done(nil, makeCLIError("at least one -ref must be provided")) |
| } |
| pkgPrefix, err := expandTemplate(args[0]) |
| if err != nil { |
| return c.done(nil, err) |
| } |
| |
| ctx := cli.GetContext(a, c, env) |
| return c.doneWithPins(visitPins(ctx, &visitPinsArgs{ |
| clientOptions: c.clientOptions, |
| packagePrefix: pkgPrefix, |
| version: c.version, |
| updatePin: func(client cipd.Client, pin common.Pin) error { |
| for _, ref := range c.refs { |
| if err := client.SetRefWhenReady(ctx, ref, pin); err != nil { |
| return err |
| } |
| } |
| return nil |
| }, |
| })) |
| } |
| |
| type visitPinsArgs struct { |
| clientOptions |
| |
| packagePrefix string |
| version string |
| |
| updatePin func(client cipd.Client, pin common.Pin) error |
| } |
| |
| func visitPins(ctx context.Context, args *visitPinsArgs) ([]pinInfo, error) { |
| client, err := args.clientOptions.makeCIPDClient(ctx) |
| if err != nil { |
| return nil, err |
| } |
| defer client.Close(ctx) |
| |
| client.BeginBatch(ctx) |
| defer client.EndBatch(ctx) |
| |
| // Do not touch anything if some packages do not have requested version. So |
| // resolve versions first and only then move refs. |
| pins, err := performBatchOperation(ctx, batchOperation{ |
| client: client, |
| packagePrefix: args.packagePrefix, |
| callback: func(pkg string) (common.Pin, error) { |
| return client.ResolveVersion(ctx, pkg, args.version) |
| }, |
| }) |
| if err != nil { |
| return nil, err |
| } |
| if hasErrors(pins) { |
| printPinsAndError(map[string][]pinInfo{"": pins}) |
| return nil, fmt.Errorf("can't find %q version in all packages, aborting", args.version) |
| } |
| |
| // Prepare for the next batch call. |
| packages := make([]string, len(pins)) |
| pinsToUse := make(map[string]common.Pin, len(pins)) |
| for i, p := range pins { |
| packages[i] = p.Pkg |
| pinsToUse[p.Pkg] = *p.Pin |
| } |
| |
| // Update all refs or tags. |
| return performBatchOperation(ctx, batchOperation{ |
| client: client, |
| packages: packages, |
| callback: func(pkg string) (common.Pin, error) { |
| pin := pinsToUse[pkg] |
| if err := args.updatePin(client, pin); err != nil { |
| return common.Pin{}, err |
| } |
| return pin, nil |
| }, |
| }) |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 'set-tag' subcommand. |
| |
| func cmdSetTag(params Parameters) *subcommands.Command { |
| return &subcommands.Command{ |
| UsageLine: "set-tag <package or package prefix> -tag key:value [options]", |
| ShortDesc: "tags package of a specific version", |
| LongDesc: "Tags package of a specific version", |
| CommandRun: func() subcommands.CommandRun { |
| c := &setTagRun{} |
| c.registerBaseFlags() |
| c.tagsOptions.registerFlags(&c.Flags) |
| c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads) |
| c.Flags.StringVar(&c.version, "version", "<version>", |
| "Package version to resolve. Could also be a tag or a ref.") |
| return c |
| }, |
| } |
| } |
| |
| type setTagRun struct { |
| cipdSubcommand |
| tagsOptions |
| clientOptions |
| |
| version string |
| } |
| |
| func (c *setTagRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if !c.checkArgs(args, 1, 1) { |
| return 1 |
| } |
| if len(c.tags) == 0 { |
| return c.done(nil, makeCLIError("at least one -tag must be provided")) |
| } |
| pkgPrefix, err := expandTemplate(args[0]) |
| if err != nil { |
| return c.done(nil, err) |
| } |
| |
| ctx := cli.GetContext(a, c, env) |
| return c.done(visitPins(ctx, &visitPinsArgs{ |
| clientOptions: c.clientOptions, |
| packagePrefix: pkgPrefix, |
| version: c.version, |
| updatePin: func(client cipd.Client, pin common.Pin) error { |
| return client.AttachTagsWhenReady(ctx, pin, c.tags) |
| }, |
| })) |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 'set-metadata' subcommand. |
| |
| func cmdSetMetadata(params Parameters) *subcommands.Command { |
| return &subcommands.Command{ |
| UsageLine: "set-metadata <package or package prefix> -metadata key:value -metadata-from-file key:path [options]", |
| ShortDesc: "attaches metadata to an instance", |
| LongDesc: "Attaches metadata to an instance", |
| CommandRun: func() subcommands.CommandRun { |
| c := &setMetadataRun{} |
| c.registerBaseFlags() |
| c.metadataOptions.registerFlags(&c.Flags) |
| c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads) |
| c.Flags.StringVar(&c.version, "version", "<version>", |
| "Package version to resolve. Could also be a tag or a ref.") |
| return c |
| }, |
| } |
| } |
| |
| type setMetadataRun struct { |
| cipdSubcommand |
| metadataOptions |
| clientOptions |
| |
| version string |
| } |
| |
| func (c *setMetadataRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if !c.checkArgs(args, 1, 1) { |
| return 1 |
| } |
| |
| ctx := cli.GetContext(a, c, env) |
| |
| md, err := c.metadataOptions.load(ctx) |
| if err == nil && len(md) == 0 { |
| err = makeCLIError("at least one -metadata or -metadata-from-file must be provided") |
| } |
| if err != nil { |
| return c.done(nil, err) |
| } |
| |
| pkgPrefix, err := expandTemplate(args[0]) |
| if err != nil { |
| return c.done(nil, err) |
| } |
| |
| return c.doneWithPins(visitPins(ctx, &visitPinsArgs{ |
| clientOptions: c.clientOptions, |
| packagePrefix: pkgPrefix, |
| version: c.version, |
| updatePin: func(client cipd.Client, pin common.Pin) error { |
| return client.AttachMetadataWhenReady(ctx, pin, md) |
| }, |
| })) |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 'ls' subcommand. |
| |
| func cmdListPackages(params Parameters) *subcommands.Command { |
| return &subcommands.Command{ |
| UsageLine: "ls [-r] [<prefix string>]", |
| ShortDesc: "lists matching packages on the server", |
| LongDesc: "Queries the backend for a list of packages in the given path to " + |
| "which the user has access, optionally recursively.", |
| CommandRun: func() subcommands.CommandRun { |
| c := &listPackagesRun{} |
| c.registerBaseFlags() |
| c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads) |
| c.Flags.BoolVar(&c.recursive, "r", false, "Whether to list packages in subdirectories.") |
| c.Flags.BoolVar(&c.showHidden, "h", false, "Whether also to list hidden packages.") |
| return c |
| }, |
| } |
| } |
| |
| type listPackagesRun struct { |
| cipdSubcommand |
| clientOptions |
| |
| recursive bool |
| showHidden bool |
| } |
| |
| func (c *listPackagesRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if !c.checkArgs(args, 0, 1) { |
| return 1 |
| } |
| path, err := "", error(nil) |
| if len(args) == 1 { |
| path, err = expandTemplate(args[0]) |
| if err != nil { |
| return c.done(nil, err) |
| } |
| } |
| ctx := cli.GetContext(a, c, env) |
| return c.done(listPackages(ctx, path, c.recursive, c.showHidden, c.clientOptions)) |
| } |
| |
| func listPackages(ctx context.Context, path string, recursive, showHidden bool, clientOpts clientOptions) ([]string, error) { |
| client, err := clientOpts.makeCIPDClient(ctx) |
| if err != nil { |
| return nil, err |
| } |
| defer client.Close(ctx) |
| |
| packages, err := client.ListPackages(ctx, path, recursive, showHidden) |
| if err != nil { |
| return nil, err |
| } |
| if len(packages) == 0 { |
| fmt.Println("No matching packages.") |
| } else { |
| for _, p := range packages { |
| fmt.Println(p) |
| } |
| } |
| return packages, nil |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 'search' subcommand. |
| |
| func cmdSearch(params Parameters) *subcommands.Command { |
| return &subcommands.Command{ |
| UsageLine: "search <package> -tag key:value [options]", |
| ShortDesc: "searches for package instances by tag", |
| LongDesc: "Searches for instances of some package with all given tags.", |
| CommandRun: func() subcommands.CommandRun { |
| c := &searchRun{} |
| c.registerBaseFlags() |
| c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads) |
| c.tagsOptions.registerFlags(&c.Flags) |
| return c |
| }, |
| } |
| } |
| |
| type searchRun struct { |
| cipdSubcommand |
| clientOptions |
| tagsOptions |
| } |
| |
| func (c *searchRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if !c.checkArgs(args, 1, 1) { |
| return 1 |
| } |
| if len(c.tags) == 0 { |
| return c.done(nil, makeCLIError("at least one -tag must be provided")) |
| } |
| packageName, err := expandTemplate(args[0]) |
| if err != nil { |
| return c.done(nil, err) |
| } |
| ctx := cli.GetContext(a, c, env) |
| return c.done(searchInstances(ctx, packageName, c.tags, c.clientOptions)) |
| } |
| |
| func searchInstances(ctx context.Context, packageName string, tags []string, clientOpts clientOptions) ([]common.Pin, error) { |
| client, err := clientOpts.makeCIPDClient(ctx) |
| if err != nil { |
| return nil, err |
| } |
| defer client.Close(ctx) |
| |
| pins, err := client.SearchInstances(ctx, packageName, tags) |
| if err != nil { |
| return nil, err |
| } |
| if len(pins) == 0 { |
| fmt.Println("No matching instances.") |
| } else { |
| fmt.Println("Instances:") |
| for _, pin := range pins { |
| fmt.Printf(" %s\n", pin) |
| } |
| } |
| return pins, err |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 'acl-list' subcommand. |
| |
| func cmdListACL(params Parameters) *subcommands.Command { |
| return &subcommands.Command{ |
| Advanced: true, |
| UsageLine: "acl-list <package subpath>", |
| ShortDesc: "lists package path Access Control List", |
| LongDesc: "Lists package path Access Control List.", |
| CommandRun: func() subcommands.CommandRun { |
| c := &listACLRun{} |
| c.registerBaseFlags() |
| c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads) |
| return c |
| }, |
| } |
| } |
| |
| type listACLRun struct { |
| cipdSubcommand |
| clientOptions |
| } |
| |
| func (c *listACLRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if !c.checkArgs(args, 1, 1) { |
| return 1 |
| } |
| pkg, err := expandTemplate(args[0]) |
| if err != nil { |
| return c.done(nil, err) |
| } |
| |
| ctx := cli.GetContext(a, c, env) |
| return c.done(listACL(ctx, pkg, c.clientOptions)) |
| } |
| |
| func listACL(ctx context.Context, packagePath string, clientOpts clientOptions) (map[string][]cipd.PackageACL, error) { |
| client, err := clientOpts.makeCIPDClient(ctx) |
| if err != nil { |
| return nil, err |
| } |
| defer client.Close(ctx) |
| |
| acls, err := client.FetchACL(ctx, packagePath) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Split by role, drop empty ACLs. |
| byRole := map[string][]cipd.PackageACL{} |
| for _, a := range acls { |
| if len(a.Principals) != 0 { |
| byRole[a.Role] = append(byRole[a.Role], a) |
| } |
| } |
| |
| listRoleACL := func(title string, acls []cipd.PackageACL) { |
| fmt.Printf("%s:\n", title) |
| if len(acls) == 0 { |
| fmt.Printf(" none\n") |
| return |
| } |
| for _, a := range acls { |
| fmt.Printf(" via %q:\n", a.PackagePath) |
| for _, u := range a.Principals { |
| fmt.Printf(" %s\n", u) |
| } |
| } |
| } |
| |
| listRoleACL("Owners", byRole["OWNER"]) |
| listRoleACL("Writers", byRole["WRITER"]) |
| listRoleACL("Readers", byRole["READER"]) |
| |
| return byRole, nil |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 'acl-edit' subcommand. |
| |
| // principalsList is used as custom flag value. It implements flag.Value. |
| type principalsList []string |
| |
| func (l *principalsList) String() string { |
| return fmt.Sprintf("%v", *l) |
| } |
| |
| func (l *principalsList) Set(value string) error { |
| // Ensure <type>:<id> syntax is used. Let the backend to validate the rest. |
| chunks := strings.Split(value, ":") |
| if len(chunks) != 2 { |
| return makeCLIError("%q doesn't look like principal id (<type>:<id>)", value) |
| } |
| *l = append(*l, value) |
| return nil |
| } |
| |
| func cmdEditACL(params Parameters) *subcommands.Command { |
| return &subcommands.Command{ |
| Advanced: true, |
| UsageLine: "acl-edit <package subpath> [options]", |
| ShortDesc: "modifies package path Access Control List", |
| LongDesc: "Modifies package path Access Control List.", |
| CommandRun: func() subcommands.CommandRun { |
| c := &editACLRun{} |
| c.registerBaseFlags() |
| c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads) |
| c.Flags.Var(&c.owner, "owner", "Users (user:email) or groups (`group:name`) to grant OWNER role.") |
| c.Flags.Var(&c.writer, "writer", "Users (user:email) or groups (`group:name`) to grant WRITER role.") |
| c.Flags.Var(&c.reader, "reader", "Users (user:email) or groups (`group:name`) to grant READER role.") |
| c.Flags.Var(&c.revoke, "revoke", "Users (user:email) or groups (`group:name`) to remove from all roles.") |
| return c |
| }, |
| } |
| } |
| |
| type editACLRun struct { |
| cipdSubcommand |
| clientOptions |
| |
| owner principalsList |
| writer principalsList |
| reader principalsList |
| revoke principalsList |
| } |
| |
| func (c *editACLRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if !c.checkArgs(args, 1, 1) { |
| return 1 |
| } |
| pkg, err := expandTemplate(args[0]) |
| if err != nil { |
| return c.done(nil, err) |
| } |
| |
| ctx := cli.GetContext(a, c, env) |
| return c.done(nil, editACL(ctx, pkg, c.owner, c.writer, c.reader, c.revoke, c.clientOptions)) |
| } |
| |
| func editACL(ctx context.Context, packagePath string, owners, writers, readers, revoke principalsList, clientOpts clientOptions) error { |
| changes := []cipd.PackageACLChange{} |
| |
| makeChanges := func(action cipd.PackageACLChangeAction, role string, list principalsList) { |
| for _, p := range list { |
| changes = append(changes, cipd.PackageACLChange{ |
| Action: action, |
| Role: role, |
| Principal: p, |
| }) |
| } |
| } |
| |
| makeChanges(cipd.GrantRole, "OWNER", owners) |
| makeChanges(cipd.GrantRole, "WRITER", writers) |
| makeChanges(cipd.GrantRole, "READER", readers) |
| |
| makeChanges(cipd.RevokeRole, "OWNER", revoke) |
| makeChanges(cipd.RevokeRole, "WRITER", revoke) |
| makeChanges(cipd.RevokeRole, "READER", revoke) |
| |
| if len(changes) == 0 { |
| return nil |
| } |
| |
| client, err := clientOpts.makeCIPDClient(ctx) |
| if err != nil { |
| return err |
| } |
| defer client.Close(ctx) |
| |
| err = client.ModifyACL(ctx, packagePath, changes) |
| if err != nil { |
| return err |
| } |
| fmt.Println("ACL changes applied.") |
| return nil |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 'acl-check' subcommand. |
| |
| func cmdCheckACL(params Parameters) *subcommands.Command { |
| return &subcommands.Command{ |
| Advanced: true, |
| UsageLine: "acl-check <package subpath> [options]", |
| ShortDesc: "checks whether the caller has given roles in a package", |
| LongDesc: "Checks whether the caller has given roles in a package.", |
| CommandRun: func() subcommands.CommandRun { |
| c := &checkACLRun{} |
| c.registerBaseFlags() |
| c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads) |
| c.Flags.BoolVar(&c.owner, "owner", false, "Check for OWNER role.") |
| c.Flags.BoolVar(&c.writer, "writer", false, "Check for WRITER role.") |
| c.Flags.BoolVar(&c.reader, "reader", false, "Check for READER role.") |
| return c |
| }, |
| } |
| } |
| |
| type checkACLRun struct { |
| cipdSubcommand |
| clientOptions |
| |
| owner bool |
| writer bool |
| reader bool |
| } |
| |
| func (c *checkACLRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if !c.checkArgs(args, 1, 1) { |
| return 1 |
| } |
| |
| var roles []string |
| if c.owner { |
| roles = append(roles, "OWNER") |
| } |
| if c.writer { |
| roles = append(roles, "WRITER") |
| } |
| if c.reader { |
| roles = append(roles, "READER") |
| } |
| |
| // By default, check for READER access. |
| if len(roles) == 0 { |
| roles = append(roles, "READER") |
| } |
| |
| pkg, err := expandTemplate(args[0]) |
| if err != nil { |
| return c.done(nil, err) |
| } |
| |
| ctx := cli.GetContext(a, c, env) |
| return c.done(checkACL(ctx, pkg, roles, c.clientOptions)) |
| } |
| |
| func checkACL(ctx context.Context, packagePath string, roles []string, clientOpts clientOptions) (bool, error) { |
| client, err := clientOpts.makeCIPDClient(ctx) |
| if err != nil { |
| return false, err |
| } |
| defer client.Close(ctx) |
| |
| actualRoles, err := client.FetchRoles(ctx, packagePath) |
| if err != nil { |
| return false, err |
| } |
| roleSet := stringset.NewFromSlice(actualRoles...) |
| |
| var missing []string |
| for _, r := range roles { |
| if !roleSet.Has(r) { |
| missing = append(missing, r) |
| } |
| } |
| |
| if len(missing) == 0 { |
| fmt.Printf("The caller has all requested role(s): %s\n", strings.Join(roles, ", ")) |
| return true, nil |
| } |
| |
| fmt.Printf("The caller doesn't have following role(s): %s\n", strings.Join(missing, ", ")) |
| return false, nil |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 'pkg-build' subcommand. |
| |
| func cmdBuild() *subcommands.Command { |
| return &subcommands.Command{ |
| Advanced: true, |
| UsageLine: "pkg-build [options]", |
| ShortDesc: "builds a package instance file", |
| LongDesc: "Builds a package instance producing *.cipd file.", |
| CommandRun: func() subcommands.CommandRun { |
| c := &buildRun{} |
| c.registerBaseFlags() |
| c.inputOptions.registerFlags(&c.Flags) |
| c.hashOptions.registerFlags(&c.Flags) |
| c.Flags.StringVar(&c.outputFile, "out", "<path>", "Path to a file to write the final package to.") |
| return c |
| }, |
| } |
| } |
| |
| type buildRun struct { |
| cipdSubcommand |
| inputOptions |
| hashOptions |
| |
| outputFile string |
| } |
| |
| func (c *buildRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if !c.checkArgs(args, 0, 0) { |
| return 1 |
| } |
| ctx := cli.GetContext(a, c, env) |
| _, err := buildInstanceFile(ctx, c.outputFile, c.inputOptions, c.hashAlgo()) |
| if err != nil { |
| return c.done(nil, err) |
| } |
| return c.done(inspectInstanceFile(ctx, c.outputFile, c.hashAlgo(), false)) |
| } |
| |
| func buildInstanceFile(ctx context.Context, instanceFile string, inputOpts inputOptions, algo api.HashAlgo) (common.Pin, error) { |
| // Read the list of files to add to the package. |
| buildOpts, err := inputOpts.prepareInput() |
| if err != nil { |
| return common.Pin{}, err |
| } |
| |
| // Prepare the destination, update build options with io.Writer to it. |
| out, err := os.OpenFile(instanceFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) |
| if err != nil { |
| return common.Pin{}, err |
| } |
| buildOpts.Output = out |
| buildOpts.HashAlgo = algo |
| |
| // Build the package. |
| pin, err := builder.BuildInstance(ctx, buildOpts) |
| if err != nil { |
| out.Close() |
| os.Remove(instanceFile) |
| return common.Pin{}, err |
| } |
| |
| // Make sure it is flushed properly by ensuring Close succeeds. |
| if err := out.Close(); err != nil { |
| return common.Pin{}, err |
| } |
| |
| return pin, nil |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 'pkg-deploy' subcommand. |
| |
| func cmdDeploy() *subcommands.Command { |
| return &subcommands.Command{ |
| Advanced: true, |
| UsageLine: "pkg-deploy <package instance file> [options]", |
| ShortDesc: "deploys a package instance file", |
| LongDesc: "Deploys a *.cipd package instance into a site root.", |
| CommandRun: func() subcommands.CommandRun { |
| c := &deployRun{} |
| c.registerBaseFlags() |
| c.hashOptions.registerFlags(&c.Flags) |
| c.maxThreadsOption.registerFlags(&c.Flags) |
| c.Flags.StringVar(&c.rootDir, "root", "<path>", "Path to an installation site root directory.") |
| return c |
| }, |
| } |
| } |
| |
| type deployRun struct { |
| cipdSubcommand |
| hashOptions |
| maxThreadsOption |
| |
| rootDir string |
| } |
| |
| func (c *deployRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if !c.checkArgs(args, 1, 1) { |
| return 1 |
| } |
| ctx := cli.GetContext(a, c, env) |
| maxThreads, err := c.loadMaxThreads(ctx) |
| if err != nil { |
| return c.done(nil, err) |
| } |
| return c.done(deployInstanceFile(ctx, c.rootDir, args[0], c.hashAlgo(), maxThreads)) |
| } |
| |
| func deployInstanceFile(ctx context.Context, root, instanceFile string, hashAlgo api.HashAlgo, maxThreads int) (common.Pin, error) { |
| inst, err := reader.OpenInstanceFile(ctx, instanceFile, reader.OpenInstanceOpts{ |
| VerificationMode: reader.CalculateHash, |
| HashAlgo: hashAlgo, |
| }) |
| if err != nil { |
| return common.Pin{}, err |
| } |
| defer inst.Close(ctx, false) |
| |
| inspectInstance(ctx, inst, false) |
| |
| d := deployer.New(root) |
| defer d.FS().CleanupTrash(ctx) |
| |
| // TODO(iannucci): add subdir arg to deployRun |
| |
| return d.DeployInstance(ctx, "", inst, maxThreads) |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 'pkg-fetch' subcommand. |
| |
| func cmdFetch(params Parameters) *subcommands.Command { |
| return &subcommands.Command{ |
| Advanced: true, |
| UsageLine: "pkg-fetch <package> [options]", |
| ShortDesc: "fetches a package instance file from the repository", |
| LongDesc: "Fetches a package instance file from the repository.", |
| CommandRun: func() subcommands.CommandRun { |
| c := &fetchRun{} |
| c.registerBaseFlags() |
| c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads) |
| c.Flags.StringVar(&c.version, "version", "<version>", "Package version to fetch.") |
| c.Flags.StringVar(&c.outputPath, "out", "<path>", "Path to a file to write fetch to.") |
| return c |
| }, |
| } |
| } |
| |
| type fetchRun struct { |
| cipdSubcommand |
| clientOptions |
| |
| version string |
| outputPath string |
| } |
| |
| func (c *fetchRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if !c.checkArgs(args, 1, 1) { |
| return 1 |
| } |
| pkg, err := expandTemplate(args[0]) |
| if err != nil { |
| return c.done(nil, err) |
| } |
| |
| ctx := cli.GetContext(a, c, env) |
| return c.done(fetchInstanceFile(ctx, pkg, c.version, c.outputPath, c.clientOptions)) |
| } |
| |
| func fetchInstanceFile(ctx context.Context, packageName, version, instanceFile string, clientOpts clientOptions) (common.Pin, error) { |
| client, err := clientOpts.makeCIPDClient(ctx) |
| if err != nil { |
| return common.Pin{}, err |
| } |
| defer client.Close(ctx) |
| |
| pin, err := client.ResolveVersion(ctx, packageName, version) |
| if err != nil { |
| return common.Pin{}, err |
| } |
| |
| out, err := os.OpenFile(instanceFile, os.O_CREATE|os.O_WRONLY, 0666) |
| if err != nil { |
| return common.Pin{}, err |
| } |
| ok := false |
| defer func() { |
| if !ok { |
| out.Close() |
| os.Remove(instanceFile) |
| } |
| }() |
| |
| err = client.FetchInstanceTo(ctx, pin, out) |
| if err != nil { |
| return common.Pin{}, err |
| } |
| |
| out.Close() |
| ok = true |
| |
| // Print information about the instance. 'FetchInstanceTo' already verified |
| // the hash. |
| inst, err := reader.OpenInstanceFile(ctx, instanceFile, reader.OpenInstanceOpts{ |
| VerificationMode: reader.SkipHashVerification, |
| InstanceID: pin.InstanceID, |
| }) |
| if err != nil { |
| os.Remove(instanceFile) |
| return common.Pin{}, err |
| } |
| defer inst.Close(ctx, false) |
| inspectInstance(ctx, inst, false) |
| return inst.Pin(), nil |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 'pkg-inspect' subcommand. |
| |
| func cmdInspect() *subcommands.Command { |
| return &subcommands.Command{ |
| Advanced: true, |
| UsageLine: "pkg-inspect <package instance file>", |
| ShortDesc: "inspects contents of a package instance file", |
| LongDesc: "Reads contents *.cipd file and prints information about it.", |
| CommandRun: func() subcommands.CommandRun { |
| c := &inspectRun{} |
| c.registerBaseFlags() |
| c.hashOptions.registerFlags(&c.Flags) |
| return c |
| }, |
| } |
| } |
| |
| type inspectRun struct { |
| cipdSubcommand |
| hashOptions |
| } |
| |
| func (c *inspectRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if !c.checkArgs(args, 1, 1) { |
| return 1 |
| } |
| ctx := cli.GetContext(a, c, env) |
| return c.done(inspectInstanceFile(ctx, args[0], c.hashAlgo(), true)) |
| } |
| |
| func inspectInstanceFile(ctx context.Context, instanceFile string, hashAlgo api.HashAlgo, listFiles bool) (common.Pin, error) { |
| inst, err := reader.OpenInstanceFile(ctx, instanceFile, reader.OpenInstanceOpts{ |
| VerificationMode: reader.CalculateHash, |
| HashAlgo: hashAlgo, |
| }) |
| if err != nil { |
| return common.Pin{}, err |
| } |
| defer inst.Close(ctx, false) |
| inspectInstance(ctx, inst, listFiles) |
| return inst.Pin(), nil |
| } |
| |
| func inspectPin(ctx context.Context, pin common.Pin) { |
| fmt.Printf("Instance: %s\n", pin) |
| } |
| |
| func inspectInstance(ctx context.Context, inst pkg.Instance, listFiles bool) { |
| inspectPin(ctx, inst.Pin()) |
| if listFiles { |
| fmt.Println("Package files:") |
| for _, f := range inst.Files() { |
| if f.Symlink() { |
| target, err := f.SymlinkTarget() |
| if err != nil { |
| fmt.Printf(" E %s (%s)\n", f.Name(), err) |
| } else { |
| fmt.Printf(" S %s -> %s\n", f.Name(), target) |
| } |
| } else { |
| flags := make([]string, 0, 3) |
| if f.Executable() { |
| flags = append(flags, "+x") |
| } |
| if f.WinAttrs()&fs.WinAttrHidden != 0 { |
| flags = append(flags, "+H") |
| } |
| if f.WinAttrs()&fs.WinAttrSystem != 0 { |
| flags = append(flags, "+S") |
| } |
| flagText := "" |
| if len(flags) > 0 { |
| flagText = fmt.Sprintf(" (%s)", strings.Join(flags, "")) |
| } |
| fmt.Printf(" F %s%s\n", f.Name(), flagText) |
| } |
| } |
| } |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 'pkg-register' subcommand. |
| |
| func cmdRegister(params Parameters) *subcommands.Command { |
| return &subcommands.Command{ |
| Advanced: true, |
| UsageLine: "pkg-register <package instance file>", |
| ShortDesc: "uploads and registers package instance in the package repository", |
| LongDesc: "Uploads and registers package instance in the package repository.", |
| CommandRun: func() subcommands.CommandRun { |
| c := ®isterRun{} |
| c.registerBaseFlags() |
| c.Opts.refsOptions.registerFlags(&c.Flags) |
| c.Opts.tagsOptions.registerFlags(&c.Flags) |
| c.Opts.metadataOptions.registerFlags(&c.Flags) |
| c.Opts.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads) |
| c.Opts.uploadOptions.registerFlags(&c.Flags) |
| c.Opts.hashOptions.registerFlags(&c.Flags) |
| return c |
| }, |
| } |
| } |
| |
| type registerOpts struct { |
| refsOptions |
| tagsOptions |
| metadataOptions |
| clientOptions |
| uploadOptions |
| hashOptions |
| } |
| |
| type registerRun struct { |
| cipdSubcommand |
| |
| Opts registerOpts |
| } |
| |
| func (c *registerRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if !c.checkArgs(args, 1, 1) { |
| return 1 |
| } |
| ctx := cli.GetContext(a, c, env) |
| return c.done(registerInstanceFile(ctx, args[0], nil, &c.Opts)) |
| } |
| |
| func registerInstanceFile(ctx context.Context, instanceFile string, knownPin *common.Pin, opts *registerOpts) (common.Pin, error) { |
| // Load metadata, in particular process -metadata-from-file, which may fail. |
| metadata, err := opts.metadataOptions.load(ctx) |
| if err != nil { |
| return common.Pin{}, err |
| } |
| |
| src, err := pkg.NewFileSource(instanceFile) |
| if err != nil { |
| return common.Pin{}, err |
| } |
| defer src.Close(ctx, false) |
| |
| // Calculate the pin if not yet known. |
| var pin common.Pin |
| if knownPin != nil { |
| pin = *knownPin |
| } else { |
| pin, err = reader.CalculatePin(ctx, src, opts.hashAlgo()) |
| if err != nil { |
| return common.Pin{}, err |
| } |
| } |
| inspectPin(ctx, pin) |
| |
| client, err := opts.clientOptions.makeCIPDClient(ctx) |
| if err != nil { |
| return common.Pin{}, err |
| } |
| defer client.Close(ctx) |
| |
| err = client.RegisterInstance(ctx, pin, src, opts.uploadOptions.verificationTimeout) |
| if err != nil { |
| return common.Pin{}, err |
| } |
| err = attachAndMove(ctx, client, pin, metadata, opts.tags, opts.refs) |
| if err != nil { |
| return common.Pin{}, err |
| } |
| return pin, nil |
| } |
| |
| func attachAndMove(ctx context.Context, client cipd.Client, pin common.Pin, md []cipd.Metadata, tags tagList, refs refList) error { |
| if err := client.AttachMetadataWhenReady(ctx, pin, md); err != nil { |
| return err |
| } |
| if err := client.AttachTagsWhenReady(ctx, pin, tags); err != nil { |
| return err |
| } |
| for _, ref := range refs { |
| if err := client.SetRefWhenReady(ctx, ref, pin); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 'selfupdate' subcommand. |
| |
| func cmdSelfUpdate(params Parameters) *subcommands.Command { |
| return &subcommands.Command{ |
| UsageLine: "selfupdate -version <version> | -version-file <path>", |
| ShortDesc: "updates the current CIPD client binary", |
| LongDesc: "Does an in-place upgrade to the current CIPD binary.\n\n" + |
| "Reads the version either from the command line (when using -version) or " + |
| "from a file (when using -version-file). When using -version-file, also " + |
| "loads special *.digests file (from <version-file>.digests path) with " + |
| "pinned hashes of the client binary for all platforms. When selfupdating, " + |
| "the client will verify the new downloaded binary has a hash specified in " + |
| "the *.digests file.", |
| CommandRun: func() subcommands.CommandRun { |
| c := &selfupdateRun{} |
| |
| // By default, show a reduced number of logs unless something goes wrong. |
| c.logConfig.Level = logging.Warning |
| |
| c.registerBaseFlags() |
| c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads) |
| c.Flags.StringVar(&c.version, "version", "", "Version of the client to update to (incompatible with -version-file).") |
| c.Flags.StringVar(&c.versionFile, "version-file", "", |
| "Indicates the path to read the new version from (<version-file> itself) and "+ |
| "the path to the file with pinned hashes of the CIPD binary (<version-file>.digests file).") |
| return c |
| }, |
| } |
| } |
| |
| type selfupdateRun struct { |
| cipdSubcommand |
| clientOptions |
| |
| version string |
| versionFile string |
| } |
| |
| func (c *selfupdateRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if !c.checkArgs(args, 0, 0) { |
| return 1 |
| } |
| |
| ctx := cli.GetContext(a, c, env) |
| |
| switch { |
| case c.version != "" && c.versionFile != "": |
| return c.done(nil, makeCLIError("-version and -version-file are mutually exclusive, use only one")) |
| case c.version == "" && c.versionFile == "": |
| return c.done(nil, makeCLIError("either -version or -version-file are required")) |
| } |
| |
| var version = c.version |
| var digests *digests.ClientDigestsFile |
| |
| if version == "" { // using -version-file instead? load *.digests |
| var err error |
| version, err = loadClientVersion(c.versionFile) |
| if err != nil { |
| return c.done(nil, err) |
| } |
| digests, err = loadClientDigests(c.versionFile + digestsSfx) |
| if err != nil { |
| return c.done(nil, err) |
| } |
| } |
| |
| return c.done(func() (common.Pin, error) { |
| exePath, err := os.Executable() |
| if err != nil { |
| return common.Pin{}, err |
| } |
| opts, err := c.clientOptions.toCIPDClientOpts(ctx) |
| if err != nil { |
| return common.Pin{}, err |
| } |
| return cipd.MaybeUpdateClient(ctx, opts, version, exePath, digests) |
| }()) |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 'selfupdate-roll' subcommand. |
| |
| func cmdSelfUpdateRoll(params Parameters) *subcommands.Command { |
| return &subcommands.Command{ |
| Advanced: true, |
| UsageLine: "selfupdate-roll -version-file <path> (-version <version> | -check)", |
| ShortDesc: "generates or checks the client version and *.digests files", |
| LongDesc: "Generates or checks the client version and *.digests files.\n\n" + |
| "When -version is specified, takes its value as CIPD client version, " + |
| "resolves it into a list of hashes of the client binary at this version " + |
| "for all known platforms, and (on success) puts the version into a file " + |
| "specified by -version-file (referred to as <version-file> below), and " + |
| "all hashes into <version-file>.digests file. They are later used by " + |
| "'selfupdate -version-file <version-file>' to verify validity of the " + |
| "fetched binary.\n\n" + |
| "If -version is not specified, reads it from <version-file> and generates " + |
| "<version-file>.digests file based on it.\n\n" + |
| "When using -check, just verifies hashes in the <version-file>.digests " + |
| "file match the version recorded in the <version-file>.", |
| CommandRun: func() subcommands.CommandRun { |
| c := &selfupdateRollRun{} |
| |
| c.registerBaseFlags() |
| c.clientOptions.registerFlags(&c.Flags, params, withoutRootDir, withoutMaxThreads) |
| c.Flags.StringVar(&c.version, "version", "", "Version of the client to roll to.") |
| c.Flags.StringVar(&c.versionFile, "version-file", "<version-file>", |
| "Indicates the path to a file with the version (<version-file> itself) and "+ |
| "the path to the file with pinned hashes of the CIPD binary (<version-file>.digests file).") |
| c.Flags.BoolVar(&c.check, "check", false, "If set, checks that the file with "+ |
| "pinned hashes of the CIPD binary (<version-file>.digests file) is up-to-date.") |
| return c |
| }, |
| } |
| } |
| |
| type selfupdateRollRun struct { |
| cipdSubcommand |
| clientOptions |
| |
| version string |
| versionFile string |
| check bool |
| } |
| |
| func (c *selfupdateRollRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if !c.checkArgs(args, 0, 0) { |
| return 1 |
| } |
| |
| ctx := cli.GetContext(a, c, env) |
| client, err := c.clientOptions.makeCIPDClient(ctx) |
| if err != nil { |
| return c.done(nil, err) |
| } |
| defer client.Close(ctx) |
| |
| if c.check { |
| if c.version != "" { |
| return c.done(nil, makeCLIError("-version should not be used in -check mode")) |
| } |
| version, err := loadClientVersion(c.versionFile) |
| if err != nil { |
| return c.done(nil, err) |
| } |
| return c.doneWithPins(checkClientDigests(ctx, client, c.versionFile+digestsSfx, version)) |
| } |
| |
| // Grab the version from the command line and fallback to the -version-file |
| // otherwise. The fallback is useful when we just want to regenerate *.digests |
| // without touching the version file itself. |
| version := c.version |
| if version == "" { |
| var err error |
| version, err = loadClientVersion(c.versionFile) |
| if err != nil { |
| return c.done(nil, err) |
| } |
| } |
| |
| // It really makes sense to pin only tags. Warn about that. Still proceed, |
| // maybe users are using refs and do not move them by convention. |
| switch { |
| case common.ValidateInstanceID(version, common.AnyHash) == nil: |
| return c.done(nil, fmt.Errorf("expecting a version identifier that can be "+ |
| "resolved for all per-platform CIPD client packages, not a concrete instance ID")) |
| case common.ValidateInstanceTag(version) != nil: |
| fmt.Printf( |
| "WARNING! Version %q is not a tag. The hash pinning in *.digests file is "+ |
| "only useful for unmovable version identifiers. Proceeding, assuming "+ |
| "the immutability of %q is maintained manually. If it moves, selfupdate "+ |
| "will break due to *.digests file no longer matching the packages!\n\n", |
| version, version) |
| } |
| |
| pins, err := generateClientDigests(ctx, client, c.versionFile+digestsSfx, version) |
| if err != nil { |
| return c.doneWithPins(pins, err) |
| } |
| |
| if c.version != "" { |
| if err := ioutil.WriteFile(c.versionFile, []byte(c.version+"\n"), 0666); err != nil { |
| return c.done(nil, err) |
| } |
| } |
| |
| return c.doneWithPins(pins, nil) |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| const digestsSfx = ".digests" |
| |
| func generateClientDigests(ctx context.Context, client cipd.Client, path, version string) ([]pinInfo, error) { |
| digests, pins, err := assembleClientDigests(ctx, client, version) |
| if err != nil { |
| return pins, err |
| } |
| |
| buf := bytes.Buffer{} |
| versionFileName := strings.TrimSuffix(filepath.Base(path), digestsSfx) |
| if err := digests.Serialize(&buf, version, versionFileName); err != nil { |
| return nil, err |
| } |
| if err := ioutil.WriteFile(path, buf.Bytes(), 0666); err != nil { |
| return nil, err |
| } |
| |
| fmt.Printf("The pinned client hashes have been written to %s.\n\n", filepath.Base(path)) |
| return pins, nil |
| } |
| |
| func checkClientDigests(ctx context.Context, client cipd.Client, path, version string) ([]pinInfo, error) { |
| existing, err := loadClientDigests(path) |
| if err != nil { |
| return nil, err |
| } |
| digests, pins, err := assembleClientDigests(ctx, client, version) |
| if err != nil { |
| return pins, err |
| } |
| if !digests.Equal(existing) { |
| base := filepath.Base(path) |
| return nil, fmt.Errorf("the file with pinned client hashes (%s) is stale, "+ |
| "use 'cipd selfupdate-roll -version-file %s' to update it", |
| base, strings.TrimSuffix(base, digestsSfx)) |
| } |
| fmt.Printf("The file with pinned client hashes (%s) is up-to-date.\n\n", filepath.Base(path)) |
| return pins, nil |
| } |
| |
| // loadClientVersion reads a version string from a file. |
| func loadClientVersion(path string) (string, error) { |
| blob, err := ioutil.ReadFile(path) |
| if err != nil { |
| return "", err |
| } |
| version := strings.TrimSpace(string(blob)) |
| if err := common.ValidateInstanceVersion(version); err != nil { |
| return "", err |
| } |
| return version, nil |
| } |
| |
| // loadClientDigests loads the *.digests file with client binary hashes. |
| func loadClientDigests(path string) (*digests.ClientDigestsFile, error) { |
| switch f, err := os.Open(path); { |
| case os.IsNotExist(err): |
| base := filepath.Base(path) |
| return nil, fmt.Errorf("the file with pinned client hashes (%s) doesn't exist, "+ |
| "use 'cipd selfupdate-roll -version-file %s' to generate it", |
| base, strings.TrimSuffix(base, digestsSfx)) |
| case err != nil: |
| return nil, err |
| default: |
| defer f.Close() |
| return digests.ParseClientDigestsFile(f) |
| } |
| } |
| |
| // assembleClientDigests produces the digests file by making backend RPCs. |
| func assembleClientDigests(ctx context.Context, c cipd.Client, version string) (*digests.ClientDigestsFile, []pinInfo, error) { |
| if !strings.HasSuffix(cipd.ClientPackage, "/${platform}") { |
| panic(fmt.Sprintf("client package template (%q) is expected to end with '${platform}'", cipd.ClientPackage)) |
| } |
| |
| out := &digests.ClientDigestsFile{} |
| mu := sync.Mutex{} |
| |
| // Ask the backend to give us hashes of the client binary for all platforms. |
| pins, err := performBatchOperation(ctx, batchOperation{ |
| client: c, |
| packagePrefix: strings.TrimSuffix(cipd.ClientPackage, "${platform}"), |
| callback: func(pkg string) (common.Pin, error) { |
| pin, err := c.ResolveVersion(ctx, pkg, version) |
| if err != nil { |
| return common.Pin{}, err |
| } |
| desc, err := c.DescribeClient(ctx, pin) |
| if err != nil { |
| return common.Pin{}, err |
| } |
| mu.Lock() |
| defer mu.Unlock() |
| plat := pkg[strings.LastIndex(pkg, "/")+1:] |
| if err := out.AddClientRef(plat, desc.Digest); err != nil { |
| return common.Pin{}, err |
| } |
| return pin, nil |
| }, |
| }) |
| switch { |
| case err != nil: |
| return nil, pins, err |
| case hasErrors(pins): |
| return nil, pins, errors.New("failed to obtain the client binary digest for all platforms") |
| } |
| |
| out.Sort() |
| return out, pins, nil |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 'deployment-check' subcommand. |
| |
| func cmdCheckDeployment(params Parameters) *subcommands.Command { |
| return &subcommands.Command{ |
| Advanced: true, |
| UsageLine: "deployment-check [options]", |
| ShortDesc: "verifies all files that are supposed to be installed are present", |
| LongDesc: "Compares CIPD package manifests stored in .cipd/* with what's on disk.\n\n" + |
| "Useful when debugging issues with broken installations.", |
| CommandRun: func() subcommands.CommandRun { |
| c := &checkDeploymentRun{} |
| c.registerBaseFlags() |
| c.clientOptions.registerFlags(&c.Flags, params, withRootDir, withoutMaxThreads) |
| return c |
| }, |
| } |
| } |
| |
| type checkDeploymentRun struct { |
| cipdSubcommand |
| clientOptions |
| } |
| |
| func (c *checkDeploymentRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if !c.checkArgs(args, 0, 0) { |
| return 1 |
| } |
| ctx := cli.GetContext(a, c, env) |
| return c.done(checkDeployment(ctx, c.clientOptions)) |
| } |
| |
| func checkDeployment(ctx context.Context, clientOpts clientOptions) (cipd.ActionMap, error) { |
| client, err := clientOpts.makeCIPDClient(ctx) |
| if err != nil { |
| return nil, err |
| } |
| defer client.Close(ctx) |
| |
| currentDeployment, err := client.FindDeployed(ctx) |
| if err != nil { |
| return nil, err |
| } |
| |
| actions, err := client.EnsurePackages(ctx, currentDeployment, &cipd.EnsureOptions{ |
| Paranoia: cipd.CheckIntegrity, |
| DryRun: true, |
| Silent: true, |
| }) |
| if err != nil { |
| return nil, err |
| } |
| actions.Log(ctx, true) |
| if len(actions) != 0 { |
| err = fmt.Errorf("the deployment needs a repair") |
| } |
| return actions, err |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // 'deployment-repair' subcommand. |
| |
| func cmdRepairDeployment(params Parameters) *subcommands.Command { |
| return &subcommands.Command{ |
| Advanced: true, |
| UsageLine: "deployment-repair [options]", |
| ShortDesc: "attempts to repair a deployment if it is broken", |
| LongDesc: "This is equivalent of running 'ensure' in paranoia.", |
| CommandRun: func() subcommands.CommandRun { |
| c := &repairDeploymentRun{} |
| c.registerBaseFlags() |
| c.clientOptions.registerFlags(&c.Flags, params, withRootDir, withMaxThreads) |
| return c |
| }, |
| } |
| } |
| |
| type repairDeploymentRun struct { |
| cipdSubcommand |
| clientOptions |
| } |
| |
| func (c *repairDeploymentRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| if !c.checkArgs(args, 0, 0) { |
| return 1 |
| } |
| ctx := cli.GetContext(a, c, env) |
| return c.done(repairDeployment(ctx, c.clientOptions)) |
| } |
| |
| func repairDeployment(ctx context.Context, clientOpts clientOptions) (cipd.ActionMap, error) { |
| client, err := clientOpts.makeCIPDClient(ctx) |
| if err != nil { |
| return nil, err |
| } |
| defer client.Close(ctx) |
| |
| currentDeployment, err := client.FindDeployed(ctx) |
| if err != nil { |
| return nil, err |
| } |
| |
| return client.EnsurePackages(ctx, currentDeployment, &cipd.EnsureOptions{ |
| Paranoia: cipd.CheckIntegrity, |
| }) |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // Main. |
| |
| // GetApplication returns cli.Application. |
| // |
| // It can be used directly by subcommands.Run(...), or nested into another |
| // application. |
| func GetApplication(params Parameters) *cli.Application { |
| return &cli.Application{ |
| Name: "cipd", |
| Title: "Chrome Infra Package Deployer (" + cipd.UserAgent + ")", |
| |
| Context: func(ctx context.Context) context.Context { |
| loggerConfig := gologger.LoggerConfig{ |
| Format: `[P%{pid} %{time:15:04:05.000} %{shortfile} %{level:.1s}] %{message}`, |
| Out: os.Stderr, |
| } |
| ctx, cancel := context.WithCancel(loggerConfig.Use(ctx)) |
| signals.HandleInterrupt(cancel) |
| return ctx |
| }, |
| |
| EnvVars: map[string]subcommands.EnvVarDefinition{ |
| cipd.EnvHTTPUserAgentPrefix: { |
| Advanced: true, |
| ShortDesc: "Optional http User-Agent prefix.", |
| }, |
| cipd.EnvCacheDir: { |
| ShortDesc: "Directory with shared instance and tags cache " + |
| "(-cache-dir, if given, takes precedence).", |
| }, |
| cipd.EnvMaxThreads: { |
| Advanced: true, |
| ShortDesc: "Number of worker threads for extracting packages. " + |
| "If 0 or negative, uses CPU count. (-max-threads, if given and not 0, takes precedence.)", |
| }, |
| cipd.EnvParallelDownloads: { |
| Advanced: true, |
| ShortDesc: fmt.Sprintf("How many packages are allowed to be fetched concurrently. "+ |
| "If <=1, packages will be fetched sequentially. Default is %d.", cipd.DefaultParallelDownloads), |
| }, |
| cipd.EnvAdmissionPlugin: { |
| Advanced: true, |
| ShortDesc: "JSON-encoded list with a command line of a deployment admission plugin.", |
| }, |
| cipd.EnvCIPDServiceURL: { |
| Advanced: true, |
| ShortDesc: "Override CIPD service URL.", |
| }, |
| envSimpleTerminalUI: { |
| Advanced: true, |
| ShortDesc: "If set disables the fancy terminal UI with progress bars in favor of a simpler one that just logs to stderr.", |
| }, |
| }, |
| |
| Commands: []*subcommands.Command{ |
| subcommands.CmdHelp, |
| versioncli.CmdVersion(cipd.UserAgent), |
| |
| // Authentication related commands. |
| {}, // These are spacers so that the commands appear in groups. |
| authcli.SubcommandInfo(params.DefaultAuthOptions, "auth-info", true), |
| authcli.SubcommandLogin(params.DefaultAuthOptions, "auth-login", false), |
| authcli.SubcommandLogout(params.DefaultAuthOptions, "auth-logout", false), |
| |
| // High level read commands. |
| {}, |
| cmdListPackages(params), |
| cmdSearch(params), |
| cmdResolve(params), |
| cmdDescribe(params), |
| cmdInstances(params), |
| |
| // High level remote write commands. |
| {}, |
| cmdCreate(params), |
| cmdAttach(params), |
| cmdSetRef(params), |
| cmdSetTag(params), |
| cmdSetMetadata(params), |
| |
| // High level local write commands. |
| {}, |
| cmdEnsure(params), |
| cmdSelfUpdate(params), |
| cmdSelfUpdateRoll(params), |
| |
| // Advanced ensure file operations. |
| {Advanced: true}, |
| cmdEnsureFileVerify(params), |
| cmdEnsureFileResolve(params), |
| |
| // User friendly subcommands that operates within a site root. Implemented |
| // in friendly.go. These are advanced because they're half-baked. |
| {Advanced: true}, |
| cmdInit(params), |
| cmdInstall(params), |
| cmdInstalled(params), |
| |
| // ACLs. |
| {Advanced: true}, |
| cmdListACL(params), |
| cmdEditACL(params), |
| cmdCheckACL(params), |
| |
| // Low level pkg-* commands. |
| {Advanced: true}, |
| cmdBuild(), |
| cmdDeploy(), |
| cmdFetch(params), |
| cmdInspect(), |
| cmdRegister(params), |
| |
| // Low level deployment-* commands. |
| {Advanced: true}, |
| cmdCheckDeployment(params), |
| cmdRepairDeployment(params), |
| |
| // Low level misc commands. |
| {Advanced: true}, |
| cmdPuppetCheckUpdates(params), |
| }, |
| } |
| } |
| |
| // Main runs the CIPD CLI. |
| // |
| func Main(params Parameters, args []string) int { |
| return subcommands.Run(GetApplication(params), fixflagpos.FixSubcommands(args)) |
| } |