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

Commit 9ece4d7

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 257a653 commit 9ece4d7

10 files changed

+212
-2
lines changed

e2e/commands_test.go

Lines changed: 85 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,90 @@ 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+
// secret2 deliberately omitted.
460+
"--credential", "secret3=baz",
461+
"--name", "missing", bundle,
462+
)
463+
result := icmd.RunCmd(cmd).Assert(t, icmd.Expected{
464+
ExitCode: 1,
465+
Out: icmd.None,
466+
})
467+
golden.Assert(t, result.Stderr(), "credential-install-missing.golden")
468+
})
469+
470+
t.Run("full", func(t *testing.T) {
471+
cmd.Command = dockerCli.Command(
472+
"app", "install",
473+
"--credential", "secret1=foo",
474+
"--credential", "secret2=bar",
475+
"--credential", "secret3=baz",
476+
"--name", "full", bundle,
477+
)
478+
result := icmd.RunCmd(cmd).Assert(t, icmd.Success)
479+
golden.Assert(t, result.Stdout(), "credential-install-full.golden")
480+
})
481+
482+
t.Run("mixed", func(t *testing.T) {
483+
cmd.Command = dockerCli.Command(
484+
"app", "install",
485+
"--credential-set", "test-creds",
486+
"--credential", "secret3=xyzzy",
487+
"--name", "mixed", bundle,
488+
)
489+
result := icmd.RunCmd(cmd).Assert(t, icmd.Success)
490+
golden.Assert(t, result.Stdout(), "credential-install-mixed.golden")
491+
})
492+
493+
t.Run("overload", func(t *testing.T) {
494+
cmd.Command = dockerCli.Command(
495+
"app", "install",
496+
"--credential-set", "test-creds",
497+
"--credential", "secret1=overload",
498+
"--credential", "secret3=xyzzy",
499+
"--name", "overload", bundle,
500+
)
501+
result := icmd.RunCmd(cmd).Assert(t, icmd.Expected{
502+
ExitCode: 1,
503+
Out: icmd.None,
504+
})
505+
golden.Assert(t, result.Stderr(), "credential-install-overload.golden")
506+
})
507+
}
508+
424509
func initializeDockerAppEnvironment(t *testing.T, cmd *icmd.Cmd, tmpDir *fs.Dir, swarm *Container, useBindMount bool) {
425510
cmd.Env = append(cmd.Env, "DOCKER_TARGET_CONTEXT=swarm-target-context")
426511

e2e/main_test.go

Lines changed: 26 additions & 2 deletions
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

@@ -36,11 +39,17 @@ func (d dockerCliCommand) createTestCmd(ops ...ConfigFileOperator) (icmd.Cmd, fu
3639
if err != nil {
3740
panic(err)
3841
}
39-
config := dockerConfigFile.ConfigFile{CLIPluginsExtraDirs: []string{d.cliPluginDir}}
42+
configFilePath := filepath.Join(configDir, "config.json")
43+
config := dockerConfigFile.ConfigFile{
44+
CLIPluginsExtraDirs: []string{
45+
d.cliPluginDir,
46+
},
47+
Filename: configFilePath,
48+
}
4049
for _, op := range ops {
4150
op(&config)
4251
}
43-
configFile, err := os.Create(filepath.Join(configDir, "config.json"))
52+
configFile, err := os.Create(configFilePath)
4453
if err != nil {
4554
panic(err)
4655
}
@@ -60,6 +69,21 @@ func (d dockerCliCommand) Command(args ...string) []string {
6069
return append([]string{d.path}, args...)
6170
}
6271

72+
func withCredentialSet(t *testing.T, context string, creds *credentials.CredentialSet) ConfigFileOperator {
73+
t.Helper()
74+
return func(config *dockerConfigFile.ConfigFile) {
75+
configDir := filepath.Dir(config.Filename)
76+
appstore, err := store.NewApplicationStore(configDir)
77+
assert.NilError(t, err)
78+
79+
credstore, err := appstore.CredentialStore(context)
80+
assert.NilError(t, err)
81+
82+
err = credstore.Store(creds)
83+
assert.NilError(t, err)
84+
}
85+
}
86+
6387
func TestMain(m *testing.M) {
6488
flag.Parse()
6589
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)