Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 0 additions & 32 deletions cmd/docker-mcp/backup/dump.go
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get rid of the entire backup package. It's superseded by profiles and secrets and now that the policy dump and set commands are gone, can we verify whether this entire package can be removed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, I think we are safe to remove it. Could be another PR though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good, will address in a follow-up PR

Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)
Expand Down
34 changes: 1 addition & 33 deletions cmd/docker-mcp/backup/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ 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 {
func Restore(_ context.Context, backupData []byte) error {
var backup Backup
if err := json.Unmarshal(backupData, &backup); err != nil {
return err
Expand Down Expand Up @@ -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
}
4 changes: 0 additions & 4 deletions cmd/docker-mcp/backup/types.go
Original file line number Diff line number Diff line change
@@ -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"`
}
58 changes: 0 additions & 58 deletions cmd/docker-mcp/commands/policy.go

This file was deleted.

10 changes: 8 additions & 2 deletions cmd/docker-mcp/commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ func Root(ctx context.Context, cwd string, dockerCli command.Cli) *cobra.Command
return err
}

// Note: Using PersistentPreRunE in secretCommand would override this parent hook
if isSubcommandOf(cmd, []string{"secret"}) {
if err := desktop.CheckHasDockerPass(cmd.Context()); err != nil {
return err
}
}

if os.Getenv("DOCKER_MCP_IN_CONTAINER") != "1" {
if isWorkingSetsFeatureEnabled(dockerCli) {
if isSubcommandOf(cmd, []string{"catalog-next", "catalog", "profile"}) {
Expand Down Expand Up @@ -94,9 +101,8 @@ 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(secretCommand())
cmd.AddCommand(serverCommand(dockerClient, dockerCli))
cmd.AddCommand(toolsCommand(dockerClient, dockerCli))
cmd.AddCommand(versionCommand())
Expand Down
118 changes: 74 additions & 44 deletions cmd/docker-mcp/commands/secret.go
Original file line number Diff line number Diff line change
@@ -1,91 +1,141 @@
package commands

import (
"encoding/json"
"errors"
"fmt"
"slices"
"strings"

"github.com/spf13/cobra"

"github.com/docker/mcp-gateway/cmd/docker-mcp/secret-management/formatting"
"github.com/docker/mcp-gateway/cmd/docker-mcp/secret-management/secret"
"github.com/docker/mcp-gateway/pkg/docker"
)

const setSecretExample = `
### Use secrets for postgres password with default policy

> docker mcp secret set POSTGRES_PASSWORD=my-secret-password
> docker run -d -l x-secret:POSTGRES_PASSWORD=/pwd.txt -e POSTGRES_PASSWORD_FILE=/pwd.txt -p 5432 postgres
> docker mcp secret set postgres_password=my-secret-password

Inject the secret by querying by ID
> docker run -d -e POSTGRES_PASSWORD=se://docker/mcp/generic/postgres_password -p 5432 postgres

Another way to inject secrets would be to use a pattern.
> docker run -d -e POSTGRES_PASSWORD=se://**/postgres_password -p 5432 postgres

### Pass the secret via STDIN

> echo my-secret-password > pwd.txt
> cat pwd.txt | docker mcp secret set POSTGRES_PASSWORD
`

func secretCommand(docker docker.Client) *cobra.Command {
func secretCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "secret",
Short: "Manage secrets",
Short: "Manage secrets in the local OS Keychain",
Example: strings.Trim(setSecretExample, "\n"),
}
cmd.AddCommand(rmSecretCommand())
cmd.AddCommand(listSecretCommand())
cmd.AddCommand(setSecretCommand())
cmd.AddCommand(exportSecretCommand(docker))
return cmd
}

func rmSecretCommand() *cobra.Command {
var opts secret.RmOpts
var all bool
cmd := &cobra.Command{
Use: "rm name1 name2 ...",
Short: "Remove secrets from Docker Desktop's secret store",
Short: "Remove secrets from the local OS Keychain",
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateRmArgs(args, opts); err != nil {
if err := validateRmArgs(args, all); err != nil {
return err
}
return secret.Remove(cmd.Context(), args, opts)

ids := slices.Clone(args)
if all {
var err error
ids, err = secret.List(cmd.Context())
if err != nil {
return err
}
}

var errs []error
for _, s := range ids {
errs = append(errs, secret.DeleteSecret(cmd.Context(), s))
}
return errors.Join(errs...)
},
}
flags := cmd.Flags()
flags.BoolVar(&opts.All, "all", false, "Remove all secrets")
flags.BoolVar(&all, "all", false, "Remove all secrets")
return cmd
}

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

func listSecretCommand() *cobra.Command {
var opts secret.ListOptions
var outJSON bool
cmd := &cobra.Command{
Use: "ls",
Short: "List all secret names in Docker Desktop's secret store",
Short: "List all secrets from the local OS Keychain as well as any active Secrets Engine provider",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
return secret.List(cmd.Context(), opts)
// query the Secrets Engine instead to get all the secrets from
// all active providers.
l, err := secret.GetSecrets(cmd.Context())
if err != nil {
return err
}
if outJSON {
type secretListItem struct {
Name string `json:"name"`
Provider string `json:"provider,omitempty"`
}
output := make([]secretListItem, 0, len(l))
for _, env := range l {
output = append(output, secretListItem{
Name: secret.StripNamespace(env.ID),
Provider: env.Provider,
})
}
if len(output) == 0 {
output = []secretListItem{} // Guarantee empty list (instead of displaying null)
}
jsonData, err := json.MarshalIndent(output, "", " ")
if err != nil {
return err
}
fmt.Println(string(jsonData))
return nil
}
var rows [][]string
for _, v := range l {
rows = append(rows, []string{v.ID, v.Provider})
}
formatting.PrettyPrintTable(rows, []int{40, 120})
return nil
},
}
flags := cmd.Flags()
flags.BoolVar(&opts.JSON, "json", false, "Print as JSON.")
flags.BoolVar(&outJSON, "json", false, "Print as JSON.")
return cmd
}

func setSecretCommand() *cobra.Command {
opts := &secret.SetOpts{}
cmd := &cobra.Command{
Use: "set key[=value]",
Short: "Set a secret in Docker Desktop's secret store",
Short: "Set a secret in the local OS Keychain",
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)
Expand All @@ -105,30 +155,10 @@ func setSecretCommand() *cobra.Command {
}
flags := cmd.Flags()
flags.StringVar(&opts.Provider, "provider", "", "Supported: credstore, oauth/<provider>")
_ = flags.MarkDeprecated("provider", "all secrets now stored via docker pass in OS Keychain")
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
},
}
func isNotImplicitReadFromStdinSyntax(args []string, _ secret.SetOpts) bool {
return strings.Contains(args[0], "=") || len(args) > 1
}
Loading
Loading