diff --git a/plugins/pass/command.go b/plugins/pass/command.go index c70241b5..581216f2 100644 --- a/plugins/pass/command.go +++ b/plugins/pass/command.go @@ -88,7 +88,7 @@ func Root(ctx context.Context, s store.Store, info commands.VersionInfo) *cobra. cmd.AddCommand(wrapRunEWithSpan(commands.ListCommand(s))) cmd.AddCommand(wrapRunEWithSpan(commands.RmCommand(s))) cmd.AddCommand(wrapRunEWithSpan(commands.GetCommand(s))) - cmd.AddCommand(wrapRunEWithSpan(commands.RunCommand())) + cmd.AddCommand(wrapRunEWithSpan(commands.RunCommand(s))) cmd.AddCommand(commands.VersionCommand(info)) return cmd diff --git a/plugins/pass/commands/run.go b/plugins/pass/commands/run.go index ec615a93..c547ce54 100644 --- a/plugins/pass/commands/run.go +++ b/plugins/pass/commands/run.go @@ -30,6 +30,7 @@ import ( "github.com/spf13/cobra" "github.com/docker/secrets-engine/client" + "github.com/docker/secrets-engine/store" "github.com/docker/secrets-engine/x/api" "github.com/docker/secrets-engine/x/secrets" ) @@ -52,24 +53,20 @@ func (e *ExitCodeError) Error() string { //go:embed run_example.md var runExample string +//go:embed run_long.md +var runLong string + type runOpts struct { - envFiles []string + envFiles []string + osKeychain bool } -func RunCommand() *cobra.Command { +func RunCommand(s store.Store) *cobra.Command { opts := runOpts{} cmd := &cobra.Command{ - Use: "run -- CMD [ARGS...]", - Short: "Run a command with `se://` environment references resolved.", - Long: "Scans the current environment (plus any `--env-file` inputs) for variables\n" + - "whose value is exactly `se://`. Each reference is resolved through the\n" + - "secrets-engine daemon and the resolved value is passed to the child process.\n" + - "The child inherits stdin, stdout, and stderr.\n" + - "\n" + - "Requires the secrets-engine daemon (Docker Desktop) to be running.\n" + - "\n" + - "If any reference cannot be resolved, the command fails before the child is\n" + - "started and exits non-zero.", + Use: "run -- CMD [ARGS...]", + Short: "Run a command with `se://` environment references resolved.", + Long: strings.Trim(runLong, "\n"), Example: strings.Trim(runExample, "\n"), Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { @@ -78,12 +75,18 @@ func RunCommand() *cobra.Command { return err } - c, err := client.New(client.WithSocketPath(api.DefaultSocketPath())) - if err != nil { - return err + var r secrets.Resolver + if opts.osKeychain { + r = &storeResolver{s: s} + } else { + c, err := client.New(client.WithSocketPath(api.DefaultSocketPath())) + if err != nil { + return err + } + r = c } - env, err := resolveEnv(cmd.Context(), c, merged) + env, err := resolveEnv(cmd.Context(), r, merged) if err != nil { return err } @@ -139,9 +142,35 @@ func RunCommand() *cobra.Command { } cmd.Flags().StringArrayVar(&opts.envFiles, "env-file", nil, "Read environment variables from a dotenv-formatted file. Repeatable; later files override earlier files and the process environment.") + cmd.Flags().BoolVar(&opts.osKeychain, "os-keychain", false, + "Resolve `se://` references directly from the local OS keychain instead of the secrets-engine daemon.") return cmd } +type storeResolver struct { + s store.Store +} + +func (r *storeResolver) GetSecrets(ctx context.Context, p secrets.Pattern) ([]secrets.Envelope, error) { + m, err := r.s.Filter(ctx, p) + if err != nil { + return nil, err + } + out := make([]secrets.Envelope, 0, len(m)) + for id, sec := range m { + val, err := sec.Marshal() + if err != nil { + return nil, err + } + out = append(out, secrets.Envelope{ + ID: id, + Value: val, + Metadata: sec.Metadata(), + }) + } + return out, nil +} + // mergeEnv folds the process environment and any --env-file inputs into a // single deterministic KEY=VALUE slice. Precedence: process env first, then // each file in order; later entries override earlier ones. diff --git a/plugins/pass/commands/run_example.md b/plugins/pass/commands/run_example.md index dc969034..1e1ed5aa 100644 --- a/plugins/pass/commands/run_example.md +++ b/plugins/pass/commands/run_example.md @@ -21,3 +21,11 @@ $ docker pass run --env-file .env -- ./my-binary ```console $ docker pass run --env-file .env --env-file .env.local -- ./my-binary ``` + +### Resolve directly from the local OS keychain (skip the daemon): + +```console +$ SE_TOKEN=se://gh-token docker pass run --os-keychain -- gh repo list +``` + +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. diff --git a/plugins/pass/commands/run_long.md b/plugins/pass/commands/run_long.md new file mode 100644 index 00000000..3f6e84fc --- /dev/null +++ b/plugins/pass/commands/run_long.md @@ -0,0 +1,10 @@ +Scans the current environment (plus any `--env-file` inputs) for variables +whose value is exactly `se://` and resolves each reference before +launching the child process. The child inherits stdin, stdout, and stderr. + +By default, references are resolved through the secrets-engine daemon (Docker +Desktop must be running). Pass `--os-keychain` to resolve directly from the +local OS keychain instead, with no daemon required. + +If any reference cannot be resolved, the command fails before the child is +started and exits non-zero. diff --git a/plugins/pass/commands/run_test.go b/plugins/pass/commands/run_test.go index f0ef069a..7fc23db6 100644 --- a/plugins/pass/commands/run_test.go +++ b/plugins/pass/commands/run_test.go @@ -83,7 +83,7 @@ func runAsWrapper() { if err != nil { os.Exit(2) } - cmd := RunCommand() + cmd := RunCommand(nil) cmd.SetArgs([]string{exe}) cmd.SetContext(context.Background()) cmd.SilenceUsage = true @@ -241,7 +241,7 @@ func TestRunCommand(t *testing.T) { require.NoError(t, err) t.Run("no command given returns arg error", func(t *testing.T) { - cmd := RunCommand() + cmd := RunCommand(nil) cmd.SetArgs([]string{}) cmd.SetContext(t.Context()) cmd.SetOut(testWriter{t})