diff --git a/e2e/cnab_test.go b/e2e/cnab_test.go index 42be58a81..3bcf9476a 100644 --- a/e2e/cnab_test.go +++ b/e2e/cnab_test.go @@ -50,7 +50,7 @@ func TestCallCustomStatusAction(t *testing.T) { icmd.RunCmd(cmd).Assert(t, icmd.Success) // docker app install - cmd.Command = dockerCli.Command("app", "run", path.Join(testDir, "bundle.json"), "--name", testCase.name) + cmd.Command = dockerCli.Command("app", "run", "--cnab-bundle-json", path.Join(testDir, "bundle.json"), "--name", testCase.name) icmd.RunCmd(cmd).Assert(t, icmd.Success) // docker app uninstall @@ -78,7 +78,7 @@ func TestCnabParameters(t *testing.T) { }() // docker app install - cmd.Command = dockerCli.Command("app", "run", path.Join(testDir, "bundle.json"), "--name", "cnab-parameters", + cmd.Command = dockerCli.Command("app", "run", "--cnab-bundle-json", path.Join(testDir, "bundle.json"), "--name", "cnab-parameters", "--set", "boolParam=true", "--set", "stringParam=value", "--set", "intParam=42", diff --git a/e2e/commands_test.go b/e2e/commands_test.go index 0a8b98173..b89d89356 100644 --- a/e2e/commands_test.go +++ b/e2e/commands_test.go @@ -177,6 +177,23 @@ func TestInspectApp(t *testing.T) { }) } +func TestRunOnlyOne(t *testing.T) { + cmd, cleanup := dockerCli.createTestCmd() + defer cleanup() + + cmd.Command = dockerCli.Command("app", "run") + icmd.RunCmd(cmd).Assert(t, icmd.Expected{ + ExitCode: 1, + Err: `"docker app run" requires exactly 1 argument.`, + }) + + cmd.Command = dockerCli.Command("app", "run", "--cnab-bundle-json", "bundle.json", "myapp") + icmd.RunCmd(cmd).Assert(t, icmd.Expected{ + ExitCode: 1, + Err: `"docker app run" cannot run a bundle and an app image`, + }) +} + func TestDockerAppLifecycle(t *testing.T) { t.Run("withBindMounts", func(t *testing.T) { testDockerAppLifecycle(t, true) @@ -189,18 +206,21 @@ func TestDockerAppLifecycle(t *testing.T) { func testDockerAppLifecycle(t *testing.T, useBindMount bool) { cmd, cleanup := dockerCli.createTestCmd() defer cleanup() - appName := strings.Replace(t.Name(), "/", "_", 1) + appName := strings.ToLower(strings.Replace(t.Name(), "/", "_", 1)) tmpDir := fs.NewDir(t, appName) defer tmpDir.Remove() // Running a swarm using docker in docker to install the application // and run the invocation image - swarm := NewContainer("docker:18.09-dind", 2375) - swarm.Start(t) + swarm := NewContainer("docker:19.03.3-dind", 2375) + swarm.Start(t, "-e", "DOCKER_TLS_CERTDIR=") defer swarm.Stop(t) initializeDockerAppEnvironment(t, &cmd, tmpDir, swarm, useBindMount) + cmd.Command = dockerCli.Command("app", "build", "--tag", appName, "testdata/simple") + icmd.RunCmd(cmd).Assert(t, icmd.Success) + // Install an illformed Docker Application Package - cmd.Command = dockerCli.Command("app", "run", "testdata/simple/simple.dockerapp", "--set", "web_port=-1", "--name", appName) + cmd.Command = dockerCli.Command("app", "run", appName, "--set", "web_port=-1", "--name", appName) icmd.RunCmd(cmd).Assert(t, icmd.Expected{ ExitCode: 1, Err: "error decoding 'Ports': Invalid hostPort: -1", @@ -222,7 +242,7 @@ func testDockerAppLifecycle(t *testing.T, useBindMount bool) { }) // Install a Docker Application Package with an existing failed installation is fine - cmd.Command = dockerCli.Command("app", "run", "testdata/simple/simple.dockerapp", "--name", appName) + cmd.Command = dockerCli.Command("app", "run", appName, "--name", appName) checkContains(t, icmd.RunCmd(cmd).Assert(t, icmd.Success).Combined(), []string{ fmt.Sprintf("WARNING: installing over previously failed installation %q", appName), @@ -243,7 +263,7 @@ func testDockerAppLifecycle(t *testing.T, useBindMount bool) { }) // Installing again the same application is forbidden - cmd.Command = dockerCli.Command("app", "run", "testdata/simple/simple.dockerapp", "--name", appName) + cmd.Command = dockerCli.Command("app", "run", appName, "--name", appName) icmd.RunCmd(cmd).Assert(t, icmd.Expected{ ExitCode: 1, Err: fmt.Sprintf("Installation %q already exists, use 'docker app update' instead", appName), @@ -315,7 +335,8 @@ func TestCredentials(t *testing.T) { "--credential", "secret1=foo", // secret2 deliberately omitted. "--credential", "secret3=baz", - "--name", "missing", bundle, + "--name", "missing", + "--cnab-bundle-json", bundle, ) result := icmd.RunCmd(cmd).Assert(t, icmd.Expected{ ExitCode: 1, @@ -330,7 +351,8 @@ func TestCredentials(t *testing.T) { "--credential", "secret1=foo", "--credential", "secret2=bar", "--credential", "secret3=baz", - "--name", "full", bundle, + "--name", "full", + "--cnab-bundle-json", bundle, ) result := icmd.RunCmd(cmd).Assert(t, icmd.Success) golden.Assert(t, result.Stdout(), "credential-install-full.golden") @@ -341,7 +363,8 @@ func TestCredentials(t *testing.T) { "app", "run", "--credential-set", "test-creds", "--credential", "secret3=xyzzy", - "--name", "mixed-credstore", bundle, + "--name", "mixed-credstore", + "--cnab-bundle-json", bundle, ) result := icmd.RunCmd(cmd).Assert(t, icmd.Success) golden.Assert(t, result.Stdout(), "credential-install-mixed-credstore.golden") @@ -352,7 +375,8 @@ func TestCredentials(t *testing.T) { "app", "run", "--credential-set", tmpDir.Join("local", "test-creds.yaml"), "--credential", "secret3=xyzzy", - "--name", "mixed-local-cred", bundle, + "--name", "mixed-local-cred", + "--cnab-bundle-json", bundle, ) result := icmd.RunCmd(cmd).Assert(t, icmd.Success) golden.Assert(t, result.Stdout(), "credential-install-mixed-local-cred.golden") @@ -364,7 +388,8 @@ func TestCredentials(t *testing.T) { "--credential-set", "test-creds", "--credential", "secret1=overload", "--credential", "secret3=xyzzy", - "--name", "overload", bundle, + "--name", "overload", + "--cnab-bundle-json", bundle, ) result := icmd.RunCmd(cmd).Assert(t, icmd.Expected{ ExitCode: 1, diff --git a/e2e/helper_test.go b/e2e/helper_test.go index 5284e5608..82a0bb6e9 100644 --- a/e2e/helper_test.go +++ b/e2e/helper_test.go @@ -43,7 +43,7 @@ func runWithDindSwarmAndRegistry(t *testing.T, todo func(dindSwarmAndRegistryInf // Solution found is: fix the port of the registry to be the same internally and externally // and run the dind container in the same network namespace: this way 127.0.0.1: both resolves to the registry from the client and from dind - swarm := NewContainer("docker:19.03.2-dind", 2375, "--insecure-registry", fmt.Sprintf("127.0.0.1:%d", registryPort)) + swarm := NewContainer("docker:19.03.3-dind", 2375, "--insecure-registry", fmt.Sprintf("127.0.0.1:%d", registryPort)) swarm.Start(t, "--expose", strconv.FormatInt(int64(registryPort), 10), "-p", fmt.Sprintf("%d:%d", registryPort, registryPort), "-p", "2375", diff --git a/internal/cnab/cnab.go b/internal/cnab/cnab.go index 727fc9a12..576abe514 100644 --- a/internal/cnab/cnab.go +++ b/internal/cnab/cnab.go @@ -5,7 +5,6 @@ import ( "fmt" "io/ioutil" "os" - "strings" "github.com/deislabs/cnab-go/bundle" "github.com/docker/app/internal" @@ -22,17 +21,15 @@ type nameKind uint const ( _ nameKind = iota - nameKindEmpty - nameKindFile nameKindDir nameKindReference ) func getAppNameKind(name string) (string, nameKind) { if name == "" { - return name, nameKindEmpty + return name, nameKindDir } - // name can be a bundle.json or bundle.cnab file, or a dockerapp directory + // name can be a dockerapp directory st, err := os.Stat(name) if os.IsNotExist(err) { // try with .dockerapp extension @@ -47,7 +44,7 @@ func getAppNameKind(name string) (string, nameKind) { if st.IsDir() { return name, nameKindDir } - return name, nameKindFile + return name, nameKindReference } func extractAndLoadAppBasedBundle(dockerCli command.Cli, name string) (*bundle.Bundle, string, error) { @@ -60,7 +57,8 @@ func extractAndLoadAppBasedBundle(dockerCli command.Cli, name string) (*bundle.B return bndl, "", err } -func loadBundleFromFile(filename string) (*bundle.Bundle, error) { +// LoadBundleFromFile loads a bundle from a file +func LoadBundleFromFile(filename string) (*bundle.Bundle, error) { b := &bundle.Bundle{} data, err := ioutil.ReadFile(filename) if err != nil { @@ -74,19 +72,11 @@ func loadBundleFromFile(filename string) (*bundle.Bundle, error) { // a reference to the bundle if it is found in the bundlestore, and an error. func ResolveBundle(dockerCli command.Cli, bundleStore appstore.BundleStore, name string) (*bundle.Bundle, string, error) { // resolution logic: - // - if there is a docker-app package in working directory, or an http:// / https:// prefix, use packager.Extract result - // - the name has a .json or .cnab extension and refers to an existing file or web resource: load the bundle - // - name matches a bundle name:version stored in the bundle store: use it + // - if there is a docker-app package in working directory or if a directory is given use packager.Extract // - pull the bundle from the registry and add it to the bundle store name, kind := getAppNameKind(name) switch kind { - case nameKindFile: - if strings.HasSuffix(name, internal.AppExtension) { - return extractAndLoadAppBasedBundle(dockerCli, name) - } - bndl, err := loadBundleFromFile(name) - return bndl, "", err - case nameKindDir, nameKindEmpty: + case nameKindDir: return extractAndLoadAppBasedBundle(dockerCli, name) case nameKindReference: bndl, tagRef, err := GetBundle(dockerCli, bundleStore, name) diff --git a/internal/commands/run.go b/internal/commands/run.go index a0d0b7bf0..af5cbbb20 100644 --- a/internal/commands/run.go +++ b/internal/commands/run.go @@ -4,11 +4,13 @@ import ( "fmt" "os" + "github.com/docker/cli/cli" + "github.com/deislabs/cnab-go/action" + "github.com/deislabs/cnab-go/bundle" "github.com/deislabs/cnab-go/credentials" "github.com/docker/app/internal/cnab" "github.com/docker/app/internal/store" - "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/docker/pkg/namesgenerator" "github.com/pkg/errors" @@ -22,6 +24,7 @@ type runOptions struct { orchestrator string kubeNamespace string stackName string + cnabBundle string } const longDescription = `Run an application based on a docker app image.` @@ -37,9 +40,20 @@ func runCmd(dockerCli command.Cli) *cobra.Command { Short: "Run an application", Long: longDescription, Example: example, - Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runRun(dockerCli, args[0], opts) + if opts.cnabBundle != "" && len(args) != 0 { + return errors.Errorf( + "%q cannot run a bundle and an app image", + cmd.CommandPath(), + ) + } + if opts.cnabBundle == "" { + if err := cli.ExactArgs(1)(cmd, args); err != nil { + return err + } + return runDockerApp(dockerCli, args[0], opts) + } + return runCnab(dockerCli, opts) }, } opts.parametersOptions.addFlags(cmd.Flags()) @@ -47,26 +61,43 @@ func runCmd(dockerCli command.Cli) *cobra.Command { cmd.Flags().StringVar(&opts.orchestrator, "orchestrator", "", "Orchestrator to install on (swarm, kubernetes)") cmd.Flags().StringVar(&opts.kubeNamespace, "namespace", "default", "Kubernetes namespace to install into") cmd.Flags().StringVar(&opts.stackName, "name", "", "Assign a name to the installation") + cmd.Flags().StringVar(&opts.cnabBundle, "cnab-bundle-json", "", "Run a CNAB bundle instead of a Docker App") return cmd } -func runRun(dockerCli command.Cli, appname string, opts runOptions) error { - opts.SetDefaultTargetContext(dockerCli) - - bind, err := cnab.RequiredBindMount(opts.targetContext, opts.orchestrator, dockerCli.ContextStore()) +func runCnab(dockerCli command.Cli, opts runOptions) error { + bndl, err := cnab.LoadBundleFromFile(opts.cnabBundle) if err != nil { - return err + return errors.Wrapf(err, "failed to read bundle %q", opts.cnabBundle) } - bundleStore, installationStore, credentialStore, err := prepareStores(opts.targetContext) + return runBundle(dockerCli, bndl, opts, "") +} + +func runDockerApp(dockerCli command.Cli, appname string, opts runOptions) error { + bundleStore, err := prepareBundleStore() if err != nil { return err } - bndl, ref, err := cnab.ResolveBundle(dockerCli, bundleStore, appname) + bndl, ref, err := cnab.GetBundle(dockerCli, bundleStore, appname) if err != nil { return errors.Wrapf(err, "Unable to find application %q", appname) } + return runBundle(dockerCli, bndl, opts, ref.String()) +} + +func runBundle(dockerCli command.Cli, bndl *bundle.Bundle, opts runOptions, ref string) error { + opts.SetDefaultTargetContext(dockerCli) + + bind, err := cnab.RequiredBindMount(opts.targetContext, opts.orchestrator, dockerCli.ContextStore()) + if err != nil { + return err + } + _, installationStore, credentialStore, err := prepareStores(opts.targetContext) + if err != nil { + return err + } if err := bndl.Validate(); err != nil { return err }