Skip to content
This repository was archived by the owner on Jul 18, 2025. It is now read-only.

Commit efcc62d

Browse files
author
Ian Campbell
committed
Allow the user to specify individual credentials on the command line
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 <[email protected]>
1 parent 137c8cd commit efcc62d

10 files changed

+208
-1
lines changed

e2e/commands_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"strings"
99
"testing"
1010

11+
"github.com/deislabs/duffle/pkg/credentials"
1112
"github.com/docker/app/internal"
1213
"github.com/docker/app/internal/yaml"
1314
"gotest.tools/assert"
@@ -421,6 +422,88 @@ STATUS
421422
})
422423
}
423424

425+
func TestCredentials(t *testing.T) {
426+
cmd, cleanup := dockerCli.createTestCmd(
427+
withCredentialSet(t, "default", &credentials.CredentialSet{
428+
Name: "test-creds",
429+
Credentials: []credentials.CredentialStrategy{
430+
{
431+
Name: "secret1",
432+
Source: credentials.Source{
433+
Value: "secret1value",
434+
},
435+
},
436+
{
437+
Name: "secret2",
438+
Source: credentials.Source{
439+
Value: "secret2value",
440+
},
441+
},
442+
},
443+
}),
444+
)
445+
defer cleanup()
446+
447+
bundleJSON := golden.Get(t, "credential-install-bundle.json")
448+
tmpDir := fs.NewDir(t, t.Name(),
449+
fs.WithFile("bundle.json", "", fs.WithBytes(bundleJSON)),
450+
)
451+
defer tmpDir.Remove()
452+
453+
bundle := tmpDir.Join("bundle.json")
454+
455+
t.Run("missing", func(t *testing.T) {
456+
cmd.Command = dockerCli.Command(
457+
"app", "install",
458+
"--credential", "secret1=foo",
459+
"--name", "missing", bundle,
460+
)
461+
result := icmd.RunCmd(cmd).Assert(t, icmd.Expected{
462+
ExitCode: 1,
463+
Out: icmd.None,
464+
})
465+
golden.Assert(t, result.Stderr(), "credential-install-missing.golden")
466+
})
467+
468+
t.Run("full", func(t *testing.T) {
469+
cmd.Command = dockerCli.Command(
470+
"app", "install",
471+
"--credential", "secret1=foo",
472+
"--credential", "secret2=bar",
473+
"--credential", "secret3=baz",
474+
"--name", "full", bundle,
475+
)
476+
result := icmd.RunCmd(cmd).Assert(t, icmd.Success)
477+
golden.Assert(t, result.Stdout(), "credential-install-full.golden")
478+
})
479+
480+
t.Run("mixed", func(t *testing.T) {
481+
cmd.Command = dockerCli.Command(
482+
"app", "install",
483+
"--credential-set", "test-creds",
484+
"--credential", "secret3=xyzzy",
485+
"--name", "mixed", bundle,
486+
)
487+
result := icmd.RunCmd(cmd).Assert(t, icmd.Success)
488+
golden.Assert(t, result.Stdout(), "credential-install-mixed.golden")
489+
})
490+
491+
t.Run("overload", func(t *testing.T) {
492+
cmd.Command = dockerCli.Command(
493+
"app", "install",
494+
"--credential-set", "test-creds",
495+
"--credential", "secret1=overload",
496+
"--credential", "secret3=xyzzy",
497+
"--name", "overload", bundle,
498+
)
499+
result := icmd.RunCmd(cmd).Assert(t, icmd.Expected{
500+
ExitCode: 1,
501+
Out: icmd.None,
502+
})
503+
golden.Assert(t, result.Stderr(), "credential-install-overload.golden")
504+
})
505+
}
506+
424507
func initializeDockerAppEnvironment(t *testing.T, cmd *icmd.Cmd, tmpDir *fs.Dir, swarm *Container, useBindMount bool) {
425508
cmd.Env = append(cmd.Env, "DOCKER_TARGET_CONTEXT=swarm-target-context")
426509

e2e/main_test.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import (
1212
"strings"
1313
"testing"
1414

15+
"github.com/deislabs/duffle/pkg/credentials"
16+
"github.com/docker/app/internal/store"
1517
dockerConfigFile "github.com/docker/cli/cli/config/configfile"
18+
"gotest.tools/assert"
1619
"gotest.tools/icmd"
1720
)
1821

@@ -29,12 +32,17 @@ type dockerCliCommand struct {
2932
cliPluginDir string
3033
}
3134

32-
func (d dockerCliCommand) createTestCmd() (icmd.Cmd, func()) {
35+
type testCmdOp func(configDir string, config *dockerConfigFile.ConfigFile)
36+
37+
func (d dockerCliCommand) createTestCmd(ops ...testCmdOp) (icmd.Cmd, func()) {
3338
configDir, err := ioutil.TempDir("", "config")
3439
if err != nil {
3540
panic(err)
3641
}
3742
config := dockerConfigFile.ConfigFile{CLIPluginsExtraDirs: []string{d.cliPluginDir}}
43+
for _, op := range ops {
44+
op(configDir, &config)
45+
}
3846
configFile, err := os.Create(filepath.Join(configDir, "config.json"))
3947
if err != nil {
4048
panic(err)
@@ -54,6 +62,21 @@ func (d dockerCliCommand) Command(args ...string) []string {
5462
return append([]string{d.path}, args...)
5563
}
5664

65+
func withCredentialSet(t *testing.T, context string, creds *credentials.CredentialSet) testCmdOp {
66+
t.Helper()
67+
return func(configDir string, _ *dockerConfigFile.ConfigFile) {
68+
69+
appstore, err := store.NewApplicationStore(configDir)
70+
assert.NilError(t, err)
71+
72+
credstore, err := appstore.CredentialStore(context)
73+
assert.NilError(t, err)
74+
75+
err = credstore.Store(creds)
76+
assert.NilError(t, err)
77+
}
78+
}
79+
5780
func TestMain(m *testing.M) {
5881
flag.Parse()
5982
if err := os.Chdir(*e2ePath); err != nil {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "example-credentials",
3+
"version": "0.0.1",
4+
"schemaVersion": "v1.0.0-WD",
5+
"invocationImages": [
6+
{
7+
"imageType": "docker",
8+
"image": "cnab/example-credentials@sha256:b93f7279bdc9610d4ef275dab5d0a1d19cc613a784e2522977866747090059f4"
9+
}
10+
],
11+
"credentials": {
12+
"secret1": {
13+
"env" :"SECRET_ONE"
14+
},
15+
"secret2": {
16+
"path": "/var/secret_two/data.txt"
17+
},
18+
"secret3": {
19+
"env": "SECRET_THREE",
20+
"path": "/var/secret_three/data.txt"
21+
}
22+
}
23+
}
24+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
SECRET_ONE: foo
2+
/var/secret_two/data.txt
3+
bar
4+
SECRET_THREE: baz
5+
/var/secret_three/data.txt
6+
baz
7+
Application "full" installed on context "default"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
bundle requires credential for secret2
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
SECRET_ONE: secret1value
2+
/var/secret_two/data.txt
3+
secret2value
4+
SECRET_THREE: xyzzy
5+
/var/secret_three/data.txt
6+
xyzzy
7+
Application "mixed" installed on context "default"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ambiguous credential resolution: "secret1" is already present in base credential sets, cannot merge

internal/commands/cnab.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,33 @@ func addNamedCredentialSets(credStore appstore.CredentialStore, namedCredentials
6060
}
6161
}
6262

63+
func parseCommandlineCredential(c string) (string, string, error) {
64+
split := strings.SplitN(c, "=", 2)
65+
if len(split) != 2 || split[0] == "" {
66+
return "", "", errors.Errorf("failed to parse %q as a credential name=value", c)
67+
}
68+
name := split[0]
69+
value := split[1]
70+
return name, value, nil
71+
}
72+
73+
func addCredentials(strcreds []string) credentialSetOpt {
74+
return func(_ *bundle.Bundle, creds credentials.Set) error {
75+
for _, c := range strcreds {
76+
name, value, err := parseCommandlineCredential(c)
77+
if err != nil {
78+
return err
79+
}
80+
if err := creds.Merge(credentials.Set{
81+
name: value,
82+
}); err != nil {
83+
return err
84+
}
85+
}
86+
return nil
87+
}
88+
}
89+
6390
func addDockerCredentials(contextName string, store contextstore.Store) credentialSetOpt {
6491
// docker desktop contexts require some rewriting for being used within a container
6592
store = dockerDesktopAwareStore{Store: store}

internal/commands/cnab_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,34 @@ func TestShareRegistryCreds(t *testing.T) {
230230
})
231231
}
232232
}
233+
234+
func TestParseCommandlineCredential(t *testing.T) {
235+
for _, tc := range []struct {
236+
in string
237+
n, v string
238+
err string // either err or n+v are non-""
239+
}{
240+
{in: "", err: `failed to parse "" as a credential name=value`},
241+
{in: "A", err: `failed to parse "A" as a credential name=value`},
242+
{in: "=B", err: `failed to parse "=B" as a credential name=value`},
243+
{in: "A=", n: "A", v: ""},
244+
{in: "A=B", n: "A", v: "B"},
245+
{in: "A==", n: "A", v: "="},
246+
{in: "A=B=C", n: "A", v: "B=C"},
247+
} {
248+
n := tc.in
249+
if n == "" {
250+
n = "«empty»"
251+
}
252+
t.Run(n, func(t *testing.T) {
253+
n, v, err := parseCommandlineCredential(tc.in)
254+
if tc.err != "" {
255+
assert.Error(t, err, tc.err)
256+
} else {
257+
assert.NilError(t, err)
258+
assert.Equal(t, tc.n, n)
259+
assert.Equal(t, tc.v, v)
260+
}
261+
})
262+
}
263+
}

internal/commands/root.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,14 @@ func (o *parametersOptions) addFlags(flags *pflag.FlagSet) {
9191
type credentialOptions struct {
9292
targetContext string
9393
credentialsets []string
94+
credentials []string
9495
sendRegistryAuth bool
9596
}
9697

9798
func (o *credentialOptions) addFlags(flags *pflag.FlagSet) {
9899
flags.StringVar(&o.targetContext, "target-context", "", "Context on which the application is installed (default: <current-context>)")
99100
flags.StringArrayVar(&o.credentialsets, "credential-set", []string{}, "Use a YAML file containing a credential set or a credential set present in the credential store")
101+
flags.StringArrayVar(&o.credentials, "credential", nil, "Add a single credential, additive ontop of any --credential-set used")
100102
flags.BoolVar(&o.sendRegistryAuth, "with-registry-auth", false, "Sends registry auth")
101103
}
102104

@@ -107,6 +109,7 @@ func (o *credentialOptions) SetDefaultTargetContext(dockerCli command.Cli) {
107109
func (o *credentialOptions) CredentialSetOpts(dockerCli command.Cli, credentialStore store.CredentialStore) []credentialSetOpt {
108110
return []credentialSetOpt{
109111
addNamedCredentialSets(credentialStore, o.credentialsets),
112+
addCredentials(o.credentials),
110113
addDockerCredentials(o.targetContext, dockerCli.ContextStore()),
111114
addRegistryCredentials(o.sendRegistryAuth, dockerCli),
112115
}

0 commit comments

Comments
 (0)