Skip to content

Commit d0c1c43

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

File tree

10 files changed

+150
-314
lines changed

10 files changed

+150
-314
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: 69 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,30 @@
11
package commands
22

33
import (
4+
"encoding/json"
45
"errors"
56
"fmt"
7+
"slices"
68
"strings"
79

810
"github.com/spf13/cobra"
911

12+
"github.com/docker/mcp-gateway/cmd/docker-mcp/secret-management/formatting"
1013
"github.com/docker/mcp-gateway/cmd/docker-mcp/secret-management/secret"
14+
"github.com/docker/mcp-gateway/pkg/desktop"
1115
"github.com/docker/mcp-gateway/pkg/docker"
1216
)
1317

1418
const setSecretExample = `
1519
### Use secrets for postgres password with default policy
1620
17-
> docker mcp secret set POSTGRES_PASSWORD=my-secret-password
18-
> docker run -d -l x-secret:POSTGRES_PASSWORD=/pwd.txt -e POSTGRES_PASSWORD_FILE=/pwd.txt -p 5432 postgres
21+
> docker mcp secret set postgres_password=my-secret-password
22+
23+
Inject the secret by querying by ID
24+
> docker run -d -e POSTGRES_PASSWORD=se://docker/mcp/generic/postgres_password -p 5432 postgres
25+
26+
Another way to inject secrets would be to use a pattern.
27+
> docker run -d -e POSTGRES_PASSWORD=se://**/postgres_password -p 5432 postgres
1928
2029
### Pass the secret via STDIN
2130
@@ -26,66 +35,105 @@ const setSecretExample = `
2635
func secretCommand(docker docker.Client) *cobra.Command {
2736
cmd := &cobra.Command{
2837
Use: "secret",
29-
Short: "Manage secrets",
38+
Short: "Manage secrets in the local OS Keychain",
3039
Example: strings.Trim(setSecretExample, "\n"),
40+
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
41+
err := desktop.CheckHasDockerPass(cmd.Context())
42+
if err != nil {
43+
return err
44+
}
45+
return nil
46+
},
3147
}
3248
cmd.AddCommand(rmSecretCommand())
3349
cmd.AddCommand(listSecretCommand())
3450
cmd.AddCommand(setSecretCommand())
35-
cmd.AddCommand(exportSecretCommand(docker))
3651
return cmd
3752
}
3853

3954
func rmSecretCommand() *cobra.Command {
40-
var opts secret.RmOpts
55+
var all bool
4156
cmd := &cobra.Command{
4257
Use: "rm name1 name2 ...",
43-
Short: "Remove secrets from Docker Desktop's secret store",
58+
Short: "Remove secrets from the OS Keychain",
4459
RunE: func(cmd *cobra.Command, args []string) error {
45-
if err := validateRmArgs(args, opts); err != nil {
60+
if err := validateRmArgs(args, all); err != nil {
4661
return err
4762
}
48-
return secret.Remove(cmd.Context(), args, opts)
63+
64+
ids := slices.Clone(args)
65+
if all {
66+
var err error
67+
ids, err = secret.List(cmd.Context())
68+
if err != nil {
69+
return err
70+
}
71+
}
72+
73+
var errs []error
74+
for _, s := range ids {
75+
errs = append(errs, secret.DeleteSecret(cmd.Context(), s))
76+
}
77+
return errors.Join(errs...)
4978
},
5079
}
5180
flags := cmd.Flags()
52-
flags.BoolVar(&opts.All, "all", false, "Remove all secrets")
81+
flags.BoolVar(&all, "all", false, "Remove all secrets")
5382
return cmd
5483
}
5584

56-
func validateRmArgs(args []string, opts secret.RmOpts) error {
57-
if len(args) == 0 && !opts.All {
85+
func validateRmArgs(args []string, all bool) error {
86+
if len(args) == 0 && !all {
5887
return errors.New("either provide a secret name or use --all to remove all secrets")
5988
}
6089
return nil
6190
}
6291

6392
func listSecretCommand() *cobra.Command {
64-
var opts secret.ListOptions
93+
var outJSON bool
6594
cmd := &cobra.Command{
6695
Use: "ls",
67-
Short: "List all secret names in Docker Desktop's secret store",
96+
Short: "List all secrets from the local OS Keychain as well as any active Secrets Engine provider",
6897
Args: cobra.NoArgs,
6998
RunE: func(cmd *cobra.Command, _ []string) error {
70-
return secret.List(cmd.Context(), opts)
99+
// query the Secrets Engine instead to get all the secrets from
100+
// all active providers.
101+
l, err := secret.GetSecrets(cmd.Context())
102+
if err != nil {
103+
return err
104+
}
105+
if outJSON {
106+
if len(l) == 0 {
107+
l = []secret.Envelope{} // Guarantee empty list (instead of displaying null)
108+
}
109+
jsonData, err := json.MarshalIndent(l, "", " ")
110+
if err != nil {
111+
return err
112+
}
113+
fmt.Println(string(jsonData))
114+
return nil
115+
}
116+
var rows [][]string
117+
for _, v := range l {
118+
rows = append(rows, []string{v.Id, v.Provider})
119+
}
120+
formatting.PrettyPrintTable(rows, []int{40, 120})
121+
return nil
71122
},
72123
}
73124
flags := cmd.Flags()
74-
flags.BoolVar(&opts.JSON, "json", false, "Print as JSON.")
125+
flags.BoolVar(&outJSON, "json", false, "Print as JSON.")
75126
return cmd
76127
}
77128

78129
func setSecretCommand() *cobra.Command {
79130
opts := &secret.SetOpts{}
80131
cmd := &cobra.Command{
81132
Use: "set key[=value]",
82-
Short: "Set a secret in Docker Desktop's secret store",
133+
Short: "Set a secret in the local OS Keychain",
83134
Example: strings.Trim(setSecretExample, "\n"),
84135
Args: cobra.ExactArgs(1),
85136
RunE: func(cmd *cobra.Command, args []string) error {
86-
if !secret.IsValidProvider(opts.Provider) {
87-
return fmt.Errorf("invalid provider: %s", opts.Provider)
88-
}
89137
var s secret.Secret
90138
if isNotImplicitReadFromStdinSyntax(args, *opts) {
91139
va, err := secret.ParseArg(args[0], *opts)
@@ -105,30 +153,10 @@ func setSecretCommand() *cobra.Command {
105153
}
106154
flags := cmd.Flags()
107155
flags.StringVar(&opts.Provider, "provider", "", "Supported: credstore, oauth/<provider>")
156+
flags.MarkDeprecated("provider", "option will be ignored")
108157
return cmd
109158
}
110159

111160
func isNotImplicitReadFromStdinSyntax(args []string, opts secret.SetOpts) bool {
112-
return strings.Contains(args[0], "=") || len(args) > 1 || opts.Provider != ""
113-
}
114-
115-
func exportSecretCommand(docker docker.Client) *cobra.Command {
116-
return &cobra.Command{
117-
Use: "export [server1] [server2] ...",
118-
Short: "Export secrets for the specified servers",
119-
Hidden: true,
120-
Args: cobra.MinimumNArgs(1),
121-
RunE: func(cmd *cobra.Command, args []string) error {
122-
secrets, err := secret.Export(cmd.Context(), docker, args)
123-
if err != nil {
124-
return err
125-
}
126-
127-
for name, secret := range secrets {
128-
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s=%s\n", name, secret)
129-
}
130-
131-
return nil
132-
},
133-
}
161+
return strings.Contains(args[0], "=") || len(args) > 1
134162
}
Lines changed: 52 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,125 +1,75 @@
1+
// This package stores secrets in the local OS Keychain.
12
package secret
23

34
import (
5+
"bufio"
6+
"bytes"
47
"context"
5-
"io"
8+
"fmt"
69
"os/exec"
10+
"path"
711
"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"
1312
)
1413

15-
type CredStoreProvider struct {
16-
credentialHelper credentials.Helper
17-
}
18-
19-
func NewCredStoreProvider() *CredStoreProvider {
20-
return &CredStoreProvider{credentialHelper: GetHelper()}
21-
}
22-
14+
type CredStoreProvider struct{}
15+
16+
func cmd(ctx context.Context, args ...string) *exec.Cmd {
17+
return exec.CommandContext(ctx, "docker", append([]string{"pass"}, args...)...)
18+
}
19+
20+
// getSecretKey prefixes the secrets with the docker/mcp/generic namespace.
21+
// Additional namespaces can be added when defining the secretName.
22+
//
23+
// Example:
24+
//
25+
// secretName = "mysecret/application/id"
26+
// return "docker/mcp/generic/mysecret/application/id"
27+
//
28+
// This can later then be queried by the Secrets Engine using a pattern or direct
29+
// ID match.
30+
//
31+
// Example:
32+
//
33+
// # anything under mcp/generic
34+
// pattern = "docker/mcp/generic/**"
35+
// # specific to mysecret
36+
// pattern = "docker/mcp/generic/mysecret/application/**"
2337
func getSecretKey(secretName string) string {
24-
return "sm_" + secretName
38+
return path.Join("docker/mcp/generic/", secretName)
2539
}
2640

27-
func (store *CredStoreProvider) GetSecret(id string) (string, error) {
28-
_, val, err := store.credentialHelper.Get(getSecretKey(id))
41+
func List(ctx context.Context) ([]string, error) {
42+
c := cmd(ctx, "ls")
43+
out, err := c.Output()
2944
if err != nil {
30-
return "", err
45+
return nil, fmt.Errorf("could not list secrets: %s\n%s", bytes.TrimSpace(out), err)
3146
}
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...)}
47+
scanner := bufio.NewScanner(bytes.NewReader(out))
48+
var secrets []string
49+
for scanner.Scan() {
50+
secret := scanner.Text()
51+
if len(secret) == 0 {
52+
continue
53+
}
54+
secrets = append(secrets, secret)
5855
}
56+
return secrets, nil
5957
}
6058

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 {
96-
return err
59+
func setSecret(ctx context.Context, id string, value string) error {
60+
c := cmd(ctx, "set", getSecretKey(id))
61+
c.Stdin = strings.NewReader(value)
62+
out, err := c.CombinedOutput()
63+
if err != nil {
64+
return fmt.Errorf("could not store secret: %s\n%s", bytes.TrimSpace(out), err)
9765
}
9866
return nil
9967
}
10068

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-
}
107-
return err
108-
}
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)
69+
func DeleteSecret(ctx context.Context, id string) error {
70+
out, err := cmd(ctx, "rm", getSecretKey(id)).CombinedOutput()
11571
if err != nil {
116-
return "", "", err
72+
return fmt.Errorf("could not delete secret: %s\n%s\n%s", id, bytes.TrimSpace(out), err)
11773
}
118-
return creds.Username, creds.Secret, nil
119-
}
120-
121-
func isErrDecryption(err error) bool {
122-
return err != nil && strings.Contains(err.Error(), "gpg: decryption failed: No secret key")
74+
return nil
12375
}
124-
125-
var _ credentials.Helper = Helper{}

0 commit comments

Comments
 (0)