Skip to content

Commit 0fc3322

Browse files
authored
Merge pull request #16 from docker/keychain
store/keychain: cross-platform test cli and more tests
2 parents 7aa6bab + 63ac2a4 commit 0fc3322

76 files changed

Lines changed: 13271 additions & 18 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

store/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,19 @@ require (
99
github.com/docker/secrets-engine v0.0.0-00010101000000-000000000000
1010
github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a
1111
github.com/keybase/go-keychain v0.0.1
12+
github.com/spf13/cobra v1.9.1
1213
github.com/stretchr/testify v1.10.0
1314
golang.org/x/sys v0.29.0
1415
golang.org/x/text v0.21.0
1516
)
1617

1718
require (
1819
github.com/davecgh/go-spew v1.1.1 // indirect
20+
github.com/inconshreveable/mousetrap v1.1.0 // indirect
1921
github.com/kr/text v0.2.0 // indirect
2022
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
2123
github.com/pmezard/go-difflib v1.0.0 // indirect
24+
github.com/spf13/pflag v1.0.6 // indirect
2225
golang.org/x/crypto v0.32.0 // indirect
2326
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
2427
gopkg.in/yaml.v3 v3.0.1 // indirect

store/go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
12
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
23
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
34
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
45
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
56
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7+
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
8+
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
69
github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a h1:K0EAzgzEQHW4Y5lxrmvPMltmlRDzlhLfGmots9EHUTI=
710
github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a/go.mod h1:YPNKjjE7Ubp9dTbnWvsP3HT+hYnY6TfXzubYTBeUxc8=
811
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
@@ -15,6 +18,11 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb
1518
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
1619
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
1720
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
21+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
22+
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
23+
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
24+
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
25+
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
1826
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
1927
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
2028
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=

store/keychain/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,15 @@ func main() {
3030
The `keychain` assumes that any secret stored would conform to the `store.Secret`
3131
interface. This allows the `keychain` to store secrets of any type and leaves
3232
it up to the implementer to decide how they would like their secret parsed.
33+
34+
## Example CLI
35+
36+
The `keychain` package also contains an example CLI tool to test out how a real
37+
application might interact with the host keychain.
38+
39+
You can build the CLI by running `go build` inside the `store/` root directory.
40+
41+
```console
42+
$ go build -o keychain-cli ./keychain/cmd/
43+
$ ./keychain-cli
44+
```

store/keychain/cmd/main.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"log"
8+
"path"
9+
10+
"github.com/docker/secrets-engine/store"
11+
"github.com/docker/secrets-engine/store/keychain"
12+
"github.com/docker/secrets-engine/store/mocks"
13+
"github.com/spf13/cobra"
14+
)
15+
16+
// newCommand creates an example CLI that uses the keychain library
17+
// It supports windows, linux and macOS.
18+
func newCommand() (*cobra.Command, error) {
19+
kc, err := keychain.New(
20+
"io.docker.Secrets",
21+
"docker-example-cli",
22+
func() *mocks.MockCredential {
23+
return &mocks.MockCredential{}
24+
},
25+
)
26+
if err != nil {
27+
return nil, err
28+
}
29+
list := &cobra.Command{
30+
Use: "list",
31+
Aliases: []string{"ls"},
32+
RunE: func(cmd *cobra.Command, args []string) error {
33+
secrets, err := kc.GetAll(cmd.Context())
34+
if errors.Is(err, store.ErrCredentialNotFound) {
35+
fmt.Println("No Secrets found")
36+
return nil
37+
}
38+
if err != nil {
39+
return err
40+
}
41+
42+
var e error
43+
for k, v := range secrets {
44+
vv, err := v.Marshal()
45+
if err != nil {
46+
e = errors.Join(e, err)
47+
}
48+
fmt.Printf("\nID: %s\nValue: %s\n", k, vv)
49+
}
50+
return e
51+
},
52+
}
53+
54+
var (
55+
username string
56+
password string
57+
)
58+
save := &cobra.Command{
59+
Use: "store",
60+
Aliases: []string{"set", "save"},
61+
RunE: func(cmd *cobra.Command, args []string) error {
62+
id, err := store.ParseID(path.Join("keystore-cli", username))
63+
if err != nil {
64+
return err
65+
}
66+
creds := &mocks.MockCredential{
67+
Username: username,
68+
Password: password,
69+
}
70+
return kc.Save(cmd.Context(), id, creds)
71+
},
72+
}
73+
save.PersistentFlags().StringVar(&username, "username", "", "The secret key")
74+
save.PersistentFlags().StringVar(&password, "password", "", "The secret value")
75+
save.MarkFlagsRequiredTogether("username", "password")
76+
77+
get := &cobra.Command{
78+
Use: "get",
79+
Args: cobra.ExactArgs(1),
80+
RunE: func(cmd *cobra.Command, args []string) error {
81+
id, err := store.ParseID(path.Join("keystore-cli", args[0]))
82+
if err != nil {
83+
return err
84+
}
85+
secret, err := kc.Get(cmd.Context(), id)
86+
if err != nil {
87+
return err
88+
}
89+
val, err := secret.Marshal()
90+
if err != nil {
91+
return err
92+
}
93+
fmt.Printf("Secret:\nID:%s\nValue:%s\n", id.String(), val)
94+
return nil
95+
},
96+
}
97+
98+
erase := &cobra.Command{
99+
Use: "delete",
100+
Args: cobra.ExactArgs(1),
101+
Aliases: []string{"rm", "remove"},
102+
RunE: func(cmd *cobra.Command, args []string) error {
103+
id, err := store.ParseID(path.Join("keystore-cli", args[0]))
104+
if err != nil {
105+
return err
106+
}
107+
return kc.Delete(cmd.Context(), id)
108+
},
109+
}
110+
root := &cobra.Command{}
111+
root.AddCommand(list, save, get, erase)
112+
113+
return root, nil
114+
}
115+
116+
func main() {
117+
ctx := context.Background()
118+
cmd, err := newCommand()
119+
if err != nil {
120+
log.Fatalf("could not create CLI: %v", err)
121+
}
122+
cmd.SetContext(ctx)
123+
if err := cmd.Execute(); err != nil {
124+
log.Fatal(err)
125+
}
126+
}

store/keychain/keychain_test.go

Lines changed: 132 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,96 @@
11
package keychain
22

33
import (
4-
"maps"
4+
"errors"
55
"testing"
66

77
"github.com/docker/secrets-engine/store"
88
"github.com/docker/secrets-engine/store/mocks"
99
"github.com/stretchr/testify/require"
1010
)
1111

12-
func TestKeychain(t *testing.T) {
13-
ks, err := New("com.test.test", "test",
14-
func() *mocks.MockCredential {
12+
type mustMarshalError struct{}
13+
14+
var _ store.Secret = &mustMarshalError{}
15+
16+
func (m *mustMarshalError) Marshal() ([]byte, error) {
17+
return nil, errors.New("i am failing on purpose")
18+
}
19+
20+
func (m *mustMarshalError) Unmarshal(data []byte) error {
21+
return nil
22+
}
23+
24+
type mustUnmarshalError struct{}
25+
26+
var _ store.Secret = &mustUnmarshalError{}
27+
28+
func (m *mustUnmarshalError) Marshal() ([]byte, error) {
29+
return []byte("eeyyy"), nil
30+
}
31+
32+
func (m *mustUnmarshalError) Unmarshal(data []byte) error {
33+
return errors.New("i am failing on purpose")
34+
}
35+
36+
func setupKeychain(t *testing.T, secretFactory func() store.Secret) store.Store {
37+
t.Helper()
38+
if secretFactory == nil {
39+
secretFactory = func() store.Secret {
1540
return &mocks.MockCredential{}
16-
},
17-
)
41+
}
42+
}
43+
44+
ks, err := New("com.test.test", "test", secretFactory)
1845
require.NoError(t, err)
46+
return ks
47+
}
1948

49+
func TestKeychain(t *testing.T) {
2050
t.Run("save credentials", func(t *testing.T) {
51+
ks := setupKeychain(t, nil)
2152
id := store.ID("com.test.test/test/bob")
2253
require.NoError(t, id.Valid())
2354
creds := &mocks.MockCredential{
2455
Username: "bob",
2556
Password: "bob-password",
2657
}
58+
t.Cleanup(func() {
59+
require.NoError(t, ks.Delete(t.Context(), id))
60+
})
2761
require.NoError(t, ks.Save(t.Context(), id, creds))
2862
})
2963

3064
t.Run("get credential", func(t *testing.T) {
65+
ks := setupKeychain(t, nil)
3166
id := store.ID("com.test.test/test/bob")
67+
creds := &mocks.MockCredential{
68+
Username: "bob",
69+
Password: "bob-password",
70+
}
71+
t.Cleanup(func() {
72+
require.NoError(t, ks.Delete(t.Context(), id))
73+
})
74+
require.NoError(t, ks.Save(t.Context(), id, creds))
3275
require.NoError(t, id.Valid())
3376
secret, err := ks.Get(t.Context(), id)
3477
require.NoError(t, err)
3578

3679
actual, ok := secret.(*mocks.MockCredential)
3780
require.True(t, ok)
3881

39-
expected := &mocks.MockCredential{
40-
Username: "bob",
41-
Password: "bob-password",
42-
}
82+
expected := creds
4383
require.EqualValues(t, expected, actual)
4484
})
4585

4686
t.Run("list all credentials", func(t *testing.T) {
87+
ks := setupKeychain(t, nil)
88+
4789
moreCreds := map[store.ID]*mocks.MockCredential{
90+
"com.test.test/test/bob": {
91+
Username: "bob",
92+
Password: "bob-password",
93+
},
4894
"com.test.test/test/jeff": {
4995
Username: "jeff",
5096
Password: "jeff-password",
@@ -54,6 +100,11 @@ func TestKeychain(t *testing.T) {
54100
Password: "pete-password",
55101
},
56102
}
103+
t.Cleanup(func() {
104+
for id := range moreCreds {
105+
require.NoError(t, ks.Delete(t.Context(), id))
106+
}
107+
})
57108

58109
for id, anotherCred := range moreCreds {
59110
require.NoError(t, ks.Save(t.Context(), id, anotherCred))
@@ -67,22 +118,85 @@ func TestKeychain(t *testing.T) {
67118
}
68119
require.Len(t, actual, 3)
69120

70-
expected := map[store.ID]*mocks.MockCredential{
71-
"com.test.test/test/bob": {
72-
Username: "bob",
73-
Password: "bob-password",
74-
},
75-
}
76-
maps.Copy(expected, moreCreds)
77-
require.Len(t, expected, 3)
121+
expected := moreCreds
78122
require.Equal(t, expected, actual)
79123
})
80124

81125
t.Run("delete credential", func(t *testing.T) {
126+
ks := setupKeychain(t, nil)
82127
id := store.ID("com.test.test/test/bob")
83128
require.NoError(t, id.Valid())
129+
creds := &mocks.MockCredential{
130+
Username: "bob",
131+
Password: "bob-password",
132+
}
133+
require.NoError(t, ks.Save(t.Context(), id, creds))
84134
require.NoError(t, ks.Delete(t.Context(), id))
85135
_, err := ks.Get(t.Context(), id)
86136
require.ErrorIs(t, err, store.ErrCredentialNotFound)
87137
})
138+
139+
t.Run("delete non-existent credential", func(t *testing.T) {
140+
ks := setupKeychain(t, nil)
141+
id := store.ID("com.test.test/test/does-not-exist")
142+
require.NoError(t, id.Valid())
143+
require.NoError(t, ks.Delete(t.Context(), id))
144+
})
145+
146+
t.Run("invalid ID", func(t *testing.T) {
147+
id := store.ID("completely*&@#$@invalid")
148+
kc := setupKeychain(t, nil)
149+
150+
operations := []string{"save", "get", "delete"}
151+
152+
for _, op := range operations {
153+
t.Run(op, func(t *testing.T) {
154+
var err error
155+
switch op {
156+
case "save":
157+
err = kc.Save(t.Context(), id, nil)
158+
case "delete":
159+
err = kc.Delete(t.Context(), id)
160+
case "get":
161+
_, err = kc.Get(t.Context(), id)
162+
}
163+
require.ErrorContains(t, err, "invalid identifier")
164+
})
165+
}
166+
})
167+
168+
t.Run("marshal error on save", func(t *testing.T) {
169+
kc := setupKeychain(t, nil)
170+
id, err := store.ParseID("something/will/fail")
171+
require.NoError(t, err)
172+
require.ErrorContains(t, kc.Save(t.Context(), id, &mustMarshalError{}), "i am failing on purpose")
173+
})
174+
175+
t.Run("unmarshal error on get", func(t *testing.T) {
176+
kc := setupKeychain(t, func() store.Secret {
177+
return &mustUnmarshalError{}
178+
})
179+
id, err := store.ParseID("something/will/fail")
180+
require.NoError(t, err)
181+
t.Cleanup(func() {
182+
require.NoError(t, kc.Delete(t.Context(), id))
183+
})
184+
require.NoError(t, kc.Save(t.Context(), id, &mustUnmarshalError{}))
185+
_, err = kc.Get(t.Context(), id)
186+
require.ErrorContains(t, err, "i am failing on purpose")
187+
})
188+
189+
t.Run("unmarshal error on getAll", func(t *testing.T) {
190+
kc := setupKeychain(t, func() store.Secret {
191+
return &mustUnmarshalError{}
192+
})
193+
id, err := store.ParseID("something/will/fail")
194+
require.NoError(t, err)
195+
t.Cleanup(func() {
196+
require.NoError(t, kc.Delete(t.Context(), id))
197+
})
198+
require.NoError(t, kc.Save(t.Context(), id, &mustUnmarshalError{}))
199+
_, err = kc.GetAll(t.Context())
200+
require.ErrorContains(t, err, "i am failing on purpose")
201+
})
88202
}

0 commit comments

Comments
 (0)