From 257a6538b895b3ed67638b5110343bb69a286a45 Mon Sep 17 00:00:00 2001 From: Ian Campbell Date: Fri, 10 May 2019 10:48:48 +0100 Subject: [PATCH 1/2] e2e: Close the config file after writing it. Signed-off-by: Ian Campbell --- e2e/main_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/e2e/main_test.go b/e2e/main_test.go index 89cef52e8..4833a296a 100644 --- a/e2e/main_test.go +++ b/e2e/main_test.go @@ -44,6 +44,7 @@ func (d dockerCliCommand) createTestCmd(ops ...ConfigFileOperator) (icmd.Cmd, fu if err != nil { panic(err) } + defer configFile.Close() err = json.NewEncoder(configFile).Encode(config) if err != nil { panic(err) From 9ece4d778f8926efd074087b15d44b772f14f91d Mon Sep 17 00:00:00 2001 From: Ian Campbell Date: Fri, 3 May 2019 16:19:10 +0100 Subject: [PATCH 2/2] Allow the user to specify individual credentials on the command line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit e.g. docker app install --credential name=somevalue bundle.json Credentials added with `--credential` always come after those added with `--credential-set` (irrespective of the order on the command line). A credential specified with `--credential` cannot override any previous credential, including those specified in a credential set. The test bnudle used is based on /~https://github.com/deislabs/example-bundles/blob/0e8af9a2f1270bd72045a515637a432e74743d5d/example-credentials/bundle.json But with `cnab/example-credentials:latest` → a digested ref (with the digest I pulled today) Signed-off-by: Ian Campbell --- e2e/commands_test.go | 85 +++++++++++++++++++ e2e/main_test.go | 28 +++++- e2e/testdata/credential-install-bundle.json | 24 ++++++ e2e/testdata/credential-install-full.golden | 7 ++ .../credential-install-missing.golden | 1 + e2e/testdata/credential-install-mixed.golden | 7 ++ .../credential-install-overload.golden | 1 + internal/commands/cnab.go | 27 ++++++ internal/commands/cnab_test.go | 31 +++++++ internal/commands/root.go | 3 + 10 files changed, 212 insertions(+), 2 deletions(-) create mode 100644 e2e/testdata/credential-install-bundle.json create mode 100644 e2e/testdata/credential-install-full.golden create mode 100644 e2e/testdata/credential-install-missing.golden create mode 100644 e2e/testdata/credential-install-mixed.golden create mode 100644 e2e/testdata/credential-install-overload.golden diff --git a/e2e/commands_test.go b/e2e/commands_test.go index 77667a0cf..1d0da6c39 100644 --- a/e2e/commands_test.go +++ b/e2e/commands_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/deislabs/duffle/pkg/credentials" "github.com/docker/app/internal" "github.com/docker/app/internal/yaml" "gotest.tools/assert" @@ -421,6 +422,90 @@ STATUS }) } +func TestCredentials(t *testing.T) { + cmd, cleanup := dockerCli.createTestCmd( + withCredentialSet(t, "default", &credentials.CredentialSet{ + Name: "test-creds", + Credentials: []credentials.CredentialStrategy{ + { + Name: "secret1", + Source: credentials.Source{ + Value: "secret1value", + }, + }, + { + Name: "secret2", + Source: credentials.Source{ + Value: "secret2value", + }, + }, + }, + }), + ) + defer cleanup() + + bundleJSON := golden.Get(t, "credential-install-bundle.json") + tmpDir := fs.NewDir(t, t.Name(), + fs.WithFile("bundle.json", "", fs.WithBytes(bundleJSON)), + ) + defer tmpDir.Remove() + + bundle := tmpDir.Join("bundle.json") + + t.Run("missing", func(t *testing.T) { + cmd.Command = dockerCli.Command( + "app", "install", + "--credential", "secret1=foo", + // secret2 deliberately omitted. + "--credential", "secret3=baz", + "--name", "missing", bundle, + ) + result := icmd.RunCmd(cmd).Assert(t, icmd.Expected{ + ExitCode: 1, + Out: icmd.None, + }) + golden.Assert(t, result.Stderr(), "credential-install-missing.golden") + }) + + t.Run("full", func(t *testing.T) { + cmd.Command = dockerCli.Command( + "app", "install", + "--credential", "secret1=foo", + "--credential", "secret2=bar", + "--credential", "secret3=baz", + "--name", "full", bundle, + ) + result := icmd.RunCmd(cmd).Assert(t, icmd.Success) + golden.Assert(t, result.Stdout(), "credential-install-full.golden") + }) + + t.Run("mixed", func(t *testing.T) { + cmd.Command = dockerCli.Command( + "app", "install", + "--credential-set", "test-creds", + "--credential", "secret3=xyzzy", + "--name", "mixed", bundle, + ) + result := icmd.RunCmd(cmd).Assert(t, icmd.Success) + golden.Assert(t, result.Stdout(), "credential-install-mixed.golden") + }) + + t.Run("overload", func(t *testing.T) { + cmd.Command = dockerCli.Command( + "app", "install", + "--credential-set", "test-creds", + "--credential", "secret1=overload", + "--credential", "secret3=xyzzy", + "--name", "overload", bundle, + ) + result := icmd.RunCmd(cmd).Assert(t, icmd.Expected{ + ExitCode: 1, + Out: icmd.None, + }) + golden.Assert(t, result.Stderr(), "credential-install-overload.golden") + }) +} + func initializeDockerAppEnvironment(t *testing.T, cmd *icmd.Cmd, tmpDir *fs.Dir, swarm *Container, useBindMount bool) { cmd.Env = append(cmd.Env, "DOCKER_TARGET_CONTEXT=swarm-target-context") diff --git a/e2e/main_test.go b/e2e/main_test.go index 4833a296a..5be2b4fa2 100644 --- a/e2e/main_test.go +++ b/e2e/main_test.go @@ -12,7 +12,10 @@ import ( "strings" "testing" + "github.com/deislabs/duffle/pkg/credentials" + "github.com/docker/app/internal/store" dockerConfigFile "github.com/docker/cli/cli/config/configfile" + "gotest.tools/assert" "gotest.tools/icmd" ) @@ -36,11 +39,17 @@ func (d dockerCliCommand) createTestCmd(ops ...ConfigFileOperator) (icmd.Cmd, fu if err != nil { panic(err) } - config := dockerConfigFile.ConfigFile{CLIPluginsExtraDirs: []string{d.cliPluginDir}} + configFilePath := filepath.Join(configDir, "config.json") + config := dockerConfigFile.ConfigFile{ + CLIPluginsExtraDirs: []string{ + d.cliPluginDir, + }, + Filename: configFilePath, + } for _, op := range ops { op(&config) } - configFile, err := os.Create(filepath.Join(configDir, "config.json")) + configFile, err := os.Create(configFilePath) if err != nil { panic(err) } @@ -60,6 +69,21 @@ func (d dockerCliCommand) Command(args ...string) []string { return append([]string{d.path}, args...) } +func withCredentialSet(t *testing.T, context string, creds *credentials.CredentialSet) ConfigFileOperator { + t.Helper() + return func(config *dockerConfigFile.ConfigFile) { + configDir := filepath.Dir(config.Filename) + appstore, err := store.NewApplicationStore(configDir) + assert.NilError(t, err) + + credstore, err := appstore.CredentialStore(context) + assert.NilError(t, err) + + err = credstore.Store(creds) + assert.NilError(t, err) + } +} + func TestMain(m *testing.M) { flag.Parse() if err := os.Chdir(*e2ePath); err != nil { diff --git a/e2e/testdata/credential-install-bundle.json b/e2e/testdata/credential-install-bundle.json new file mode 100644 index 000000000..c70670b4d --- /dev/null +++ b/e2e/testdata/credential-install-bundle.json @@ -0,0 +1,24 @@ +{ + "name": "example-credentials", + "version": "0.0.1", + "schemaVersion": "v1.0.0-WD", + "invocationImages": [ + { + "imageType": "docker", + "image": "cnab/example-credentials@sha256:b93f7279bdc9610d4ef275dab5d0a1d19cc613a784e2522977866747090059f4" + } + ], + "credentials": { + "secret1": { + "env" :"SECRET_ONE" + }, + "secret2": { + "path": "/var/secret_two/data.txt" + }, + "secret3": { + "env": "SECRET_THREE", + "path": "/var/secret_three/data.txt" + } + } +} + diff --git a/e2e/testdata/credential-install-full.golden b/e2e/testdata/credential-install-full.golden new file mode 100644 index 000000000..fad6a2956 --- /dev/null +++ b/e2e/testdata/credential-install-full.golden @@ -0,0 +1,7 @@ +SECRET_ONE: foo +/var/secret_two/data.txt +bar +SECRET_THREE: baz +/var/secret_three/data.txt +baz +Application "full" installed on context "default" diff --git a/e2e/testdata/credential-install-missing.golden b/e2e/testdata/credential-install-missing.golden new file mode 100644 index 000000000..fc6ae345e --- /dev/null +++ b/e2e/testdata/credential-install-missing.golden @@ -0,0 +1 @@ +bundle requires credential for secret2 diff --git a/e2e/testdata/credential-install-mixed.golden b/e2e/testdata/credential-install-mixed.golden new file mode 100644 index 000000000..33604a74d --- /dev/null +++ b/e2e/testdata/credential-install-mixed.golden @@ -0,0 +1,7 @@ +SECRET_ONE: secret1value +/var/secret_two/data.txt +secret2value +SECRET_THREE: xyzzy +/var/secret_three/data.txt +xyzzy +Application "mixed" installed on context "default" diff --git a/e2e/testdata/credential-install-overload.golden b/e2e/testdata/credential-install-overload.golden new file mode 100644 index 000000000..ef477b42f --- /dev/null +++ b/e2e/testdata/credential-install-overload.golden @@ -0,0 +1 @@ +ambiguous credential resolution: "secret1" is already present in base credential sets, cannot merge diff --git a/internal/commands/cnab.go b/internal/commands/cnab.go index a0b110b1d..3a9524148 100644 --- a/internal/commands/cnab.go +++ b/internal/commands/cnab.go @@ -60,6 +60,33 @@ func addNamedCredentialSets(credStore appstore.CredentialStore, namedCredentials } } +func parseCommandlineCredential(c string) (string, string, error) { + split := strings.SplitN(c, "=", 2) + if len(split) != 2 || split[0] == "" { + return "", "", errors.Errorf("failed to parse %q as a credential name=value", c) + } + name := split[0] + value := split[1] + return name, value, nil +} + +func addCredentials(strcreds []string) credentialSetOpt { + return func(_ *bundle.Bundle, creds credentials.Set) error { + for _, c := range strcreds { + name, value, err := parseCommandlineCredential(c) + if err != nil { + return err + } + if err := creds.Merge(credentials.Set{ + name: value, + }); err != nil { + return err + } + } + return nil + } +} + func addDockerCredentials(contextName string, store contextstore.Store) credentialSetOpt { // docker desktop contexts require some rewriting for being used within a container store = dockerDesktopAwareStore{Store: store} diff --git a/internal/commands/cnab_test.go b/internal/commands/cnab_test.go index 2c2cb9014..fc6fac34e 100644 --- a/internal/commands/cnab_test.go +++ b/internal/commands/cnab_test.go @@ -230,3 +230,34 @@ func TestShareRegistryCreds(t *testing.T) { }) } } + +func TestParseCommandlineCredential(t *testing.T) { + for _, tc := range []struct { + in string + n, v string + err string // either err or n+v are non-"" + }{ + {in: "", err: `failed to parse "" as a credential name=value`}, + {in: "A", err: `failed to parse "A" as a credential name=value`}, + {in: "=B", err: `failed to parse "=B" as a credential name=value`}, + {in: "A=", n: "A", v: ""}, + {in: "A=B", n: "A", v: "B"}, + {in: "A==", n: "A", v: "="}, + {in: "A=B=C", n: "A", v: "B=C"}, + } { + n := tc.in + if n == "" { + n = "«empty»" + } + t.Run(n, func(t *testing.T) { + n, v, err := parseCommandlineCredential(tc.in) + if tc.err != "" { + assert.Error(t, err, tc.err) + } else { + assert.NilError(t, err) + assert.Equal(t, tc.n, n) + assert.Equal(t, tc.v, v) + } + }) + } +} diff --git a/internal/commands/root.go b/internal/commands/root.go index c24a40e8a..24774e034 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -91,12 +91,14 @@ func (o *parametersOptions) addFlags(flags *pflag.FlagSet) { type credentialOptions struct { targetContext string credentialsets []string + credentials []string sendRegistryAuth bool } func (o *credentialOptions) addFlags(flags *pflag.FlagSet) { flags.StringVar(&o.targetContext, "target-context", "", "Context on which the application is installed (default: )") flags.StringArrayVar(&o.credentialsets, "credential-set", []string{}, "Use a YAML file containing a credential set or a credential set present in the credential store") + flags.StringArrayVar(&o.credentials, "credential", nil, "Add a single credential, additive ontop of any --credential-set used") flags.BoolVar(&o.sendRegistryAuth, "with-registry-auth", false, "Sends registry auth") } @@ -107,6 +109,7 @@ func (o *credentialOptions) SetDefaultTargetContext(dockerCli command.Cli) { func (o *credentialOptions) CredentialSetOpts(dockerCli command.Cli, credentialStore store.CredentialStore) []credentialSetOpt { return []credentialSetOpt{ addNamedCredentialSets(credentialStore, o.credentialsets), + addCredentials(o.credentials), addDockerCredentials(o.targetContext, dockerCli.ContextStore()), addRegistryCredentials(o.sendRegistryAuth, dockerCli), }