diff options
Diffstat (limited to 'cmd/podman')
26 files changed, 271 insertions, 51 deletions
diff --git a/cmd/podman/containers/commit.go b/cmd/podman/containers/commit.go index b3c3d7626..79e2a32a7 100644 --- a/cmd/podman/containers/commit.go +++ b/cmd/podman/containers/commit.go @@ -22,7 +22,7 @@ var ( Short: "Create new image based on the changed container", Long: commitDescription, RunE: commit, - Args: cobra.MinimumNArgs(1), + Args: cobra.RangeArgs(1, 2), Example: `podman commit -q --message "committing container to image" reverent_golick image-committed podman commit -q --author "firstName lastName" reverent_golick image-committed podman commit -q --pause=false containerID image-committed @@ -30,7 +30,7 @@ var ( } containerCommitCommand = &cobra.Command{ - Args: cobra.MinimumNArgs(1), + Args: commitCommand.Args, Use: commitCommand.Use, Short: commitCommand.Short, Long: commitCommand.Long, @@ -82,7 +82,7 @@ func init() { func commit(cmd *cobra.Command, args []string) error { container := args[0] - if len(args) > 1 { + if len(args) == 2 { commitOptions.ImageName = args[1] } if !commitOptions.Quiet { diff --git a/cmd/podman/containers/export.go b/cmd/podman/containers/export.go index bbb6a6bc9..66e768ccb 100644 --- a/cmd/podman/containers/export.go +++ b/cmd/podman/containers/export.go @@ -28,7 +28,7 @@ var ( } containerExportCommand = &cobra.Command{ - Args: cobra.MinimumNArgs(1), + Args: cobra.ExactArgs(1), Use: exportCommand.Use, Short: exportCommand.Short, Long: exportCommand.Long, diff --git a/cmd/podman/containers/inspect.go b/cmd/podman/containers/inspect.go index 8556ebe83..e49fcc2e0 100644 --- a/cmd/podman/containers/inspect.go +++ b/cmd/podman/containers/inspect.go @@ -10,7 +10,7 @@ import ( var ( // podman container _inspect_ inspectCmd = &cobra.Command{ - Use: "inspect [flags] CONTAINER", + Use: "inspect [flags] CONTAINER [CONTAINER...]", Short: "Display the configuration of a container", Long: `Displays the low-level information on a container identified by name or ID.`, RunE: inspectExec, diff --git a/cmd/podman/containers/mount.go b/cmd/podman/containers/mount.go index 7f15616de..5c73cceaa 100644 --- a/cmd/podman/containers/mount.go +++ b/cmd/podman/containers/mount.go @@ -23,7 +23,7 @@ var ( ` mountCommand = &cobra.Command{ - Use: "mount [flags] [CONTAINER]", + Use: "mount [flags] [CONTAINER...]", Short: "Mount a working container's root filesystem", Long: mountDescription, RunE: mount, diff --git a/cmd/podman/containers/port.go b/cmd/podman/containers/port.go index d058a6aaf..115adc2a7 100644 --- a/cmd/podman/containers/port.go +++ b/cmd/podman/containers/port.go @@ -89,6 +89,9 @@ func port(cmd *cobra.Command, args []string) error { container = args[0] } port := "" + if len(args) > 2 { + return errors.Errorf("`port` accepts at most 2 arguments") + } if len(args) > 1 && !portOpts.Latest { port = args[1] } diff --git a/cmd/podman/containers/ps.go b/cmd/podman/containers/ps.go index ffd2054a6..5d3c9263e 100644 --- a/cmd/podman/containers/ps.go +++ b/cmd/podman/containers/ps.go @@ -110,7 +110,12 @@ func checkFlags(c *cobra.Command) error { } func jsonOut(responses []entities.ListContainer) error { - b, err := json.MarshalIndent(responses, "", " ") + r := make([]entities.ListContainer, 0) + for _, con := range responses { + con.CreatedAt = units.HumanDuration(time.Since(time.Unix(con.Created, 0))) + " ago" + r = append(r, con) + } + b, err := json.MarshalIndent(r, "", " ") if err != nil { return err } diff --git a/cmd/podman/containers/top.go b/cmd/podman/containers/top.go index d2b11ec77..afab12a14 100644 --- a/cmd/podman/containers/top.go +++ b/cmd/podman/containers/top.go @@ -25,7 +25,7 @@ var ( topOptions = entities.TopOptions{} topCommand = &cobra.Command{ - Use: "top [flags] CONTAINER [FORMAT-DESCRIPTORS|ARGS]", + Use: "top [flags] CONTAINER [FORMAT-DESCRIPTORS|ARGS...]", Short: "Display the running processes of a container", Long: topDescription, RunE: top, diff --git a/cmd/podman/generate/systemd.go b/cmd/podman/generate/systemd.go index e4fdd8690..b4ab0f9df 100644 --- a/cmd/podman/generate/systemd.go +++ b/cmd/podman/generate/systemd.go @@ -20,7 +20,7 @@ var ( Short: "Generate systemd units.", Long: systemdDescription, RunE: systemd, - Args: cobra.MinimumNArgs(1), + Args: cobra.ExactArgs(1), Example: `podman generate systemd CTR podman generate systemd --new --time 10 CTR podman generate systemd --files --name POD`, diff --git a/cmd/podman/images/build.go b/cmd/podman/images/build.go index 23bfcab79..dfde896a1 100644 --- a/cmd/podman/images/build.go +++ b/cmd/podman/images/build.go @@ -45,6 +45,7 @@ var ( Long: buildDescription, TraverseChildren: true, RunE: build, + Args: cobra.MaximumNArgs(1), Example: `podman build . podman build --creds=username:password -t imageName -f Containerfile.simple . podman build --layers --force-rm --tag imageName .`, diff --git a/cmd/podman/images/inspect.go b/cmd/podman/images/inspect.go index f6a10ba44..a1a9e91eb 100644 --- a/cmd/podman/images/inspect.go +++ b/cmd/podman/images/inspect.go @@ -10,7 +10,7 @@ import ( var ( // Command: podman image _inspect_ inspectCmd = &cobra.Command{ - Use: "inspect [flags] IMAGE", + Use: "inspect [flags] IMAGE [IMAGE...]", Short: "Display the configuration of an image", Long: `Displays the low-level information of an image identified by name or ID.`, RunE: inspectExec, diff --git a/cmd/podman/images/list.go b/cmd/podman/images/list.go index b7a8b8911..de7cca40d 100644 --- a/cmd/podman/images/list.go +++ b/cmd/podman/images/list.go @@ -32,7 +32,7 @@ type listFlagType struct { var ( // Command: podman image _list_ listCmd = &cobra.Command{ - Use: "list [FLAGS] [IMAGE]", + Use: "list [flags] [IMAGE]", Aliases: []string{"ls"}, Args: cobra.MaximumNArgs(1), Short: "List images in local storage", diff --git a/cmd/podman/images/push.go b/cmd/podman/images/push.go index a1614dc7a..7af2d9343 100644 --- a/cmd/podman/images/push.go +++ b/cmd/podman/images/push.go @@ -29,10 +29,11 @@ var ( // Command: podman push pushCmd = &cobra.Command{ - Use: "push [flags] SOURCE DESTINATION", + Use: "push [flags] SOURCE [DESTINATION]", Short: "Push an image to a specified destination", Long: pushDescription, RunE: imagePush, + Args: cobra.RangeArgs(1, 2), Example: `podman push imageID docker://registry.example.com/repository:tag podman push imageID oci-archive:/path/to/layout:image:tag`, } @@ -45,6 +46,7 @@ var ( Short: pushCmd.Short, Long: pushCmd.Long, RunE: pushCmd.RunE, + Args: pushCmd.Args, Example: `podman image push imageID docker://registry.example.com/repository:tag podman image push imageID oci-archive:/path/to/layout:image:tag`, } @@ -96,19 +98,8 @@ func pushFlags(flags *pflag.FlagSet) { // imagePush is implement the command for pushing images. func imagePush(cmd *cobra.Command, args []string) error { - var source, destination string - switch len(args) { - case 1: - source = args[0] - destination = args[0] - case 2: - source = args[0] - destination = args[1] - case 0: - fallthrough - default: - return errors.New("push requires at least one image name, or optionally a second to specify a different destination") - } + source := args[0] + destination := args[len(args)-1] // TLS verification in c/image is controlled via a `types.OptionalBool` // which allows for distinguishing among set-true, set-false, unspecified diff --git a/cmd/podman/images/save.go b/cmd/podman/images/save.go index 56953e41c..9b03c1383 100644 --- a/cmd/podman/images/save.go +++ b/cmd/podman/images/save.go @@ -23,8 +23,8 @@ var ( saveDescription = `Save an image to docker-archive or oci-archive on the local machine. Default is docker-archive.` saveCommand = &cobra.Command{ - Use: "save [flags] IMAGE", - Short: "Save image to an archive", + Use: "save [flags] IMAGE [IMAGE...]", + Short: "Save image(s) to an archive", Long: saveDescription, RunE: save, Args: func(cmd *cobra.Command, args []string) error { diff --git a/cmd/podman/images/trust_show.go b/cmd/podman/images/trust_show.go index 23ee6c709..dbaa800a4 100644 --- a/cmd/podman/images/trust_show.go +++ b/cmd/podman/images/trust_show.go @@ -18,6 +18,7 @@ var ( Short: "Display trust policy for the system", Long: showTrustDescription, RunE: showTrust, + Args: cobra.MaximumNArgs(1), Example: "", } ) diff --git a/cmd/podman/inspect.go b/cmd/podman/inspect.go index a5fdaedc2..6c4607d88 100644 --- a/cmd/podman/inspect.go +++ b/cmd/podman/inspect.go @@ -10,7 +10,7 @@ import ( var ( // Command: podman _inspect_ Object_ID inspectCmd = &cobra.Command{ - Use: "inspect [flags] {CONTAINER_ID | IMAGE_ID}", + Use: "inspect [flags] {CONTAINER_ID | IMAGE_ID} [...]", Short: "Display the configuration of object denoted by ID", Long: "Displays the low-level information on an object identified by name or ID", TraverseChildren: true, diff --git a/cmd/podman/main.go b/cmd/podman/main.go index 76ec7bc8e..f502e7a67 100644 --- a/cmd/podman/main.go +++ b/cmd/podman/main.go @@ -35,7 +35,7 @@ func main() { _, found := c.Command.Annotations[registry.ParentNSRequired] if rootless.IsRootless() && found { c.Command.RunE = func(cmd *cobra.Command, args []string) error { - return fmt.Errorf("cannot `%s` in rootless mode", cmd.CommandPath()) + return fmt.Errorf("cannot run command %q in rootless mode", cmd.CommandPath()) } } diff --git a/cmd/podman/manifest/create.go b/cmd/podman/manifest/create.go index 9c0097b90..2ab1fccea 100644 --- a/cmd/podman/manifest/create.go +++ b/cmd/podman/manifest/create.go @@ -20,7 +20,7 @@ var ( Example: `podman manifest create mylist:v1.11 podman manifest create mylist:v1.11 arch-specific-image-to-add podman manifest create --all mylist:v1.11 transport:tagged-image-to-add`, - Args: cobra.MinimumNArgs(1), + Args: cobra.RangeArgs(1, 2), } ) diff --git a/cmd/podman/networks/create.go b/cmd/podman/networks/create.go index 5d28c7140..2d29beddd 100644 --- a/cmd/podman/networks/create.go +++ b/cmd/podman/networks/create.go @@ -8,7 +8,6 @@ import ( "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/network" - "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -20,6 +19,7 @@ var ( Short: "network create", Long: networkCreateDescription, RunE: networkCreate, + Args: cobra.MaximumNArgs(1), Example: `podman network create podman1`, Annotations: map[string]string{ registry.ParentNSRequired: "", @@ -62,14 +62,10 @@ func networkCreate(cmd *cobra.Command, args []string) error { if err := network.IsSupportedDriver(networkCreateOptions.Driver); err != nil { return err } - if len(args) > 1 { - return errors.Errorf("only one network can be created at a time") - } - if len(args) > 0 && !define.NameRegex.MatchString(args[0]) { - return define.RegexError - } - if len(args) > 0 { + if !define.NameRegex.MatchString(args[0]) { + return define.RegexError + } name = args[0] } response, err := registry.ContainerEngine().NetworkCreate(registry.Context(), name, networkCreateOptions) diff --git a/cmd/podman/networks/inspect.go b/cmd/podman/networks/inspect.go index 1b2e89909..31269e836 100644 --- a/cmd/podman/networks/inspect.go +++ b/cmd/podman/networks/inspect.go @@ -16,7 +16,7 @@ import ( var ( networkinspectDescription = `Inspect network` networkinspectCommand = &cobra.Command{ - Use: "inspect NETWORK [NETWORK...] [flags] ", + Use: "inspect [flags] NETWORK [NETWORK...]", Short: "network inspect", Long: networkinspectDescription, RunE: networkInspect, diff --git a/cmd/podman/pods/top.go b/cmd/podman/pods/top.go index ba1efb638..8df00f92a 100644 --- a/cmd/podman/pods/top.go +++ b/cmd/podman/pods/top.go @@ -22,7 +22,7 @@ var ( topOptions = entities.PodTopOptions{} topCommand = &cobra.Command{ - Use: "top [flags] POD [FORMAT-DESCRIPTORS|ARGS]", + Use: "top [flags] POD [FORMAT-DESCRIPTORS|ARGS...]", Short: "Display the running processes of containers in a pod", Long: topDescription, RunE: top, diff --git a/cmd/podman/registry/config.go b/cmd/podman/registry/config.go index 49d5bca74..a67568d73 100644 --- a/cmd/podman/registry/config.go +++ b/cmd/podman/registry/config.go @@ -68,7 +68,6 @@ func newPodmanConfig() { } } - // FIXME: for rootless, add flag to get the path to override configuration cfg, err := config.NewConfig("") if err != nil { fmt.Fprint(os.Stderr, "Failed to obtain podman configuration: "+err.Error()) @@ -83,7 +82,7 @@ func newPodmanConfig() { podmanOptions = entities.PodmanConfig{Config: cfg, EngineMode: mode} } -// SetXdgDirs ensures the XDG_RUNTIME_DIR env and XDG_CONFIG_HOME variables are set. +// setXdgDirs ensures the XDG_RUNTIME_DIR env and XDG_CONFIG_HOME variables are set. // containers/image uses XDG_RUNTIME_DIR to locate the auth file, XDG_CONFIG_HOME is // use for the libpod.conf configuration file. func setXdgDirs() error { diff --git a/cmd/podman/registry/config_tunnel.go b/cmd/podman/registry/config_tunnel.go index bb3da947e..4f9f51163 100644 --- a/cmd/podman/registry/config_tunnel.go +++ b/cmd/podman/registry/config_tunnel.go @@ -2,6 +2,13 @@ package registry +import ( + "os" +) + func init() { abiSupport = false + + // Enforce that podman-remote == podman --remote + os.Args = append(os.Args, "--remote") } diff --git a/cmd/podman/root.go b/cmd/podman/root.go index 4f834e87d..25e53cbee 100644 --- a/cmd/podman/root.go +++ b/cmd/podman/root.go @@ -8,6 +8,7 @@ import ( "runtime/pprof" "strings" + "github.com/containers/common/pkg/config" "github.com/containers/libpod/cmd/podman/registry" "github.com/containers/libpod/cmd/podman/validate" "github.com/containers/libpod/pkg/domain/entities" @@ -103,13 +104,13 @@ func persistentPreRunE(cmd *cobra.Command, args []string) error { // TODO: Remove trace statement in podman V2.1 logrus.Debugf("Called %s.PersistentPreRunE(%s)", cmd.Name(), strings.Join(os.Args, " ")) - cfg := registry.PodmanConfig() - // Help is a special case, no need for more setup if cmd.Name() == "help" { return nil } + cfg := registry.PodmanConfig() + // Prep the engines if _, err := registry.NewImageEngine(cmd, args); err != nil { return err @@ -211,10 +212,14 @@ func loggingHook() { func rootFlags(opts *entities.PodmanConfig, flags *pflag.FlagSet) { // V2 flags flags.BoolVarP(&opts.Remote, "remote", "r", false, "Access remote Podman service (default false)") - // TODO Read uri from containers.config when available - flags.StringVar(&opts.URI, "url", registry.DefaultAPIAddress(), "URL to access Podman service (CONTAINER_HOST)") - flags.StringSliceVar(&opts.Identities, "identity", []string{}, "path to SSH identity file, (CONTAINER_SSHKEY)") - flags.StringVar(&opts.PassPhrase, "passphrase", "", "passphrase for identity file (not secure, CONTAINER_PASSPHRASE), ssh-agent always supported") + + custom, _ := config.ReadCustomConfig() + defaultURI := custom.Engine.RemoteURI + if defaultURI == "" { + defaultURI = registry.DefaultAPIAddress() + } + flags.StringVar(&opts.URI, "url", defaultURI, "URL to access Podman service (CONTAINER_HOST)") + flags.StringVar(&opts.Identity, "identity", custom.Engine.RemoteIdentity, "path to SSH identity file, (CONTAINER_SSHKEY)") cfg := opts.Config flags.StringVar(&cfg.Engine.CgroupManager, "cgroup-manager", cfg.Engine.CgroupManager, "Cgroup manager to use (\"cgroupfs\"|\"systemd\")") diff --git a/cmd/podman/system/connection.go b/cmd/podman/system/connection.go new file mode 100644 index 000000000..9f80a454b --- /dev/null +++ b/cmd/podman/system/connection.go @@ -0,0 +1,209 @@ +package system + +import ( + "bytes" + "fmt" + "net" + "net/url" + "os" + "os/user" + "regexp" + + "github.com/containers/common/pkg/config" + "github.com/containers/libpod/cmd/podman/registry" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/terminal" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" +) + +const schemaPattern = "^[A-Za-z][A-Za-z0-9+.-]*:" + +var ( + // Skip creating engines since this command will obtain connection information to engine + noOp = func(cmd *cobra.Command, args []string) error { + return nil + } + connectionCmd = &cobra.Command{ + Use: "connection [flags] DESTINATION", + Args: cobra.ExactArgs(1), + Long: `Store ssh destination information in podman configuration. + "destination" is of the form [user@]hostname or + an URI of the form ssh://[user@]hostname[:port] +`, + Short: "Record remote ssh destination", + PersistentPreRunE: noOp, + PersistentPostRunE: noOp, + TraverseChildren: false, + RunE: connection, + Example: `podman system connection server.fubar.com + podman system connection --identity ~/.ssh/dev_rsa ssh://root@server.fubar.com:2222 + podman system connection --identity ~/.ssh/dev_rsa --port 22 root@server.fubar.com`, + } + + cOpts = struct { + Identity string + Port int + UDSPath string + }{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: connectionCmd, + Parent: systemCmd, + }) + + flags := connectionCmd.Flags() + flags.StringVar(&cOpts.Identity, "identity", "", "path to ssh identity file") + flags.IntVarP(&cOpts.Port, "port", "p", 22, "port number for destination") + flags.StringVar(&cOpts.UDSPath, "socket-path", "", "path to podman socket on remote host. (default '/run/podman/podman.sock' or '/run/user/{uid}/podman/podman.sock)") +} + +func connection(cmd *cobra.Command, args []string) error { + // Default to ssh: schema if none given + dest := []byte(args[0]) + if match, err := regexp.Match(schemaPattern, dest); err != nil { + return errors.Wrapf(err, "internal regex error %q", schemaPattern) + } else if !match { + dest = append([]byte("ssh://"), dest...) + } + + uri, err := url.Parse(string(dest)) + if err != nil { + return errors.Wrapf(err, "failed to parse %q", string(dest)) + } + + if uri.User.Username() == "" { + if uri.User, err = getUserInfo(uri); err != nil { + return err + } + } + + if cmd.Flag("socket-path").Changed { + uri.Path = cmd.Flag("socket-path").Value.String() + } + + if cmd.Flag("port").Changed { + uri.Host = net.JoinHostPort(uri.Hostname(), cmd.Flag("port").Value.String()) + } + + if uri.Port() == "" { + uri.Host = net.JoinHostPort(uri.Hostname(), cmd.Flag("port").DefValue) + } + + if uri.Path == "" { + if uri.Path, err = getUDS(cmd, uri); err != nil { + return errors.Wrapf(err, "failed to connect to %q", uri.String()) + } + } + + custom, err := config.ReadCustomConfig() + if err != nil { + return err + } + + if cmd.Flag("identity").Changed { + custom.Engine.RemoteIdentity = cOpts.Identity + } + + custom.Engine.RemoteURI = uri.String() + return custom.Write() +} + +func getUserInfo(uri *url.URL) (*url.Userinfo, error) { + var ( + usr *user.User + err error + ) + if u, found := os.LookupEnv("_CONTAINERS_ROOTLESS_UID"); found { + usr, err = user.LookupId(u) + if err != nil { + return nil, errors.Wrapf(err, "failed to find user %q", u) + } + } else { + usr, err = user.Current() + if err != nil { + return nil, errors.Wrapf(err, "failed to obtain current user") + } + } + + pw, set := uri.User.Password() + if set { + return url.UserPassword(usr.Username, pw), nil + } + return url.User(usr.Username), nil +} + +func getUDS(cmd *cobra.Command, uri *url.URL) (string, error) { + var authMethods []ssh.AuthMethod + passwd, set := uri.User.Password() + if set { + authMethods = append(authMethods, ssh.Password(passwd)) + } + + ident := cmd.Flag("identity") + if ident.Changed { + auth, err := terminal.PublicKey(ident.Value.String(), []byte(passwd)) + if err != nil { + return "", errors.Wrapf(err, "Failed to read identity %q", ident.Value.String()) + } + authMethods = append(authMethods, auth) + } + + if sock, found := os.LookupEnv("SSH_AUTH_SOCK"); found { + logrus.Debugf("Found SSH_AUTH_SOCK %q, ssh-agent signer enabled", sock) + + c, err := net.Dial("unix", sock) + if err != nil { + return "", err + } + a := agent.NewClient(c) + authMethods = append(authMethods, ssh.PublicKeysCallback(a.Signers)) + } + + config := &ssh.ClientConfig{ + User: uri.User.Username(), + Auth: authMethods, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + dial, err := ssh.Dial("tcp", uri.Host, config) + if err != nil { + return "", errors.Wrapf(err, "failed to connect to %q", uri.Host) + } + defer dial.Close() + + session, err := dial.NewSession() + if err != nil { + return "", errors.Wrapf(err, "failed to create new ssh session on %q", uri.Host) + } + defer session.Close() + + // Override podman binary for testing etc + podman := "podman" + if v, found := os.LookupEnv("PODMAN_BINARY"); found { + podman = v + } + run := podman + " info --format=json" + + var buffer bytes.Buffer + session.Stdout = &buffer + if err := session.Run(run); err != nil { + return "", errors.Wrapf(err, "failed to run %q", run) + } + + var info define.Info + if err := json.Unmarshal(buffer.Bytes(), &info); err != nil { + return "", errors.Wrapf(err, "failed to parse 'podman info' results") + } + + if info.Host.RemoteSocket == nil || len(info.Host.RemoteSocket.Path) == 0 { + return "", fmt.Errorf("remote podman %q failed to report its UDS socket", uri.Host) + } + return info.Host.RemoteSocket.Path, nil +} diff --git a/cmd/podman/system/unshare.go b/cmd/podman/system/unshare.go index 7db5d36d2..e5d41e06d 100644 --- a/cmd/podman/system/unshare.go +++ b/cmd/podman/system/unshare.go @@ -13,7 +13,7 @@ import ( var ( unshareDescription = "Runs a command in a modified user namespace." unshareCommand = &cobra.Command{ - Use: "unshare [flags] [COMMAND [ARG]]", + Use: "unshare [flags] [COMMAND [ARG ...]]", Short: "Run a command in a modified user namespace", Long: unshareDescription, RunE: unshare, diff --git a/cmd/podman/validate/args.go b/cmd/podman/validate/args.go index 69240798f..d170447ee 100644 --- a/cmd/podman/validate/args.go +++ b/cmd/podman/validate/args.go @@ -25,8 +25,11 @@ func SubCommandExists(cmd *cobra.Command, args []string) error { // IDOrLatestArgs used to validate a nameOrId was provided or the "--latest" flag func IDOrLatestArgs(cmd *cobra.Command, args []string) error { - if len(args) > 1 || (len(args) == 0 && !cmd.Flag("latest").Changed) { - return fmt.Errorf("`%s` requires a name, id or the \"--latest\" flag", cmd.CommandPath()) + if len(args) > 1 { + return fmt.Errorf("`%s` accepts at most one argument", cmd.CommandPath()) + } + if len(args) == 0 && !cmd.Flag("latest").Changed { + return fmt.Errorf("`%s` requires a name, id, or the \"--latest\" flag", cmd.CommandPath()) } return nil } |