Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

### Enhancements:

- feat(kvstoreentry/delete): Add support for multiple-key deletion using a key prefix. ([#XXX](https://github.com/fastly/cli/pull/XXX))

### Dependencies:

## [v15.2.0](https://github.com/fastly/cli/releases/tag/v15.2.0) (2026-06-10)
Expand Down
2 changes: 1 addition & 1 deletion pkg/commands/kvstore/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func (c *DeleteCommand) Exec(in io.Reader, out io.Writer) error {
PoolSize: c.poolSize,
StoreID: c.Input.StoreID,
}
if err := dc.DeleteAllKeys(out); err != nil {
if err := dc.DeleteMultipleKeys(out, ""); err != nil {
return err
}
text.Break(out)
Expand Down
44 changes: 34 additions & 10 deletions pkg/commands/kvstoreentry/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ const DeleteKeysMaxErrors int = 100
type DeleteCommand struct {
argparser.Base
argparser.JSONOutput
key argparser.OptionalString
key argparser.OptionalString
prefix argparser.OptionalString

// NOTE: Public fields can be set via `kv-store delete`.
DeleteAll bool
Expand All @@ -46,7 +47,7 @@ func NewDeleteCommand(parent argparser.Registerer, g *global.Data) *DeleteComman
Globals: g,
},
}
c.CmdClause = parent.Command("delete", "Delete a key")
c.CmdClause = parent.Command("delete", "Delete one, multiple, or all keys")

// Required.
c.CmdClause.Flag("store-id", "Store ID").Short('s').Required().StringVar(&c.StoreID)
Expand All @@ -58,6 +59,7 @@ func NewDeleteCommand(parent argparser.Registerer, g *global.Data) *DeleteComman
c.CmdClause.Flag("if-generation-match", "Value which must match the current generation marker in an item for a delete operation to proceed").StringVar(&c.IfGenerationMatch)
c.RegisterFlagBool(c.JSONFlag()) // --json
c.CmdClause.Flag("key", "Key name").Short('k').Action(c.key.Set).StringVar(&c.key.Value)
c.CmdClause.Flag("prefix", "Delete items whose keys match this prefix").Action(c.prefix.Set).StringVar(&c.prefix.Value)
c.CmdClause.Flag("max-errors", "The number of errors to accept before stopping (ignored when set without the --all flag)").Default(strconv.Itoa(DeleteKeysMaxErrors)).Short('m').IntVar(&c.MaxErrors)

return &c
Expand All @@ -69,14 +71,20 @@ func (c *DeleteCommand) Exec(in io.Reader, out io.Writer) error {
return fsterr.ErrInvalidVerboseJSONCombo
}
// TODO: Support --json for bulk deletions.
if c.DeleteAll && c.JSONOutput.Enabled {
return fsterr.ErrInvalidDeleteAllJSONKeyCombo
if (c.DeleteAll || c.prefix.WasSet) && c.JSONOutput.Enabled {
return fsterr.ErrInvalidDeleteMultipleJSONKeyCombo
}
if c.DeleteAll && c.key.WasSet {
return fsterr.ErrInvalidDeleteAllKeyCombo
return fsterr.ErrInvalidDeleteMultipleKeyCombo
}
if !c.DeleteAll && !c.key.WasSet {
return fsterr.ErrMissingDeleteAllKeyCombo
if c.DeleteAll && c.prefix.WasSet {
return fsterr.ErrInvalidDeleteMultipleKeyCombo
}
if c.prefix.WasSet && c.key.WasSet {
return fsterr.ErrInvalidDeleteMultipleKeyCombo
}
if !c.DeleteAll && !c.prefix.WasSet && !c.key.WasSet {
return fsterr.ErrMissingDeleteMultipleKeyCombo
}

if c.DeleteAll {
Expand All @@ -91,7 +99,22 @@ func (c *DeleteCommand) Exec(in io.Reader, out io.Writer) error {
}
text.Break(out)
}
return c.DeleteAllKeys(out)
return c.DeleteMultipleKeys(out, "")
}

if c.prefix.WasSet {
if !c.Globals.Flags.AutoYes && !c.Globals.Flags.NonInteractive {
text.Warning(out, "This may delete MANY entries from your store!\n\n")
cont, err := text.AskYesNo(out, "Are you sure you want to continue? [y/N]: ", in)
if err != nil {
return err
}
if !cont {
return nil
}
text.Break(out)
}
return c.DeleteMultipleKeys(out, c.prefix.Value)
}

input := fastly.DeleteKVStoreKeyInput{
Expand Down Expand Up @@ -133,9 +156,9 @@ func (c *DeleteCommand) Exec(in io.Reader, out io.Writer) error {
return nil
}

// DeleteAllKeys deletes all keys within the specified KV Store.
// DeleteMultipleKeys deletes multiple keys from the specified KV Store.
// NOTE: It's a public method as it can be called via `kv-store delete --all`.
func (c *DeleteCommand) DeleteAllKeys(out io.Writer) error {
func (c *DeleteCommand) DeleteMultipleKeys(out io.Writer, prefix string) error {
spinnerMessage := "Deleting keys"
var spinner text.Spinner

Expand All @@ -152,6 +175,7 @@ func (c *DeleteCommand) DeleteAllKeys(out io.Writer) error {

p := c.Globals.APIClient.NewListKVStoreKeysPaginator(context.TODO(), &fastly.ListKVStoreKeysInput{
StoreID: c.StoreID,
Prefix: prefix,
})

errorsCh := make(chan string, c.MaxErrors)
Expand Down
52 changes: 48 additions & 4 deletions pkg/commands/kvstoreentry/kvstoreentry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,15 +203,27 @@ func TestDeleteCommand(t *testing.T) {
},
{
Args: "--store-id " + storeID,
WantError: "invalid command, neither --all or --key provided",
WantError: "invalid command, none of --all, --prefix, or --key provided",
},
{
Args: "--json --all --store-id " + storeID,
WantError: "invalid flag combination, --all and --json",
WantError: "invalid flag combination, --all/--prefix and --json",
},
{
Args: "--key a-key --all --store-id " + storeID,
WantError: "invalid flag combination, --all and --key",
Args: "--json --prefix a-prefix --store-id " + storeID,
WantError: "invalid flag combination, --all/--prefix and --json",
},
{
Args: "--all --key a-key --store-id " + storeID,
WantError: "invalid flag combination, more than one of --all, --prefix, or --key specified",
},
{
Args: "--key a-key --prefix a-prefix --store-id " + storeID,
WantError: "invalid flag combination, more than one of --all, --prefix, or --key specified",
},
{
Args: "--all --prefix a-prefix --store-id " + storeID,
WantError: "invalid flag combination, more than one of --all, --prefix, or --key specified",
},
{
Args: fmt.Sprintf("--store-id %s --key %s", storeID, itemKey),
Expand Down Expand Up @@ -303,6 +315,38 @@ func TestDeleteCommand(t *testing.T) {
},
WantError: "failed to delete 3 keys",
},
{
// there is no way to mock the effect of the --prefix flag
Args: fmt.Sprintf("--store-id %s --prefix a-prefix --auto-yes", storeID),
API: &mock.API{
NewListKVStoreKeysPaginatorFn: func(_ context.Context, _ *fastly.ListKVStoreKeysInput) fastly.PaginatorKVStoreEntries {
return &mockKVStoresEntriesPaginator{
next: true,
keys: []string{"foo", "bar", "baz"},
}
},
DeleteKVStoreKeyFn: func(_ context.Context, _ *fastly.DeleteKVStoreKeyInput) error {
return nil
},
},
WantOutput: "Deleting keys...",
},
{
// there is no way to mock the effect of the --prefix flag
Args: fmt.Sprintf("--store-id %s --prefix a-prefix --auto-yes", storeID),
API: &mock.API{
NewListKVStoreKeysPaginatorFn: func(_ context.Context, _ *fastly.ListKVStoreKeysInput) fastly.PaginatorKVStoreEntries {
return &mockKVStoresEntriesPaginator{
next: true,
keys: []string{"foo", "bar", "baz"},
}
},
DeleteKVStoreKeyFn: func(_ context.Context, _ *fastly.DeleteKVStoreKeyInput) error {
return errors.New("whoops")
},
},
WantError: "failed to delete 3 keys",
},
}

testutil.RunCLIScenarios(t, []string{root.CommandName, "delete"}, scenarios)
Expand Down
23 changes: 23 additions & 0 deletions pkg/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,29 @@ var ErrMissingDeleteAllKeyCombo = RemediationError{
Remediation: "Provide at least one of: --all or --key, not both.",
}

// ErrInvalidDeleteMultipleJSONKeyCombo means the user requested
// multiple-key delete and provided the --json flag which are mutually
// exclusive behaviours.
var ErrInvalidDeleteMultipleJSONKeyCombo = RemediationError{
Inner: fmt.Errorf("invalid flag combination, --all/--prefix and --json"),
Remediation: "Use either --all/--prefix or --json, not both.",
}

// ErrInvalidDeleteMultipleKeyCombo means the user more than one of
// the --all, --prefix, and --key flags which are mutually exclusive
// behaviours.
var ErrInvalidDeleteMultipleKeyCombo = RemediationError{
Inner: fmt.Errorf("invalid flag combination, more than one of --all, --prefix, or --key specified"),
Remediation: "Use --all, --prefix, or --key, not more than one.",
}

// ErrMissingDeleteMultipleKeyCombo means the user omitted all of the
// --all, --prefix, and --key flags and we need one of them.
var ErrMissingDeleteMultipleKeyCombo = RemediationError{
Inner: fmt.Errorf("invalid command, none of --all, --prefix, or --key provided"),
Remediation: "Provide one of --all, --prefix, or --key.",
}

// ErrNoSTDINData indicates the --stdin flag was specified but no data was piped
// into stdin.
var ErrNoSTDINData = RemediationError{
Expand Down
Loading