Skip to content

Commit e33a18a

Browse files
committed
feat(pass): add --os-keychain flag to pass run
Lets `pass run` resolve `se://` references directly from the local OS keychain (the same store used by `pass set`/`pass get`) instead of going through the secrets-engine daemon. Useful when Docker Desktop is not running or when symmetric round-tripping is desired. Implemented as a `secrets.Resolver` adapter over `store.Store.Filter`, so `resolveEnv`/`resolveRef` semantics (pattern parsing, single-match enforcement, ErrNotFound) stay identical between daemon and keychain modes. Also moves the `run` long description into an embedded markdown file (`run_long.md`), matching the existing pattern for examples.
1 parent f0417af commit e33a18a

5 files changed

Lines changed: 67 additions & 20 deletions

File tree

plugins/pass/command.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func Root(ctx context.Context, s store.Store, info commands.VersionInfo) *cobra.
8888
cmd.AddCommand(wrapRunEWithSpan(commands.ListCommand(s)))
8989
cmd.AddCommand(wrapRunEWithSpan(commands.RmCommand(s)))
9090
cmd.AddCommand(wrapRunEWithSpan(commands.GetCommand(s)))
91-
cmd.AddCommand(wrapRunEWithSpan(commands.RunCommand()))
91+
cmd.AddCommand(wrapRunEWithSpan(commands.RunCommand(s)))
9292
cmd.AddCommand(commands.VersionCommand(info))
9393

9494
return cmd

plugins/pass/commands/run.go

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"github.com/spf13/cobra"
3131

3232
"github.com/docker/secrets-engine/client"
33+
"github.com/docker/secrets-engine/store"
3334
"github.com/docker/secrets-engine/x/api"
3435
"github.com/docker/secrets-engine/x/secrets"
3536
)
@@ -52,24 +53,20 @@ func (e *ExitCodeError) Error() string {
5253
//go:embed run_example.md
5354
var runExample string
5455

56+
//go:embed run_long.md
57+
var runLong string
58+
5559
type runOpts struct {
56-
envFiles []string
60+
envFiles []string
61+
osKeychain bool
5762
}
5863

59-
func RunCommand() *cobra.Command {
64+
func RunCommand(s store.Store) *cobra.Command {
6065
opts := runOpts{}
6166
cmd := &cobra.Command{
62-
Use: "run -- CMD [ARGS...]",
63-
Short: "Run a command with `se://` environment references resolved.",
64-
Long: "Scans the current environment (plus any `--env-file` inputs) for variables\n" +
65-
"whose value is exactly `se://<ID|pattern>`. Each reference is resolved through the\n" +
66-
"secrets-engine daemon and the resolved value is passed to the child process.\n" +
67-
"The child inherits stdin, stdout, and stderr.\n" +
68-
"\n" +
69-
"Requires the secrets-engine daemon (Docker Desktop) to be running.\n" +
70-
"\n" +
71-
"If any reference cannot be resolved, the command fails before the child is\n" +
72-
"started and exits non-zero.",
67+
Use: "run -- CMD [ARGS...]",
68+
Short: "Run a command with `se://` environment references resolved.",
69+
Long: strings.Trim(runLong, "\n"),
7370
Example: strings.Trim(runExample, "\n"),
7471
Args: cobra.MinimumNArgs(1),
7572
RunE: func(cmd *cobra.Command, args []string) error {
@@ -78,12 +75,18 @@ func RunCommand() *cobra.Command {
7875
return err
7976
}
8077

81-
c, err := client.New(client.WithSocketPath(api.DefaultSocketPath()))
82-
if err != nil {
83-
return err
78+
var r secrets.Resolver
79+
if opts.osKeychain {
80+
r = &storeResolver{s: s}
81+
} else {
82+
c, err := client.New(client.WithSocketPath(api.DefaultSocketPath()))
83+
if err != nil {
84+
return err
85+
}
86+
r = c
8487
}
8588

86-
env, err := resolveEnv(cmd.Context(), c, merged)
89+
env, err := resolveEnv(cmd.Context(), r, merged)
8790
if err != nil {
8891
return err
8992
}
@@ -139,9 +142,35 @@ func RunCommand() *cobra.Command {
139142
}
140143
cmd.Flags().StringArrayVar(&opts.envFiles, "env-file", nil,
141144
"Read environment variables from a dotenv-formatted file. Repeatable; later files override earlier files and the process environment.")
145+
cmd.Flags().BoolVar(&opts.osKeychain, "os-keychain", false,
146+
"Resolve `se://` references directly from the local OS keychain instead of the secrets-engine daemon.")
142147
return cmd
143148
}
144149

150+
type storeResolver struct {
151+
s store.Store
152+
}
153+
154+
func (r *storeResolver) GetSecrets(ctx context.Context, p secrets.Pattern) ([]secrets.Envelope, error) {
155+
m, err := r.s.Filter(ctx, p)
156+
if err != nil {
157+
return nil, err
158+
}
159+
out := make([]secrets.Envelope, 0, len(m))
160+
for id, sec := range m {
161+
val, err := sec.Marshal()
162+
if err != nil {
163+
return nil, err
164+
}
165+
out = append(out, secrets.Envelope{
166+
ID: id,
167+
Value: val,
168+
Metadata: sec.Metadata(),
169+
})
170+
}
171+
return out, nil
172+
}
173+
145174
// mergeEnv folds the process environment and any --env-file inputs into a
146175
// single deterministic KEY=VALUE slice. Precedence: process env first, then
147176
// each file in order; later entries override earlier ones.

plugins/pass/commands/run_example.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,11 @@ $ docker pass run --env-file .env -- ./my-binary
2121
```console
2222
$ docker pass run --env-file .env --env-file .env.local -- ./my-binary
2323
```
24+
25+
### Resolve directly from the local OS keychain (skip the daemon):
26+
27+
```console
28+
$ SE_TOKEN=se://gh-token docker pass run --os-keychain -- gh repo list
29+
```
30+
31+
References are read from the same store used by `docker pass set`/`docker pass get`, so the secrets-engine daemon does not need to be running.

plugins/pass/commands/run_long.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Scans the current environment (plus any `--env-file` inputs) for variables
2+
whose value is exactly `se://<ID|pattern>` and resolves each reference before
3+
launching the child process. The child inherits stdin, stdout, and stderr.
4+
5+
By default, references are resolved through the secrets-engine daemon (Docker
6+
Desktop must be running). Pass `--os-keychain` to resolve directly from the
7+
local OS keychain instead, with no daemon required.
8+
9+
If any reference cannot be resolved, the command fails before the child is
10+
started and exits non-zero.

plugins/pass/commands/run_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func runAsWrapper() {
8383
if err != nil {
8484
os.Exit(2)
8585
}
86-
cmd := RunCommand()
86+
cmd := RunCommand(nil)
8787
cmd.SetArgs([]string{exe})
8888
cmd.SetContext(context.Background())
8989
cmd.SilenceUsage = true
@@ -241,7 +241,7 @@ func TestRunCommand(t *testing.T) {
241241
require.NoError(t, err)
242242

243243
t.Run("no command given returns arg error", func(t *testing.T) {
244-
cmd := RunCommand()
244+
cmd := RunCommand(nil)
245245
cmd.SetArgs([]string{})
246246
cmd.SetContext(t.Context())
247247
cmd.SetOut(testWriter{t})

0 commit comments

Comments
 (0)