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

Commit f2fbcae

Browse files
authored
Merge pull request #531 from ijc/individual-credentials
Allow the user to specify individual credentials on the command line
2 parents b953958 + 9ece4d7 commit f2fbcae

10 files changed

+213
-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"
@@ -447,6 +448,90 @@ STATUS
447448
})
448449
}
449450

451+
func TestCredentials(t *testing.T) {
452+
cmd, cleanup := dockerCli.createTestCmd(
453+
withCredentialSet(t, "default", &credentials.CredentialSet{
454+
Name: "test-creds",
455+
Credentials: []credentials.CredentialStrategy{
456+
{
457+
Name: "secret1",
458+
Source: credentials.Source{
459+
Value: "secret1value",
460+
},
461+
},
462+
{
463+
Name: "secret2",
464+
Source: credentials.Source{
465+
Value: "secret2value",
466+
},
467+
},
468+
},
469+
}),
470+
)
471+
defer cleanup()
472+
473+
bundleJSON := golden.Get(t, "credential-install-bundle.json")
474+
tmpDir := fs.NewDir(t, t.Name(),
475+
fs.WithFile("bundle.json", "", fs.WithBytes(bundleJSON)),
476+
)
477+
defer tmpDir.Remove()
478+
479+
bundle := tmpDir.Join("bundle.json")
480+
481+
t.Run("missing", func(t *testing.T) {
482+
cmd.Command = dockerCli.Command(
483+
"app", "install",
484+
"--credential", "secret1=foo",
485+
// secret2 deliberately omitted.
486+
"--credential", "secret3=baz",
487+
"--name", "missing", bundle,
488+
)
489+
result := icmd.RunCmd(cmd).Assert(t, icmd.Expected{
490+
ExitCode: 1,
491+
Out: icmd.None,
492+
})
493+
golden.Assert(t, result.Stderr(), "credential-install-missing.golden")
494+
})
495+
496+
t.Run("full", func(t *testing.T) {
497+
cmd.Command = dockerCli.Command(
498+
"app", "install",
499+
"--credential", "secret1=foo",
500+
"--credential", "secret2=bar",
501+
"--credential", "secret3=baz",
502+
"--name", "full", bundle,
503+
)
504+
result := icmd.RunCmd(cmd).Assert(t, icmd.Success)
505+
golden.Assert(t, result.Stdout(), "credential-install-full.golden")
506+
})
507+
508+
t.Run("mixed", func(t *testing.T) {
509+
cmd.Command = dockerCli.Command(
510+
"app", "install",
511+
"--credential-set", "test-creds",
512+
"--credential", "secret3=xyzzy",
513+
"--name", "mixed", bundle,
514+
)
515+
result := icmd.RunCmd(cmd).Assert(t, icmd.Success)
516+
golden.Assert(t, result.Stdout(), "credential-install-mixed.golden")
517+
})
518+
519+
t.Run("overload", func(t *testing.T) {
520+
cmd.Command = dockerCli.Command(
521+
"app", "install",
522+
"--credential-set", "test-creds",
523+
"--credential", "secret1=overload",
524+
"--credential", "secret3=xyzzy",
525+
"--name", "overload", bundle,
526+
)
527+
result := icmd.RunCmd(cmd).Assert(t, icmd.Expected{
528+
ExitCode: 1,
529+
Out: icmd.None,
530+
})
531+
golden.Assert(t, result.Stderr(), "credential-install-overload.golden")
532+
})
533+
}
534+
450535
func initializeDockerAppEnvironment(t *testing.T, cmd *icmd.Cmd, tmpDir *fs.Dir, swarm *Container, useBindMount bool) {
451536
cmd.Env = append(cmd.Env, "DOCKER_TARGET_CONTEXT=swarm-target-context")
452537

e2e/main_test.go

Lines changed: 27 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,14 +39,21 @@ 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
}
56+
defer configFile.Close()
4757
err = json.NewEncoder(configFile).Encode(config)
4858
if err != nil {
4959
panic(err)
@@ -59,6 +69,21 @@ func (d dockerCliCommand) Command(args ...string) []string {
5969
return append([]string{d.path}, args...)
6070
}
6171

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+
6287
func TestMain(m *testing.M) {
6388
flag.Parse()
6489
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
@@ -103,12 +103,14 @@ func (o *parametersOptions) addFlags(flags *pflag.FlagSet) {
103103
type credentialOptions struct {
104104
targetContext string
105105
credentialsets []string
106+
credentials []string
106107
sendRegistryAuth bool
107108
}
108109

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

@@ -119,6 +121,7 @@ func (o *credentialOptions) SetDefaultTargetContext(dockerCli command.Cli) {
119121
func (o *credentialOptions) CredentialSetOpts(dockerCli command.Cli, credentialStore store.CredentialStore) []credentialSetOpt {
120122
return []credentialSetOpt{
121123
addNamedCredentialSets(credentialStore, o.credentialsets),
124+
addCredentials(o.credentials),
122125
addDockerCredentials(o.targetContext, dockerCli.ContextStore()),
123126
addRegistryCredentials(o.sendRegistryAuth, dockerCli),
124127
}

0 commit comments

Comments
 (0)