2019-03-01 09:50:57 -05:00
|
|
|
package cliplugins
|
|
|
|
|
|
|
|
import (
|
2019-04-03 06:47:53 -04:00
|
|
|
"os"
|
2019-03-01 09:50:57 -05:00
|
|
|
"testing"
|
|
|
|
|
|
|
|
"gotest.tools/icmd"
|
|
|
|
)
|
|
|
|
|
|
|
|
// TestRunGoodArgument ensures correct behaviour when running a valid plugin with an `--argument`.
|
|
|
|
func TestRunGoodArgument(t *testing.T) {
|
|
|
|
run, _, cleanup := prepare(t)
|
|
|
|
defer cleanup()
|
|
|
|
|
|
|
|
res := icmd.RunCmd(run("helloworld", "--who", "Cleveland"))
|
|
|
|
res.Assert(t, icmd.Expected{
|
|
|
|
ExitCode: 0,
|
|
|
|
Out: "Hello Cleveland!",
|
|
|
|
})
|
|
|
|
}
|
2019-03-01 11:03:14 -05:00
|
|
|
|
|
|
|
// TestClashWithGlobalArgs ensures correct behaviour when a plugin
|
|
|
|
// has an argument with the same name as one of the globals.
|
|
|
|
func TestClashWithGlobalArgs(t *testing.T) {
|
|
|
|
run, _, cleanup := prepare(t)
|
|
|
|
defer cleanup()
|
|
|
|
|
|
|
|
for _, tc := range []struct {
|
|
|
|
name string
|
|
|
|
args []string
|
|
|
|
expectedOut, expectedErr string
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "short-without-val",
|
|
|
|
args: []string{"-D"},
|
|
|
|
expectedOut: "Hello World!",
|
|
|
|
expectedErr: "Plugin debug mode enabled",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "long-without-val",
|
|
|
|
args: []string{"--debug"},
|
|
|
|
expectedOut: "Hello World!",
|
|
|
|
expectedErr: "Plugin debug mode enabled",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "short-with-val",
|
|
|
|
args: []string{"-c", "Christmas"},
|
|
|
|
expectedOut: "Merry Christmas!",
|
2019-04-03 06:07:52 -04:00
|
|
|
expectedErr: icmd.None,
|
2019-03-01 11:03:14 -05:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "short-with-val",
|
|
|
|
args: []string{"--context", "Christmas"},
|
|
|
|
expectedOut: "Merry Christmas!",
|
2019-04-03 06:07:52 -04:00
|
|
|
expectedErr: icmd.None,
|
2019-03-01 11:03:14 -05:00
|
|
|
},
|
|
|
|
} {
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
args := append([]string{"helloworld"}, tc.args...)
|
|
|
|
res := icmd.RunCmd(run(args...))
|
|
|
|
res.Assert(t, icmd.Expected{
|
|
|
|
ExitCode: 0,
|
|
|
|
Out: tc.expectedOut,
|
|
|
|
Err: tc.expectedErr,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-03 06:47:53 -04:00
|
|
|
// TestGlobalArgsOnlyParsedOnce checks that global args are only parsed
|
|
|
|
// once (cf https://github.com/docker/cli/issues/1801). These tests
|
|
|
|
// rely on `-H` being a list type (i.e. NewNamedListOptsRef) which
|
|
|
|
// reject multiple uses dynamically (see `getServerHost()` in
|
|
|
|
// github.com/docker/cli/cli/command/cli.go) in order to detect this
|
|
|
|
// scenario.
|
|
|
|
func TestGlobalArgsOnlyParsedOnce(t *testing.T) {
|
|
|
|
run, _, cleanup := prepare(t)
|
|
|
|
defer cleanup()
|
|
|
|
|
|
|
|
// We can rely on `$DOCKER_HOST` being set due to the call to
|
|
|
|
// `environment.Setup` in our `TestMain`.
|
|
|
|
dh := os.Getenv("DOCKER_HOST")
|
|
|
|
|
|
|
|
for _, tc := range []struct {
|
|
|
|
name string
|
|
|
|
args []string
|
|
|
|
expectedExitCode int
|
|
|
|
expectedOut, expectedErr string
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
// This is checking the precondition wrt -H mentioned in the function comment
|
|
|
|
name: "fails-if-H-used-twice",
|
|
|
|
args: []string{"-H", dh, "-H", dh, "version", "-f", "{{.Client.Version}}"},
|
|
|
|
expectedExitCode: 1,
|
|
|
|
expectedOut: icmd.None,
|
|
|
|
expectedErr: "Please specify only one -H",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "builtin",
|
|
|
|
args: []string{"-H", dh, "version", "-f", "{{.Client.Version}}"},
|
|
|
|
expectedExitCode: 0,
|
|
|
|
expectedOut: "", // Will be the client version, but the specifics aren't important so long as stderr is empty.
|
|
|
|
expectedErr: icmd.None,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "plugin",
|
|
|
|
args: []string{"-H", dh, "helloworld", "apiversion"},
|
|
|
|
expectedExitCode: 0,
|
|
|
|
expectedOut: "", // Will be the client version, but the specifics aren't important so long as stderr is empty.
|
|
|
|
expectedErr: icmd.None,
|
|
|
|
},
|
|
|
|
} {
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
res := icmd.RunCmd(run(tc.args...))
|
|
|
|
res.Assert(t, icmd.Expected{
|
|
|
|
ExitCode: tc.expectedExitCode,
|
|
|
|
Out: tc.expectedOut,
|
|
|
|
Err: tc.expectedErr,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-01 11:03:14 -05:00
|
|
|
// TestUnknownGlobal checks that unknown globals report errors
|
|
|
|
func TestUnknownGlobal(t *testing.T) {
|
|
|
|
run, _, cleanup := prepare(t)
|
|
|
|
defer cleanup()
|
|
|
|
|
allow plugins to have argument which match a top-level flag.
The issue with plugin options clashing with globals is that when cobra is
parsing the command line and it comes across an argument which doesn't start
with a `-` it (in the absence of plugins) distinguishes between "argument to
current command" and "new subcommand" based on the list of registered sub
commands.
Plugins breaks that model. When presented with `docker -D plugin -c foo` cobra
parses up to the `plugin`, sees it isn't a registered sub-command of the
top-level docker (because it isn't, it's a plugin) so it accumulates it as an
argument to the top-level `docker` command. Then it sees the `-c`, and thinks
it is the global `-c` (for AKA `--context`) option and tries to treat it as
that, which fails.
In the specific case of the top-level `docker` subcommand we know that it has
no arguments which aren't `--flags` (or `-f` short flags) and so anything which
doesn't start with a `-` must either be a (known) subcommand or an attempt to
execute a plugin.
We could simply scan for and register all installed plugins at start of day, so
that cobra can do the right thing, but we want to avoid that since it would
involve executing each plugin to fetch the metadata, even if the command wasn't
going to end up hitting a plugin.
Instead we can parse the initial set of global arguments separately before
hitting the main cobra `Execute` path, which works here exactly because we know
that the top-level has no non-flag arguments.
One slight wrinkle is that the top-level `PersistentPreRunE` is no longer
called on the plugins path (since it no longer goes via `Execute`), so we
arrange for the initialisation done there (which has to be done after global
flags are parsed to handle e.g. `--config`) to happen explictly after the
global flags are parsed. Rather than make `newDockerCommand` return the
complicated set of results needed to make this happen, instead return a closure
which achieves this.
The new functionality is introduced via a common `TopLevelCommand` abstraction
which lets us adjust the plugin entrypoint to use the same strategy for parsing
the global arguments. This isn't strictly required (in this case the stuff in
cobra's `Execute` works fine) but doing it this way avoids the possibility of
subtle differences in behaviour.
Fixes #1699, and also, as a side-effect, the first item in #1661.
Signed-off-by: Ian Campbell <ijc@docker.com>
2019-03-06 05:29:42 -05:00
|
|
|
for name, args := range map[string][]string{
|
|
|
|
"no-val": {"--unknown", "helloworld"},
|
|
|
|
"separate-val": {"--unknown", "foo", "helloworld"},
|
|
|
|
"joined-val": {"--unknown=foo", "helloworld"},
|
|
|
|
} {
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
res := icmd.RunCmd(run(args...))
|
|
|
|
res.Assert(t, icmd.Expected{
|
|
|
|
ExitCode: 125,
|
|
|
|
Out: icmd.None,
|
|
|
|
Err: "unknown flag: --unknown",
|
|
|
|
})
|
2019-03-01 11:03:14 -05:00
|
|
|
})
|
allow plugins to have argument which match a top-level flag.
The issue with plugin options clashing with globals is that when cobra is
parsing the command line and it comes across an argument which doesn't start
with a `-` it (in the absence of plugins) distinguishes between "argument to
current command" and "new subcommand" based on the list of registered sub
commands.
Plugins breaks that model. When presented with `docker -D plugin -c foo` cobra
parses up to the `plugin`, sees it isn't a registered sub-command of the
top-level docker (because it isn't, it's a plugin) so it accumulates it as an
argument to the top-level `docker` command. Then it sees the `-c`, and thinks
it is the global `-c` (for AKA `--context`) option and tries to treat it as
that, which fails.
In the specific case of the top-level `docker` subcommand we know that it has
no arguments which aren't `--flags` (or `-f` short flags) and so anything which
doesn't start with a `-` must either be a (known) subcommand or an attempt to
execute a plugin.
We could simply scan for and register all installed plugins at start of day, so
that cobra can do the right thing, but we want to avoid that since it would
involve executing each plugin to fetch the metadata, even if the command wasn't
going to end up hitting a plugin.
Instead we can parse the initial set of global arguments separately before
hitting the main cobra `Execute` path, which works here exactly because we know
that the top-level has no non-flag arguments.
One slight wrinkle is that the top-level `PersistentPreRunE` is no longer
called on the plugins path (since it no longer goes via `Execute`), so we
arrange for the initialisation done there (which has to be done after global
flags are parsed to handle e.g. `--config`) to happen explictly after the
global flags are parsed. Rather than make `newDockerCommand` return the
complicated set of results needed to make this happen, instead return a closure
which achieves this.
The new functionality is introduced via a common `TopLevelCommand` abstraction
which lets us adjust the plugin entrypoint to use the same strategy for parsing
the global arguments. This isn't strictly required (in this case the stuff in
cobra's `Execute` works fine) but doing it this way avoids the possibility of
subtle differences in behaviour.
Fixes #1699, and also, as a side-effect, the first item in #1661.
Signed-off-by: Ian Campbell <ijc@docker.com>
2019-03-06 05:29:42 -05:00
|
|
|
}
|
2019-03-01 11:03:14 -05:00
|
|
|
}
|
2019-03-08 06:16:22 -05:00
|
|
|
|
|
|
|
// TestCliPluginsVersion checks that `-v` and friends DTRT
|
|
|
|
func TestCliPluginsVersion(t *testing.T) {
|
|
|
|
run, _, cleanup := prepare(t)
|
|
|
|
defer cleanup()
|
|
|
|
|
|
|
|
for _, tc := range []struct {
|
|
|
|
name string
|
|
|
|
args []string
|
|
|
|
expCode int
|
|
|
|
expOut, expErr string
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "global-version",
|
|
|
|
args: []string{"version"},
|
|
|
|
expCode: 0,
|
|
|
|
expOut: "Client:\n Version:",
|
|
|
|
expErr: icmd.None,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "global-version-flag",
|
|
|
|
args: []string{"--version"},
|
|
|
|
expCode: 0,
|
|
|
|
expOut: "Docker version",
|
|
|
|
expErr: icmd.None,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "global-short-version-flag",
|
|
|
|
args: []string{"-v"},
|
|
|
|
expCode: 0,
|
|
|
|
expOut: "Docker version",
|
|
|
|
expErr: icmd.None,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "global-with-unknown-arg",
|
|
|
|
args: []string{"version", "foo"},
|
|
|
|
expCode: 1,
|
|
|
|
expOut: icmd.None,
|
|
|
|
expErr: `"docker version" accepts no arguments.`,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "global-with-plugin-arg",
|
|
|
|
args: []string{"version", "helloworld"},
|
|
|
|
expCode: 1,
|
|
|
|
expOut: icmd.None,
|
|
|
|
expErr: `"docker version" accepts no arguments.`,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "global-version-flag-with-unknown-arg",
|
|
|
|
args: []string{"--version", "foo"},
|
|
|
|
expCode: 0,
|
|
|
|
expOut: "Docker version",
|
|
|
|
expErr: icmd.None,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "global-short-version-flag-with-unknown-arg",
|
|
|
|
args: []string{"-v", "foo"},
|
|
|
|
expCode: 0,
|
|
|
|
expOut: "Docker version",
|
|
|
|
expErr: icmd.None,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "global-version-flag-with-plugin",
|
|
|
|
args: []string{"--version", "helloworld"},
|
|
|
|
expCode: 125,
|
|
|
|
expOut: icmd.None,
|
|
|
|
expErr: "unknown flag: --version",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "global-short-version-flag-with-plugin",
|
|
|
|
args: []string{"-v", "helloworld"},
|
|
|
|
expCode: 125,
|
|
|
|
expOut: icmd.None,
|
|
|
|
expErr: "unknown shorthand flag: 'v' in -v",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "plugin-with-version",
|
|
|
|
args: []string{"helloworld", "version"},
|
|
|
|
expCode: 0,
|
|
|
|
expOut: "Hello World!",
|
|
|
|
expErr: icmd.None,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "plugin-with-version-flag",
|
|
|
|
args: []string{"helloworld", "--version"},
|
|
|
|
expCode: 125,
|
|
|
|
expOut: icmd.None,
|
|
|
|
expErr: "unknown flag: --version",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "plugin-with-short-version-flag",
|
|
|
|
args: []string{"helloworld", "-v"},
|
|
|
|
expCode: 125,
|
|
|
|
expOut: icmd.None,
|
|
|
|
expErr: "unknown shorthand flag: 'v' in -v",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "",
|
|
|
|
args: []string{},
|
|
|
|
expCode: 0,
|
|
|
|
expOut: "",
|
|
|
|
expErr: "",
|
|
|
|
},
|
|
|
|
} {
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
res := icmd.RunCmd(run(tc.args...))
|
|
|
|
res.Assert(t, icmd.Expected{
|
|
|
|
ExitCode: tc.expCode,
|
|
|
|
Out: tc.expOut,
|
|
|
|
Err: tc.expErr,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|