Skip to content

Commit 7a9fceb

Browse files
committed
cmd/secret: use new store
Signed-off-by: Alano Terblanche <[email protected]>
1 parent 7145062 commit 7a9fceb

File tree

10 files changed

+182
-161
lines changed

10 files changed

+182
-161
lines changed

cmd/docker-mcp/commands/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ func Root(ctx context.Context, cwd string, dockerCli command.Cli) *cobra.Command
4040
HiddenDefaultCmd: true,
4141
},
4242
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
43-
cmd.SetContext(ctx)
4443
if err := plugin.PersistentPreRunE(cmd, args); err != nil {
4544
return err
4645
}
@@ -60,6 +59,7 @@ func Root(ctx context.Context, cwd string, dockerCli command.Cli) *cobra.Command
6059
},
6160
Version: version.Version,
6261
}
62+
cmd.SetContext(ctx)
6363
cmd.SetVersionTemplate("{{.Version}}\n")
6464
cmd.Flags().BoolP("version", "v", false, "Print version information and quit")
6565
cmd.SetHelpTemplate(helpTemplate)

cmd/docker-mcp/commands/secret.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/spf13/cobra"
99

1010
"github.com/docker/mcp-gateway/cmd/docker-mcp/secret-management/secret"
11+
"github.com/docker/mcp-gateway/pkg/desktop"
1112
"github.com/docker/mcp-gateway/pkg/docker"
1213
)
1314

@@ -28,6 +29,21 @@ func secretCommand(docker docker.Client) *cobra.Command {
2829
Use: "secret",
2930
Short: "Manage secrets",
3031
Example: strings.Trim(setSecretExample, "\n"),
32+
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
33+
err := desktop.CheckHasSecretsEngine(cmd.Context())
34+
if err == nil {
35+
cmd.SetContext(secret.SetHasSecretsEngine(cmd.Context()))
36+
return nil
37+
}
38+
// we don't want to error if secrets engine is not supported.
39+
// but we do want to propagate other errors (if any).
40+
if !errors.Is(err, desktop.ErrSecretsEngineUnsupported) {
41+
return err
42+
}
43+
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Warning: using a deprecated version of Docker Desktop. Please upgrade to get access to the latest features")
44+
45+
return nil
46+
},
3147
}
3248
cmd.AddCommand(rmSecretCommand())
3349
cmd.AddCommand(listSecretCommand())
@@ -83,9 +99,6 @@ func setSecretCommand() *cobra.Command {
8399
Example: strings.Trim(setSecretExample, "\n"),
84100
Args: cobra.ExactArgs(1),
85101
RunE: func(cmd *cobra.Command, args []string) error {
86-
if !secret.IsValidProvider(opts.Provider) {
87-
return fmt.Errorf("invalid provider: %s", opts.Provider)
88-
}
89102
var s secret.Secret
90103
if isNotImplicitReadFromStdinSyntax(args, *opts) {
91104
va, err := secret.ParseArg(args[0], *opts)
@@ -105,11 +118,12 @@ func setSecretCommand() *cobra.Command {
105118
}
106119
flags := cmd.Flags()
107120
flags.StringVar(&opts.Provider, "provider", "", "Supported: credstore, oauth/<provider>")
121+
flags.MarkDeprecated("provider", "option will be ignored")
108122
return cmd
109123
}
110124

111125
func isNotImplicitReadFromStdinSyntax(args []string, opts secret.SetOpts) bool {
112-
return strings.Contains(args[0], "=") || len(args) > 1 || opts.Provider != ""
126+
return strings.Contains(args[0], "=") || len(args) > 1
113127
}
114128

115129
func exportSecretCommand(docker docker.Client) *cobra.Command {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package secret
2+
3+
import "context"
4+
5+
type CtxKey struct{}
6+
7+
func SetHasSecretsEngine(ctx context.Context) context.Context {
8+
return context.WithValue(ctx, CtxKey{}, true)
9+
}
10+
11+
func HasSecretsEngine(ctx context.Context) bool {
12+
val, ok := ctx.Value(CtxKey{}).(bool)
13+
if !ok {
14+
return false
15+
}
16+
return val
17+
}
Lines changed: 33 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,125 +1,61 @@
11
package secret
22

33
import (
4+
"bufio"
5+
"bytes"
46
"context"
5-
"io"
67
"os/exec"
7-
"strings"
8-
9-
"github.com/docker/docker-credential-helpers/client"
10-
"github.com/docker/docker-credential-helpers/credentials"
11-
12-
"github.com/docker/mcp-gateway/pkg/desktop"
138
)
149

15-
type CredStoreProvider struct {
16-
credentialHelper credentials.Helper
10+
type CredStoreProvider struct{}
11+
12+
func cmd(ctx context.Context, args ...string) *exec.Cmd {
13+
return exec.CommandContext(ctx, "docker", append([]string{"pass"}, args...)...)
1714
}
1815

1916
func NewCredStoreProvider() *CredStoreProvider {
20-
return &CredStoreProvider{credentialHelper: GetHelper()}
17+
return &CredStoreProvider{}
2118
}
2219

2320
func getSecretKey(secretName string) string {
24-
return "sm_" + secretName
21+
return "docker/mcp/" + secretName
2522
}
2623

27-
func (store *CredStoreProvider) GetSecret(id string) (string, error) {
28-
_, val, err := store.credentialHelper.Get(getSecretKey(id))
24+
func (store *CredStoreProvider) List(ctx context.Context) ([]string, error) {
25+
c := cmd(ctx, "ls")
26+
out, err := c.Output()
2927
if err != nil {
30-
return "", err
31-
}
32-
return val, nil
33-
}
34-
35-
func (store *CredStoreProvider) SetSecret(id string, value string) error {
36-
return store.credentialHelper.Add(&credentials.Credentials{
37-
ServerURL: getSecretKey(id),
38-
Username: "mcp",
39-
Secret: value,
40-
})
41-
}
42-
43-
func (store *CredStoreProvider) DeleteSecret(id string) error {
44-
return store.credentialHelper.Delete(getSecretKey(id))
45-
}
46-
47-
func GetHelper() credentials.Helper {
48-
credentialHelperPath := desktop.Paths().CredentialHelperPath()
49-
return Helper{
50-
program: newShellProgramFunc(credentialHelperPath),
51-
}
52-
}
53-
54-
// newShellProgramFunc creates programs that are executed in a Shell.
55-
func newShellProgramFunc(name string) client.ProgramFunc {
56-
return func(args ...string) client.Program {
57-
return &shell{cmd: exec.CommandContext(context.Background(), name, args...)}
28+
return nil, err
29+
}
30+
scanner := bufio.NewScanner(bytes.NewReader(out))
31+
var secrets []string
32+
for scanner.Scan() {
33+
secret := scanner.Text()
34+
if len(secret) == 0 {
35+
continue
36+
}
37+
secrets = append(secrets, secret)
5838
}
39+
return secrets, nil
5940
}
6041

61-
// shell invokes shell commands to talk with a remote credentials-helper.
62-
type shell struct {
63-
cmd *exec.Cmd
64-
}
65-
66-
// Output returns responses from the remote credentials-helper.
67-
func (s *shell) Output() ([]byte, error) {
68-
return s.cmd.Output()
69-
}
70-
71-
// Input sets the input to send to a remote credentials-helper.
72-
func (s *shell) Input(in io.Reader) {
73-
s.cmd.Stdin = in
74-
}
75-
76-
// Helper wraps credential helper program.
77-
type Helper struct {
78-
// name string
79-
program client.ProgramFunc
80-
}
81-
82-
func (h Helper) List() (map[string]string, error) {
83-
return map[string]string{}, nil
84-
}
85-
86-
// Add stores new credentials.
87-
func (h Helper) Add(creds *credentials.Credentials) error {
88-
username, secret, err := h.Get(creds.ServerURL)
89-
if err != nil && !credentials.IsErrCredentialsNotFound(err) && !isErrDecryption(err) {
90-
return err
91-
}
92-
if username == creds.Username && secret == creds.Secret {
93-
return nil
94-
}
95-
if err := client.Store(h.program, creds); err != nil {
42+
func (store *CredStoreProvider) SetSecret(ctx context.Context, id string, value string) error {
43+
c := cmd(ctx, "set", getSecretKey(id))
44+
in, err := c.StdinPipe()
45+
if err != nil {
9646
return err
9747
}
98-
return nil
99-
}
100-
101-
// Delete removes credentials.
102-
func (h Helper) Delete(serverURL string) error {
103-
if _, _, err := h.Get(serverURL); err != nil {
104-
if credentials.IsErrCredentialsNotFound(err) {
105-
return nil
106-
}
48+
if err := c.Start(); err != nil {
10749
return err
10850
}
109-
return client.Erase(h.program, serverURL)
110-
}
111-
112-
// Get returns the username and secret to use for a given registry server URL.
113-
func (h Helper) Get(serverURL string) (string, string, error) {
114-
creds, err := client.Get(h.program, serverURL)
51+
_, err = in.Write([]byte(value))
11552
if err != nil {
116-
return "", "", err
53+
return err
11754
}
118-
return creds.Username, creds.Secret, nil
55+
return c.Wait()
11956
}
12057

121-
func isErrDecryption(err error) bool {
122-
return err != nil && strings.Contains(err.Error(), "gpg: decryption failed: No secret key")
58+
func (store *CredStoreProvider) DeleteSecret(ctx context.Context, id string) error {
59+
c := cmd(ctx, "rm", getSecretKey(id))
60+
return c.Run()
12361
}
124-
125-
var _ credentials.Helper = Helper{}

cmd/docker-mcp/secret-management/secret/list.go

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,36 @@ type ListOptions struct {
1414
}
1515

1616
func List(ctx context.Context, opts ListOptions) error {
17-
l, err := desktop.NewSecretsClient().ListJfsSecrets(ctx)
18-
if err != nil {
19-
return err
17+
secrets := make([]desktop.StoredSecret, 0, 0)
18+
if HasSecretsEngine(ctx) {
19+
p := NewCredStoreProvider()
20+
s, err := p.List(ctx)
21+
if err != nil {
22+
return err
23+
}
24+
for _, secret := range s {
25+
secrets = append(secrets, desktop.StoredSecret{
26+
Name: secret,
27+
})
28+
}
29+
} else {
30+
l, err := desktop.NewSecretsClient().ListJfsSecrets(ctx)
31+
if err != nil {
32+
return err
33+
}
34+
secrets = l
2035
}
2136

2237
if opts.JSON {
23-
if len(l) == 0 {
24-
l = []desktop.StoredSecret{} // Guarantee empty list (instead of displaying null)
25-
}
26-
jsonData, err := json.MarshalIndent(l, "", " ")
38+
jsonData, err := json.MarshalIndent(secrets, "", " ")
2739
if err != nil {
2840
return err
2941
}
3042
fmt.Println(string(jsonData))
3143
return nil
3244
}
3345
var rows [][]string
34-
for _, v := range l {
46+
for _, v := range secrets {
3547
rows = append(rows, []string{v.Name, v.Provider})
3648
}
3749
formatting.PrettyPrintTable(rows, []int{40, 120})

cmd/docker-mcp/secret-management/secret/rm.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"slices"
78

89
"github.com/docker/mcp-gateway/pkg/desktop"
910
)
@@ -13,24 +14,40 @@ type RmOpts struct {
1314
}
1415

1516
func Remove(ctx context.Context, names []string, opts RmOpts) error {
17+
secrets := slices.Clone(names)
18+
if HasSecretsEngine(ctx) {
19+
p := NewCredStoreProvider()
20+
if opts.All && len(secrets) == 0 {
21+
var err error
22+
secrets, err = p.List(ctx)
23+
if err != nil {
24+
return err
25+
}
26+
}
27+
for _, secret := range secrets {
28+
if err := p.DeleteSecret(ctx, secret); err != nil {
29+
return err
30+
}
31+
}
32+
}
1633
c := desktop.NewSecretsClient()
17-
if opts.All && len(names) == 0 {
34+
if opts.All && len(secrets) == 0 {
1835
l, err := c.ListJfsSecrets(ctx)
1936
if err != nil {
2037
return err
2138
}
2239
for _, secret := range l {
23-
names = append(names, secret.Name)
40+
secrets = append(secrets, secret.Name)
2441
}
2542
}
2643
var errs []error
27-
for _, name := range names {
28-
if err := c.DeleteJfsSecret(ctx, name); err != nil {
44+
for _, secret := range secrets {
45+
if err := c.DeleteJfsSecret(ctx, secret); err != nil {
2946
errs = append(errs, err)
30-
fmt.Printf("failed removing secret %s\n", name)
47+
fmt.Printf("failed removing secret %s\n", secret)
3148
continue
3249
}
33-
fmt.Printf("removed secret %s\n", name)
50+
fmt.Printf("removed secret %s\n", secret)
3451
}
3552
return errors.Join(errs...)
3653
}

cmd/docker-mcp/secret-management/secret/secret_test.go

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package secret
22

33
import (
4-
"errors"
54
"testing"
65

76
"github.com/stretchr/testify/assert"
@@ -36,28 +35,3 @@ func TestIsDirectValueProvider(t *testing.T) {
3635
assert.True(t, isDirectValueProvider(Credstore))
3736
assert.False(t, isDirectValueProvider("oauth/github"))
3837
}
39-
40-
func TestIsValidProvider(t *testing.T) {
41-
// Valid providers
42-
assert.True(t, IsValidProvider(""))
43-
assert.True(t, IsValidProvider(Credstore))
44-
assert.True(t, IsValidProvider("oauth/github"))
45-
assert.True(t, IsValidProvider("oauth/google"))
46-
47-
// Invalid providers
48-
assert.False(t, IsValidProvider("invalid"))
49-
assert.False(t, IsValidProvider("oauth"))
50-
}
51-
52-
func TestIsErrDecryption(t *testing.T) {
53-
// Test decryption error detection
54-
decryptErr := errors.New("gpg: decryption failed: No secret key")
55-
assert.True(t, isErrDecryption(decryptErr))
56-
57-
// Test other errors
58-
otherErr := errors.New("some other error")
59-
assert.False(t, isErrDecryption(otherErr))
60-
61-
// Test nil
62-
assert.False(t, isErrDecryption(nil))
63-
}

0 commit comments

Comments
 (0)