Skip to content

Commit a79bd74

Browse files
committed
fix(secret): fail fast when Docker Desktop is not available
Signed-off-by: pnkcaht <samzoovsk19@gmail.com>
1 parent cc7998a commit a79bd74

File tree

1 file changed

+76
-6
lines changed
  • cmd/docker-mcp/secret-management/secret

1 file changed

+76
-6
lines changed

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

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import (
66
"os"
77
"strings"
88

9+
"github.com/docker/cli/cli/command"
910
"github.com/docker/mcp-gateway/pkg/desktop"
11+
dockerenv "github.com/docker/mcp-gateway/pkg/docker"
1012
"github.com/docker/mcp-gateway/pkg/tui"
1113
)
1214

@@ -19,9 +21,12 @@ type SetOpts struct {
1921
}
2022

2123
func MappingFromSTDIN(ctx context.Context, key string) (*Secret, error) {
24+
// Read the entire secret value from STDIN.
25+
// This allows piping values securely without exposing them
26+
// via command-line arguments or shell history.
2227
data, err := tui.ReadAllWithContext(ctx, os.Stdin)
2328
if err != nil {
24-
return nil, err
29+
return nil, fmt.Errorf("failed to read secret value from STDIN: %w", err)
2530
}
2631

2732
return &Secret{
@@ -30,36 +35,93 @@ func MappingFromSTDIN(ctx context.Context, key string) (*Secret, error) {
3035
}, nil
3136
}
3237

38+
// Secret represents a key/value pair used by the secret management commands.
39+
// The fields are intentionally unexported to avoid accidental exposure
40+
// outside of the secret management package.
3341
type Secret struct {
3442
key string
3543
val string
3644
}
3745

3846
func ParseArg(arg string, opts SetOpts) (*Secret, error) {
39-
if !isDirectValueProvider(opts.Provider) && strings.Contains(arg, "=") {
40-
return nil, fmt.Errorf("provider cannot be used with key=value pairs: %s", arg)
47+
// Direct-value providers expect secrets in the form key=value.
48+
// Non-direct providers only accept the key, with the value
49+
// being resolved by the provider itself.
50+
directProvider := isDirectValueProvider(opts.Provider)
51+
52+
// Reject key=value syntax when the provider does not accept direct values.
53+
if !directProvider && strings.Contains(arg, "=") {
54+
return nil, fmt.Errorf(
55+
"provider %q does not support key=value syntax: %s",
56+
opts.Provider, arg,
57+
)
4158
}
42-
if !isDirectValueProvider(opts.Provider) {
59+
60+
// For non-direct providers, only the key is required.
61+
if !directProvider {
4362
return &Secret{key: arg, val: ""}, nil
4463
}
64+
65+
// Split key=value input for direct providers.
4566
parts := strings.SplitN(arg, "=", 2)
4667
if len(parts) != 2 {
47-
return nil, fmt.Errorf("no key=value pair: %s", arg)
68+
return nil, fmt.Errorf("expected key=value pair, got: %s", arg)
4869
}
49-
return &Secret{key: parts[0], val: parts[1]}, nil
70+
71+
return &Secret{
72+
key: parts[0],
73+
val: parts[1],
74+
}, nil
5075
}
5176

5277
func isDirectValueProvider(provider string) bool {
78+
// Direct-value providers receive the secret value directly
79+
// from the CLI input (key=value).
80+
// Currently supported direct providers:
81+
// - empty provider (default)
82+
// - credstore
5383
return provider == "" || provider == Credstore
5484
}
5585

5686
func Set(ctx context.Context, s Secret, opts SetOpts) error {
87+
// Handle the credstore provider first.
88+
// This provider does not depend on Docker Desktop or JFS.
5789
if opts.Provider == Credstore {
5890
p := NewCredStoreProvider()
5991
if err := p.SetSecret(s.key, s.val); err != nil {
6092
return err
6193
}
6294
}
95+
96+
// Initialize Docker CLI to detect the runtime environment.
97+
// This is required to determine whether Docker Desktop is available.
98+
dockerCli, err := command.NewDockerCli()
99+
if err != nil {
100+
return fmt.Errorf("failed to create Docker CLI: %w", err)
101+
}
102+
103+
if err := dockerCli.Initialize(nil); err != nil {
104+
return fmt.Errorf("failed to initialize Docker CLI: %w", err)
105+
}
106+
107+
// Detect if we are running on Docker Engine (non-Desktop).
108+
// On headless Docker Engine setups, Docker Desktop services
109+
// (including the JFS secrets backend) are not available.
110+
isCE, err := dockerenv.RunningInDockerCE(ctx, dockerCli)
111+
if err != nil {
112+
return err
113+
}
114+
115+
if isCE {
116+
return fmt.Errorf(
117+
"Docker Desktop is not available. " +
118+
"`docker mcp secret set` requires Docker Desktop to manage secrets. " +
119+
"If you are running Docker Engine in a headless environment, " +
120+
"use --secrets with a .env file instead.",
121+
)
122+
}
123+
124+
// Docker Desktop is available: proceed with the JFS-backed secrets client.
63125
return desktop.NewSecretsClient().SetJfsSecret(ctx, desktop.Secret{
64126
Name: s.key,
65127
Value: s.val,
@@ -68,14 +130,22 @@ func Set(ctx context.Context, s Secret, opts SetOpts) error {
68130
}
69131

70132
func IsValidProvider(provider string) bool {
133+
// An empty provider is valid and represents the default behavior.
71134
if provider == "" {
72135
return true
73136
}
137+
138+
// OAuth-based providers are identified by the "oauth/" prefix.
139+
// The concrete provider implementation is resolved at runtime.
74140
if strings.HasPrefix(provider, "oauth/") {
75141
return true
76142
}
143+
144+
// Credstore is a built-in provider that stores secrets locally.
77145
if provider == Credstore {
78146
return true
79147
}
148+
149+
// Any other provider value is considered invalid.
80150
return false
81151
}

0 commit comments

Comments
 (0)