Skip to content

Commit 441a53c

Browse files
committed
feat(pass): add --force flag to set command
Adds a --force/-f flag to `docker pass set` that routes through store.Upsert instead of store.Save, allowing callers to overwrite an existing secret atomically on every platform. Without --force the default Save behavior is preserved (errors on macOS when the secret already exists; silently overwrites on Linux and Windows). Documents the platform-dependent overwrite semantics in the command's Long description.
1 parent 9a06652 commit 441a53c

2 files changed

Lines changed: 45 additions & 2 deletions

File tree

plugins/pass/command_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,33 @@ func Test_rootCommand(t *testing.T) {
135135
assert.ErrorIs(t, err, errInvalidID)
136136
assert.Equal(t, "Error: "+errInvalidID.Error()+"\n", out)
137137
})
138+
t.Run("--force overwrites existing secret", func(t *testing.T) {
139+
// Make Save return an error so the test fails if --force does not
140+
// route the call through Upsert.
141+
mock := teststore.NewMockStore(
142+
teststore.WithStore(map[store.ID]store.Secret{
143+
store.MustParseID("foo"): pass.NewPassValue([]byte("old")),
144+
}),
145+
teststore.WithStoreSaveErr(errors.New("save should not be called when --force is set")),
146+
)
147+
out, err := executeCommand(Root(t.Context(), mock, mockInfo), "set", "foo=new", "--force")
148+
assert.NoError(t, err)
149+
assert.Empty(t, out)
150+
s, err := mock.Get(t.Context(), secrets.MustParseID("foo"))
151+
require.NoError(t, err)
152+
impl, ok := s.(*pass.PassValue)
153+
require.True(t, ok)
154+
v, err := impl.Marshal()
155+
require.NoError(t, err)
156+
assert.Equal(t, "new", string(v))
157+
})
158+
t.Run("--force surfaces upsert error", func(t *testing.T) {
159+
errUpsert := errors.New("upsert error")
160+
mock := teststore.NewMockStore(teststore.WithStoreUpsertErr(errUpsert))
161+
out, err := executeCommand(Root(t.Context(), mock, mockInfo), "set", "foo=bar", "--force")
162+
assert.ErrorIs(t, errUpsert, err)
163+
assert.Equal(t, "Error: "+errUpsert.Error()+"\n", out)
164+
})
138165
})
139166
t.Run("list", func(t *testing.T) {
140167
t.Run("ok", func(t *testing.T) {

plugins/pass/commands/set.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,14 @@ docker pass set POSTGRES_PASSWORD=my-secret-password --metadata owner=alice --me
4242
4343
### Or pass a JSON payload with secret and metadata via STDIN:
4444
echo '{"secret":"my-secret-password","metadata":{"owner":"alice"}}' | docker pass set POSTGRES_PASSWORD
45+
46+
### Overwrite an existing secret:
47+
docker pass set POSTGRES_PASSWORD=new-secret-password --force
4548
`
4649

4750
type setOpts struct {
4851
metadata []string // raw "key=value" strings from --metadata flag
52+
force bool // if true, overwrite existing secret instead of erroring
4953
}
5054

5155
type stdinPayload struct {
@@ -59,8 +63,16 @@ func SetCommand(kc store.Store) *cobra.Command {
5963
Use: "set id[=value]",
6064
Aliases: []string{"store", "save"},
6165
Short: "Set a secret",
62-
Long: `Stores a secret in the local OS keychain. The secret value can be
63-
provided inline (NAME=VALUE) or piped via STDIN.`,
66+
Long: `Stores a secret in the local OS keychain. The secret value can be provided inline (NAME=VALUE) or piped via STDIN.
67+
68+
Behavior when a secret with the same id already exists is platform-dependent:
69+
- macOS (Keychain): the command fails with a duplicate-item error.
70+
- Linux (Secret Service) and Windows (Credential Manager): the existing
71+
value is silently overwritten.
72+
73+
Pass --force to overwrite an existing secret on every platform. With --force
74+
the secret is replaced atomically, so concurrent reads never observe a missing
75+
value.`,
6476
Example: strings.Trim(setExample, "\n"),
6577
Args: cobra.ExactArgs(1),
6678
RunE: func(cmd *cobra.Command, args []string) error {
@@ -106,11 +118,15 @@ provided inline (NAME=VALUE) or piped via STDIN.`,
106118
return err
107119
}
108120
}
121+
if opts.force {
122+
return kc.Upsert(cmd.Context(), id, pv)
123+
}
109124
return kc.Save(cmd.Context(), id, pv)
110125
},
111126
}
112127
flags := cmd.Flags()
113128
flags.StringArrayVar(&opts.metadata, "metadata", nil, "Non-sensitive key=value metadata (repeatable)")
129+
flags.BoolVarP(&opts.force, "force", "f", false, "Overwrite existing secret if it already exists")
114130
return cmd
115131
}
116132

0 commit comments

Comments
 (0)