From ac854b92fbcf39d615b509be1801980c75ee2bee Mon Sep 17 00:00:00 2001 From: Silvin Lubecki Date: Mon, 25 Feb 2019 15:00:16 +0100 Subject: [PATCH 01/12] =?UTF-8?q?Convert=20docker-app=20to=20a=20docker=20?= =?UTF-8?q?cli=20plugin=20*=20Use=20the=20docker/cli/cli-plugins/plugin.Ru?= =?UTF-8?q?n=20helper=20to=20create=20and=20execute=20the=20commands=20as?= =?UTF-8?q?=20a=20plugin=20*=20Refactor=20e2e=20tests=20to=20use=20?= =?UTF-8?q?=E2=80=9Cdocker=20app=E2=80=9D=20(docker=20cli=20invoking=20doc?= =?UTF-8?q?ker-app=20plugin)=20*=20Add=20CLI=20plugin=20invocation=20e2e?= =?UTF-8?q?=20tests=20*=20Bump=20docker-cli=20to=20include=20the=20plugin?= =?UTF-8?q?=20work=20*=20Bump=20gotest.tools=20to=202.3.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Silvin Lubecki --- Gopkg.lock | 16 +- Gopkg.toml | 2 +- cmd/docker-app/inspect.go | 2 +- cmd/docker-app/install.go | 2 +- cmd/docker-app/main.go | 25 ++- cmd/docker-app/root.go | 35 ++-- cmd/docker-app/status.go | 2 +- cmd/docker-app/uninstall.go | 2 +- cmd/docker-app/upgrade.go | 2 +- e2e/cnab_test.go | 21 ++- e2e/commands_test.go | 109 ++++++------ e2e/example_test.go | 2 +- e2e/helper.go | 77 -------- e2e/helper_test.go | 70 ++++++++ e2e/main_test.go | 40 ++++- e2e/plugin_test.go | 29 ++++ e2e/pushpull_test.go | 35 ++-- e2e/testdata/plugin-usage.golden | 23 +++ internal/packager/extract.go | 2 +- .../docker/cli/cli-plugins/plugin/plugin.go | 164 ++++++++++++++++++ .../cli/cli/command/service/formatter.go | 10 ++ .../docker/cli/cli/command/service/list.go | 13 +- .../docker/cli/cli/command/service/opts.go | 9 + .../docker/cli/cli/command/service/update.go | 4 + 24 files changed, 485 insertions(+), 211 deletions(-) delete mode 100644 e2e/helper.go create mode 100644 e2e/plugin_test.go create mode 100644 e2e/testdata/plugin-usage.golden create mode 100644 vendor/github.com/docker/cli/cli-plugins/plugin/plugin.go diff --git a/Gopkg.lock b/Gopkg.lock index 49dcdfbf8..73a2e1932 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -176,7 +176,7 @@ revision = "461401dc8f19d80baa4b70178935e4501286c00b" [[projects]] - digest = "1:76a167b7658125360bbd52626d85031c89383c0ec6f3e4e25afd776c9a27f286" + digest = "1:9e2dbc126e8e5314cba3ab654b2183376dc81cb657ca8c588f86990f5da88cbc" name = "github.com/coreos/etcd" packages = ["raft/raftpb"] pruneopts = "NUT" @@ -210,11 +210,12 @@ revision = "eaa7595bd231ca353ec886708405fe4deba91968" [[projects]] - digest = "1:835103dce2493c1d69db6374052729fef1716ec30eb0e2050d868ca531f7cccf" + digest = "1:0b5bf13e4971fc511f8ee5b4f13490ece442bcd16c079bcb372005c553fb886f" name = "github.com/docker/cli" packages = [ "cli", "cli-plugins/manager", + "cli-plugins/plugin", "cli/command", "cli/command/bundlefile", "cli/command/formatter", @@ -263,7 +264,7 @@ "types", ] pruneopts = "UT" - revision = "06b837a7d7e1115f3d2aa65c47765e25d4bf845b" + revision = "3ddb3133f5b5a74dd4e3f721ced21f4d0b9651b6" [[projects]] branch = "master" @@ -322,7 +323,7 @@ [[projects]] branch = "master" - digest = "1:4f55cc4abd8ca6cafba0028f9b8848334210a62dd5beea880a3bc4f635dba093" + digest = "1:f6a479179001d036487879645957a865d6692d6c634a562b62561eecff892640" name = "github.com/docker/docker" packages = [ "api", @@ -1074,7 +1075,7 @@ source = "https://github.com/simonferquel/yaml" [[projects]] - digest = "1:fba52eeb003a81a7ad2eb9c92f139c43a8b0a4e8cc51c049fd03a2eb8f6894ae" + digest = "1:8eed93b935c1abf503719923397fce5258a94a7834f35323e41c5c61b9e7bfe6" name = "gotest.tools" packages = [ "assert", @@ -1243,7 +1244,7 @@ revision = "kubernetes-1.11.1" [[projects]] - digest = "1:34af467590a9aa09701c1c3bea1fa6cdd5e8a8478e0baff9b8f6d4ba9518a42d" + digest = "1:18254705f2e912ec0142b7b36546c5d38d74e9d137c8d243985bff4d247eed81" name = "k8s.io/kubernetes" packages = ["pkg/api/v1/pod"] pruneopts = "NUT" @@ -1262,6 +1263,7 @@ analyzer-version = 1 input-imports = [ "github.com/cbroglie/mustache", + "github.com/containerd/containerd/platforms", "github.com/deislabs/duffle/pkg/action", "github.com/deislabs/duffle/pkg/bundle", "github.com/deislabs/duffle/pkg/claim", @@ -1271,6 +1273,8 @@ "github.com/deislabs/duffle/pkg/loader", "github.com/deislabs/duffle/pkg/utils/crud", "github.com/docker/cli/cli", + "github.com/docker/cli/cli-plugins/manager", + "github.com/docker/cli/cli-plugins/plugin", "github.com/docker/cli/cli/command", "github.com/docker/cli/cli/command/stack", "github.com/docker/cli/cli/command/stack/options", diff --git a/Gopkg.toml b/Gopkg.toml index 1097d6003..cb29ffd95 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -38,7 +38,7 @@ required = ["github.com/wadey/gocovmerge"] [[override]] name = "github.com/docker/cli" - revision = "06b837a7d7e1115f3d2aa65c47765e25d4bf845b" + revision = "3ddb3133f5b5a74dd4e3f721ced21f4d0b9651b6" [[override]] name = "github.com/deislabs/duffle" diff --git a/cmd/docker-app/inspect.go b/cmd/docker-app/inspect.go index bf02b1206..aa9cf3d1d 100644 --- a/cmd/docker-app/inspect.go +++ b/cmd/docker-app/inspect.go @@ -33,7 +33,7 @@ func inspectCmd(dockerCli command.Cli) *cobra.Command { } func runInspect(dockerCli command.Cli, appname string, opts inspectOptions) error { - muteDockerCli(dockerCli) + defer muteDockerCli(dockerCli)() c, err := claim.New("inspect") if err != nil { diff --git a/cmd/docker-app/install.go b/cmd/docker-app/install.go index b6649f19b..b14efdcd7 100644 --- a/cmd/docker-app/install.go +++ b/cmd/docker-app/install.go @@ -68,7 +68,7 @@ func installCmd(dockerCli command.Cli) *cobra.Command { } func runInstall(dockerCli command.Cli, appname string, opts installOptions) error { - muteDockerCli(dockerCli) + defer muteDockerCli(dockerCli)() if opts.sendRegistryAuth { return errors.New("with-registry-auth is not supported at the moment") } diff --git a/cmd/docker-app/main.go b/cmd/docker-app/main.go index 4a1c1e45c..51a0e5965 100644 --- a/cmd/docker-app/main.go +++ b/cmd/docker-app/main.go @@ -1,22 +1,19 @@ package main import ( - "fmt" - "os" - + "github.com/docker/app/internal" + "github.com/docker/cli/cli-plugins/manager" + "github.com/docker/cli/cli-plugins/plugin" "github.com/docker/cli/cli/command" - "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) func main() { - dockerCli, err := command.NewDockerCli() - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - logrus.SetOutput(dockerCli.Err()) - cmd := newRootCmd(dockerCli) - if err := cmd.Execute(); err != nil { - os.Exit(1) - } + plugin.Run(func(dockerCli command.Cli) *cobra.Command { + return newRootCmd(dockerCli) + }, manager.Metadata{ + SchemaVersion: "0.1.0", + Vendor: "Docker Inc.", + Version: internal.Version, + }) } diff --git a/cmd/docker-app/root.go b/cmd/docker-app/root.go index 39e30c4e0..ea1593d66 100644 --- a/cmd/docker-app/root.go +++ b/cmd/docker-app/root.go @@ -1,39 +1,21 @@ package main import ( - "fmt" "io/ioutil" - "github.com/docker/app/internal" - "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" - cliflags "github.com/docker/cli/cli/flags" "github.com/spf13/cobra" "github.com/spf13/pflag" ) // rootCmd represents the base command when called without any subcommands -func newRootCmd(dockerCli *command.DockerCli) *cobra.Command { - var ( - opts *cliflags.ClientOptions - flags *pflag.FlagSet - ) - +// FIXME(vdemeester) use command.Cli interface +func newRootCmd(dockerCli command.Cli) *cobra.Command { cmd := &cobra.Command{ - Use: "docker-app", - Short: "Docker Application Packages", - Long: `Build and deploy Docker Application Packages.`, - SilenceUsage: true, - TraverseChildren: true, - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - opts.Common.SetDefaultOptions(flags) - return dockerCli.Initialize(opts) - }, - Version: fmt.Sprintf("%s, build %s", internal.Version, internal.GitCommit), + Use: "app", + Short: "Docker Application Packages", + Long: `Build and deploy Docker Application Packages.`, } - opts, flags, _ = cli.SetupRootCommand(cmd) - flags.BoolP("version", "v", false, "Print version information") - cmd.SetVersionTemplate("docker-app version {{.Version}}\n") addCommands(cmd, dockerCli) return cmd } @@ -66,8 +48,13 @@ func firstOrEmpty(list []string) string { return "" } -func muteDockerCli(dockerCli command.Cli) { +func muteDockerCli(dockerCli command.Cli) func() { + stdout := dockerCli.Out() + stderr := dockerCli.Err() dockerCli.Apply(command.WithCombinedStreams(ioutil.Discard)) + return func() { + dockerCli.Apply(command.WithOutputStream(stdout), command.WithErrorStream(stderr)) + } } type parametersOptions struct { diff --git a/cmd/docker-app/status.go b/cmd/docker-app/status.go index 873f11225..d51fb5329 100644 --- a/cmd/docker-app/status.go +++ b/cmd/docker-app/status.go @@ -29,7 +29,7 @@ func statusCmd(dockerCli command.Cli) *cobra.Command { } func runStatus(dockerCli command.Cli, claimName string, opts credentialOptions) error { - muteDockerCli(dockerCli) + defer muteDockerCli(dockerCli)() h := duffleHome() claimStore := claim.NewClaimStore(crud.NewFileSystemStore(h.Claims(), "json")) diff --git a/cmd/docker-app/uninstall.go b/cmd/docker-app/uninstall.go index 2bc573336..f49cd44d4 100644 --- a/cmd/docker-app/uninstall.go +++ b/cmd/docker-app/uninstall.go @@ -29,7 +29,7 @@ func uninstallCmd(dockerCli command.Cli) *cobra.Command { } func runUninstall(dockerCli command.Cli, claimName string, opts credentialOptions) error { - muteDockerCli(dockerCli) + defer muteDockerCli(dockerCli)() h := duffleHome() claimStore := claim.NewClaimStore(crud.NewFileSystemStore(h.Claims(), "json")) diff --git a/cmd/docker-app/upgrade.go b/cmd/docker-app/upgrade.go index 4c8c3b143..31811339f 100644 --- a/cmd/docker-app/upgrade.go +++ b/cmd/docker-app/upgrade.go @@ -39,7 +39,7 @@ func upgradeCmd(dockerCli command.Cli) *cobra.Command { } func runUpgrade(dockerCli command.Cli, installationName string, opts upgradeOptions) error { - muteDockerCli(dockerCli) + defer muteDockerCli(dockerCli)() targetContext := getTargetContext(opts.targetContext, dockerCli.CurrentContext()) h := duffleHome() claimStore := claim.NewClaimStore(crud.NewFileSystemStore(h.Claims(), "json")) diff --git a/e2e/cnab_test.go b/e2e/cnab_test.go index 1e2facb32..d59781cf0 100644 --- a/e2e/cnab_test.go +++ b/e2e/cnab_test.go @@ -2,6 +2,7 @@ package e2e import ( "fmt" + "os" "path" "runtime" "testing" @@ -28,7 +29,7 @@ func TestCallCustomStatusAction(t *testing.T) { { name: "missingCustomStatusAction", exitCode: 1, - expectedOutput: "Error: Status failed: action not defined for bundle", + expectedOutput: "Status failed: action not defined for bundle", cnab: "cnab-without-status", }, } @@ -38,9 +39,7 @@ func TestCallCustomStatusAction(t *testing.T) { tmpDir := fs.NewDir(t, t.Name()) defer tmpDir.Remove() testDir := path.Join("testdata", testCase.cnab) - cmd := icmd.Cmd{ - Env: []string{fmt.Sprintf("DUFFLE_HOME=%s", tmpDir.Path())}, - } + cmd := icmd.Cmd{Env: append(os.Environ(), fmt.Sprintf("DUFFLE_HOME=%s", tmpDir.Path()))} // We need to explicitly set the SYSTEMROOT on windows // otherwise we get the error: @@ -50,21 +49,21 @@ func TestCallCustomStatusAction(t *testing.T) { cmd.Env = append(cmd.Env, `SYSTEMROOT=C:\WINDOWS`) } // Build CNAB invocation image - cmd.Command = []string{"docker", "build", "-f", path.Join(testDir, "cnab", "build", "Dockerfile"), "-t", fmt.Sprintf("e2e/%s:v0.1.0", testCase.cnab), testDir} + cmd.Command = dockerCli.Command("build", "--file", path.Join(testDir, "cnab", "build", "Dockerfile"), "--tag", fmt.Sprintf("e2e/%s:v0.1.0", testCase.cnab), testDir) icmd.RunCmd(cmd).Assert(t, icmd.Success) - // docker-app install - cmd.Command = []string{dockerApp, "install", path.Join(testDir, "bundle.json"), "--name", testCase.name} + // docker app install + cmd.Command = dockerCli.Command("app", "install", path.Join(testDir, "bundle.json"), "--name", testCase.name) icmd.RunCmd(cmd).Assert(t, icmd.Success) - // docker-app uninstall + // docker app uninstall defer func() { - cmd.Command = []string{dockerApp, "uninstall", testCase.name} + cmd.Command = dockerCli.Command("app", "uninstall", testCase.name) icmd.RunCmd(cmd).Assert(t, icmd.Success) }() - // docker-app status - cmd.Command = []string{dockerApp, "status", testCase.name} + // docker app status + cmd.Command = dockerCli.Command("app", "status", testCase.name) result := icmd.RunCmd(cmd) result.Assert(t, icmd.Expected{ExitCode: testCase.exitCode}) assert.Assert(t, is.Contains(result.Combined(), testCase.expectedOutput)) diff --git a/e2e/commands_test.go b/e2e/commands_test.go index ff5fe7817..ae4df9af5 100644 --- a/e2e/commands_test.go +++ b/e2e/commands_test.go @@ -50,11 +50,9 @@ func testRenderApp(appPath string, env ...string) func(*testing.T) { data, err := ioutil.ReadFile(filepath.Join(appPath, "env.yml")) assert.NilError(t, err) assert.NilError(t, yaml.Unmarshal(data, &envParameters)) - args := []string{dockerApp, "render", filepath.Join(appPath, "my.dockerapp"), - "-f", filepath.Join(appPath, "parameters-0.yml"), - } + args := dockerCli.Command("app", "render", filepath.Join(appPath, "my.dockerapp"), "--parameters-files", filepath.Join(appPath, "parameters-0.yml")) for k, v := range envParameters { - args = append(args, "-s", fmt.Sprintf("%s=%s", k, v)) + args = append(args, "--set", fmt.Sprintf("%s=%s", k, v)) } result := icmd.RunCmd(icmd.Cmd{ Command: args, @@ -66,10 +64,12 @@ func testRenderApp(appPath string, env ...string) func(*testing.T) { func TestRenderFormatters(t *testing.T) { appPath := filepath.Join("testdata", "simple", "simple.dockerapp") - result := icmd.RunCommand(dockerApp, "render", "--formatter", "json", appPath).Assert(t, icmd.Success) + cmd := icmd.Cmd{Command: dockerCli.Command("app", "render", "--formatter", "json", appPath)} + result := icmd.RunCmd(cmd).Assert(t, icmd.Success) golden.Assert(t, result.Stdout(), "expected-json-render.golden") - result = icmd.RunCommand(dockerApp, "render", "--formatter", "yaml", appPath).Assert(t, icmd.Success) + cmd.Command = dockerCli.Command("app", "render", "--formatter", "yaml", appPath) + result = icmd.RunCmd(cmd).Assert(t, icmd.Success) golden.Assert(t, result.Stdout(), "expected-yaml-render.golden") } @@ -105,12 +105,12 @@ maintainers: cmd := icmd.Cmd{Dir: tmpDir.Path()} - cmd.Command = []string{dockerApp, + cmd.Command = dockerCli.Command("app", "init", testAppName, - "-c", tmpDir.Join(internal.ComposeFileName), - "-d", "my cool app", - "-m", "bob", - "-m", "joe:joe@joe.com"} + "--compose-file", tmpDir.Join(internal.ComposeFileName), + "--description", "my cool app", + "--maintainer", "bob", + "--maintainer", "joe:joe@joe.com") icmd.RunCmd(cmd).Assert(t, icmd.Success) manifest := fs.Expected( @@ -123,28 +123,28 @@ maintainers: assert.Assert(t, fs.Equal(tmpDir.Join(dirName), manifest)) // validate metadata with JSON Schema - cmd.Command = []string{dockerApp, "validate", testAppName} + cmd.Command = dockerCli.Command("app", "validate", testAppName) icmd.RunCmd(cmd).Assert(t, icmd.Success) // test single-file init - cmd.Command = []string{dockerApp, + cmd.Command = dockerCli.Command("app", "init", "tac", - "-c", tmpDir.Join(internal.ComposeFileName), - "-d", "my cool app", - "-m", "bob", - "-m", "joe:joe@joe.com", - "-s", - } + "--compose-file", tmpDir.Join(internal.ComposeFileName), + "--description", "my cool app", + "--maintainer", "bob", + "--maintainer", "joe:joe@joe.com", + "--single-file", + ) icmd.RunCmd(cmd).Assert(t, icmd.Success) appData, err := ioutil.ReadFile(tmpDir.Join("tac.dockerapp")) assert.NilError(t, err) golden.Assert(t, string(appData), "init-singlefile.dockerapp") // Check various commands work on single-file app package - cmd.Command = []string{dockerApp, "inspect", "tac"} + cmd.Command = dockerCli.Command("app", "inspect", "tac") icmd.RunCmd(cmd).Assert(t, icmd.Success) - cmd.Command = []string{dockerApp, "render", "tac"} + cmd.Command = dockerCli.Command("app", "render", "tac") icmd.RunCmd(cmd).Assert(t, icmd.Success) } @@ -159,19 +159,19 @@ func TestDetectApp(t *testing.T) { ) defer dir.Remove() icmd.RunCmd(icmd.Cmd{ - Command: []string{dockerApp, "inspect"}, + Command: dockerCli.Command("app", "inspect"), Dir: dir.Path(), }).Assert(t, icmd.Success) icmd.RunCmd(icmd.Cmd{ - Command: []string{dockerApp, "inspect"}, + Command: dockerCli.Command("app", "inspect"), Dir: dir.Join("attachments.dockerapp"), }).Assert(t, icmd.Success) icmd.RunCmd(icmd.Cmd{ - Command: []string{dockerApp, "inspect", "."}, + Command: dockerCli.Command("app", "inspect", "."), Dir: dir.Join("attachments.dockerapp"), }).Assert(t, icmd.Success) result := icmd.RunCmd(icmd.Cmd{ - Command: []string{dockerApp, "inspect"}, + Command: dockerCli.Command("app", "inspect"), Dir: dir.Join("render"), }) result.Assert(t, icmd.Expected{ @@ -184,28 +184,29 @@ func TestSplitMerge(t *testing.T) { tmpDir := fs.NewDir(t, "split_merge") defer tmpDir.Remove() - icmd.RunCommand(dockerApp, "merge", "testdata/render/envvariables/my.dockerapp", "-o", tmpDir.Join("remerged.dockerapp")).Assert(t, icmd.Success) + cmd := icmd.Cmd{Command: dockerCli.Command("app", "merge", "testdata/render/envvariables/my.dockerapp", "--output", tmpDir.Join("remerged.dockerapp"))} + icmd.RunCmd(cmd).Assert(t, icmd.Success) - cmd := icmd.Cmd{Dir: tmpDir.Path()} + cmd.Dir = tmpDir.Path() // test that inspect works on single-file - cmd.Command = []string{dockerApp, "inspect", "remerged"} + cmd.Command = dockerCli.Command("app", "inspect", "remerged") result := icmd.RunCmd(cmd).Assert(t, icmd.Success) golden.Assert(t, result.Combined(), "envvariables-inspect.golden") // split it - cmd.Command = []string{dockerApp, "split", "remerged", "-o", "split.dockerapp"} + cmd.Command = dockerCli.Command("app", "split", "remerged", "--output", "split.dockerapp") icmd.RunCmd(cmd).Assert(t, icmd.Success) - cmd.Command = []string{dockerApp, "inspect", "remerged"} + cmd.Command = dockerCli.Command("app", "inspect", "remerged") result = icmd.RunCmd(cmd).Assert(t, icmd.Success) golden.Assert(t, result.Combined(), "envvariables-inspect.golden") // test inplace - cmd.Command = []string{dockerApp, "merge", "split"} + cmd.Command = dockerCli.Command("app", "merge", "split") icmd.RunCmd(cmd).Assert(t, icmd.Success) - cmd.Command = []string{dockerApp, "split", "split"} + cmd.Command = dockerCli.Command("app", "split", "split") icmd.RunCmd(cmd).Assert(t, icmd.Success) } @@ -221,34 +222,34 @@ func TestBundle(t *testing.T) { defer dind.Stop(t) // Create a build context - cmd.Command = []string{dockerCli, "context", "create", "build-context", "--docker", fmt.Sprintf(`"host=tcp://%s"`, dind.GetAddress(t))} + cmd.Command = dockerCli.Command("context", "create", "build-context", "--docker", fmt.Sprintf(`"host=tcp://%s"`, dind.GetAddress(t))) icmd.RunCmd(cmd).Assert(t, icmd.Success) // The dind doesn't have the cnab-app-base image so we save it in order to load it later - cmd.Command = []string{dockerCli, "save", fmt.Sprintf("docker/cnab-app-base:%s", internal.Version), "-o", tmpDir.Join("cnab-app-base.tar.gz")} + cmd.Command = dockerCli.Command("save", fmt.Sprintf("docker/cnab-app-base:%s", internal.Version), "--output", tmpDir.Join("cnab-app-base.tar.gz")) icmd.RunCmd(cmd).Assert(t, icmd.Success) cmd.Env = append(cmd.Env, "DOCKER_CONTEXT=build-context") - cmd.Command = []string{dockerCli, "load", "-i", tmpDir.Join("cnab-app-base.tar.gz")} + cmd.Command = dockerCli.Command("load", "-i", tmpDir.Join("cnab-app-base.tar.gz")) icmd.RunCmd(cmd).Assert(t, icmd.Success) // Bundle the docker application package to a CNAB bundle, using the build-context. - cmd.Command = []string{dockerApp, "bundle", filepath.Join("testdata", "simple", "simple.dockerapp"), "--out", tmpDir.Join("bundle.json")} + cmd.Command = dockerCli.Command("app", "bundle", filepath.Join("testdata", "simple", "simple.dockerapp"), "--out", tmpDir.Join("bundle.json")) icmd.RunCmd(cmd).Assert(t, icmd.Success) // Check the resulting CNAB bundle.json golden.Assert(t, string(golden.Get(t, tmpDir.Join("bundle.json"))), "simple-bundle.json.golden") // List the images on the build context daemon and checks the invocation image is there - cmd.Command = []string{dockerCli, "image", "ls", "--format", "{{.Repository}}:{{.Tag}}"} + cmd.Command = dockerCli.Command("image", "ls", "--format", "{{.Repository}}:{{.Tag}}") icmd.RunCmd(cmd).Assert(t, icmd.Expected{ExitCode: 0, Out: "simple:1.1.0-beta1-invoc"}) // Copy all the files from the invocation image and check them - cmd.Command = []string{dockerCli, "create", "--name", "invocation", "simple:1.1.0-beta1-invoc"} + cmd.Command = dockerCli.Command("create", "--name", "invocation", "simple:1.1.0-beta1-invoc") id := strings.TrimSpace(icmd.RunCmd(cmd).Assert(t, icmd.Success).Stdout()) - cmd.Command = []string{dockerCli, "cp", "invocation:/cnab/app/simple.dockerapp", tmpDir.Join("simple.dockerapp")} + cmd.Command = dockerCli.Command("cp", "invocation:/cnab/app/simple.dockerapp", tmpDir.Join("simple.dockerapp")) icmd.RunCmd(cmd).Assert(t, icmd.Success) - cmd.Command = []string{dockerCli, "rm", "--force", id} + cmd.Command = dockerCli.Command("rm", "--force", id) icmd.RunCmd(cmd).Assert(t, icmd.Success) appDir := filepath.Join("testdata", "simple", "simple.dockerapp") @@ -282,33 +283,41 @@ func TestDockerAppLifecycle(t *testing.T) { defer swarm.Stop(t) // The dind doesn't have the cnab-app-base image so we save it in order to load it later - icmd.RunCommand(dockerCli, "save", fmt.Sprintf("docker/cnab-app-base:%s", internal.Version), "-o", tmpDir.Join("cnab-app-base.tar.gz")).Assert(t, icmd.Success) + icmd.RunCommand(dockerCli.path, "save", fmt.Sprintf("docker/cnab-app-base:%s", internal.Version), "--output", tmpDir.Join("cnab-app-base.tar.gz")).Assert(t, icmd.Success) // We need two contexts: // - one for `docker` so that it connects to the dind swarm created before // - the target context for the invocation image to install within the swarm - cmd.Command = []string{dockerCli, "context", "create", "swarm-context", "--docker", fmt.Sprintf(`"host=tcp://%s"`, swarm.GetAddress(t)), "--default-stack-orchestrator", "swarm"} + cmd.Command = dockerCli.Command("context", "create", "swarm-context", "--docker", fmt.Sprintf(`"host=tcp://%s"`, swarm.GetAddress(t)), "--default-stack-orchestrator", "swarm") icmd.RunCmd(cmd).Assert(t, icmd.Success) + defer func() { + cmd.Command = dockerCli.Command("context", "rm", "--force", "swarm-context") + icmd.RunCmd(cmd) + }() // When creating a context on a Windows host we cannot use // the unix socket but it's needed inside the invocation image. // The workaround is to create a context with an empty host. // This host will default to the unix socket inside the // invocation image - cmd.Command = []string{dockerCli, "context", "create", "swarm-target-context", "--docker", "host=", "--default-stack-orchestrator", "swarm"} + cmd.Command = dockerCli.Command("context", "create", "swarm-target-context", "--docker", "host=", "--default-stack-orchestrator", "swarm") icmd.RunCmd(cmd).Assert(t, icmd.Success) + defer func() { + cmd.Command = dockerCli.Command("context", "rm", "--force", "swarm-target-context") + icmd.RunCmd(cmd) + }() // Initialize the swarm cmd.Env = append(cmd.Env, "DOCKER_CONTEXT=swarm-context") - cmd.Command = []string{dockerCli, "swarm", "init"} + cmd.Command = dockerCli.Command("swarm", "init") icmd.RunCmd(cmd).Assert(t, icmd.Success) // Load the needed base cnab image into the swarm docker engine - cmd.Command = []string{dockerCli, "load", "-i", tmpDir.Join("cnab-app-base.tar.gz")} + cmd.Command = dockerCli.Command("load", "--input", tmpDir.Join("cnab-app-base.tar.gz")) icmd.RunCmd(cmd).Assert(t, icmd.Success) // Install a Docker Application Package - cmd.Command = []string{dockerApp, "install", "testdata/simple/simple.dockerapp", "--name", t.Name()} + cmd.Command = dockerCli.Command("app", "install", "testdata/simple/simple.dockerapp", "--name", t.Name()) checkContains(t, icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined(), []string{ fmt.Sprintf("Creating network %s_back", t.Name()), @@ -319,7 +328,7 @@ func TestDockerAppLifecycle(t *testing.T) { }) // Query the application status - cmd.Command = []string{dockerApp, "status", t.Name()} + cmd.Command = dockerCli.Command("app", "status", t.Name()) checkContains(t, icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined(), []string{ fmt.Sprintf("[[:alnum:]]+ %s_db replicated [0-1]/1 postgres:9.3", t.Name()), @@ -328,7 +337,7 @@ func TestDockerAppLifecycle(t *testing.T) { }) // Upgrade the application, changing the port - cmd.Command = []string{dockerApp, "upgrade", t.Name(), "--set", "web_port=8081"} + cmd.Command = dockerCli.Command("app", "upgrade", t.Name(), "--set", "web_port=8081") checkContains(t, icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined(), []string{ fmt.Sprintf("Updating service %s_db", t.Name()), @@ -337,11 +346,11 @@ func TestDockerAppLifecycle(t *testing.T) { }) // Query the application status again, the port should have change - cmd.Command = []string{dockerApp, "status", t.Name()} + cmd.Command = dockerCli.Command("app", "status", t.Name()) icmd.RunCmd(cmd).Assert(t, icmd.Expected{ExitCode: 0, Out: "8081"}) // Uninstall the application - cmd.Command = []string{dockerApp, "uninstall", t.Name()} + cmd.Command = dockerCli.Command("app", "uninstall", t.Name()) checkContains(t, icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined(), []string{ fmt.Sprintf("Removing service %s_api", t.Name()), diff --git a/e2e/example_test.go b/e2e/example_test.go index 7e9634b69..c600c76fa 100644 --- a/e2e/example_test.go +++ b/e2e/example_test.go @@ -24,7 +24,7 @@ func TestExamplesAreValid(t *testing.T) { case os.IsNotExist(statErr): return nil default: - result := icmd.RunCommand(dockerApp, "validate", appPath) + result := icmd.RunCmd(icmd.Cmd{Command: dockerCli.Command("app", "validate", appPath)}) result.Assert(t, icmd.Success) return filepath.SkipDir } diff --git a/e2e/helper.go b/e2e/helper.go deleted file mode 100644 index 9a5037561..000000000 --- a/e2e/helper.go +++ /dev/null @@ -1,77 +0,0 @@ -package e2e - -import ( - "fmt" - "strconv" - "strings" - "testing" - "time" - - "gotest.tools/icmd" -) - -// Container represents a docker container -type Container struct { - image string - privatePort int - address string - container string - parentContainer string - args []string -} - -// NewContainer creates a new Container -func NewContainer(image string, privatePort int, args ...string) *Container { - return &Container{ - image: image, - privatePort: privatePort, - args: args, - } -} - -// Start starts a new docker container on a random port -func (c *Container) Start(t *testing.T, dockerArgs ...string) { - args := []string{"run", "--rm", "--privileged", "-d", "-P"} - args = append(args, dockerArgs...) - args = append(args, c.image) - args = append(args, c.args...) - result := icmd.RunCommand("docker", args...).Assert(t, icmd.Success) - c.container = strings.Trim(result.Stdout(), " \r\n") - time.Sleep(time.Second * 3) -} - -// StartWithContainerNetwork starts a new container using an existing container network -func (c *Container) StartWithContainerNetwork(t *testing.T, other *Container, dockerArgs ...string) { - args := []string{"run", "--rm", "--privileged", "-d", "--network=container:" + other.container} - args = append(args, dockerArgs...) - args = append(args, c.image) - args = append(args, c.args...) - result := icmd.RunCommand("docker", args...).Assert(t, icmd.Success) - c.container = strings.Trim(result.Stdout(), " \r\n") - time.Sleep(time.Second * 3) - c.parentContainer = other.container -} - -// Stop terminates this container -func (c *Container) Stop(t *testing.T) { - icmd.RunCommand("docker", "stop", c.container).Assert(t, icmd.Success) -} - -// StopNoFail terminates this container -func (c *Container) StopNoFail() { - icmd.RunCommand("docker", "stop", c.container) -} - -// GetAddress returns the host:port this container listens on -func (c *Container) GetAddress(t *testing.T) string { - if c.address != "" { - return c.address - } - container := c.parentContainer - if container == "" { - container = c.container - } - result := icmd.RunCommand("docker", "port", container, strconv.Itoa(c.privatePort)).Assert(t, icmd.Success) - c.address = fmt.Sprintf("127.0.0.1:%v", strings.Trim(strings.Split(result.Stdout(), ":")[1], " \r\n")) - return c.address -} diff --git a/e2e/helper_test.go b/e2e/helper_test.go index a3d38a91a..224ee156e 100644 --- a/e2e/helper_test.go +++ b/e2e/helper_test.go @@ -1,11 +1,15 @@ package e2e import ( + "fmt" "io/ioutil" + "strconv" "strings" "testing" + "time" "gotest.tools/assert" + "gotest.tools/icmd" ) // readFile returns the content of the file at the designated path normalizing @@ -27,3 +31,69 @@ func checkRenderers(appname string, enabled string) bool { } return true } + +// Container represents a docker container +type Container struct { + image string + privatePort int + address string + container string + parentContainer string + args []string +} + +// NewContainer creates a new Container +func NewContainer(image string, privatePort int, args ...string) *Container { + return &Container{ + image: image, + privatePort: privatePort, + args: args, + } +} + +// Start starts a new docker container on a random port +func (c *Container) Start(t *testing.T, dockerArgs ...string) { + args := []string{"run", "--rm", "--privileged", "-d", "-P"} + args = append(args, dockerArgs...) + args = append(args, c.image) + args = append(args, c.args...) + result := icmd.RunCommand(dockerCli.path, args...).Assert(t, icmd.Success) + c.container = strings.Trim(result.Stdout(), " \r\n") + time.Sleep(time.Second * 3) +} + +// StartWithContainerNetwork starts a new container using an existing container network +func (c *Container) StartWithContainerNetwork(t *testing.T, other *Container, dockerArgs ...string) { + args := []string{"run", "--rm", "--privileged", "-d", "--network=container:" + other.container} + args = append(args, dockerArgs...) + args = append(args, c.image) + args = append(args, c.args...) + result := icmd.RunCommand(dockerCli.path, args...).Assert(t, icmd.Success) + c.container = strings.Trim(result.Stdout(), " \r\n") + time.Sleep(time.Second * 3) + c.parentContainer = other.container +} + +// Stop terminates this container +func (c *Container) Stop(t *testing.T) { + icmd.RunCommand(dockerCli.path, "stop", c.container).Assert(t, icmd.Success) +} + +// StopNoFail terminates this container +func (c *Container) StopNoFail() { + icmd.RunCommand(dockerCli.path, "stop", c.container) +} + +// GetAddress returns the host:port this container listens on +func (c *Container) GetAddress(t *testing.T) string { + if c.address != "" { + return c.address + } + container := c.parentContainer + if container == "" { + container = c.container + } + result := icmd.RunCommand(dockerCli.path, "port", container, strconv.Itoa(c.privatePort)).Assert(t, icmd.Success) + c.address = fmt.Sprintf("127.0.0.1:%v", strings.Trim(strings.Split(result.Stdout(), ":")[1], " \r\n")) + return c.address +} diff --git a/e2e/main_test.go b/e2e/main_test.go index 23cbd0eab..3252ed867 100644 --- a/e2e/main_test.go +++ b/e2e/main_test.go @@ -3,6 +3,8 @@ package e2e import ( "bytes" "flag" + "fmt" + "io/ioutil" "os" "os/exec" "path/filepath" @@ -12,12 +14,25 @@ import ( var ( e2ePath = flag.String("e2e-path", ".", "Set path to the e2e directory") - dockerApp = os.Getenv("DOCKERAPP_BINARY") - dockerCli = os.Getenv("DOCKERCLI_BINARY") + dockerCliPath = os.Getenv("DOCKERCLI_BINARY") hasExperimental = false renderers = "" + dockerCli dockerCliCommand ) +const config = `{ + "cliPluginsExtraDirs": ["%s"] +}` + +type dockerCliCommand struct { + path string + config string +} + +func (d dockerCliCommand) Command(args ...string) []string { + return append([]string{d.path, "--config", d.config}, args...) +} + func TestMain(m *testing.M) { flag.Parse() if err := os.Chdir(*e2ePath); err != nil { @@ -27,6 +42,7 @@ func TestMain(m *testing.M) { if err != nil { panic(err) } + dockerApp := os.Getenv("DOCKERAPP_BINARY") if dockerApp == "" { dockerApp = filepath.Join(cwd, "../bin/docker-app") } @@ -34,10 +50,24 @@ func TestMain(m *testing.M) { if err != nil { panic(err) } - if dockerCli == "" { - dockerCli = "docker" + // Prepare docker cli to call the docker-app plugin binary: + // - Create a config dir with a custom config file + // - Create a symbolic link with the dockerApp binary to the plugin directory + if dockerCliPath == "" { + dockerCliPath = "docker" } - cmd := exec.Command(dockerApp, "version") + configDir, err := ioutil.TempDir("", "config") + if err != nil { + panic(err.Error()) + } + defer os.RemoveAll(configDir) + dockerCli = dockerCliCommand{path: dockerCliPath, config: configDir} + ioutil.WriteFile(filepath.Join(configDir, "config.json"), []byte(fmt.Sprintf(config, configDir)), 0644) + if err := os.Symlink(dockerApp, filepath.Join(configDir, "docker-app")); err != nil { + panic(err.Error()) + } + + cmd := exec.Command(dockerApp, "app", "version") output, err := cmd.CombinedOutput() if err != nil { panic(err) diff --git a/e2e/plugin_test.go b/e2e/plugin_test.go new file mode 100644 index 000000000..fb49e8b51 --- /dev/null +++ b/e2e/plugin_test.go @@ -0,0 +1,29 @@ +package e2e + +import ( + "regexp" + "testing" + + "gotest.tools/assert" + "gotest.tools/golden" + "gotest.tools/icmd" +) + +func TestInvokePluginFromCLI(t *testing.T) { + // docker --help should list app as a top command + cmd := icmd.Cmd{Command: dockerCli.Command("--help")} + icmd.RunCmd(cmd).Assert(t, icmd.Expected{ + Out: "app* Docker Application Packages (Docker Inc.,", + }) + + // docker app --help prints docker-app help + cmd.Command = dockerCli.Command("app", "--help") + usage := icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined() + golden.Assert(t, usage, "plugin-usage.golden") + + // docker info should print app version and short description + cmd.Command = dockerCli.Command("info") + re := regexp.MustCompile(`app: Docker Application Packages \(Docker Inc\., .*\)`) + output := icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined() + assert.Assert(t, re.MatchString(output)) +} diff --git a/e2e/pushpull_test.go b/e2e/pushpull_test.go index d8c17bcad..953ef2fc8 100644 --- a/e2e/pushpull_test.go +++ b/e2e/pushpull_test.go @@ -38,7 +38,8 @@ func runWithDindSwarmAndRegistry(t *testing.T, todo func(dindSwarmAndRegistryInf } // The dind doesn't have the cnab-app-base image so we save it in order to load it later - icmd.RunCommand(dockerCli, "save", fmt.Sprintf("docker/cnab-app-base:%s", internal.Version), "-o", tmpDir.Join("cnab-app-base.tar.gz")).Assert(t, icmd.Success) + saveCmd := icmd.Cmd{Command: dockerCli.Command("save", fmt.Sprintf("docker/cnab-app-base:%s", internal.Version), "-o", tmpDir.Join("cnab-app-base.tar.gz"))} + icmd.RunCmd(saveCmd).Assert(t, icmd.Success) // we have a difficult constraint here: // - the registry must be reachable from the client side (for cnab-to-oci, which does not use the docker daemon to access the registry) @@ -60,23 +61,31 @@ func runWithDindSwarmAndRegistry(t *testing.T, todo func(dindSwarmAndRegistryInf // We need two contexts: // - one for `docker` so that it connects to the dind swarm created before // - the target context for the invocation image to install within the swarm - cmd.Command = []string{dockerCli, "context", "create", "swarm-context", "--docker", fmt.Sprintf(`"host=tcp://%s"`, swarm.GetAddress(t)), "--default-stack-orchestrator", "swarm"} + cmd.Command = dockerCli.Command("context", "create", "swarm-context", "--docker", fmt.Sprintf(`"host=tcp://%s"`, swarm.GetAddress(t)), "--default-stack-orchestrator", "swarm") icmd.RunCmd(cmd).Assert(t, icmd.Success) + defer func() { + cmd.Command = dockerCli.Command("context", "rm", "--force", "swarm-context") + icmd.RunCmd(cmd) + }() // When creating a context on a Windows host we cannot use // the unix socket but it's needed inside the invocation image. // The workaround is to create a context with an empty host. // This host will default to the unix socket inside the // invocation image - cmd.Command = []string{dockerCli, "context", "create", "swarm-target-context", "--docker", "host=", "--default-stack-orchestrator", "swarm"} + cmd.Command = dockerCli.Command("context", "create", "swarm-target-context", "--docker", "host=", "--default-stack-orchestrator", "swarm") icmd.RunCmd(cmd).Assert(t, icmd.Success) + defer func() { + cmd.Command = dockerCli.Command("context", "rm", "--force", "swarm-target-context") + icmd.RunCmd(cmd) + }() // Initialize the swarm cmd.Env = append(cmd.Env, "DOCKER_CONTEXT=swarm-context") - cmd.Command = []string{dockerCli, "swarm", "init"} + cmd.Command = dockerCli.Command("swarm", "init") icmd.RunCmd(cmd).Assert(t, icmd.Success) // Load the needed base cnab image into the swarm docker engine - cmd.Command = []string{dockerCli, "load", "-i", tmpDir.Join("cnab-app-base.tar.gz")} + cmd.Command = dockerCli.Command("load", "-i", tmpDir.Join("cnab-app-base.tar.gz")) icmd.RunCmd(cmd).Assert(t, icmd.Success) info := dindSwarmAndRegistryInfo{ @@ -93,12 +102,12 @@ func TestPushInstall(t *testing.T) { runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) { cmd := info.configuredCmd ref := info.registryAddress + "/test/push-pull" - cmd.Command = []string{dockerApp, "push", "-t", ref, "--insecure-registries=" + info.registryAddress, filepath.Join("testdata", "push-pull", "push-pull.dockerapp")} + cmd.Command = dockerCli.Command("app", "push", "--tag", ref, "--insecure-registries="+info.registryAddress, filepath.Join("testdata", "push-pull", "push-pull.dockerapp")) icmd.RunCmd(cmd).Assert(t, icmd.Success) - cmd.Command = []string{dockerApp, "install", "--insecure-registries=" + info.registryAddress, ref, "--name", t.Name()} + cmd.Command = dockerCli.Command("app", "install", "--insecure-registries="+info.registryAddress, ref, "--name", t.Name()) icmd.RunCmd(cmd).Assert(t, icmd.Success) - cmd.Command = []string{dockerCli, "service", "ls"} + cmd.Command = dockerCli.Command("service", "ls") assert.Check(t, cmp.Contains(icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined(), ref)) }) } @@ -107,22 +116,22 @@ func TestPushPullInstall(t *testing.T) { runWithDindSwarmAndRegistry(t, func(info dindSwarmAndRegistryInfo) { cmd := info.configuredCmd ref := info.registryAddress + "/test/push-pull" - cmd.Command = []string{dockerApp, "push", "-t", ref, "--insecure-registries=" + info.registryAddress, filepath.Join("testdata", "push-pull", "push-pull.dockerapp")} + cmd.Command = dockerCli.Command("app", "push", "--tag", ref, "--insecure-registries="+info.registryAddress, filepath.Join("testdata", "push-pull", "push-pull.dockerapp")) icmd.RunCmd(cmd).Assert(t, icmd.Success) - cmd.Command = []string{dockerApp, "pull", ref, "--insecure-registries=" + info.registryAddress} + cmd.Command = dockerCli.Command("app", "pull", ref, "--insecure-registries="+info.registryAddress) icmd.RunCmd(cmd).Assert(t, icmd.Success) // stop the registry info.stopRegistry() // install without --pull should succeed (rely on local store) - cmd.Command = []string{dockerApp, "install", "--insecure-registries=" + info.registryAddress, ref, "--name", t.Name()} + cmd.Command = dockerCli.Command("app", "install", "--insecure-registries="+info.registryAddress, ref, "--name", t.Name()) icmd.RunCmd(cmd).Assert(t, icmd.Success) - cmd.Command = []string{dockerCli, "service", "ls"} + cmd.Command = dockerCli.Command("service", "ls") assert.Check(t, cmp.Contains(icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined(), ref)) // install with --pull should fail (registry is stopped) - cmd.Command = []string{dockerApp, "install", "--pull", "--insecure-registries=" + info.registryAddress, ref, "--name", t.Name() + "2"} + cmd.Command = dockerCli.Command("app", "install", "--pull", "--insecure-registries="+info.registryAddress, ref, "--name", t.Name()+"2") assert.Check(t, cmp.Contains(icmd.RunCmd(cmd).Assert(t, icmd.Expected{ExitCode: 1}).Combined(), "failed to resolve bundle manifest")) }) } diff --git a/e2e/testdata/plugin-usage.golden b/e2e/testdata/plugin-usage.golden new file mode 100644 index 000000000..6cae686ae --- /dev/null +++ b/e2e/testdata/plugin-usage.golden @@ -0,0 +1,23 @@ + +Usage: docker app COMMAND + +Build and deploy Docker Application Packages. + +Commands: + bundle Create a CNAB invocation image and bundle.json for the application. + completion Generates completion scripts for the specified shell (bash or zsh) + init Start building a Docker application + inspect Shows metadata, parameters and a summary of the compose file for a given application + install Install an application + merge Merge a multi-file application into a single file + pull Pull an application from a registry + push Push the application to a registry + render Render the Compose file for the application + split Split a single-file application into multiple files + status Get the installation status. If the installation is a docker application, the status shows the stack services. + uninstall Uninstall an application + upgrade Upgrade an installed application + validate Checks the rendered application is syntactically correct + version Print version information + +Run 'docker app COMMAND --help' for more information on a command. diff --git a/internal/packager/extract.go b/internal/packager/extract.go index dbd794634..2c3665b37 100644 --- a/internal/packager/extract.go +++ b/internal/packager/extract.go @@ -30,7 +30,7 @@ func findApp() (string, error) { for _, c := range content { if strings.HasSuffix(c.Name(), internal.AppExtension) { if hit != "" { - return "", fmt.Errorf("multiple applications found in current directory, specify the application name on the command line") + return "", fmt.Errorf("Error: multiple applications found in current directory, specify the application name on the command line") } hit = c.Name() } diff --git a/vendor/github.com/docker/cli/cli-plugins/plugin/plugin.go b/vendor/github.com/docker/cli/cli-plugins/plugin/plugin.go new file mode 100644 index 000000000..99ef36d84 --- /dev/null +++ b/vendor/github.com/docker/cli/cli-plugins/plugin/plugin.go @@ -0,0 +1,164 @@ +package plugin + +import ( + "encoding/json" + "fmt" + "os" + "sync" + + "github.com/docker/cli/cli" + "github.com/docker/cli/cli-plugins/manager" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/connhelper" + cliflags "github.com/docker/cli/cli/flags" + "github.com/docker/docker/client" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function. +func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) { + dockerCli, err := command.NewDockerCli() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + plugin := makeCmd(dockerCli) + + cmd := newPluginCommand(dockerCli, plugin, meta) + + if err := cmd.Execute(); err != nil { + if sterr, ok := err.(cli.StatusError); ok { + if sterr.Status != "" { + fmt.Fprintln(dockerCli.Err(), sterr.Status) + } + // StatusError should only be used for errors, and all errors should + // have a non-zero exit status, so never exit with 0 + if sterr.StatusCode == 0 { + os.Exit(1) + } + os.Exit(sterr.StatusCode) + } + fmt.Fprintln(dockerCli.Err(), err) + os.Exit(1) + } +} + +// options encapsulates the ClientOptions and FlagSet constructed by +// `newPluginCommand` such that they can be finalized by our +// `PersistentPreRunE`. This is necessary because otherwise a plugin's +// own use of that hook will shadow anything we add to the top-level +// command meaning the CLI is never Initialized. +var options struct { + name string + init, prerun sync.Once + opts *cliflags.ClientOptions + flags *pflag.FlagSet + dockerCli *command.DockerCli +} + +// PersistentPreRunE must be called by any plugin command (or +// subcommand) which uses the cobra `PersistentPreRun*` hook. Plugins +// which do not make use of `PersistentPreRun*` do not need to call +// this (although it remains safe to do so). Plugins are recommended +// to use `PersistenPreRunE` to enable the error to be +// returned. Should not be called outside of a commands +// PersistentPreRunE hook and must not be run unless Run has been +// called. +func PersistentPreRunE(cmd *cobra.Command, args []string) error { + var err error + options.prerun.Do(func() { + if options.opts == nil || options.flags == nil || options.dockerCli == nil { + panic("PersistentPreRunE called without Run successfully called first") + } + // flags must be the original top-level command flags, not cmd.Flags() + options.opts.Common.SetDefaultOptions(options.flags) + err = options.dockerCli.Initialize(options.opts, withPluginClientConn(options.name)) + }) + return err +} + +func withPluginClientConn(name string) command.InitializeOpt { + return command.WithInitializeClient(func(dockerCli *command.DockerCli) (client.APIClient, error) { + cmd := "docker" + if x := os.Getenv(manager.ReexecEnvvar); x != "" { + cmd = x + } + var flags []string + + // Accumulate all the global arguments, that is those + // up to (but not including) the plugin's name. This + // ensures that `docker system dial-stdio` is + // evaluating the same set of `--config`, `--tls*` etc + // global options as the plugin was called with, which + // in turn is the same as what the original docker + // invocation was passed. + for _, a := range os.Args[1:] { + if a == name { + break + } + flags = append(flags, a) + } + flags = append(flags, "system", "dial-stdio") + + helper, err := connhelper.GetCommandConnectionHelper(cmd, flags...) + if err != nil { + return nil, err + } + + return client.NewClientWithOpts(client.WithDialContext(helper.Dialer)) + }) +} + +func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) *cobra.Command { + name := plugin.Name() + fullname := manager.NamePrefix + name + + cmd := &cobra.Command{ + Use: fmt.Sprintf("docker [OPTIONS] %s [ARG...]", name), + Short: fullname + " is a Docker CLI plugin", + SilenceUsage: true, + SilenceErrors: true, + TraverseChildren: true, + PersistentPreRunE: PersistentPreRunE, + DisableFlagsInUseLine: true, + } + opts, flags := cli.SetupPluginRootCommand(cmd) + + cmd.SetOutput(dockerCli.Out()) + + cmd.AddCommand( + plugin, + newMetadataSubcommand(plugin, meta), + ) + + cli.DisableFlagsInUseLine(cmd) + + options.init.Do(func() { + options.name = name + options.opts = opts + options.flags = flags + options.dockerCli = dockerCli + }) + return cmd +} + +func newMetadataSubcommand(plugin *cobra.Command, meta manager.Metadata) *cobra.Command { + if meta.ShortDescription == "" { + meta.ShortDescription = plugin.Short + } + cmd := &cobra.Command{ + Use: manager.MetadataSubcommandName, + Hidden: true, + // Suppress the global/parent PersistentPreRunE, which needlessly initializes the client and tries to connect to the daemon. + PersistentPreRun: func(cmd *cobra.Command, args []string) {}, + RunE: func(cmd *cobra.Command, args []string) error { + enc := json.NewEncoder(os.Stdout) + enc.SetEscapeHTML(false) + enc.SetIndent("", " ") + return enc.Encode(meta) + }, + } + return cmd +} diff --git a/vendor/github.com/docker/cli/cli/command/service/formatter.go b/vendor/github.com/docker/cli/cli/command/service/formatter.go index 4c81406a4..972129a7a 100644 --- a/vendor/github.com/docker/cli/cli/command/service/formatter.go +++ b/vendor/github.com/docker/cli/cli/command/service/formatter.go @@ -49,6 +49,9 @@ Placement: {{- if .TaskPlacementPreferences }} Preferences: {{ .TaskPlacementPreferences }} {{- end }} +{{- if .MaxReplicas }} + Max Replicas Per Node: {{ .MaxReplicas }} +{{- end }} {{- if .HasUpdateConfig }} UpdateConfig: Parallelism: {{ .UpdateParallelism }} @@ -284,6 +287,13 @@ func (ctx *serviceInspectContext) TaskPlacementPreferences() []string { return strings } +func (ctx *serviceInspectContext) MaxReplicas() uint64 { + if ctx.Service.Spec.TaskTemplate.Placement != nil { + return ctx.Service.Spec.TaskTemplate.Placement.MaxReplicas + } + return 0 +} + func (ctx *serviceInspectContext) HasUpdateConfig() bool { return ctx.Service.Spec.UpdateConfig != nil } diff --git a/vendor/github.com/docker/cli/cli/command/service/list.go b/vendor/github.com/docker/cli/cli/command/service/list.go index 188ffb6e5..90fe33010 100644 --- a/vendor/github.com/docker/cli/cli/command/service/list.go +++ b/vendor/github.com/docker/cli/cli/command/service/list.go @@ -120,9 +120,16 @@ func GetServicesStatus(services []swarm.Service, nodes []swarm.Node, tasks []swa for _, service := range services { info[service.ID] = ListInfo{} if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil { - info[service.ID] = ListInfo{ - Mode: "replicated", - Replicas: fmt.Sprintf("%d/%d", running[service.ID], *service.Spec.Mode.Replicated.Replicas), + if service.Spec.TaskTemplate.Placement != nil && service.Spec.TaskTemplate.Placement.MaxReplicas > 0 { + info[service.ID] = ListInfo{ + Mode: "replicated", + Replicas: fmt.Sprintf("%d/%d (max %d per node)", running[service.ID], *service.Spec.Mode.Replicated.Replicas, service.Spec.TaskTemplate.Placement.MaxReplicas), + } + } else { + info[service.ID] = ListInfo{ + Mode: "replicated", + Replicas: fmt.Sprintf("%d/%d", running[service.ID], *service.Spec.Mode.Replicated.Replicas), + } } } else if service.Spec.Mode.Global != nil { info[service.ID] = ListInfo{ diff --git a/vendor/github.com/docker/cli/cli/command/service/opts.go b/vendor/github.com/docker/cli/cli/command/service/opts.go index 2478ca8e8..ec4ea403d 100644 --- a/vendor/github.com/docker/cli/cli/command/service/opts.go +++ b/vendor/github.com/docker/cli/cli/command/service/opts.go @@ -500,6 +500,7 @@ type serviceOptions struct { restartPolicy restartPolicyOptions constraints opts.ListOpts placementPrefs placementPrefOpts + maxReplicas uint64 update updateOptions rollback updateOptions networks opts.NetworkOpt @@ -541,6 +542,10 @@ func (options *serviceOptions) ToServiceMode() (swarm.ServiceMode, error) { return serviceMode, errors.Errorf("replicas can only be used with replicated mode") } + if options.maxReplicas > 0 { + return serviceMode, errors.New("replicas-max-per-node can only be used with replicated mode") + } + serviceMode.Global = &swarm.GlobalService{} case "replicated": serviceMode.Replicated = &swarm.ReplicatedService{ @@ -645,6 +650,7 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N Placement: &swarm.Placement{ Constraints: options.constraints.GetAll(), Preferences: options.placementPrefs.prefs, + MaxReplicas: options.maxReplicas, }, LogDriver: options.logDriver.toLogDriver(), }, @@ -747,6 +753,8 @@ func addServiceFlags(flags *pflag.FlagSet, opts *serviceOptions, defaultFlagValu flags.Var(&opts.stopGrace, flagStopGracePeriod, flagDesc(flagStopGracePeriod, "Time to wait before force killing a container (ns|us|ms|s|m|h)")) flags.Var(&opts.replicas, flagReplicas, "Number of tasks") + flags.Uint64Var(&opts.maxReplicas, flagMaxReplicas, defaultFlagValues.getUint64(flagMaxReplicas), "Maximum number of tasks per node (default 0 = unlimited)") + flags.SetAnnotation(flagMaxReplicas, "version", []string{"1.40"}) flags.StringVar(&opts.restartPolicy.condition, flagRestartCondition, "", flagDesc(flagRestartCondition, `Restart when condition is met ("none"|"on-failure"|"any")`)) flags.Var(&opts.restartPolicy.delay, flagRestartDelay, flagDesc(flagRestartDelay, "Delay between restart attempts (ns|us|ms|s|m|h)")) @@ -853,6 +861,7 @@ const ( flagLabelAdd = "label-add" flagLimitCPU = "limit-cpu" flagLimitMemory = "limit-memory" + flagMaxReplicas = "replicas-max-per-node" flagMode = "mode" flagMount = "mount" flagMountRemove = "mount-rm" diff --git a/vendor/github.com/docker/cli/cli/command/service/update.go b/vendor/github.com/docker/cli/cli/command/service/update.go index 97a67576a..12ac6ed11 100644 --- a/vendor/github.com/docker/cli/cli/command/service/update.go +++ b/vendor/github.com/docker/cli/cli/command/service/update.go @@ -387,6 +387,10 @@ func updateService(ctx context.Context, apiClient client.NetworkAPIClient, flags return err } + if anyChanged(flags, flagMaxReplicas) { + updateUint64(flagMaxReplicas, &task.Placement.MaxReplicas) + } + if anyChanged(flags, flagUpdateParallelism, flagUpdateDelay, flagUpdateMonitor, flagUpdateFailureAction, flagUpdateMaxFailureRatio, flagUpdateOrder) { if spec.UpdateConfig == nil { spec.UpdateConfig = updateConfigFromDefaults(defaults.Service.Update) From 2475ad25af395df66963feccd739cff3f4a3b791 Mon Sep 17 00:00:00 2001 From: Silvin Lubecki Date: Tue, 26 Feb 2019 17:05:21 +0100 Subject: [PATCH 02/12] Move all the commands to their own package internal/commands so they can be re-used by another binary. Signed-off-by: Silvin Lubecki --- cmd/docker-app/main.go | 3 ++- {cmd/docker-app => internal/commands}/bundle.go | 2 +- {cmd/docker-app => internal/commands}/bundle_test.go | 2 +- {cmd/docker-app => internal/commands}/cnab.go | 2 +- {cmd/docker-app => internal/commands}/completion.go | 2 +- {cmd/docker-app => internal/commands}/init.go | 2 +- {cmd/docker-app => internal/commands}/inspect.go | 2 +- {cmd/docker-app => internal/commands}/install.go | 2 +- {cmd/docker-app => internal/commands}/merge.go | 2 +- {cmd/docker-app => internal/commands}/parameters.go | 2 +- .../commands}/parameters_test.go | 2 +- {cmd/docker-app => internal/commands}/pull.go | 2 +- {cmd/docker-app => internal/commands}/push.go | 2 +- {cmd/docker-app => internal/commands}/push_test.go | 2 +- {cmd/docker-app => internal/commands}/render.go | 2 +- {cmd/docker-app => internal/commands}/root.go | 10 ++++------ {cmd/docker-app => internal/commands}/split.go | 2 +- {cmd/docker-app => internal/commands}/status.go | 2 +- {cmd/docker-app => internal/commands}/uninstall.go | 2 +- {cmd/docker-app => internal/commands}/upgrade.go | 2 +- {cmd/docker-app => internal/commands}/validate.go | 2 +- {cmd/docker-app => internal/commands}/version.go | 2 +- 22 files changed, 26 insertions(+), 27 deletions(-) rename {cmd/docker-app => internal/commands}/bundle.go (99%) rename {cmd/docker-app => internal/commands}/bundle_test.go (98%) rename {cmd/docker-app => internal/commands}/cnab.go (99%) rename {cmd/docker-app => internal/commands}/completion.go (99%) rename {cmd/docker-app => internal/commands}/init.go (98%) rename {cmd/docker-app => internal/commands}/inspect.go (99%) rename {cmd/docker-app => internal/commands}/install.go (99%) rename {cmd/docker-app => internal/commands}/merge.go (99%) rename {cmd/docker-app => internal/commands}/parameters.go (99%) rename {cmd/docker-app => internal/commands}/parameters_test.go (99%) rename {cmd/docker-app => internal/commands}/pull.go (98%) rename {cmd/docker-app => internal/commands}/push.go (99%) rename {cmd/docker-app => internal/commands}/push_test.go (99%) rename {cmd/docker-app => internal/commands}/render.go (99%) rename {cmd/docker-app => internal/commands}/root.go (88%) rename {cmd/docker-app => internal/commands}/split.go (98%) rename {cmd/docker-app => internal/commands}/status.go (98%) rename {cmd/docker-app => internal/commands}/uninstall.go (98%) rename {cmd/docker-app => internal/commands}/upgrade.go (99%) rename {cmd/docker-app => internal/commands}/validate.go (98%) rename {cmd/docker-app => internal/commands}/version.go (95%) diff --git a/cmd/docker-app/main.go b/cmd/docker-app/main.go index 51a0e5965..99fdbaa01 100644 --- a/cmd/docker-app/main.go +++ b/cmd/docker-app/main.go @@ -2,6 +2,7 @@ package main import ( "github.com/docker/app/internal" + app "github.com/docker/app/internal/commands" "github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli-plugins/plugin" "github.com/docker/cli/cli/command" @@ -10,7 +11,7 @@ import ( func main() { plugin.Run(func(dockerCli command.Cli) *cobra.Command { - return newRootCmd(dockerCli) + return app.NewRootCmd("app", dockerCli) }, manager.Metadata{ SchemaVersion: "0.1.0", Vendor: "Docker Inc.", diff --git a/cmd/docker-app/bundle.go b/internal/commands/bundle.go similarity index 99% rename from cmd/docker-app/bundle.go rename to internal/commands/bundle.go index a0c1ad516..488b9cf3e 100644 --- a/cmd/docker-app/bundle.go +++ b/internal/commands/bundle.go @@ -1,4 +1,4 @@ -package main +package commands import ( "bytes" diff --git a/cmd/docker-app/bundle_test.go b/internal/commands/bundle_test.go similarity index 98% rename from cmd/docker-app/bundle_test.go rename to internal/commands/bundle_test.go index 58e9ab375..d6b92a87b 100644 --- a/cmd/docker-app/bundle_test.go +++ b/internal/commands/bundle_test.go @@ -1,4 +1,4 @@ -package main +package commands import ( "testing" diff --git a/cmd/docker-app/cnab.go b/internal/commands/cnab.go similarity index 99% rename from cmd/docker-app/cnab.go rename to internal/commands/cnab.go index c74085d35..1d003d2d5 100644 --- a/cmd/docker-app/cnab.go +++ b/internal/commands/cnab.go @@ -1,4 +1,4 @@ -package main +package commands import ( "fmt" diff --git a/cmd/docker-app/completion.go b/internal/commands/completion.go similarity index 99% rename from cmd/docker-app/completion.go rename to internal/commands/completion.go index 5daadcf53..9e573ab27 100644 --- a/cmd/docker-app/completion.go +++ b/internal/commands/completion.go @@ -1,4 +1,4 @@ -package main +package commands import ( "bytes" diff --git a/cmd/docker-app/init.go b/internal/commands/init.go similarity index 98% rename from cmd/docker-app/init.go rename to internal/commands/init.go index 68d51230c..e26847010 100644 --- a/cmd/docker-app/init.go +++ b/internal/commands/init.go @@ -1,4 +1,4 @@ -package main +package commands import ( "github.com/docker/app/internal/packager" diff --git a/cmd/docker-app/inspect.go b/internal/commands/inspect.go similarity index 99% rename from cmd/docker-app/inspect.go rename to internal/commands/inspect.go index aa9cf3d1d..ab8f0237e 100644 --- a/cmd/docker-app/inspect.go +++ b/internal/commands/inspect.go @@ -1,4 +1,4 @@ -package main +package commands import ( "github.com/deislabs/duffle/pkg/action" diff --git a/cmd/docker-app/install.go b/internal/commands/install.go similarity index 99% rename from cmd/docker-app/install.go rename to internal/commands/install.go index b14efdcd7..02522901f 100644 --- a/cmd/docker-app/install.go +++ b/internal/commands/install.go @@ -1,4 +1,4 @@ -package main +package commands import ( "fmt" diff --git a/cmd/docker-app/merge.go b/internal/commands/merge.go similarity index 99% rename from cmd/docker-app/merge.go rename to internal/commands/merge.go index 304947466..3903ed824 100644 --- a/cmd/docker-app/merge.go +++ b/internal/commands/merge.go @@ -1,4 +1,4 @@ -package main +package commands import ( "fmt" diff --git a/cmd/docker-app/parameters.go b/internal/commands/parameters.go similarity index 99% rename from cmd/docker-app/parameters.go rename to internal/commands/parameters.go index 5a42d607c..a1a6558ae 100644 --- a/cmd/docker-app/parameters.go +++ b/internal/commands/parameters.go @@ -1,4 +1,4 @@ -package main +package commands import ( "fmt" diff --git a/cmd/docker-app/parameters_test.go b/internal/commands/parameters_test.go similarity index 99% rename from cmd/docker-app/parameters_test.go rename to internal/commands/parameters_test.go index a926d4f4d..ac7a3f744 100644 --- a/cmd/docker-app/parameters_test.go +++ b/internal/commands/parameters_test.go @@ -1,4 +1,4 @@ -package main +package commands import ( "testing" diff --git a/cmd/docker-app/pull.go b/internal/commands/pull.go similarity index 98% rename from cmd/docker-app/pull.go rename to internal/commands/pull.go index b07fa6e3b..611a546b4 100644 --- a/cmd/docker-app/pull.go +++ b/internal/commands/pull.go @@ -1,4 +1,4 @@ -package main +package commands import ( "fmt" diff --git a/cmd/docker-app/push.go b/internal/commands/push.go similarity index 99% rename from cmd/docker-app/push.go rename to internal/commands/push.go index 666592cdc..252544755 100644 --- a/cmd/docker-app/push.go +++ b/internal/commands/push.go @@ -1,4 +1,4 @@ -package main +package commands import ( "bytes" diff --git a/cmd/docker-app/push_test.go b/internal/commands/push_test.go similarity index 99% rename from cmd/docker-app/push_test.go rename to internal/commands/push_test.go index 0b241d4a3..0c5da20d4 100644 --- a/cmd/docker-app/push_test.go +++ b/internal/commands/push_test.go @@ -1,4 +1,4 @@ -package main +package commands import ( "testing" diff --git a/cmd/docker-app/render.go b/internal/commands/render.go similarity index 99% rename from cmd/docker-app/render.go rename to internal/commands/render.go index 71f28e703..79de0096d 100644 --- a/cmd/docker-app/render.go +++ b/internal/commands/render.go @@ -1,4 +1,4 @@ -package main +package commands import ( "fmt" diff --git a/cmd/docker-app/root.go b/internal/commands/root.go similarity index 88% rename from cmd/docker-app/root.go rename to internal/commands/root.go index ea1593d66..f4cae27f2 100644 --- a/cmd/docker-app/root.go +++ b/internal/commands/root.go @@ -1,4 +1,4 @@ -package main +package commands import ( "io/ioutil" @@ -8,19 +8,17 @@ import ( "github.com/spf13/pflag" ) -// rootCmd represents the base command when called without any subcommands -// FIXME(vdemeester) use command.Cli interface -func newRootCmd(dockerCli command.Cli) *cobra.Command { +// NewRootCmd returns the base root command. +func NewRootCmd(use string, dockerCli command.Cli) *cobra.Command { cmd := &cobra.Command{ - Use: "app", Short: "Docker Application Packages", Long: `Build and deploy Docker Application Packages.`, + Use: use, } addCommands(cmd, dockerCli) return cmd } -// addCommands adds all the commands from cli/command to the root command func addCommands(cmd *cobra.Command, dockerCli command.Cli) { cmd.AddCommand( installCmd(dockerCli), diff --git a/cmd/docker-app/split.go b/internal/commands/split.go similarity index 98% rename from cmd/docker-app/split.go rename to internal/commands/split.go index fca59fed6..54e637fad 100644 --- a/cmd/docker-app/split.go +++ b/internal/commands/split.go @@ -1,4 +1,4 @@ -package main +package commands import ( "github.com/docker/app/internal/packager" diff --git a/cmd/docker-app/status.go b/internal/commands/status.go similarity index 98% rename from cmd/docker-app/status.go rename to internal/commands/status.go index d51fb5329..43f428cb9 100644 --- a/cmd/docker-app/status.go +++ b/internal/commands/status.go @@ -1,4 +1,4 @@ -package main +package commands import ( "github.com/deislabs/duffle/pkg/action" diff --git a/cmd/docker-app/uninstall.go b/internal/commands/uninstall.go similarity index 98% rename from cmd/docker-app/uninstall.go rename to internal/commands/uninstall.go index f49cd44d4..35b89d7e8 100644 --- a/cmd/docker-app/uninstall.go +++ b/internal/commands/uninstall.go @@ -1,4 +1,4 @@ -package main +package commands import ( "fmt" diff --git a/cmd/docker-app/upgrade.go b/internal/commands/upgrade.go similarity index 99% rename from cmd/docker-app/upgrade.go rename to internal/commands/upgrade.go index 31811339f..1c88c4d0d 100644 --- a/cmd/docker-app/upgrade.go +++ b/internal/commands/upgrade.go @@ -1,4 +1,4 @@ -package main +package commands import ( "fmt" diff --git a/cmd/docker-app/validate.go b/internal/commands/validate.go similarity index 98% rename from cmd/docker-app/validate.go rename to internal/commands/validate.go index e74f7be1b..97581a25f 100644 --- a/cmd/docker-app/validate.go +++ b/internal/commands/validate.go @@ -1,4 +1,4 @@ -package main +package commands import ( "github.com/docker/app/internal/packager" diff --git a/cmd/docker-app/version.go b/internal/commands/version.go similarity index 95% rename from cmd/docker-app/version.go rename to internal/commands/version.go index b4c47c6a7..3b3c99011 100644 --- a/cmd/docker-app/version.go +++ b/internal/commands/version.go @@ -1,4 +1,4 @@ -package main +package commands import ( "fmt" From 3930fe75f13b74bdad1645a5291dc859006d9579 Mon Sep 17 00:00:00 2001 From: Silvin Lubecki Date: Tue, 26 Feb 2019 17:33:15 +0100 Subject: [PATCH 03/12] Add a new binary docker-app-standalone This is in fact the old docker-app before its pluginization. Added it to cross compilation Now each released tar.gz file per os comes with two binaries in it: docker-app-plugin-os and docker-app-standalone-os Signed-off-by: Silvin Lubecki --- Makefile | 14 ++++++++- cmd/docker-app-standalone/main.go | 50 +++++++++++++++++++++++++++++++ docker.Makefile | 12 ++++++-- vars.mk | 1 + 4 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 cmd/docker-app-standalone/main.go diff --git a/Makefile b/Makefile index 4937aa0a7..2a6e18ebd 100644 --- a/Makefile +++ b/Makefile @@ -33,10 +33,22 @@ check_go_env: @test $$(go list) = "$(PKG_NAME)" || \ (echo "Invalid Go environment - The local directory structure must match: $(PKG_NAME)" && false) -cross: bin/$(BIN_NAME)-linux bin/$(BIN_NAME)-darwin bin/$(BIN_NAME)-windows.exe ## cross-compile binaries (linux, darwin, windows) +cross: cross-plugin cross-standalone ## cross-compile binaries (linux, darwin, windows) + +cross-plugin: bin/$(BIN_NAME)-linux bin/$(BIN_NAME)-darwin bin/$(BIN_NAME)-windows.exe + +cross-standalone: bin/${BIN_STANDALONE_NAME}-linux bin/${BIN_STANDALONE_NAME}-darwin bin/${BIN_STANDALONE_NAME}-windows.exe e2e-cross: bin/$(BIN_NAME)-e2e-linux bin/$(BIN_NAME)-e2e-darwin bin/$(BIN_NAME)-e2e-windows.exe +.PHONY: bin/${BIN_STANDALONE_NAME}-windows +bin/${BIN_STANDALONE_NAME}-%.exe bin/${BIN_STANDALONE_NAME}-%: cmd/${BIN_STANDALONE_NAME} check_go_env + GOOS=$* $(GO_BUILD) -o $@ ./$< + +.PHONY: bin/${BIN_STANDALONE_NAME} +bin/${BIN_STANDALONE_NAME}: cmd/${BIN_STANDALONE_NAME} check_go_env + $(GO_BUILD) -o $@$(EXEC_EXT) ./$< + .PHONY: bin/$(BIN_NAME)-e2e-windows bin/$(BIN_NAME)-e2e-%.exe bin/$(BIN_NAME)-e2e-%: e2e bin/$(BIN_NAME)-% GOOS=$* $(GO_TEST) -c -o $@ ./e2e/ diff --git a/cmd/docker-app-standalone/main.go b/cmd/docker-app-standalone/main.go new file mode 100644 index 000000000..17d124da1 --- /dev/null +++ b/cmd/docker-app-standalone/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/docker/app/internal" + app "github.com/docker/app/internal/commands" + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" + cliflags "github.com/docker/cli/cli/flags" + "github.com/sirupsen/logrus" +) + +func main() { + dockerCli, err := command.NewDockerCli() + if err != nil { + fmt.Fprintln(os.Stderr, err) + } + logrus.SetOutput(dockerCli.Err()) + + cmd := app.NewRootCmd("docker-app", dockerCli) + configureRootCmd(cmd, dockerCli) + + if err := cmd.Execute(); err != nil { + os.Exit(1) + } +} + +func configureRootCmd(cmd *cobra.Command, dockerCli *command.DockerCli) { + var ( + opts *cliflags.ClientOptions + flags *pflag.FlagSet + ) + + cmd.SilenceUsage = true + cmd.TraverseChildren = true + cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { + opts.Common.SetDefaultOptions(flags) + return dockerCli.Initialize(opts) + } + cmd.Version = fmt.Sprintf("%s, build %s", internal.Version, internal.GitCommit) + + opts, flags, _ = cli.SetupRootCommand(cmd) + flags.BoolP("version", "v", false, "Print version information") + cmd.SetVersionTemplate("docker-app version {{.Version}}\n") +} diff --git a/docker.Makefile b/docker.Makefile index 0fcf13f88..efe14671b 100644 --- a/docker.Makefile +++ b/docker.Makefile @@ -43,10 +43,16 @@ cross: create_bin ## cross-compile binaries (linux, darwin, windows) docker cp $(CROSS_CTNR_NAME):$(PKG_PATH)/bin/$(BIN_NAME)-linux bin/$(BIN_NAME)-linux docker cp $(CROSS_CTNR_NAME):$(PKG_PATH)/bin/$(BIN_NAME)-darwin bin/$(BIN_NAME)-darwin docker cp $(CROSS_CTNR_NAME):$(PKG_PATH)/bin/$(BIN_NAME)-windows.exe bin/$(BIN_NAME)-windows.exe + docker cp $(CROSS_CTNR_NAME):$(PKG_PATH)/bin/${BIN_STANDALONE_NAME}-linux bin/${BIN_STANDALONE_NAME}-linux + docker cp $(CROSS_CTNR_NAME):$(PKG_PATH)/bin/${BIN_STANDALONE_NAME}-darwin bin/${BIN_STANDALONE_NAME}-darwin + docker cp $(CROSS_CTNR_NAME):$(PKG_PATH)/bin/${BIN_STANDALONE_NAME}-windows.exe bin/${BIN_STANDALONE_NAME}-windows.exe docker rm $(CROSS_CTNR_NAME) @$(call chmod,+x,bin/$(BIN_NAME)-linux) @$(call chmod,+x,bin/$(BIN_NAME)-darwin) @$(call chmod,+x,bin/$(BIN_NAME)-windows.exe) + @$(call chmod,+x,bin/${BIN_STANDALONE_NAME}-linux) + @$(call chmod,+x,bin/${BIN_STANDALONE_NAME}-darwin) + @$(call chmod,+x,bin/${BIN_STANDALONE_NAME}-windows.exe) cli-cross: create_bin docker build $(BUILD_ARGS) --target=build -t $(CLI_IMAGE_NAME) . @@ -71,11 +77,11 @@ e2e-cross: create_bin @$(call chmod,+x,bin/$(BIN_NAME)-e2e-windows.exe) tars: - tar czf bin/$(BIN_NAME)-linux.tar.gz -C bin $(BIN_NAME)-linux + tar --transform='flags=r;s|$(BIN_NAME)-linux|$(BIN_NAME)-plugin-linux|' -czf bin/$(BIN_NAME)-linux.tar.gz -C bin $(BIN_NAME)-linux ${BIN_STANDALONE_NAME}-linux tar czf bin/$(BIN_NAME)-e2e-linux.tar.gz -C bin $(BIN_NAME)-e2e-linux - tar czf bin/$(BIN_NAME)-darwin.tar.gz -C bin $(BIN_NAME)-darwin + tar --transform='flags=r;s|$(BIN_NAME)-darwin|$(BIN_NAME)-plugin-darwin|' -czf bin/$(BIN_NAME)-darwin.tar.gz -C bin $(BIN_NAME)-darwin ${BIN_STANDALONE_NAME}-darwin tar czf bin/$(BIN_NAME)-e2e-darwin.tar.gz -C bin $(BIN_NAME)-e2e-darwin - tar czf bin/$(BIN_NAME)-windows.tar.gz -C bin $(BIN_NAME)-windows.exe + tar --transform='flags=r;s|$(BIN_NAME)-windows|$(BIN_NAME)-plugin-windows|' -czf bin/$(BIN_NAME)-windows.tar.gz -C bin $(BIN_NAME)-windows.exe ${BIN_STANDALONE_NAME}-windows.exe tar czf bin/$(BIN_NAME)-e2e-windows.tar.gz -C bin $(BIN_NAME)-e2e-windows.exe test: test-unit test-e2e ## run all tests diff --git a/vars.mk b/vars.mk index 33e37d34a..9bb0f9e2d 100644 --- a/vars.mk +++ b/vars.mk @@ -1,5 +1,6 @@ PKG_NAME := github.com/docker/app BIN_NAME ?= docker-app +BIN_STANDALONE_NAME := ${BIN_NAME}-standalone E2E_NAME := $(BIN_NAME)-e2e # Enable experimental features. "on" or "off" From eee6fb5cc44d0eef566b3c8b543f1d18a3f31c0e Mon Sep 17 00:00:00 2001 From: Silvin Lubecki Date: Thu, 28 Feb 2019 16:48:09 +0100 Subject: [PATCH 04/12] Update README with the plugin installation * Update windows installation too, which seems obsolete Signed-off-by: Silvin Lubecki --- README.md | 58 +++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 27df4b6b5..7e72e2500 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ services: - 5678:5678 ``` -With `docker-app` installed let's create an Application Package based on this Compose file: +With `docker-app` [installed](#installation) let's create an Application Package based on this Compose file: ```bash $ docker-app init --single-file hello @@ -187,30 +187,68 @@ Removing network hello_default ## Installation Pre-built binaries are available on [GitHub releases](https://github.com/docker/app/releases) for Windows, Linux and macOS. +Each tarball contains two binaries: +- `docker-app-plugin-{linux|macos|windows}` which is docker-app as a [docker cli plugin](https://github.com/docker/cli/issues/1534) +- `docker-app-standalone-{linux|macos|windows}` which is docker-app as a standalone utility + +To use `docker-app` plugin, just type `docker app` instead of `docker-app` and all the examples will work the same way: +```bash +$ docker app version +Version: v0.8 +Git commit: XXX +Built: Wed Feb 27 12:37:06 2019 +OS/Arch: darwin/amd64 +Experimental: off +Renderers: none + +$ docker-app version +Version: v0.8 +Git commit: XXX +Built: Wed Feb 27 12:37:06 2019 +OS/Arch: darwin/amd64 +Experimental: off +Renderers: none +``` ### Linux or macOS +Download your OS tarball: ```bash export OSTYPE="$(uname | tr A-Z a-z)" curl -fsSL --output "/tmp/docker-app-${OSTYPE}.tar.gz" "https://github.com/docker/app/releases/download/v0.6.0/docker-app-${OSTYPE}.tar.gz" tar xf "/tmp/docker-app-${OSTYPE}.tar.gz" -C /tmp/ -install -b "/tmp/docker-app-${OSTYPE}" /usr/local/bin/docker-app +``` + +To install `docker-app` as a standalone: +```bash +install -b "/tmp/docker-app-standalone-${OSTYPE}" /usr/local/bin/docker-app +``` + +To install `docker-app` as a docker cli plugin: +```bash +mkdir -p ~/.docker/cli-plugins && cp "/tmp/docker-app-plugin-${OSTYPE}" ~/.docker/cli-plugins/docker-app ``` ### Windows + +Download the Windows tarball: ```powershell -function Expand-Tar($tarFile, $dest) { +Invoke-WebRequest -Uri https://github.com/docker/app/releases/download/v0.6.0/docker-app-windows.tar.gz -OutFile docker-app.tar.gz -UseBasicParsing +tar xf "docker-app.tar.gz" +``` - if (-not (Get-Command Expand-7Zip -ErrorAction Ignore)) { - Install-Package -Scope CurrentUser -Force 7Zip4PowerShell > $null - } +To install `docker-app` as a standalone, copy it somewhere in your path: +```powershell +cp docker-app-plugin-windows.exe PATH/docker-app.exe +``` - Expand-7Zip $tarFile $dest -} -Invoke-WebRequest -Uri https://github.com/docker/app/releases/download/v0.6.0/docker-app-windows.tar.gz -Expand-Tar docker-app-windows.tar.gz docker-app-windows.exe +To install `docker-app` as a docker cli plugin: +```powershell +New-Item -ItemType Directory -Path ~/.docker/cli-plugins -ErrorAction SilentlyContinue +cp docker-app-plugin-windows.exe ~/.docker/cli-plugins/docker-app.exe ``` + **Note:** To use Application Packages as images (i.e.: `save`, `push`, or `install` when package is not present locally) on Windows, one must be in Linux container mode. ## Single file or directory representation From 80357faf819b06dadfe2a37143201b06a4f640e1 Mon Sep 17 00:00:00 2001 From: Jean-Christophe Sirot Date: Fri, 1 Mar 2019 15:32:37 +0100 Subject: [PATCH 05/12] Fix CI issues Signed-off-by: Jean-Christophe Sirot --- Jenkinsfile.baguette | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Jenkinsfile.baguette b/Jenkinsfile.baguette index 28e970a5c..0662e8de7 100644 --- a/Jenkinsfile.baguette +++ b/Jenkinsfile.baguette @@ -148,6 +148,7 @@ pipeline { dir('e2e'){ unstash "e2e" } + sh './docker-linux version' sh './docker-app-e2e-linux -test.v --e2e-path=e2e' } } @@ -180,6 +181,7 @@ pipeline { dir('e2e'){ unstash "e2e" } + sh './docker-darwin version' sh './docker-app-e2e-darwin -test.v --e2e-path=e2e' } } @@ -201,11 +203,12 @@ pipeline { steps { dir('src/github.com/docker/app') { checkout scm + unstash "binaries" + sh './docker-windows.exe version' dir('_build') { unstash "invocation-image" bat 'docker load -i invocation-image.tar' } - unstash "binaries" dir('examples') { unstash "examples" } From b8e5ce565f4d5c9155c25fde75af761e9ef6948a Mon Sep 17 00:00:00 2001 From: Ulysses Souza Date: Mon, 4 Mar 2019 12:01:45 +0100 Subject: [PATCH 06/12] Remove BUILDKIT=true from Jenkinsfile Signed-off-by: Ulysses Souza --- Dockerfile | 2 -- Jenkinsfile.baguette | 3 --- 2 files changed, 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 37efad4ed..9abefa835 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,13 +26,11 @@ RUN go get -d gopkg.in/mjibson/esc.v0 && \ rm -rf /go/src/* /go/pkg/* /go/bin/* COPY . . -# FIXME(vdemeester) change from docker-app to dev once buildkit is merged in moby/docker FROM dev AS cross ARG EXPERIMENTAL="off" ARG TAG="unknown" RUN make EXPERIMENTAL=${EXPERIMENTAL} TAG=${TAG} cross -# FIXME(vdemeester) change from docker-app to dev once buildkit is merged in moby/docker FROM cross AS e2e-cross ARG EXPERIMENTAL="off" ARG TAG="unknown" diff --git a/Jenkinsfile.baguette b/Jenkinsfile.baguette index 0662e8de7..7857f8daa 100644 --- a/Jenkinsfile.baguette +++ b/Jenkinsfile.baguette @@ -8,9 +8,6 @@ pipeline { options { skipDefaultCheckout(true) } - environment{ - DOCKER_BUILDKIT=true - } stages { stage('Build') { From 90010ffe4f95eaedd5df07ddad8470fb8b5031a0 Mon Sep 17 00:00:00 2001 From: Ulysses Souza Date: Mon, 4 Mar 2019 13:31:43 +0100 Subject: [PATCH 07/12] Fix the version the of the CLI Perform the git clone and a chechout to avoid that a Dockerfile cache and enforce determinism Signed-off-by: Ulysses Souza --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 9abefa835..0ec58af6d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ RUN apt-get install -y -q --no-install-recommends \ WORKDIR /go/src/github.com/docker/cli -RUN git clone https://github.com/docker/cli.git . +RUN git clone https://github.com/docker/cli.git . && git checkout 8ddde26af67f9a76734a1676c635e48da4fe8584 RUN make cross binary && \ cp build/docker-linux-amd64 /usr/bin/docker From 064a9172c265104fd5825a45cefe83c84a03ca7f Mon Sep 17 00:00:00 2001 From: Ulysses Souza Date: Mon, 4 Mar 2019 13:57:33 +0100 Subject: [PATCH 08/12] Fix e2e binaries path and cli vendor override - Update docker/cli is now pointing to chris-crone/cli. The change needs the merge of https://github.com/docker/cli/pull/1718 and https://github.com/docker/cli/pull/1690 - Fix issues relative paths in Jenkinsfile and Jenkinsfile.baguette - Avoid using '--config' in favor of env variable 'DOCKER_CONFIG' Signed-off-by: Ulysses Souza --- Dockerfile | 5 +- Gopkg.lock | 7 +- Gopkg.toml | 4 +- Jenkinsfile.baguette | 2 + e2e/commands_test.go | 5 +- e2e/coverage-bin | 2 +- e2e/main_test.go | 40 ++- .../docker/cli/cli-plugins/manager/cobra.go | 10 +- vendor/github.com/docker/cli/cli/cobra.go | 39 ++- .../github.com/docker/cli/cli/command/cli.go | 27 +- .../docker/cli/cli/config/configfile/file.go | 88 ++++-- .../cli/connhelper/commandconn/commandconn.go | 281 ++++++++++++++++++ .../commandconn_linux.go} | 2 +- .../commandconn_nolinux.go} | 2 +- .../docker/cli/cli/connhelper/connhelper.go | 273 +---------------- .../docker/cli/cli/connhelper/ssh/ssh.go | 45 ++- 16 files changed, 466 insertions(+), 366 deletions(-) create mode 100644 vendor/github.com/docker/cli/cli/connhelper/commandconn/commandconn.go rename vendor/github.com/docker/cli/cli/connhelper/{connhelper_linux.go => commandconn/commandconn_linux.go} (87%) rename vendor/github.com/docker/cli/cli/connhelper/{connhelper_nolinux.go => commandconn/commandconn_nolinux.go} (79%) diff --git a/Dockerfile b/Dockerfile index 0ec58af6d..445b87955 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,10 @@ RUN apt-get install -y -q --no-install-recommends \ WORKDIR /go/src/github.com/docker/cli -RUN git clone https://github.com/docker/cli.git . && git checkout 8ddde26af67f9a76734a1676c635e48da4fe8584 +RUN git clone https://github.com/chris-crone/cli . && git checkout d6bfd7e5592dad85969516c131d33910fa5ebd58 +# FIXME(ulyssessouza): Go back to the line below when PRs https://github.com/docker/cli/pull/1718 and https://github.com/docker/cli/pull/1690 hits the cli +#RUN git clone https://github.com/docker/cli.git . && git checkout 8ddde26af67f9a76734a1676c635e48da4fe8584 + RUN make cross binary && \ cp build/docker-linux-amd64 /usr/bin/docker diff --git a/Gopkg.lock b/Gopkg.lock index 73a2e1932..481c8d9d7 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -210,7 +210,7 @@ revision = "eaa7595bd231ca353ec886708405fe4deba91968" [[projects]] - digest = "1:0b5bf13e4971fc511f8ee5b4f13490ece442bcd16c079bcb372005c553fb886f" + digest = "1:9398cdf8ac469acd44527b48cec760ff8801241a93597e4acd5001a03fff5a84" name = "github.com/docker/cli" packages = [ "cli", @@ -242,6 +242,7 @@ "cli/config/credentials", "cli/config/types", "cli/connhelper", + "cli/connhelper/commandconn", "cli/connhelper/ssh", "cli/context", "cli/context/docker", @@ -264,7 +265,8 @@ "types", ] pruneopts = "UT" - revision = "3ddb3133f5b5a74dd4e3f721ced21f4d0b9651b6" + revision = "d6bfd7e5592dad85969516c131d33910fa5ebd58" + source = "https://github.com/chris-crone/cli" [[projects]] branch = "master" @@ -1284,6 +1286,7 @@ "github.com/docker/cli/cli/compose/template", "github.com/docker/cli/cli/compose/types", "github.com/docker/cli/cli/config", + "github.com/docker/cli/cli/config/configfile", "github.com/docker/cli/cli/context/docker", "github.com/docker/cli/cli/context/kubernetes", "github.com/docker/cli/cli/context/store", diff --git a/Gopkg.toml b/Gopkg.toml index cb29ffd95..ec952d42f 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -36,9 +36,11 @@ required = ["github.com/wadey/gocovmerge"] source = "github.com/simonferquel/containerd" revision = "a89234684e5884e51ba195bdb16b1c6952d17f11" +### Waiting on PR https://github.com/docker/cli/pull/1718 and https://github.com/docker/cli/pull/1690 to land on cli ### [[override]] name = "github.com/docker/cli" - revision = "3ddb3133f5b5a74dd4e3f721ced21f4d0b9651b6" + source = "https://github.com/chris-crone/cli" + revision="d6bfd7e5592dad85969516c131d33910fa5ebd58" [[override]] name = "github.com/deislabs/duffle" diff --git a/Jenkinsfile.baguette b/Jenkinsfile.baguette index 7857f8daa..6f83d002a 100644 --- a/Jenkinsfile.baguette +++ b/Jenkinsfile.baguette @@ -79,6 +79,8 @@ pipeline { parallel { stage("Unit Coverage") { environment { + DOCKERAPP_BINARY = '../e2e/coverage-bin' + DOCKERCLI_BINARY = '../docker-linux' CODECOV_TOKEN = credentials('jenkins-codecov-token') } agent { diff --git a/e2e/commands_test.go b/e2e/commands_test.go index ae4df9af5..c6eef9d1b 100644 --- a/e2e/commands_test.go +++ b/e2e/commands_test.go @@ -103,7 +103,7 @@ maintainers: testAppName := "app-test" dirName := internal.DirNameFromAppName(testAppName) - cmd := icmd.Cmd{Dir: tmpDir.Path()} + cmd := icmd.Cmd{Dir: tmpDir.Path(), Env: os.Environ()} cmd.Command = dockerCli.Command("app", "init", testAppName, @@ -214,7 +214,7 @@ func TestBundle(t *testing.T) { tmpDir := fs.NewDir(t, t.Name()) defer tmpDir.Remove() // Using a custom DOCKER_CONFIG to store contexts in a temporary directory - cmd := icmd.Cmd{Env: append(os.Environ(), "DOCKER_CONFIG="+tmpDir.Path())} + cmd := icmd.Cmd{Env: os.Environ()} // Running a docker in docker to bundle the application dind := NewContainer("docker:18.09-dind", 2375) @@ -271,7 +271,6 @@ func TestDockerAppLifecycle(t *testing.T) { cmd := icmd.Cmd{ Env: append(os.Environ(), fmt.Sprintf("DUFFLE_HOME=%s", tmpDir.Path()), - fmt.Sprintf("DOCKER_CONFIG=%s", tmpDir.Path()), "DOCKER_TARGET_CONTEXT=swarm-target-context", ), } diff --git a/e2e/coverage-bin b/e2e/coverage-bin index 9e3cd5ca3..2f1844226 100755 --- a/e2e/coverage-bin +++ b/e2e/coverage-bin @@ -3,7 +3,7 @@ # This script is a proxy that injects the required test flags and strips out test output # It allows us to use a coverage-enabled binary for e2e tests -BUILD_DIR=${BASH_SOURCE%/*}/../_build +BUILD_DIR=${GOPATH}/src/github.com/docker/app/_build $BUILD_DIR/docker-app.cov \ -test.coverprofile=$BUILD_DIR/cov/$(uuidgen).out \ diff --git a/e2e/main_test.go b/e2e/main_test.go index 3252ed867..f9de8aeec 100644 --- a/e2e/main_test.go +++ b/e2e/main_test.go @@ -2,14 +2,17 @@ package e2e import ( "bytes" + "encoding/json" "flag" - "fmt" "io/ioutil" "os" "os/exec" "path/filepath" + "runtime" "strings" "testing" + + dockerConfigFile "github.com/docker/cli/cli/config/configfile" ) var ( @@ -20,17 +23,13 @@ var ( dockerCli dockerCliCommand ) -const config = `{ - "cliPluginsExtraDirs": ["%s"] -}` - type dockerCliCommand struct { path string config string } func (d dockerCliCommand) Command(args ...string) []string { - return append([]string{d.path, "--config", d.config}, args...) + return append([]string{d.path}, args...) } func TestMain(m *testing.M) { @@ -55,22 +54,45 @@ func TestMain(m *testing.M) { // - Create a symbolic link with the dockerApp binary to the plugin directory if dockerCliPath == "" { dockerCliPath = "docker" + } else { + dockerCliPath, err = filepath.Abs(dockerCliPath) + if err != nil { + panic(err) + } } configDir, err := ioutil.TempDir("", "config") if err != nil { panic(err.Error()) } defer os.RemoveAll(configDir) + + err = os.Setenv("DOCKER_CONFIG", configDir) + if err != nil { + panic(err.Error()) + } dockerCli = dockerCliCommand{path: dockerCliPath, config: configDir} - ioutil.WriteFile(filepath.Join(configDir, "config.json"), []byte(fmt.Sprintf(config, configDir)), 0644) - if err := os.Symlink(dockerApp, filepath.Join(configDir, "docker-app")); err != nil { + + config := dockerConfigFile.ConfigFile{CLIPluginsExtraDirs: []string{configDir}} + configFile, err := os.Create(filepath.Join(configDir, "config.json")) + if err != nil { + panic(err.Error()) + } + err = json.NewEncoder(configFile).Encode(config) + if err != nil { + panic(err.Error()) + } + dockerAppExecName := "docker-app" + if runtime.GOOS == "windows" { + dockerAppExecName += ".exe" + } + if err := os.Symlink(dockerApp, filepath.Join(configDir, dockerAppExecName)); err != nil { panic(err.Error()) } cmd := exec.Command(dockerApp, "app", "version") output, err := cmd.CombinedOutput() if err != nil { - panic(err) + panic(err.Error()) } hasExperimental = bytes.Contains(output, []byte("Experimental: on")) i := strings.Index(string(output), "Renderers") diff --git a/vendor/github.com/docker/cli/cli-plugins/manager/cobra.go b/vendor/github.com/docker/cli/cli-plugins/manager/cobra.go index 692de7fdb..0fcd73e7b 100644 --- a/vendor/github.com/docker/cli/cli-plugins/manager/cobra.go +++ b/vendor/github.com/docker/cli/cli-plugins/manager/cobra.go @@ -16,6 +16,11 @@ const ( // that plugin. CommandAnnotationPluginVendor = "com.docker.cli.plugin.vendor" + // CommandAnnotationPluginVersion is added to every stub command + // added by AddPluginCommandStubs and contains the version of + // that plugin. + CommandAnnotationPluginVersion = "com.docker.cli.plugin.version" + // CommandAnnotationPluginInvalid is added to any stub command // added by AddPluginCommandStubs for an invalid command (that // is, one which failed it's candidate test) and contains the @@ -37,8 +42,9 @@ func AddPluginCommandStubs(dockerCli command.Cli, cmd *cobra.Command) error { vendor = "unknown" } annotations := map[string]string{ - CommandAnnotationPlugin: "true", - CommandAnnotationPluginVendor: vendor, + CommandAnnotationPlugin: "true", + CommandAnnotationPluginVendor: vendor, + CommandAnnotationPluginVersion: p.Version, } if p.Err != nil { annotations[CommandAnnotationPluginInvalid] = p.Err.Error() diff --git a/vendor/github.com/docker/cli/cli/cobra.go b/vendor/github.com/docker/cli/cli/cobra.go index cea1a9be4..803872ee5 100644 --- a/vendor/github.com/docker/cli/cli/cobra.go +++ b/vendor/github.com/docker/cli/cli/cobra.go @@ -22,6 +22,7 @@ func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *p flags.StringVar(&opts.ConfigDir, "config", cliconfig.Dir(), "Location of client config files") opts.Common.InstallFlags(flags) + cobra.AddTemplateFunc("add", func(a, b int) int { return a + b }) cobra.AddTemplateFunc("hasSubCommands", hasSubCommands) cobra.AddTemplateFunc("hasManagementSubCommands", hasManagementSubCommands) cobra.AddTemplateFunc("hasInvalidPlugins", hasInvalidPlugins) @@ -29,9 +30,10 @@ func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *p cobra.AddTemplateFunc("managementSubCommands", managementSubCommands) cobra.AddTemplateFunc("invalidPlugins", invalidPlugins) cobra.AddTemplateFunc("wrappedFlagUsages", wrappedFlagUsages) - cobra.AddTemplateFunc("commandVendor", commandVendor) - cobra.AddTemplateFunc("isFirstLevelCommand", isFirstLevelCommand) // is it an immediate sub-command of the root + cobra.AddTemplateFunc("vendorAndVersion", vendorAndVersion) cobra.AddTemplateFunc("invalidPluginReason", invalidPluginReason) + cobra.AddTemplateFunc("isPlugin", isPlugin) + cobra.AddTemplateFunc("decoratedName", decoratedName) rootCmd.SetUsageTemplate(usageTemplate) rootCmd.SetHelpTemplate(helpTemplate) @@ -137,7 +139,7 @@ func hasInvalidPlugins(cmd *cobra.Command) bool { func operationSubCommands(cmd *cobra.Command) []*cobra.Command { cmds := []*cobra.Command{} for _, sub := range cmd.Commands() { - if isPlugin(sub) && invalidPluginReason(sub) != "" { + if isPlugin(sub) { continue } if sub.IsAvailableCommand() && !sub.HasSubCommands() { @@ -155,25 +157,32 @@ func wrappedFlagUsages(cmd *cobra.Command) string { return cmd.Flags().FlagUsagesWrapped(width - 1) } -func isFirstLevelCommand(cmd *cobra.Command) bool { - return cmd.Parent() == cmd.Root() +func decoratedName(cmd *cobra.Command) string { + decoration := " " + if isPlugin(cmd) { + decoration = "*" + } + return cmd.Name() + decoration } -func commandVendor(cmd *cobra.Command) string { - width := 13 - if v, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVendor]; ok { - if len(v) > width-2 { - v = v[:width-3] + "…" +func vendorAndVersion(cmd *cobra.Command) string { + if vendor, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVendor]; ok && isPlugin(cmd) { + version := "" + if v, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVersion]; ok && v != "" { + version = ", " + v } - return fmt.Sprintf("%-*s", width, "("+v+")") + return fmt.Sprintf("(%s%s)", vendor, version) } - return strings.Repeat(" ", width) + return "" } func managementSubCommands(cmd *cobra.Command) []*cobra.Command { cmds := []*cobra.Command{} for _, sub := range cmd.Commands() { - if isPlugin(sub) && invalidPluginReason(sub) != "" { + if isPlugin(sub) { + if invalidPluginReason(sub) == "" { + cmds = append(cmds, sub) + } continue } if sub.IsAvailableCommand() && sub.HasSubCommands() { @@ -230,7 +239,7 @@ Options: Management Commands: {{- range managementSubCommands . }} - {{rpad .Name .NamePadding }} {{ if isFirstLevelCommand .}}{{commandVendor .}} {{ end}}{{.Short}} + {{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}{{ if isPlugin .}} {{vendorAndVersion .}}{{ end}} {{- end}} {{- end}} @@ -239,7 +248,7 @@ Management Commands: Commands: {{- range operationSubCommands . }} - {{rpad .Name .NamePadding }} {{ if isFirstLevelCommand .}}{{commandVendor .}} {{ end}}{{.Short}} + {{rpad .Name .NamePadding }} {{.Short}} {{- end}} {{- end}} diff --git a/vendor/github.com/docker/cli/cli/command/cli.go b/vendor/github.com/docker/cli/cli/command/cli.go index 47e9bdfff..3208a7904 100644 --- a/vendor/github.com/docker/cli/cli/command/cli.go +++ b/vendor/github.com/docker/cli/cli/command/cli.go @@ -209,24 +209,23 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...Initialize cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err) - if cli.client == nil { - cli.contextStore = store.New(cliconfig.ContextStoreDir(), cli.contextStoreConfig) - cli.currentContext, err = resolveContextName(opts.Common, cli.configFile, cli.contextStore) - if err != nil { - return err - } - endpoint, err := resolveDockerEndpoint(cli.contextStore, cli.currentContext, opts.Common) - if err != nil { - return errors.Wrap(err, "unable to resolve docker endpoint") - } - cli.dockerEndpoint = endpoint + cli.contextStore = store.New(cliconfig.ContextStoreDir(), cli.contextStoreConfig) + cli.currentContext, err = resolveContextName(opts.Common, cli.configFile, cli.contextStore) + if err != nil { + return err + } + cli.dockerEndpoint, err = resolveDockerEndpoint(cli.contextStore, cli.currentContext, opts.Common) + if err != nil { + return errors.Wrap(err, "unable to resolve docker endpoint") + } - cli.client, err = newAPIClientFromEndpoint(endpoint, cli.configFile) + if cli.client == nil { + cli.client, err = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile) if tlsconfig.IsErrEncryptedKey(err) { passRetriever := passphrase.PromptRetrieverWithInOut(cli.In(), cli.Out(), nil) newClient := func(password string) (client.APIClient, error) { - endpoint.TLSPassword = password - return newAPIClientFromEndpoint(endpoint, cli.configFile) + cli.dockerEndpoint.TLSPassword = password + return newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile) } cli.client, err = getClientWithPassword(passRetriever, newClient) } diff --git a/vendor/github.com/docker/cli/cli/config/configfile/file.go b/vendor/github.com/docker/cli/cli/config/configfile/file.go index 656184993..c8d601162 100644 --- a/vendor/github.com/docker/cli/cli/config/configfile/file.go +++ b/vendor/github.com/docker/cli/cli/config/configfile/file.go @@ -24,31 +24,32 @@ const ( // ConfigFile ~/.docker/config.json file info type ConfigFile struct { - AuthConfigs map[string]types.AuthConfig `json:"auths"` - HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"` - PsFormat string `json:"psFormat,omitempty"` - ImagesFormat string `json:"imagesFormat,omitempty"` - NetworksFormat string `json:"networksFormat,omitempty"` - PluginsFormat string `json:"pluginsFormat,omitempty"` - VolumesFormat string `json:"volumesFormat,omitempty"` - StatsFormat string `json:"statsFormat,omitempty"` - DetachKeys string `json:"detachKeys,omitempty"` - CredentialsStore string `json:"credsStore,omitempty"` - CredentialHelpers map[string]string `json:"credHelpers,omitempty"` - Filename string `json:"-"` // Note: for internal use only - ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"` - ServicesFormat string `json:"servicesFormat,omitempty"` - TasksFormat string `json:"tasksFormat,omitempty"` - SecretFormat string `json:"secretFormat,omitempty"` - ConfigFormat string `json:"configFormat,omitempty"` - NodesFormat string `json:"nodesFormat,omitempty"` - PruneFilters []string `json:"pruneFilters,omitempty"` - Proxies map[string]ProxyConfig `json:"proxies,omitempty"` - Experimental string `json:"experimental,omitempty"` - StackOrchestrator string `json:"stackOrchestrator,omitempty"` - Kubernetes *KubernetesConfig `json:"kubernetes,omitempty"` - CurrentContext string `json:"currentContext,omitempty"` - CLIPluginsExtraDirs []string `json:"cliPluginsExtraDirs,omitempty"` + AuthConfigs map[string]types.AuthConfig `json:"auths"` + HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"` + PsFormat string `json:"psFormat,omitempty"` + ImagesFormat string `json:"imagesFormat,omitempty"` + NetworksFormat string `json:"networksFormat,omitempty"` + PluginsFormat string `json:"pluginsFormat,omitempty"` + VolumesFormat string `json:"volumesFormat,omitempty"` + StatsFormat string `json:"statsFormat,omitempty"` + DetachKeys string `json:"detachKeys,omitempty"` + CredentialsStore string `json:"credsStore,omitempty"` + CredentialHelpers map[string]string `json:"credHelpers,omitempty"` + Filename string `json:"-"` // Note: for internal use only + ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"` + ServicesFormat string `json:"servicesFormat,omitempty"` + TasksFormat string `json:"tasksFormat,omitempty"` + SecretFormat string `json:"secretFormat,omitempty"` + ConfigFormat string `json:"configFormat,omitempty"` + NodesFormat string `json:"nodesFormat,omitempty"` + PruneFilters []string `json:"pruneFilters,omitempty"` + Proxies map[string]ProxyConfig `json:"proxies,omitempty"` + Experimental string `json:"experimental,omitempty"` + StackOrchestrator string `json:"stackOrchestrator,omitempty"` + Kubernetes *KubernetesConfig `json:"kubernetes,omitempty"` + CurrentContext string `json:"currentContext,omitempty"` + CLIPluginsExtraDirs []string `json:"cliPluginsExtraDirs,omitempty"` + Plugins map[string]map[string]string `json:"plugins,omitempty"` } // ProxyConfig contains proxy configuration settings @@ -70,6 +71,7 @@ func New(fn string) *ConfigFile { AuthConfigs: make(map[string]types.AuthConfig), HTTPHeaders: make(map[string]string), Filename: fn, + Plugins: make(map[string]map[string]string), } } @@ -330,6 +332,42 @@ func (configFile *ConfigFile) GetFilename() string { return configFile.Filename } +// PluginConfig retrieves the requested option for the given plugin. +func (configFile *ConfigFile) PluginConfig(pluginname, option string) (string, bool) { + if configFile.Plugins == nil { + return "", false + } + pluginConfig, ok := configFile.Plugins[pluginname] + if !ok { + return "", false + } + value, ok := pluginConfig[option] + return value, ok +} + +// SetPluginConfig sets the option to the given value for the given +// plugin. Passing a value of "" will remove the option. If removing +// the final config item for a given plugin then also cleans up the +// overall plugin entry. +func (configFile *ConfigFile) SetPluginConfig(pluginname, option, value string) { + if configFile.Plugins == nil { + configFile.Plugins = make(map[string]map[string]string) + } + pluginConfig, ok := configFile.Plugins[pluginname] + if !ok { + pluginConfig = make(map[string]string) + configFile.Plugins[pluginname] = pluginConfig + } + if value != "" { + pluginConfig[option] = value + } else { + delete(pluginConfig, option) + } + if len(pluginConfig) == 0 { + delete(configFile.Plugins, pluginname) + } +} + func checkKubernetesConfiguration(kubeConfig *KubernetesConfig) error { if kubeConfig == nil { return nil diff --git a/vendor/github.com/docker/cli/cli/connhelper/commandconn/commandconn.go b/vendor/github.com/docker/cli/cli/connhelper/commandconn/commandconn.go new file mode 100644 index 000000000..7e03741fa --- /dev/null +++ b/vendor/github.com/docker/cli/cli/connhelper/commandconn/commandconn.go @@ -0,0 +1,281 @@ +// Package commandconn provides a net.Conn implementation that can be used for +// proxying (or emulating) stream via a custom command. +// +// For example, to provide an http.Client that can connect to a Docker daemon +// running in a Docker container ("DIND"): +// +// httpClient := &http.Client{ +// Transport: &http.Transport{ +// DialContext: func(ctx context.Context, _network, _addr string) (net.Conn, error) { +// return commandconn.New(ctx, "docker", "exec", "-it", containerID, "docker", "system", "dial-stdio") +// }, +// }, +// } +package commandconn + +import ( + "bytes" + "context" + "fmt" + "io" + "net" + "os" + "os/exec" + "runtime" + "strings" + "sync" + "syscall" + "time" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// New returns net.Conn +func New(ctx context.Context, cmd string, args ...string) (net.Conn, error) { + var ( + c commandConn + err error + ) + c.cmd = exec.CommandContext(ctx, cmd, args...) + // we assume that args never contains sensitive information + logrus.Debugf("commandconn: starting %s with %v", cmd, args) + c.cmd.Env = os.Environ() + setPdeathsig(c.cmd) + c.stdin, err = c.cmd.StdinPipe() + if err != nil { + return nil, err + } + c.stdout, err = c.cmd.StdoutPipe() + if err != nil { + return nil, err + } + c.cmd.Stderr = &stderrWriter{ + stderrMu: &c.stderrMu, + stderr: &c.stderr, + debugPrefix: fmt.Sprintf("commandconn (%s):", cmd), + } + c.localAddr = dummyAddr{network: "dummy", s: "dummy-0"} + c.remoteAddr = dummyAddr{network: "dummy", s: "dummy-1"} + return &c, c.cmd.Start() +} + +// commandConn implements net.Conn +type commandConn struct { + cmd *exec.Cmd + cmdExited bool + cmdWaitErr error + cmdMutex sync.Mutex + stdin io.WriteCloser + stdout io.ReadCloser + stderrMu sync.Mutex + stderr bytes.Buffer + stdioClosedMu sync.Mutex // for stdinClosed and stdoutClosed + stdinClosed bool + stdoutClosed bool + localAddr net.Addr + remoteAddr net.Addr +} + +// killIfStdioClosed kills the cmd if both stdin and stdout are closed. +func (c *commandConn) killIfStdioClosed() error { + c.stdioClosedMu.Lock() + stdioClosed := c.stdoutClosed && c.stdinClosed + c.stdioClosedMu.Unlock() + if !stdioClosed { + return nil + } + return c.kill() +} + +// killAndWait tries sending SIGTERM to the process before sending SIGKILL. +func killAndWait(cmd *exec.Cmd) error { + var werr error + if runtime.GOOS != "windows" { + werrCh := make(chan error) + go func() { werrCh <- cmd.Wait() }() + cmd.Process.Signal(syscall.SIGTERM) + select { + case werr = <-werrCh: + case <-time.After(3 * time.Second): + cmd.Process.Kill() + werr = <-werrCh + } + } else { + cmd.Process.Kill() + werr = cmd.Wait() + } + return werr +} + +// kill returns nil if the command terminated, regardless to the exit status. +func (c *commandConn) kill() error { + var werr error + c.cmdMutex.Lock() + if c.cmdExited { + werr = c.cmdWaitErr + } else { + werr = killAndWait(c.cmd) + c.cmdWaitErr = werr + c.cmdExited = true + } + c.cmdMutex.Unlock() + if werr == nil { + return nil + } + wExitErr, ok := werr.(*exec.ExitError) + if ok { + if wExitErr.ProcessState.Exited() { + return nil + } + } + return errors.Wrapf(werr, "commandconn: failed to wait") +} + +func (c *commandConn) onEOF(eof error) error { + // when we got EOF, the command is going to be terminated + var werr error + c.cmdMutex.Lock() + if c.cmdExited { + werr = c.cmdWaitErr + } else { + werrCh := make(chan error) + go func() { werrCh <- c.cmd.Wait() }() + select { + case werr = <-werrCh: + c.cmdWaitErr = werr + c.cmdExited = true + case <-time.After(10 * time.Second): + c.cmdMutex.Unlock() + c.stderrMu.Lock() + stderr := c.stderr.String() + c.stderrMu.Unlock() + return errors.Errorf("command %v did not exit after %v: stderr=%q", c.cmd.Args, eof, stderr) + } + } + c.cmdMutex.Unlock() + if werr == nil { + return eof + } + c.stderrMu.Lock() + stderr := c.stderr.String() + c.stderrMu.Unlock() + return errors.Errorf("command %v has exited with %v, please make sure the URL is valid, and Docker 18.09 or later is installed on the remote host: stderr=%s", c.cmd.Args, werr, stderr) +} + +func ignorableCloseError(err error) bool { + errS := err.Error() + ss := []string{ + os.ErrClosed.Error(), + } + for _, s := range ss { + if strings.Contains(errS, s) { + return true + } + } + return false +} + +func (c *commandConn) CloseRead() error { + // NOTE: maybe already closed here + if err := c.stdout.Close(); err != nil && !ignorableCloseError(err) { + logrus.Warnf("commandConn.CloseRead: %v", err) + } + c.stdioClosedMu.Lock() + c.stdoutClosed = true + c.stdioClosedMu.Unlock() + if err := c.killIfStdioClosed(); err != nil { + logrus.Warnf("commandConn.CloseRead: %v", err) + } + return nil +} + +func (c *commandConn) Read(p []byte) (int, error) { + n, err := c.stdout.Read(p) + if err == io.EOF { + err = c.onEOF(err) + } + return n, err +} + +func (c *commandConn) CloseWrite() error { + // NOTE: maybe already closed here + if err := c.stdin.Close(); err != nil && !ignorableCloseError(err) { + logrus.Warnf("commandConn.CloseWrite: %v", err) + } + c.stdioClosedMu.Lock() + c.stdinClosed = true + c.stdioClosedMu.Unlock() + if err := c.killIfStdioClosed(); err != nil { + logrus.Warnf("commandConn.CloseWrite: %v", err) + } + return nil +} + +func (c *commandConn) Write(p []byte) (int, error) { + n, err := c.stdin.Write(p) + if err == io.EOF { + err = c.onEOF(err) + } + return n, err +} + +func (c *commandConn) Close() error { + var err error + if err = c.CloseRead(); err != nil { + logrus.Warnf("commandConn.Close: CloseRead: %v", err) + } + if err = c.CloseWrite(); err != nil { + logrus.Warnf("commandConn.Close: CloseWrite: %v", err) + } + return err +} + +func (c *commandConn) LocalAddr() net.Addr { + return c.localAddr +} +func (c *commandConn) RemoteAddr() net.Addr { + return c.remoteAddr +} +func (c *commandConn) SetDeadline(t time.Time) error { + logrus.Debugf("unimplemented call: SetDeadline(%v)", t) + return nil +} +func (c *commandConn) SetReadDeadline(t time.Time) error { + logrus.Debugf("unimplemented call: SetReadDeadline(%v)", t) + return nil +} +func (c *commandConn) SetWriteDeadline(t time.Time) error { + logrus.Debugf("unimplemented call: SetWriteDeadline(%v)", t) + return nil +} + +type dummyAddr struct { + network string + s string +} + +func (d dummyAddr) Network() string { + return d.network +} + +func (d dummyAddr) String() string { + return d.s +} + +type stderrWriter struct { + stderrMu *sync.Mutex + stderr *bytes.Buffer + debugPrefix string +} + +func (w *stderrWriter) Write(p []byte) (int, error) { + logrus.Debugf("%s%s", w.debugPrefix, string(p)) + w.stderrMu.Lock() + if w.stderr.Len() > 4096 { + w.stderr.Reset() + } + n, err := w.stderr.Write(p) + w.stderrMu.Unlock() + return n, err +} diff --git a/vendor/github.com/docker/cli/cli/connhelper/connhelper_linux.go b/vendor/github.com/docker/cli/cli/connhelper/commandconn/commandconn_linux.go similarity index 87% rename from vendor/github.com/docker/cli/cli/connhelper/connhelper_linux.go rename to vendor/github.com/docker/cli/cli/connhelper/commandconn/commandconn_linux.go index f138f5367..7d8b122e3 100644 --- a/vendor/github.com/docker/cli/cli/connhelper/connhelper_linux.go +++ b/vendor/github.com/docker/cli/cli/connhelper/commandconn/commandconn_linux.go @@ -1,4 +1,4 @@ -package connhelper +package commandconn import ( "os/exec" diff --git a/vendor/github.com/docker/cli/cli/connhelper/connhelper_nolinux.go b/vendor/github.com/docker/cli/cli/connhelper/commandconn/commandconn_nolinux.go similarity index 79% rename from vendor/github.com/docker/cli/cli/connhelper/connhelper_nolinux.go rename to vendor/github.com/docker/cli/cli/connhelper/commandconn/commandconn_nolinux.go index c8350d9d7..ab0716672 100644 --- a/vendor/github.com/docker/cli/cli/connhelper/connhelper_nolinux.go +++ b/vendor/github.com/docker/cli/cli/connhelper/commandconn/commandconn_nolinux.go @@ -1,6 +1,6 @@ // +build !linux -package connhelper +package commandconn import ( "os/exec" diff --git a/vendor/github.com/docker/cli/cli/connhelper/connhelper.go b/vendor/github.com/docker/cli/cli/connhelper/connhelper.go index 94cc1d515..da3640db1 100644 --- a/vendor/github.com/docker/cli/cli/connhelper/connhelper.go +++ b/vendor/github.com/docker/cli/cli/connhelper/connhelper.go @@ -2,23 +2,13 @@ package connhelper import ( - "bytes" "context" - "fmt" - "io" "net" "net/url" - "os" - "os/exec" - "runtime" - "strings" - "sync" - "syscall" - "time" + "github.com/docker/cli/cli/connhelper/commandconn" "github.com/docker/cli/cli/connhelper/ssh" "github.com/pkg/errors" - "github.com/sirupsen/logrus" ) // ConnectionHelper allows to connect to a remote host with custom stream provider binary. @@ -29,7 +19,8 @@ type ConnectionHelper struct { // GetConnectionHelper returns Docker-specific connection helper for the given URL. // GetConnectionHelper returns nil without error when no helper is registered for the scheme. -// URL is like "ssh://me@server01". +// +// ssh://@ URL requires Docker 18.09 or later on the remote host. func GetConnectionHelper(daemonURL string) (*ConnectionHelper, error) { u, err := url.Parse(daemonURL) if err != nil { @@ -37,13 +28,13 @@ func GetConnectionHelper(daemonURL string) (*ConnectionHelper, error) { } switch scheme := u.Scheme; scheme { case "ssh": - sshCmd, sshArgs, err := ssh.New(daemonURL) + sp, err := ssh.ParseURL(daemonURL) if err != nil { - return nil, err + return nil, errors.Wrap(err, "ssh host connection is not valid") } return &ConnectionHelper{ Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) { - return newCommandConn(ctx, sshCmd, sshArgs...) + return commandconn.New(ctx, "ssh", append(sp.Args(), []string{"--", "docker", "system", "dial-stdio"}...)...) }, Host: "http://docker", }, nil @@ -53,260 +44,12 @@ func GetConnectionHelper(daemonURL string) (*ConnectionHelper, error) { return nil, err } -// GetCommandConnectionHelper returns a ConnectionHelp constructed from an arbitrary command. +// GetCommandConnectionHelper returns Docker-specific connection helper constructed from an arbitrary command. func GetCommandConnectionHelper(cmd string, flags ...string) (*ConnectionHelper, error) { return &ConnectionHelper{ Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) { - return newCommandConn(ctx, cmd, flags...) + return commandconn.New(ctx, cmd, flags...) }, Host: "http://docker", }, nil } - -func newCommandConn(ctx context.Context, cmd string, args ...string) (net.Conn, error) { - var ( - c commandConn - err error - ) - c.cmd = exec.CommandContext(ctx, cmd, args...) - // we assume that args never contains sensitive information - logrus.Debugf("connhelper: starting %s with %v", cmd, args) - c.cmd.Env = os.Environ() - setPdeathsig(c.cmd) - c.stdin, err = c.cmd.StdinPipe() - if err != nil { - return nil, err - } - c.stdout, err = c.cmd.StdoutPipe() - if err != nil { - return nil, err - } - c.cmd.Stderr = &stderrWriter{ - stderrMu: &c.stderrMu, - stderr: &c.stderr, - debugPrefix: fmt.Sprintf("connhelper (%s):", cmd), - } - c.localAddr = dummyAddr{network: "dummy", s: "dummy-0"} - c.remoteAddr = dummyAddr{network: "dummy", s: "dummy-1"} - return &c, c.cmd.Start() -} - -// commandConn implements net.Conn -type commandConn struct { - cmd *exec.Cmd - cmdExited bool - cmdWaitErr error - cmdMutex sync.Mutex - stdin io.WriteCloser - stdout io.ReadCloser - stderrMu sync.Mutex - stderr bytes.Buffer - stdioClosedMu sync.Mutex // for stdinClosed and stdoutClosed - stdinClosed bool - stdoutClosed bool - localAddr net.Addr - remoteAddr net.Addr -} - -// killIfStdioClosed kills the cmd if both stdin and stdout are closed. -func (c *commandConn) killIfStdioClosed() error { - c.stdioClosedMu.Lock() - stdioClosed := c.stdoutClosed && c.stdinClosed - c.stdioClosedMu.Unlock() - if !stdioClosed { - return nil - } - return c.kill() -} - -// killAndWait tries sending SIGTERM to the process before sending SIGKILL. -func killAndWait(cmd *exec.Cmd) error { - var werr error - if runtime.GOOS != "windows" { - werrCh := make(chan error) - go func() { werrCh <- cmd.Wait() }() - cmd.Process.Signal(syscall.SIGTERM) - select { - case werr = <-werrCh: - case <-time.After(3 * time.Second): - cmd.Process.Kill() - werr = <-werrCh - } - } else { - cmd.Process.Kill() - werr = cmd.Wait() - } - return werr -} - -// kill returns nil if the command terminated, regardless to the exit status. -func (c *commandConn) kill() error { - var werr error - c.cmdMutex.Lock() - if c.cmdExited { - werr = c.cmdWaitErr - } else { - werr = killAndWait(c.cmd) - c.cmdWaitErr = werr - c.cmdExited = true - } - c.cmdMutex.Unlock() - if werr == nil { - return nil - } - wExitErr, ok := werr.(*exec.ExitError) - if ok { - if wExitErr.ProcessState.Exited() { - return nil - } - } - return errors.Wrapf(werr, "connhelper: failed to wait") -} - -func (c *commandConn) onEOF(eof error) error { - // when we got EOF, the command is going to be terminated - var werr error - c.cmdMutex.Lock() - if c.cmdExited { - werr = c.cmdWaitErr - } else { - werrCh := make(chan error) - go func() { werrCh <- c.cmd.Wait() }() - select { - case werr = <-werrCh: - c.cmdWaitErr = werr - c.cmdExited = true - case <-time.After(10 * time.Second): - c.cmdMutex.Unlock() - c.stderrMu.Lock() - stderr := c.stderr.String() - c.stderrMu.Unlock() - return errors.Errorf("command %v did not exit after %v: stderr=%q", c.cmd.Args, eof, stderr) - } - } - c.cmdMutex.Unlock() - if werr == nil { - return eof - } - c.stderrMu.Lock() - stderr := c.stderr.String() - c.stderrMu.Unlock() - return errors.Errorf("command %v has exited with %v, please make sure the URL is valid, and Docker 18.09 or later is installed on the remote host: stderr=%s", c.cmd.Args, werr, stderr) -} - -func ignorableCloseError(err error) bool { - errS := err.Error() - ss := []string{ - os.ErrClosed.Error(), - } - for _, s := range ss { - if strings.Contains(errS, s) { - return true - } - } - return false -} - -func (c *commandConn) CloseRead() error { - // NOTE: maybe already closed here - if err := c.stdout.Close(); err != nil && !ignorableCloseError(err) { - logrus.Warnf("commandConn.CloseRead: %v", err) - } - c.stdioClosedMu.Lock() - c.stdoutClosed = true - c.stdioClosedMu.Unlock() - if err := c.killIfStdioClosed(); err != nil { - logrus.Warnf("commandConn.CloseRead: %v", err) - } - return nil -} - -func (c *commandConn) Read(p []byte) (int, error) { - n, err := c.stdout.Read(p) - if err == io.EOF { - err = c.onEOF(err) - } - return n, err -} - -func (c *commandConn) CloseWrite() error { - // NOTE: maybe already closed here - if err := c.stdin.Close(); err != nil && !ignorableCloseError(err) { - logrus.Warnf("commandConn.CloseWrite: %v", err) - } - c.stdioClosedMu.Lock() - c.stdinClosed = true - c.stdioClosedMu.Unlock() - if err := c.killIfStdioClosed(); err != nil { - logrus.Warnf("commandConn.CloseWrite: %v", err) - } - return nil -} - -func (c *commandConn) Write(p []byte) (int, error) { - n, err := c.stdin.Write(p) - if err == io.EOF { - err = c.onEOF(err) - } - return n, err -} - -func (c *commandConn) Close() error { - var err error - if err = c.CloseRead(); err != nil { - logrus.Warnf("commandConn.Close: CloseRead: %v", err) - } - if err = c.CloseWrite(); err != nil { - logrus.Warnf("commandConn.Close: CloseWrite: %v", err) - } - return err -} - -func (c *commandConn) LocalAddr() net.Addr { - return c.localAddr -} -func (c *commandConn) RemoteAddr() net.Addr { - return c.remoteAddr -} -func (c *commandConn) SetDeadline(t time.Time) error { - logrus.Debugf("unimplemented call: SetDeadline(%v)", t) - return nil -} -func (c *commandConn) SetReadDeadline(t time.Time) error { - logrus.Debugf("unimplemented call: SetReadDeadline(%v)", t) - return nil -} -func (c *commandConn) SetWriteDeadline(t time.Time) error { - logrus.Debugf("unimplemented call: SetWriteDeadline(%v)", t) - return nil -} - -type dummyAddr struct { - network string - s string -} - -func (d dummyAddr) Network() string { - return d.network -} - -func (d dummyAddr) String() string { - return d.s -} - -type stderrWriter struct { - stderrMu *sync.Mutex - stderr *bytes.Buffer - debugPrefix string -} - -func (w *stderrWriter) Write(p []byte) (int, error) { - logrus.Debugf("%s%s", w.debugPrefix, string(p)) - w.stderrMu.Lock() - if w.stderr.Len() > 4096 { - w.stderr.Reset() - } - n, err := w.stderr.Write(p) - w.stderrMu.Unlock() - return n, err -} diff --git a/vendor/github.com/docker/cli/cli/connhelper/ssh/ssh.go b/vendor/github.com/docker/cli/cli/connhelper/ssh/ssh.go index f134df138..06cb98364 100644 --- a/vendor/github.com/docker/cli/cli/connhelper/ssh/ssh.go +++ b/vendor/github.com/docker/cli/cli/connhelper/ssh/ssh.go @@ -1,5 +1,4 @@ // Package ssh provides the connection helper for ssh:// URL. -// Requires Docker 18.09 or later on the remote host. package ssh import ( @@ -8,16 +7,8 @@ import ( "github.com/pkg/errors" ) -// New returns cmd and its args -func New(daemonURL string) (string, []string, error) { - sp, err := parseSSHURL(daemonURL) - if err != nil { - return "", nil, errors.Wrap(err, "SSH host connection is not valid") - } - return "ssh", append(sp.Args(), []string{"--", "docker", "system", "dial-stdio"}...), nil -} - -func parseSSHURL(daemonURL string) (*sshSpec, error) { +// ParseURL parses URL +func ParseURL(daemonURL string) (*Spec, error) { u, err := url.Parse(daemonURL) if err != nil { return nil, err @@ -26,19 +17,19 @@ func parseSSHURL(daemonURL string) (*sshSpec, error) { return nil, errors.Errorf("expected scheme ssh, got %q", u.Scheme) } - var sp sshSpec + var sp Spec if u.User != nil { - sp.user = u.User.Username() + sp.User = u.User.Username() if _, ok := u.User.Password(); ok { return nil, errors.New("plain-text password is not supported") } } - sp.host = u.Hostname() - if sp.host == "" { + sp.Host = u.Hostname() + if sp.Host == "" { return nil, errors.Errorf("no host specified") } - sp.port = u.Port() + sp.Port = u.Port() if u.Path != "" { return nil, errors.Errorf("extra path after the host: %q", u.Path) } @@ -51,20 +42,22 @@ func parseSSHURL(daemonURL string) (*sshSpec, error) { return &sp, err } -type sshSpec struct { - user string - host string - port string +// Spec of SSH URL +type Spec struct { + User string + Host string + Port string } -func (sp *sshSpec) Args() []string { +// Args returns args except "ssh" itself and "-- ..." +func (sp *Spec) Args() []string { var args []string - if sp.user != "" { - args = append(args, "-l", sp.user) + if sp.User != "" { + args = append(args, "-l", sp.User) } - if sp.port != "" { - args = append(args, "-p", sp.port) + if sp.Port != "" { + args = append(args, "-p", sp.Port) } - args = append(args, sp.host) + args = append(args, sp.Host) return args } From 9678c4baa0d16808b2bbe18c1c5420f2911b4374 Mon Sep 17 00:00:00 2001 From: Ulysses Souza Date: Thu, 7 Mar 2019 13:23:58 +0100 Subject: [PATCH 09/12] Fix EXPERIMENTAL=on tests Signed-off-by: Ulysses Souza --- e2e/commands_test.go | 2 +- e2e/plugin_test.go | 7 +++++- e2e/testdata/plugin-usage-experimental.golden | 23 +++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 e2e/testdata/plugin-usage-experimental.golden diff --git a/e2e/commands_test.go b/e2e/commands_test.go index c6eef9d1b..779e31e81 100644 --- a/e2e/commands_test.go +++ b/e2e/commands_test.go @@ -56,7 +56,7 @@ func testRenderApp(appPath string, env ...string) func(*testing.T) { } result := icmd.RunCmd(icmd.Cmd{ Command: args, - Env: env, + Env: append(os.Environ(), env...), }).Assert(t, icmd.Success) assert.Assert(t, is.Equal(readFile(t, filepath.Join(appPath, "expected.txt")), result.Stdout()), "rendering mismatch") } diff --git a/e2e/plugin_test.go b/e2e/plugin_test.go index fb49e8b51..0e1a3387c 100644 --- a/e2e/plugin_test.go +++ b/e2e/plugin_test.go @@ -19,7 +19,12 @@ func TestInvokePluginFromCLI(t *testing.T) { // docker app --help prints docker-app help cmd.Command = dockerCli.Command("app", "--help") usage := icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined() - golden.Assert(t, usage, "plugin-usage.golden") + + goldenFile := "plugin-usage.golden" + if hasExperimental { + goldenFile = "plugin-usage-experimental.golden" + } + golden.Assert(t, usage, goldenFile) // docker info should print app version and short description cmd.Command = dockerCli.Command("info") diff --git a/e2e/testdata/plugin-usage-experimental.golden b/e2e/testdata/plugin-usage-experimental.golden new file mode 100644 index 000000000..6cae686ae --- /dev/null +++ b/e2e/testdata/plugin-usage-experimental.golden @@ -0,0 +1,23 @@ + +Usage: docker app COMMAND + +Build and deploy Docker Application Packages. + +Commands: + bundle Create a CNAB invocation image and bundle.json for the application. + completion Generates completion scripts for the specified shell (bash or zsh) + init Start building a Docker application + inspect Shows metadata, parameters and a summary of the compose file for a given application + install Install an application + merge Merge a multi-file application into a single file + pull Pull an application from a registry + push Push the application to a registry + render Render the Compose file for the application + split Split a single-file application into multiple files + status Get the installation status. If the installation is a docker application, the status shows the stack services. + uninstall Uninstall an application + upgrade Upgrade an installed application + validate Checks the rendered application is syntactically correct + version Print version information + +Run 'docker app COMMAND --help' for more information on a command. From c9c81eb535fdeb6adac449f7fe287badeeb571b7 Mon Sep 17 00:00:00 2001 From: Ulysses Souza Date: Thu, 7 Mar 2019 15:13:31 +0100 Subject: [PATCH 10/12] Make e2e test arguments uniform Signed-off-by: Ulysses Souza --- e2e/main_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/e2e/main_test.go b/e2e/main_test.go index f9de8aeec..0f00ac81a 100644 --- a/e2e/main_test.go +++ b/e2e/main_test.go @@ -62,37 +62,37 @@ func TestMain(m *testing.M) { } configDir, err := ioutil.TempDir("", "config") if err != nil { - panic(err.Error()) + panic(err) } defer os.RemoveAll(configDir) err = os.Setenv("DOCKER_CONFIG", configDir) if err != nil { - panic(err.Error()) + panic(err) } dockerCli = dockerCliCommand{path: dockerCliPath, config: configDir} config := dockerConfigFile.ConfigFile{CLIPluginsExtraDirs: []string{configDir}} configFile, err := os.Create(filepath.Join(configDir, "config.json")) if err != nil { - panic(err.Error()) + panic(err) } err = json.NewEncoder(configFile).Encode(config) if err != nil { - panic(err.Error()) + panic(err) } dockerAppExecName := "docker-app" if runtime.GOOS == "windows" { dockerAppExecName += ".exe" } if err := os.Symlink(dockerApp, filepath.Join(configDir, dockerAppExecName)); err != nil { - panic(err.Error()) + panic(err) } cmd := exec.Command(dockerApp, "app", "version") output, err := cmd.CombinedOutput() if err != nil { - panic(err.Error()) + panic(err) } hasExperimental = bytes.Contains(output, []byte("Experimental: on")) i := strings.Index(string(output), "Renderers") From 02db9b446041da376b4ca72d33c6ce46cf8317a9 Mon Sep 17 00:00:00 2001 From: Ulysses Souza Date: Thu, 7 Mar 2019 15:14:39 +0100 Subject: [PATCH 11/12] Fix plugins related text in README.md Signed-off-by: Ulysses Souza --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7e72e2500..f18d5e3b2 100644 --- a/README.md +++ b/README.md @@ -188,10 +188,11 @@ Removing network hello_default Pre-built binaries are available on [GitHub releases](https://github.com/docker/app/releases) for Windows, Linux and macOS. Each tarball contains two binaries: -- `docker-app-plugin-{linux|macos|windows}` which is docker-app as a [docker cli plugin](https://github.com/docker/cli/issues/1534) -- `docker-app-standalone-{linux|macos|windows}` which is docker-app as a standalone utility +- `docker-app-plugin-{linux|darwin|windows.exe}` which is docker-app as a [docker cli plugin](https://github.com/docker/cli/issues/1534). **Note**: This requires a pre-release version of the +Docker CLI +- `docker-app-standalone-{linux|darwin|windows.exe}` which is docker-app as a standalone utility -To use `docker-app` plugin, just type `docker app` instead of `docker-app` and all the examples will work the same way: +To use the `docker-app` plugin, just type `docker app` instead of `docker-app` and all the examples will work the same way: ```bash $ docker app version Version: v0.8 @@ -248,9 +249,6 @@ New-Item -ItemType Directory -Path ~/.docker/cli-plugins -ErrorAction SilentlyCo cp docker-app-plugin-windows.exe ~/.docker/cli-plugins/docker-app.exe ``` - -**Note:** To use Application Packages as images (i.e.: `save`, `push`, or `install` when package is not present locally) on Windows, one must be in Linux container mode. - ## Single file or directory representation If you prefer having the three core documents in separate YAML files, omit the `-s` / `--single-file` option to From 30fda72af706c394f6bbde000278f75c3a00b1d7 Mon Sep 17 00:00:00 2001 From: Ulysses Souza Date: Thu, 7 Mar 2019 16:37:38 +0100 Subject: [PATCH 12/12] Use unique config for each test Signed-off-by: Ulysses Souza --- e2e/cnab_test.go | 6 ++- e2e/commands_test.go | 87 +++++++++++++++++++++++--------------------- e2e/example_test.go | 7 +++- e2e/main_test.go | 67 ++++++++++++++++++++-------------- e2e/plugin_test.go | 4 +- e2e/pushpull_test.go | 21 +++-------- 6 files changed, 101 insertions(+), 91 deletions(-) diff --git a/e2e/cnab_test.go b/e2e/cnab_test.go index d59781cf0..0bf72bf89 100644 --- a/e2e/cnab_test.go +++ b/e2e/cnab_test.go @@ -2,7 +2,6 @@ package e2e import ( "fmt" - "os" "path" "runtime" "testing" @@ -36,10 +35,13 @@ func TestCallCustomStatusAction(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { + cmd, cleanup := dockerCli.createTestCmd() + defer cleanup() + tmpDir := fs.NewDir(t, t.Name()) defer tmpDir.Remove() testDir := path.Join("testdata", testCase.cnab) - cmd := icmd.Cmd{Env: append(os.Environ(), fmt.Sprintf("DUFFLE_HOME=%s", tmpDir.Path()))} + cmd.Env = append(cmd.Env, "DUFFLE_HOME="+tmpDir.Path()) // We need to explicitly set the SYSTEMROOT on windows // otherwise we get the error: diff --git a/e2e/commands_test.go b/e2e/commands_test.go index 779e31e81..4e0d44b34 100644 --- a/e2e/commands_test.go +++ b/e2e/commands_test.go @@ -3,7 +3,6 @@ package e2e import ( "fmt" "io/ioutil" - "os" "path/filepath" "regexp" "strings" @@ -46,6 +45,9 @@ func TestRender(t *testing.T) { func testRenderApp(appPath string, env ...string) func(*testing.T) { return func(t *testing.T) { + cmd, cleanup := dockerCli.createTestCmd() + defer cleanup() + envParameters := map[string]string{} data, err := ioutil.ReadFile(filepath.Join(appPath, "env.yml")) assert.NilError(t, err) @@ -54,17 +56,19 @@ func testRenderApp(appPath string, env ...string) func(*testing.T) { for k, v := range envParameters { args = append(args, "--set", fmt.Sprintf("%s=%s", k, v)) } - result := icmd.RunCmd(icmd.Cmd{ - Command: args, - Env: append(os.Environ(), env...), - }).Assert(t, icmd.Success) + cmd.Command = args + cmd.Env = append(cmd.Env, env...) + result := icmd.RunCmd(cmd).Assert(t, icmd.Success) assert.Assert(t, is.Equal(readFile(t, filepath.Join(appPath, "expected.txt")), result.Stdout()), "rendering mismatch") } } func TestRenderFormatters(t *testing.T) { + cmd, cleanup := dockerCli.createTestCmd() + defer cleanup() + appPath := filepath.Join("testdata", "simple", "simple.dockerapp") - cmd := icmd.Cmd{Command: dockerCli.Command("app", "render", "--formatter", "json", appPath)} + cmd.Command = dockerCli.Command("app", "render", "--formatter", "json", appPath) result := icmd.RunCmd(cmd).Assert(t, icmd.Success) golden.Assert(t, result.Stdout(), "expected-json-render.golden") @@ -74,6 +78,9 @@ func TestRenderFormatters(t *testing.T) { } func TestInit(t *testing.T) { + cmd, cleanup := dockerCli.createTestCmd() + defer cleanup() + composeData := `version: "3.2" services: nginx: @@ -103,8 +110,7 @@ maintainers: testAppName := "app-test" dirName := internal.DirNameFromAppName(testAppName) - cmd := icmd.Cmd{Dir: tmpDir.Path(), Env: os.Environ()} - + cmd.Dir = tmpDir.Path() cmd.Command = dockerCli.Command("app", "init", testAppName, "--compose-file", tmpDir.Join(internal.ComposeFileName), @@ -149,6 +155,9 @@ maintainers: } func TestDetectApp(t *testing.T) { + cmd, cleanup := dockerCli.createTestCmd() + defer cleanup() + // cwd = e2e dir := fs.NewDir(t, "detect-app-binary", fs.WithDir("attachments.dockerapp", fs.FromDir("testdata/attachments.dockerapp")), @@ -158,33 +167,35 @@ func TestDetectApp(t *testing.T) { ), ) defer dir.Remove() - icmd.RunCmd(icmd.Cmd{ - Command: dockerCli.Command("app", "inspect"), - Dir: dir.Path(), - }).Assert(t, icmd.Success) - icmd.RunCmd(icmd.Cmd{ - Command: dockerCli.Command("app", "inspect"), - Dir: dir.Join("attachments.dockerapp"), - }).Assert(t, icmd.Success) - icmd.RunCmd(icmd.Cmd{ - Command: dockerCli.Command("app", "inspect", "."), - Dir: dir.Join("attachments.dockerapp"), - }).Assert(t, icmd.Success) - result := icmd.RunCmd(icmd.Cmd{ - Command: dockerCli.Command("app", "inspect"), - Dir: dir.Join("render"), - }) - result.Assert(t, icmd.Expected{ + + cmd.Command = dockerCli.Command("app", "inspect") + cmd.Dir = dir.Path() + icmd.RunCmd(cmd).Assert(t, icmd.Success) + + cmd.Command = dockerCli.Command("app", "inspect") + cmd.Dir = dir.Join("attachments.dockerapp") + icmd.RunCmd(cmd).Assert(t, icmd.Success) + + cmd.Command = dockerCli.Command("app", "inspect", ".") + cmd.Dir = dir.Join("attachments.dockerapp") + icmd.RunCmd(cmd).Assert(t, icmd.Success) + + cmd.Command = dockerCli.Command("app", "inspect") + cmd.Dir = dir.Join("render") + icmd.RunCmd(cmd).Assert(t, icmd.Expected{ ExitCode: 1, Err: "Error: multiple applications found in current directory, specify the application name on the command line", }) } func TestSplitMerge(t *testing.T) { + cmd, cleanup := dockerCli.createTestCmd() + defer cleanup() + tmpDir := fs.NewDir(t, "split_merge") defer tmpDir.Remove() - cmd := icmd.Cmd{Command: dockerCli.Command("app", "merge", "testdata/render/envvariables/my.dockerapp", "--output", tmpDir.Join("remerged.dockerapp"))} + cmd.Command = dockerCli.Command("app", "merge", "testdata/render/envvariables/my.dockerapp", "--output", tmpDir.Join("remerged.dockerapp")) icmd.RunCmd(cmd).Assert(t, icmd.Success) cmd.Dir = tmpDir.Path() @@ -211,10 +222,11 @@ func TestSplitMerge(t *testing.T) { } func TestBundle(t *testing.T) { + cmd, cleanup := dockerCli.createTestCmd() + defer cleanup() + tmpDir := fs.NewDir(t, t.Name()) defer tmpDir.Remove() - // Using a custom DOCKER_CONFIG to store contexts in a temporary directory - cmd := icmd.Cmd{Env: os.Environ()} // Running a docker in docker to bundle the application dind := NewContainer("docker:18.09-dind", 2375) @@ -265,15 +277,14 @@ func TestBundle(t *testing.T) { } func TestDockerAppLifecycle(t *testing.T) { + cmd, cleanup := dockerCli.createTestCmd() + defer cleanup() + tmpDir := fs.NewDir(t, t.Name()) defer tmpDir.Remove() - cmd := icmd.Cmd{ - Env: append(os.Environ(), - fmt.Sprintf("DUFFLE_HOME=%s", tmpDir.Path()), - "DOCKER_TARGET_CONTEXT=swarm-target-context", - ), - } + cmd.Env = append(cmd.Env, "DUFFLE_HOME="+tmpDir.Path()) + cmd.Env = append(cmd.Env, "DOCKER_TARGET_CONTEXT=swarm-target-context") // Running a swarm using docker in docker to install the application // and run the invocation image @@ -289,10 +300,6 @@ func TestDockerAppLifecycle(t *testing.T) { // - the target context for the invocation image to install within the swarm cmd.Command = dockerCli.Command("context", "create", "swarm-context", "--docker", fmt.Sprintf(`"host=tcp://%s"`, swarm.GetAddress(t)), "--default-stack-orchestrator", "swarm") icmd.RunCmd(cmd).Assert(t, icmd.Success) - defer func() { - cmd.Command = dockerCli.Command("context", "rm", "--force", "swarm-context") - icmd.RunCmd(cmd) - }() // When creating a context on a Windows host we cannot use // the unix socket but it's needed inside the invocation image. @@ -301,10 +308,6 @@ func TestDockerAppLifecycle(t *testing.T) { // invocation image cmd.Command = dockerCli.Command("context", "create", "swarm-target-context", "--docker", "host=", "--default-stack-orchestrator", "swarm") icmd.RunCmd(cmd).Assert(t, icmd.Success) - defer func() { - cmd.Command = dockerCli.Command("context", "rm", "--force", "swarm-target-context") - icmd.RunCmd(cmd) - }() // Initialize the swarm cmd.Env = append(cmd.Env, "DOCKER_CONTEXT=swarm-context") diff --git a/e2e/example_test.go b/e2e/example_test.go index c600c76fa..be985d7f0 100644 --- a/e2e/example_test.go +++ b/e2e/example_test.go @@ -11,6 +11,9 @@ import ( ) func TestExamplesAreValid(t *testing.T) { + cmd, cleanup := dockerCli.createTestCmd() + defer cleanup() + err := filepath.Walk("../examples", func(p string, info os.FileInfo, err error) error { appPath := filepath.Join(p, filepath.Base(p)+".dockerapp") _, statErr := os.Stat(appPath) @@ -24,8 +27,8 @@ func TestExamplesAreValid(t *testing.T) { case os.IsNotExist(statErr): return nil default: - result := icmd.RunCmd(icmd.Cmd{Command: dockerCli.Command("app", "validate", appPath)}) - result.Assert(t, icmd.Success) + cmd.Command = dockerCli.Command("app", "validate", appPath) + icmd.RunCmd(cmd).Assert(t, icmd.Success) return filepath.SkipDir } }) diff --git a/e2e/main_test.go b/e2e/main_test.go index 0f00ac81a..eae3ea049 100644 --- a/e2e/main_test.go +++ b/e2e/main_test.go @@ -13,6 +13,7 @@ import ( "testing" dockerConfigFile "github.com/docker/cli/cli/config/configfile" + "gotest.tools/icmd" ) var ( @@ -24,8 +25,29 @@ var ( ) type dockerCliCommand struct { - path string - config string + path string + cliPluginDir string +} + +func (d dockerCliCommand) createTestCmd() (icmd.Cmd, func()) { + configDir, err := ioutil.TempDir("", "config") + if err != nil { + panic(err) + } + config := dockerConfigFile.ConfigFile{CLIPluginsExtraDirs: []string{d.cliPluginDir}} + configFile, err := os.Create(filepath.Join(configDir, "config.json")) + if err != nil { + panic(err) + } + err = json.NewEncoder(configFile).Encode(config) + if err != nil { + panic(err) + } + cleanup := func() { + os.RemoveAll(configDir) + } + env := append(os.Environ(), "DOCKER_CONFIG="+configDir) + return icmd.Cmd{Env: env}, cleanup } func (d dockerCliCommand) Command(args ...string) []string { @@ -49,9 +71,6 @@ func TestMain(m *testing.M) { if err != nil { panic(err) } - // Prepare docker cli to call the docker-app plugin binary: - // - Create a config dir with a custom config file - // - Create a symbolic link with the dockerApp binary to the plugin directory if dockerCliPath == "" { dockerCliPath = "docker" } else { @@ -60,27 +79,29 @@ func TestMain(m *testing.M) { panic(err) } } - configDir, err := ioutil.TempDir("", "config") + // Prepare docker cli to call the docker-app plugin binary: + // - Create a symbolic link with the dockerApp binary to the plugin directory + cliPluginDir, err := ioutil.TempDir("", "configContent") if err != nil { panic(err) } - defer os.RemoveAll(configDir) + defer os.RemoveAll(cliPluginDir) + createDockerAppSymLink(dockerApp, cliPluginDir) - err = os.Setenv("DOCKER_CONFIG", configDir) - if err != nil { - panic(err) - } - dockerCli = dockerCliCommand{path: dockerCliPath, config: configDir} + dockerCli = dockerCliCommand{path: dockerCliPath, cliPluginDir: cliPluginDir} - config := dockerConfigFile.ConfigFile{CLIPluginsExtraDirs: []string{configDir}} - configFile, err := os.Create(filepath.Join(configDir, "config.json")) - if err != nil { - panic(err) - } - err = json.NewEncoder(configFile).Encode(config) + cmd := exec.Command(dockerApp, "app", "version") + output, err := cmd.CombinedOutput() if err != nil { panic(err) } + hasExperimental = bytes.Contains(output, []byte("Experimental: on")) + i := strings.Index(string(output), "Renderers") + renderers = string(output)[i+10:] + os.Exit(m.Run()) +} + +func createDockerAppSymLink(dockerApp, configDir string) { dockerAppExecName := "docker-app" if runtime.GOOS == "windows" { dockerAppExecName += ".exe" @@ -88,14 +109,4 @@ func TestMain(m *testing.M) { if err := os.Symlink(dockerApp, filepath.Join(configDir, dockerAppExecName)); err != nil { panic(err) } - - cmd := exec.Command(dockerApp, "app", "version") - output, err := cmd.CombinedOutput() - if err != nil { - panic(err) - } - hasExperimental = bytes.Contains(output, []byte("Experimental: on")) - i := strings.Index(string(output), "Renderers") - renderers = string(output)[i+10:] - os.Exit(m.Run()) } diff --git a/e2e/plugin_test.go b/e2e/plugin_test.go index 0e1a3387c..3415a3763 100644 --- a/e2e/plugin_test.go +++ b/e2e/plugin_test.go @@ -10,8 +10,10 @@ import ( ) func TestInvokePluginFromCLI(t *testing.T) { + cmd, cleanup := dockerCli.createTestCmd() + defer cleanup() // docker --help should list app as a top command - cmd := icmd.Cmd{Command: dockerCli.Command("--help")} + cmd.Command = dockerCli.Command("--help") icmd.RunCmd(cmd).Assert(t, icmd.Expected{ Out: "app* Docker Application Packages (Docker Inc.,", }) diff --git a/e2e/pushpull_test.go b/e2e/pushpull_test.go index 953ef2fc8..be22f722b 100644 --- a/e2e/pushpull_test.go +++ b/e2e/pushpull_test.go @@ -4,7 +4,6 @@ import ( "fmt" "math/rand" "net" - "os" "path/filepath" "strconv" "testing" @@ -25,17 +24,15 @@ type dindSwarmAndRegistryInfo struct { } func runWithDindSwarmAndRegistry(t *testing.T, todo func(dindSwarmAndRegistryInfo)) { + cmd, cleanup := dockerCli.createTestCmd() + defer cleanup() + registryPort := findAvailablePort() tmpDir := fs.NewDir(t, t.Name()) defer tmpDir.Remove() - cmd := icmd.Cmd{ - Env: append(os.Environ(), - fmt.Sprintf("DUFFLE_HOME=%s", tmpDir.Path()), - fmt.Sprintf("DOCKER_CONFIG=%s", tmpDir.Path()), - "DOCKER_TARGET_CONTEXT=swarm-target-context", - ), - } + cmd.Env = append(cmd.Env, "DUFFLE_HOME="+tmpDir.Path()) + cmd.Env = append(cmd.Env, "DOCKER_TARGET_CONTEXT=swarm-target-context") // The dind doesn't have the cnab-app-base image so we save it in order to load it later saveCmd := icmd.Cmd{Command: dockerCli.Command("save", fmt.Sprintf("docker/cnab-app-base:%s", internal.Version), "-o", tmpDir.Join("cnab-app-base.tar.gz"))} @@ -63,10 +60,6 @@ func runWithDindSwarmAndRegistry(t *testing.T, todo func(dindSwarmAndRegistryInf // - the target context for the invocation image to install within the swarm cmd.Command = dockerCli.Command("context", "create", "swarm-context", "--docker", fmt.Sprintf(`"host=tcp://%s"`, swarm.GetAddress(t)), "--default-stack-orchestrator", "swarm") icmd.RunCmd(cmd).Assert(t, icmd.Success) - defer func() { - cmd.Command = dockerCli.Command("context", "rm", "--force", "swarm-context") - icmd.RunCmd(cmd) - }() // When creating a context on a Windows host we cannot use // the unix socket but it's needed inside the invocation image. @@ -75,10 +68,6 @@ func runWithDindSwarmAndRegistry(t *testing.T, todo func(dindSwarmAndRegistryInf // invocation image cmd.Command = dockerCli.Command("context", "create", "swarm-target-context", "--docker", "host=", "--default-stack-orchestrator", "swarm") icmd.RunCmd(cmd).Assert(t, icmd.Success) - defer func() { - cmd.Command = dockerCli.Command("context", "rm", "--force", "swarm-target-context") - icmd.RunCmd(cmd) - }() // Initialize the swarm cmd.Env = append(cmd.Env, "DOCKER_CONTEXT=swarm-context")