diff --git a/cmd/docker-mcp/backup/dump.go b/cmd/docker-mcp/backup/dump.go index 94f6b7cd..4f89ded2 100644 --- a/cmd/docker-mcp/backup/dump.go +++ b/cmd/docker-mcp/backup/dump.go @@ -6,7 +6,6 @@ import ( "github.com/docker/mcp-gateway/cmd/docker-mcp/catalog" "github.com/docker/mcp-gateway/pkg/config" - "github.com/docker/mcp-gateway/pkg/desktop" "github.com/docker/mcp-gateway/pkg/docker" ) @@ -45,43 +44,12 @@ func Dump(ctx context.Context, docker docker.Client) ([]byte, error) { catalogFiles[name] = string(catalogFileContent) } - secretsClient := desktop.NewSecretsClient() - storedSecrets, err := secretsClient.ListJfsSecrets(ctx) - if err != nil { - return nil, err - } - - var secretNames []string - for _, secret := range storedSecrets { - secretNames = append(secretNames, secret.Name) - } - secretValues, err := docker.ReadSecrets(ctx, secretNames, false) - if err != nil { - return nil, err - } - - var secrets []desktop.Secret - for _, secret := range storedSecrets { - secrets = append(secrets, desktop.Secret{ - Name: secret.Name, - Provider: secret.Provider, - Value: secretValues[secret.Name], - }) - } - - policy, err := secretsClient.GetJfsPolicy(ctx) - if err != nil { - return nil, err - } - backup := Backup{ Config: string(configContent), Registry: string(registryContent), Catalog: string(catalogContent), CatalogFiles: catalogFiles, Tools: string(toolsConfig), - Secrets: secrets, - Policy: policy, } return json.Marshal(backup) diff --git a/cmd/docker-mcp/backup/restore.go b/cmd/docker-mcp/backup/restore.go index 6d0280a0..3def1c2d 100644 --- a/cmd/docker-mcp/backup/restore.go +++ b/cmd/docker-mcp/backup/restore.go @@ -6,7 +6,6 @@ import ( "github.com/docker/mcp-gateway/cmd/docker-mcp/catalog" "github.com/docker/mcp-gateway/pkg/config" - "github.com/docker/mcp-gateway/pkg/desktop" ) func Restore(ctx context.Context, backupData []byte) error { @@ -50,36 +49,5 @@ func Restore(ctx context.Context, backupData []byte) error { } } - secretsClient := desktop.NewSecretsClient() - - secretsBefore, err := secretsClient.ListJfsSecrets(ctx) - if err != nil { - return err - } - - secretsKeep := map[string]bool{} - for _, secret := range backup.Secrets { - if err := secretsClient.SetJfsSecret(ctx, desktop.Secret{ - Name: secret.Name, - Value: secret.Value, - Provider: secret.Provider, - }); err != nil { - return err - } - secretsKeep[secret.Name] = true - } - - for _, secret := range secretsBefore { - if !secretsKeep[secret.Name] { - if err := secretsClient.DeleteJfsSecret(ctx, secret.Name); err != nil { - return err - } - } - } - - if err := secretsClient.SetJfsPolicy(ctx, backup.Policy); err != nil { - return err - } - return nil } diff --git a/cmd/docker-mcp/backup/types.go b/cmd/docker-mcp/backup/types.go index 79e223e8..c34b9223 100644 --- a/cmd/docker-mcp/backup/types.go +++ b/cmd/docker-mcp/backup/types.go @@ -1,13 +1,9 @@ package backup -import "github.com/docker/mcp-gateway/pkg/desktop" - type Backup struct { Config string `json:"config"` Registry string `json:"registry"` Catalog string `json:"catalog"` CatalogFiles map[string]string `json:"catalogFiles"` Tools string `json:"tools"` - Secrets []desktop.Secret `json:"secrets"` - Policy string `json:"policy"` } diff --git a/cmd/docker-mcp/commands/policy.go b/cmd/docker-mcp/commands/policy.go deleted file mode 100644 index b5713757..00000000 --- a/cmd/docker-mcp/commands/policy.go +++ /dev/null @@ -1,58 +0,0 @@ -package commands - -import ( - "os" - "strings" - - "github.com/spf13/cobra" - - "github.com/docker/mcp-gateway/cmd/docker-mcp/secret-management/policy" - "github.com/docker/mcp-gateway/pkg/tui" -) - -const setPolicyExample = ` -### Backup the current policy to a file -docker mcp policy dump > policy.conf - -### Set a new policy -docker mcp policy set "my-secret allows postgres" - -### Restore the previous policy -cat policy.conf | docker mcp policy set -` - -func policyCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "policy", - Aliases: []string{"policies"}, - Short: "Manage secret policies", - } - - cmd.AddCommand(&cobra.Command{ - Use: "set ", - Short: "Set a policy for secret management in Docker Desktop", - Args: cobra.MaximumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - bytes, err := tui.ReadAllWithContext(cmd.Context(), os.Stdin) - if err != nil { - return err - } - args = append(args, string(bytes)) - } - return policy.Set(cmd.Context(), args[0]) - }, - Example: strings.Trim(setPolicyExample, "\n"), - }) - - cmd.AddCommand(&cobra.Command{ - Use: "dump", - Short: "Dump the policy content", - Args: cobra.NoArgs, - RunE: func(cmd *cobra.Command, _ []string) error { - return policy.Dump(cmd.Context()) - }, - }) - - return cmd -} diff --git a/cmd/docker-mcp/commands/root.go b/cmd/docker-mcp/commands/root.go index 2f9dfdc4..eb46c753 100644 --- a/cmd/docker-mcp/commands/root.go +++ b/cmd/docker-mcp/commands/root.go @@ -40,7 +40,6 @@ func Root(ctx context.Context, cwd string, dockerCli command.Cli) *cobra.Command HiddenDefaultCmd: true, }, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - cmd.SetContext(ctx) if err := plugin.PersistentPreRunE(cmd, args); err != nil { return err } @@ -60,6 +59,7 @@ func Root(ctx context.Context, cwd string, dockerCli command.Cli) *cobra.Command }, Version: version.Version, } + cmd.SetContext(ctx) cmd.SetVersionTemplate("{{.Version}}\n") cmd.Flags().BoolP("version", "v", false, "Print version information and quit") cmd.SetHelpTemplate(helpTemplate) @@ -79,7 +79,6 @@ func Root(ctx context.Context, cwd string, dockerCli command.Cli) *cobra.Command cmd.AddCommand(featureCommand(dockerCli)) cmd.AddCommand(gatewayCommand(dockerClient, dockerCli)) cmd.AddCommand(oauthCommand()) - cmd.AddCommand(policyCommand()) cmd.AddCommand(registryCommand()) cmd.AddCommand(secretCommand(dockerClient)) cmd.AddCommand(serverCommand(dockerClient, dockerCli)) diff --git a/cmd/docker-mcp/commands/secret.go b/cmd/docker-mcp/commands/secret.go index d5b7dc50..4f310352 100644 --- a/cmd/docker-mcp/commands/secret.go +++ b/cmd/docker-mcp/commands/secret.go @@ -2,12 +2,12 @@ package commands import ( "errors" - "fmt" "strings" "github.com/spf13/cobra" "github.com/docker/mcp-gateway/cmd/docker-mcp/secret-management/secret" + "github.com/docker/mcp-gateway/pkg/desktop" "github.com/docker/mcp-gateway/pkg/docker" ) @@ -28,11 +28,17 @@ func secretCommand(docker docker.Client) *cobra.Command { Use: "secret", Short: "Manage secrets", Example: strings.Trim(setSecretExample, "\n"), + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + err := desktop.CheckHasDockerPass(cmd.Context()) + if err != nil { + return err + } + return nil + }, } cmd.AddCommand(rmSecretCommand()) cmd.AddCommand(listSecretCommand()) cmd.AddCommand(setSecretCommand()) - cmd.AddCommand(exportSecretCommand(docker)) return cmd } @@ -83,9 +89,6 @@ func setSecretCommand() *cobra.Command { Example: strings.Trim(setSecretExample, "\n"), Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - if !secret.IsValidProvider(opts.Provider) { - return fmt.Errorf("invalid provider: %s", opts.Provider) - } var s secret.Secret if isNotImplicitReadFromStdinSyntax(args, *opts) { va, err := secret.ParseArg(args[0], *opts) @@ -105,30 +108,10 @@ func setSecretCommand() *cobra.Command { } flags := cmd.Flags() flags.StringVar(&opts.Provider, "provider", "", "Supported: credstore, oauth/") + flags.MarkDeprecated("provider", "option will be ignored") return cmd } func isNotImplicitReadFromStdinSyntax(args []string, opts secret.SetOpts) bool { - return strings.Contains(args[0], "=") || len(args) > 1 || opts.Provider != "" -} - -func exportSecretCommand(docker docker.Client) *cobra.Command { - return &cobra.Command{ - Use: "export [server1] [server2] ...", - Short: "Export secrets for the specified servers", - Hidden: true, - Args: cobra.MinimumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - secrets, err := secret.Export(cmd.Context(), docker, args) - if err != nil { - return err - } - - for name, secret := range secrets { - _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s=%s\n", name, secret) - } - - return nil - }, - } + return strings.Contains(args[0], "=") || len(args) > 1 } diff --git a/cmd/docker-mcp/secret-management/policy/dump.go b/cmd/docker-mcp/secret-management/policy/dump.go deleted file mode 100644 index 9d843990..00000000 --- a/cmd/docker-mcp/secret-management/policy/dump.go +++ /dev/null @@ -1,18 +0,0 @@ -package policy - -import ( - "context" - "fmt" - - "github.com/docker/mcp-gateway/pkg/desktop" -) - -func Dump(ctx context.Context) error { - l, err := desktop.NewSecretsClient().GetJfsPolicy(ctx) - if err != nil { - return err - } - - fmt.Println(l) - return nil -} diff --git a/cmd/docker-mcp/secret-management/policy/set.go b/cmd/docker-mcp/secret-management/policy/set.go deleted file mode 100644 index e9a7a2ea..00000000 --- a/cmd/docker-mcp/secret-management/policy/set.go +++ /dev/null @@ -1,11 +0,0 @@ -package policy - -import ( - "context" - - "github.com/docker/mcp-gateway/pkg/desktop" -) - -func Set(ctx context.Context, data string) error { - return desktop.NewSecretsClient().SetJfsPolicy(ctx, data) -} diff --git a/cmd/docker-mcp/secret-management/secret/credstore.go b/cmd/docker-mcp/secret-management/secret/credstore.go index e1d3adfe..d12f80cf 100644 --- a/cmd/docker-mcp/secret-management/secret/credstore.go +++ b/cmd/docker-mcp/secret-management/secret/credstore.go @@ -1,125 +1,61 @@ package secret import ( + "bufio" + "bytes" "context" - "io" "os/exec" - "strings" - - "github.com/docker/docker-credential-helpers/client" - "github.com/docker/docker-credential-helpers/credentials" - - "github.com/docker/mcp-gateway/pkg/desktop" ) -type CredStoreProvider struct { - credentialHelper credentials.Helper +type CredStoreProvider struct{} + +func cmd(ctx context.Context, args ...string) *exec.Cmd { + return exec.CommandContext(ctx, "docker", append([]string{"pass"}, args...)...) } func NewCredStoreProvider() *CredStoreProvider { - return &CredStoreProvider{credentialHelper: GetHelper()} + return &CredStoreProvider{} } func getSecretKey(secretName string) string { - return "sm_" + secretName + return "docker/mcp/generic/" + secretName } -func (store *CredStoreProvider) GetSecret(id string) (string, error) { - _, val, err := store.credentialHelper.Get(getSecretKey(id)) +func (store *CredStoreProvider) List(ctx context.Context) ([]string, error) { + c := cmd(ctx, "ls") + out, err := c.Output() if err != nil { - return "", err - } - return val, nil -} - -func (store *CredStoreProvider) SetSecret(id string, value string) error { - return store.credentialHelper.Add(&credentials.Credentials{ - ServerURL: getSecretKey(id), - Username: "mcp", - Secret: value, - }) -} - -func (store *CredStoreProvider) DeleteSecret(id string) error { - return store.credentialHelper.Delete(getSecretKey(id)) -} - -func GetHelper() credentials.Helper { - credentialHelperPath := desktop.Paths().CredentialHelperPath() - return Helper{ - program: newShellProgramFunc(credentialHelperPath), - } -} - -// newShellProgramFunc creates programs that are executed in a Shell. -func newShellProgramFunc(name string) client.ProgramFunc { - return func(args ...string) client.Program { - return &shell{cmd: exec.CommandContext(context.Background(), name, args...)} + return nil, err + } + scanner := bufio.NewScanner(bytes.NewReader(out)) + var secrets []string + for scanner.Scan() { + secret := scanner.Text() + if len(secret) == 0 { + continue + } + secrets = append(secrets, secret) } + return secrets, nil } -// shell invokes shell commands to talk with a remote credentials-helper. -type shell struct { - cmd *exec.Cmd -} - -// Output returns responses from the remote credentials-helper. -func (s *shell) Output() ([]byte, error) { - return s.cmd.Output() -} - -// Input sets the input to send to a remote credentials-helper. -func (s *shell) Input(in io.Reader) { - s.cmd.Stdin = in -} - -// Helper wraps credential helper program. -type Helper struct { - // name string - program client.ProgramFunc -} - -func (h Helper) List() (map[string]string, error) { - return map[string]string{}, nil -} - -// Add stores new credentials. -func (h Helper) Add(creds *credentials.Credentials) error { - username, secret, err := h.Get(creds.ServerURL) - if err != nil && !credentials.IsErrCredentialsNotFound(err) && !isErrDecryption(err) { - return err - } - if username == creds.Username && secret == creds.Secret { - return nil - } - if err := client.Store(h.program, creds); err != nil { +func (store *CredStoreProvider) SetSecret(ctx context.Context, id string, value string) error { + c := cmd(ctx, "set", getSecretKey(id)) + in, err := c.StdinPipe() + if err != nil { return err } - return nil -} - -// Delete removes credentials. -func (h Helper) Delete(serverURL string) error { - if _, _, err := h.Get(serverURL); err != nil { - if credentials.IsErrCredentialsNotFound(err) { - return nil - } + if err := c.Start(); err != nil { return err } - return client.Erase(h.program, serverURL) -} - -// Get returns the username and secret to use for a given registry server URL. -func (h Helper) Get(serverURL string) (string, string, error) { - creds, err := client.Get(h.program, serverURL) + _, err = in.Write([]byte(value)) if err != nil { - return "", "", err + return err } - return creds.Username, creds.Secret, nil + return c.Wait() } -func isErrDecryption(err error) bool { - return err != nil && strings.Contains(err.Error(), "gpg: decryption failed: No secret key") +func (store *CredStoreProvider) DeleteSecret(ctx context.Context, id string) error { + c := cmd(ctx, "rm", getSecretKey(id)) + return c.Run() } - -var _ credentials.Helper = Helper{} diff --git a/cmd/docker-mcp/secret-management/secret/export.go b/cmd/docker-mcp/secret-management/secret/export.go deleted file mode 100644 index 3c7c271c..00000000 --- a/cmd/docker-mcp/secret-management/secret/export.go +++ /dev/null @@ -1,40 +0,0 @@ -package secret - -import ( - "context" - "fmt" - "sort" - "strings" - - "github.com/docker/mcp-gateway/pkg/catalog" - "github.com/docker/mcp-gateway/pkg/docker" -) - -func Export(ctx context.Context, docker docker.Client, serverNames []string) (map[string]string, error) { - catalog, err := catalog.Get(ctx) - if err != nil { - return nil, err - } - - var secretNames []string - for _, serverName := range serverNames { - serverName = strings.TrimSpace(serverName) - - serverSpec, ok := catalog.Servers[serverName] - if !ok { - return nil, fmt.Errorf("server %s not found in catalog", serverName) - } - - for _, s := range serverSpec.Secrets { - secretNames = append(secretNames, s.Name) - } - } - - if len(secretNames) == 0 { - return map[string]string{}, nil - } - - sort.Strings(secretNames) - - return docker.ReadSecrets(ctx, secretNames, false) -} diff --git a/cmd/docker-mcp/secret-management/secret/list.go b/cmd/docker-mcp/secret-management/secret/list.go index 3893c3a4..6f4d91ff 100644 --- a/cmd/docker-mcp/secret-management/secret/list.go +++ b/cmd/docker-mcp/secret-management/secret/list.go @@ -6,24 +6,22 @@ import ( "fmt" "github.com/docker/mcp-gateway/cmd/docker-mcp/secret-management/formatting" - "github.com/docker/mcp-gateway/pkg/desktop" ) type ListOptions struct { JSON bool } +// TODO: List needs to query the secrets engine func List(ctx context.Context, opts ListOptions) error { - l, err := desktop.NewSecretsClient().ListJfsSecrets(ctx) - if err != nil { - return err + var secrets []struct { + Name string + Provider string } + // fetch secrets from secrets engine if opts.JSON { - if len(l) == 0 { - l = []desktop.StoredSecret{} // Guarantee empty list (instead of displaying null) - } - jsonData, err := json.MarshalIndent(l, "", " ") + jsonData, err := json.MarshalIndent(secrets, "", " ") if err != nil { return err } @@ -31,7 +29,7 @@ func List(ctx context.Context, opts ListOptions) error { return nil } var rows [][]string - for _, v := range l { + for _, v := range secrets { rows = append(rows, []string{v.Name, v.Provider}) } formatting.PrettyPrintTable(rows, []int{40, 120}) diff --git a/cmd/docker-mcp/secret-management/secret/rm.go b/cmd/docker-mcp/secret-management/secret/rm.go index 1e6e78d2..7fb9f001 100644 --- a/cmd/docker-mcp/secret-management/secret/rm.go +++ b/cmd/docker-mcp/secret-management/secret/rm.go @@ -4,8 +4,7 @@ import ( "context" "errors" "fmt" - - "github.com/docker/mcp-gateway/pkg/desktop" + "slices" ) type RmOpts struct { @@ -13,24 +12,25 @@ type RmOpts struct { } func Remove(ctx context.Context, names []string, opts RmOpts) error { - c := desktop.NewSecretsClient() - if opts.All && len(names) == 0 { - l, err := c.ListJfsSecrets(ctx) + secrets := slices.Clone(names) + p := NewCredStoreProvider() + if opts.All && len(secrets) == 0 { + var err error + secrets, err = p.List(ctx) if err != nil { return err } - for _, secret := range l { - names = append(names, secret.Name) - } } + var errs []error - for _, name := range names { - if err := c.DeleteJfsSecret(ctx, name); err != nil { + for _, secret := range secrets { + if err := p.DeleteSecret(ctx, secret); err != nil { errs = append(errs, err) - fmt.Printf("failed removing secret %s\n", name) + fmt.Printf("failed removing secret %s\n", secret) continue } - fmt.Printf("removed secret %s\n", name) + fmt.Printf("removed secret %s\n", secret) } + return errors.Join(errs...) } diff --git a/cmd/docker-mcp/secret-management/secret/secret_test.go b/cmd/docker-mcp/secret-management/secret/secret_test.go index 24577d8c..3a2434a2 100644 --- a/cmd/docker-mcp/secret-management/secret/secret_test.go +++ b/cmd/docker-mcp/secret-management/secret/secret_test.go @@ -1,7 +1,6 @@ package secret import ( - "errors" "testing" "github.com/stretchr/testify/assert" @@ -36,28 +35,3 @@ func TestIsDirectValueProvider(t *testing.T) { assert.True(t, isDirectValueProvider(Credstore)) assert.False(t, isDirectValueProvider("oauth/github")) } - -func TestIsValidProvider(t *testing.T) { - // Valid providers - assert.True(t, IsValidProvider("")) - assert.True(t, IsValidProvider(Credstore)) - assert.True(t, IsValidProvider("oauth/github")) - assert.True(t, IsValidProvider("oauth/google")) - - // Invalid providers - assert.False(t, IsValidProvider("invalid")) - assert.False(t, IsValidProvider("oauth")) -} - -func TestIsErrDecryption(t *testing.T) { - // Test decryption error detection - decryptErr := errors.New("gpg: decryption failed: No secret key") - assert.True(t, isErrDecryption(decryptErr)) - - // Test other errors - otherErr := errors.New("some other error") - assert.False(t, isErrDecryption(otherErr)) - - // Test nil - assert.False(t, isErrDecryption(nil)) -} diff --git a/cmd/docker-mcp/secret-management/secret/set.go b/cmd/docker-mcp/secret-management/secret/set.go index db5847fa..ba2523a4 100644 --- a/cmd/docker-mcp/secret-management/secret/set.go +++ b/cmd/docker-mcp/secret-management/secret/set.go @@ -6,15 +6,19 @@ import ( "os" "strings" - "github.com/docker/mcp-gateway/pkg/desktop" "github.com/docker/mcp-gateway/pkg/tui" ) const ( + // Specify to use the credential store provider. + // + // Deprecated: Not used. Credstore = "credstore" ) type SetOpts struct { + // Provider sets the store provider + // Deprecated: this field will be removed in the next release Provider string } @@ -36,9 +40,6 @@ type Secret struct { } func ParseArg(arg string, opts SetOpts) (*Secret, error) { - if !isDirectValueProvider(opts.Provider) && strings.Contains(arg, "=") { - return nil, fmt.Errorf("provider cannot be used with key=value pairs: %s", arg) - } if !isDirectValueProvider(opts.Provider) { return &Secret{key: arg, val: ""}, nil } @@ -54,28 +55,6 @@ func isDirectValueProvider(provider string) bool { } func Set(ctx context.Context, s Secret, opts SetOpts) error { - if opts.Provider == Credstore { - p := NewCredStoreProvider() - if err := p.SetSecret(s.key, s.val); err != nil { - return err - } - } - return desktop.NewSecretsClient().SetJfsSecret(ctx, desktop.Secret{ - Name: s.key, - Value: s.val, - Provider: opts.Provider, - }) -} - -func IsValidProvider(provider string) bool { - if provider == "" { - return true - } - if strings.HasPrefix(provider, "oauth/") { - return true - } - if provider == Credstore { - return true - } - return false + p := NewCredStoreProvider() + return p.SetSecret(ctx, s.key, s.val) } diff --git a/pkg/desktop/connection_other.go b/pkg/desktop/connection_other.go index 9c6533b9..afc9f3ae 100644 --- a/pkg/desktop/connection_other.go +++ b/pkg/desktop/connection_other.go @@ -16,10 +16,6 @@ func dialAuth(ctx context.Context) (net.Conn, error) { return dial(ctx, Paths().ToolsSocket) } -func dialSecrets(ctx context.Context) (net.Conn, error) { - return dial(ctx, Paths().JFSSocket) -} - func dial(ctx context.Context, path string) (net.Conn, error) { dialer := net.Dialer{} return dialer.DialContext(ctx, "unix", path) diff --git a/pkg/desktop/features.go b/pkg/desktop/features.go index f4839892..5f7ee085 100644 --- a/pkg/desktop/features.go +++ b/pkg/desktop/features.go @@ -4,7 +4,9 @@ import ( "context" "encoding/json" "errors" + "fmt" "os" + "os/exec" "github.com/PaesslerAG/jsonpath" ) @@ -36,6 +38,7 @@ func CheckFeatureIsEnabled(ctx context.Context, settingName string, label string //nolint:staticcheck return errors.New("Docker Desktop is not running") } + fmt.Fprintf(os.Stdout, "DD settings: %+v\n", settings) value, _ := jsonpath.Get("$.desktop."+settingName+".value", settings) if value == false { return errors.New("The \"" + label + "\" feature needs to be enabled in Docker Desktop Settings") @@ -44,6 +47,37 @@ func CheckFeatureIsEnabled(ctx context.Context, settingName string, label string return nil } +func GetVersion(ctx context.Context) (string, error) { + var result any + if err := ClientBackend.Get(ctx, "/versions", &result); err != nil { + return "", err + } + val, err := jsonpath.Get("$.appVersion", result) + if err != nil { + return "", err + } + version, ok := val.(string) + if !ok { + return "", errors.New("could not get docker desktop version") + } + return version, nil +} + +var ErrDockerPassUnsupported = errors.New("docker pass has not been installed") + +func CheckHasDockerPass(ctx context.Context) error { + err := exec.CommandContext(ctx, "docker", "pass").Run() + execStatus, ok := err.(*exec.ExitError) + if !ok { + return err + } + execStatus.ExitCode() + if execStatus.ExitCode() > 0 { + return ErrDockerPassUnsupported + } + return nil +} + func getAdminSettings() (map[string]any, error) { buf, err := os.ReadFile(Paths().AdminSettingPath) if err != nil { diff --git a/pkg/desktop/secrets.go b/pkg/desktop/secrets.go deleted file mode 100644 index b53befa7..00000000 --- a/pkg/desktop/secrets.go +++ /dev/null @@ -1,61 +0,0 @@ -package desktop - -import ( - "context" - "fmt" -) - -type StoredSecret struct { - Name string `json:"name"` - Provider string `json:"provider,omitempty"` -} - -type Secret struct { - Name string `json:"name"` - Provider string `json:"provider,omitempty"` - Value string `json:"value"` -} - -func NewSecretsClient() *Secrets { - return &Secrets{ - rawClient: newRawClient(dialSecrets), - } -} - -type Secrets struct { - rawClient *RawClient -} - -func (c *Secrets) DeleteJfsSecret(ctx context.Context, secret string) error { - AvoidResourceSaverMode(ctx) - - return c.rawClient.Delete(ctx, fmt.Sprintf("/secrets/%v", secret)) -} - -func (c *Secrets) GetJfsPolicy(ctx context.Context) (string, error) { - AvoidResourceSaverMode(ctx) - - var result string - err := c.rawClient.Get(ctx, "/policy", &result) - return result, err -} - -func (c *Secrets) ListJfsSecrets(ctx context.Context) ([]StoredSecret, error) { - AvoidResourceSaverMode(ctx) - - var result []StoredSecret - err := c.rawClient.Get(ctx, "/secrets", &result) - return result, err -} - -func (c *Secrets) SetJfsPolicy(ctx context.Context, body string) error { - AvoidResourceSaverMode(ctx) - - return c.rawClient.Post(ctx, "/policy", body, nil) -} - -func (c *Secrets) SetJfsSecret(ctx context.Context, secret Secret) error { - AvoidResourceSaverMode(ctx) - - return c.rawClient.Post(ctx, "/secrets", secret, nil) -} diff --git a/pkg/docker/client.go b/pkg/docker/client.go index dda22fcc..d861d476 100644 --- a/pkg/docker/client.go +++ b/pkg/docker/client.go @@ -34,7 +34,6 @@ type Client interface { RemoveNetwork(ctx context.Context, name string) error ConnectNetwork(ctx context.Context, networkName, container, hostname string) error InspectVolume(ctx context.Context, name string) (volume.Volume, error) - ReadSecrets(ctx context.Context, names []string, lenient bool) (map[string]string, error) } type dockerClient struct { diff --git a/pkg/docker/secrets.go b/pkg/docker/secrets.go deleted file mode 100644 index e52bf49b..00000000 --- a/pkg/docker/secrets.go +++ /dev/null @@ -1,112 +0,0 @@ -package docker - -import ( - "context" - "encoding/json" - "fmt" - "maps" - "os" - "os/exec" - "runtime" - "strings" - - "github.com/docker/mcp-gateway/pkg/desktop" - "github.com/docker/mcp-gateway/pkg/log" -) - -const jcatImage = "docker/jcat@sha256:76719466e8b99a65dd1d37d9ab94108851f009f0f687dce7ff8a6fc90575c4d4" - -func (c *dockerClient) ReadSecrets(ctx context.Context, names []string, lenient bool) (map[string]string, error) { - if len(names) == 0 { - return map[string]string{}, nil // No secrets to read - } - - if err := c.PullImage(ctx, jcatImage); err != nil { - return nil, err - } - - if lenient && len(names) == 1 { - // If there's only one secret, read it directly and fall back to one-by-one reading if needed - return c.readSecretsOneByOneOptional(ctx, names) - } - - secrets, err := c.readSecrets(ctx, names) - if err != nil { - if lenient && strings.Contains(err.Error(), "no such secret") { - return c.readSecretsOneByOneOptional(ctx, names) - } - - return nil, fmt.Errorf("reading secrets %w", err) - } - - return secrets, nil -} - -func (c *dockerClient) readSecrets(ctx context.Context, names []string) (map[string]string, error) { - flags := []string{"--network=none", "--pull=never"} - var command []string - - for i, name := range names { - file := fmt.Sprintf("/.s%d", i) - flags = append(flags, "-l", "x-secret:"+name+"="+file) - command = append(command, file) - } - - var args []string - - // When running in cloud mode but not in a container, we might be able to use Docker Desktop's special socket - // to read the secrets. - if os.Getenv("DOCKER_MCP_IN_CONTAINER") != "1" { - var path string - switch runtime.GOOS { - case "windows": - path = "npipe://" + strings.ReplaceAll(desktop.Paths().RawDockerSocket, `\`, `/`) - default: - // On Darwin/Linux, we do it only if the socket actually exists. - if _, err := os.Stat(desktop.Paths().RawDockerSocket); err == nil { - path = "unix://" + desktop.Paths().RawDockerSocket - } - } - if path != "" { - args = append(args, "-H", path) - } - } - args = append(args, "run", "--rm") - args = append(args, flags...) - args = append(args, jcatImage) - args = append(args, command...) - buf, err := exec.CommandContext(ctx, "docker", args...).CombinedOutput() - if err != nil { - return nil, fmt.Errorf("reading secrets %w: %s", err, string(buf)) - } - - var list []string - if err := json.Unmarshal(buf, &list); err != nil { - return nil, err - } - - values := map[string]string{} - for i := range names { - values[names[i]] = list[i] - } - - return values, nil -} - -// readSecretsOneByOne reads secrets one by one, which is useful for lenient mode. -// It's slower but can handle cases where some secrets might not exist. -func (c *dockerClient) readSecretsOneByOneOptional(ctx context.Context, names []string) (map[string]string, error) { - secrets := map[string]string{} - - for _, name := range names { - values, err := c.readSecrets(ctx, []string{name}) - if err != nil { - log.Logf("couldn't read secret %s: %v", name, err) - continue - } - - maps.Copy(secrets, values) - } - - return secrets, nil -} diff --git a/pkg/gateway/configuration.go b/pkg/gateway/configuration.go index dff91af2..780f9702 100644 --- a/pkg/gateway/configuration.go +++ b/pkg/gateway/configuration.go @@ -33,7 +33,6 @@ type Configuration struct { servers map[string]catalog.Server config map[string]map[string]any tools config.ToolsConfig - secrets map[string]string SessionName string } @@ -84,7 +83,6 @@ func (c *Configuration) Find(serverName string) (*catalog.ServerConfig, *map[str Config: map[string]any{ oci.CanonicalizeServerName(serverName): c.config[oci.CanonicalizeServerName(serverName)], }, - Secrets: c.secrets, // TODO: we could keep just the secrets for this server }, nil, true } @@ -155,7 +153,6 @@ func (c *WorkingSetConfiguration) readOnce(ctx context.Context) (Configuration, servers: servers, config: make(map[string]map[string]any), tools: config.ToolsConfig{}, - secrets: make(map[string]string), }, nil } @@ -439,37 +436,12 @@ func (c *FileBasedConfiguration) readOnce(ctx context.Context) (Configuration, e return Configuration{}, fmt.Errorf("reading tools: %w", err) } - var secrets map[string]string - if c.SecretsPath == "docker-desktop" { - secrets, err = c.readDockerDesktopSecrets(ctx, servers, serverNames) - if err != nil { - return Configuration{}, fmt.Errorf("reading MCP Toolkit's secrets: %w", err) - } - } else { - // Unless SecretsPath is only `docker-desktop`, we don't fail if secrets can't be read. - // It's ok for the MCP tookit's to not be available (in Cloud Run, for example). - // It's ok for secrets .env file to not exist. - var err error - for secretPath := range strings.SplitSeq(c.SecretsPath, ":") { - if secretPath == "docker-desktop" { - secrets, err = c.readDockerDesktopSecrets(ctx, servers, serverNames) - } else { - secrets, err = c.readSecretsFromFile(ctx, secretPath) - } - - if err == nil { - break - } - } - } - log.Log("- Configuration read in", time.Since(start)) return Configuration{ serverNames: serverNames, servers: servers, config: serversConfig, tools: serverToolsConfig, - secrets: secrets, }, nil } @@ -587,42 +559,6 @@ func (c *FileBasedConfiguration) readToolsConfig(ctx context.Context) (config.To return mergedToolsConfig, nil } -func (c *FileBasedConfiguration) readDockerDesktopSecrets(ctx context.Context, servers map[string]catalog.Server, serverNames []string) (map[string]string, error) { - // Use a map to deduplicate secret names - uniqueSecretNames := make(map[string]struct{}) - - for _, serverName := range serverNames { - serverName := strings.TrimSpace(serverName) - - serverSpec, ok := servers[serverName] - if !ok { - continue - } - - for _, s := range serverSpec.Secrets { - uniqueSecretNames[s.Name] = struct{}{} - } - } - - if len(uniqueSecretNames) == 0 { - return map[string]string{}, nil - } - - // Convert map keys to slice - var secretNames []string - for name := range uniqueSecretNames { - secretNames = append(secretNames, name) - } - - log.Log(" - Reading secrets", secretNames) - secretsByName, err := c.docker.ReadSecrets(ctx, secretNames, true) - if err != nil { - return nil, fmt.Errorf("finding secrets %s: %w", secretNames, err) - } - - return secretsByName, nil -} - func (c *FileBasedConfiguration) readSecretsFromFile(ctx context.Context, path string) (map[string]string, error) { secrets := map[string]string{}