Thanks to visit codestin.com
Credit goes to github.com

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
15 changes: 15 additions & 0 deletions pkg/app/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,21 @@
]
}
},
"config-store-entry": {
"delete": {
"examples": [
{
"cmd": "fastly config-store-entry delete --all --store-id <STORE_ID> --batch-size 30 --auto-yes",
"description": "Delete all entries from the Config Store in batches of 30 and ignoring the warning prompt.",
"title": "Delete all entries from the Config Store"
}
],
"apis": [
"https://developer.fastly.com/reference/api/services/resources/config-store-item/#list-config-store-items",
"https://developer.fastly.com/reference/api/services/resources/config-store-item/#delete-config-store-item"
]
}
},
"dictionary": {
"create": {
"apis": [
Expand Down
61 changes: 59 additions & 2 deletions pkg/commands/configstoreentry/configstoreentry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/fastly/cli/pkg/mock"
"github.com/fastly/cli/pkg/testutil"
"github.com/fastly/cli/pkg/text"
"github.com/fastly/cli/pkg/threadsafe"
)

func TestCreateEntryCommand(t *testing.T) {
Expand Down Expand Up @@ -97,11 +98,36 @@ func TestDeleteEntryCommand(t *testing.T) {
itemKey = "key"
)

now := time.Now()

testItems := make([]*fastly.ConfigStoreItem, 3)
for i := range testItems {
testItems[i] = &fastly.ConfigStoreItem{
StoreID: storeID,
Key: fmt.Sprintf("key-%02d", i),
Value: fmt.Sprintf("value %02d", i),
CreatedAt: &now,
UpdatedAt: &now,
}
}

scenarios := []testutil.TestScenario{
{
Args: testutil.Args(configstoreentry.RootName + " delete --key a-key"),
WantError: "error parsing arguments: required flag --store-id not provided",
},
{
Args: testutil.Args(configstoreentry.RootName + " delete --store-id " + storeID),
WantError: "invalid command, neither --all or --key provided",
},
{
Args: testutil.Args(configstoreentry.RootName + " delete --json --all --store-id " + storeID),
WantError: "invalid flag combination, --all and --json",
},
{
Args: testutil.Args(configstoreentry.RootName + " delete --key a-key --all --store-id " + storeID),
WantError: "invalid flag combination, --all and --key",
},
{
Args: testutil.Args(fmt.Sprintf("%s delete --store-id %s --key %s", configstoreentry.RootName, storeID, itemKey)),
API: mock.API{
Expand Down Expand Up @@ -137,20 +163,51 @@ func TestDeleteEntryCommand(t *testing.T) {
true,
}),
},
{
Args: testutil.Args(fmt.Sprintf("%s delete --store-id %s --all --auto-yes", configstoreentry.RootName, storeID)),
API: mock.API{
ListConfigStoreItemsFn: func(i *fastly.ListConfigStoreItemsInput) ([]*fastly.ConfigStoreItem, error) {
return testItems, nil
},
DeleteConfigStoreItemFn: func(i *fastly.DeleteConfigStoreItemInput) error {
return nil
},
},
WantOutput: fmt.Sprintf(`Deleting key: key-00
Deleting key: key-01
Deleting key: key-02

SUCCESS: Deleted all keys from Config Store '%s'
`, storeID),
},
{
Args: testutil.Args(fmt.Sprintf("%s delete --store-id %s --all --auto-yes", configstoreentry.RootName, storeID)),
API: mock.API{
ListConfigStoreItemsFn: func(i *fastly.ListConfigStoreItemsInput) ([]*fastly.ConfigStoreItem, error) {
return testItems, nil
},
DeleteConfigStoreItemFn: func(i *fastly.DeleteConfigStoreItemInput) error {
return errors.New("whoops")
},
},
WantError: "failed to delete keys: key-00, key-01, key-02",
},
}

for _, testcase := range scenarios {
testcase := testcase
t.Run(testcase.Name, func(t *testing.T) {
var stdout bytes.Buffer
var stdout threadsafe.Buffer
opts := testutil.NewRunOpts(testcase.Args, &stdout)

opts.APIClient = mock.APIClient(testcase.API)

err := app.Run(opts)

t.Log(stdout.String())

testutil.AssertErrorContains(t, err, testcase.WantError)
testutil.AssertString(t, testcase.WantOutput, stdout.String())
testutil.AssertStringContains(t, stdout.String(), testcase.WantOutput)
})
}
}
Expand Down
125 changes: 117 additions & 8 deletions pkg/commands/configstoreentry/delete.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package configstoreentry

import (
"fmt"
"io"
"strings"
"sync"

"github.com/fastly/go-fastly/v8/fastly"

Expand All @@ -12,6 +15,14 @@ import (
"github.com/fastly/cli/pkg/text"
)

// deleteKeysConcurrencyLimit is used to limit the concurrency when deleting ALL keys.
// This is effectively the 'thread pool' size.
const deleteKeysConcurrencyLimit int = 100

// batchLimit is used to split the list of items into batches.
// The batch size of 100 aligns with the KV Store pagination default limit.
const batchLimit int = 100

// NewDeleteCommand returns a usable command registered under the parent.
func NewDeleteCommand(parent cmd.Registerer, g *global.Data, m manifest.Data) *DeleteCommand {
c := DeleteCommand{
Expand All @@ -24,17 +35,19 @@ func NewDeleteCommand(parent cmd.Registerer, g *global.Data, m manifest.Data) *D
c.CmdClause = parent.Command("delete", "Delete a config store item")

// Required.
c.RegisterFlag(cmd.StoreIDFlag(&c.input.StoreID)) // --store-id

// Optional.
c.CmdClause.Flag("all", "Delete all entries within the store").Short('a').BoolVar(&c.deleteAll)
c.CmdClause.Flag("batch-size", "Key batch processing size (ignored when set without the --all flag)").Short('b').Action(c.batchSize.Set).IntVar(&c.batchSize.Value)
c.CmdClause.Flag("concurrency", "Control thread pool size (ignored when set without the --all flag)").Short('c').Action(c.concurrency.Set).IntVar(&c.concurrency.Value)
c.RegisterFlagBool(c.JSONFlag()) // --json
c.RegisterFlag(cmd.StringFlagOpts{
Name: "key",
Short: 'k',
Description: "Item name",
Dst: &c.input.Key,
Required: true,
})
c.RegisterFlag(cmd.StoreIDFlag(&c.input.StoreID)) // --store-id

// Optional.
c.RegisterFlagBool(c.JSONFlag()) // --json

return &c
}
Expand All @@ -44,15 +57,44 @@ type DeleteCommand struct {
cmd.Base
cmd.JSONOutput

input fastly.DeleteConfigStoreItemInput
manifest manifest.Data
batchSize cmd.OptionalInt
concurrency cmd.OptionalInt
deleteAll bool
input fastly.DeleteConfigStoreItemInput
manifest manifest.Data
}

// Exec invokes the application logic for the command.
func (c *DeleteCommand) Exec(_ io.Reader, out io.Writer) error {
func (c *DeleteCommand) Exec(in io.Reader, out io.Writer) error {
if c.Globals.Verbose() && c.JSONOutput.Enabled {
return fsterr.ErrInvalidVerboseJSONCombo
}
// TODO: Support --json for bulk deletions.
if c.deleteAll && c.JSONOutput.Enabled {
return fsterr.ErrInvalidDeleteAllJSONKeyCombo
}
if c.deleteAll && c.input.Key != "" {
return fsterr.ErrInvalidDeleteAllKeyCombo
}
if !c.deleteAll && c.input.Key == "" {
return fsterr.ErrMissingDeleteAllKeyCombo
}

if c.deleteAll {
if !c.Globals.Flags.AutoYes && !c.Globals.Flags.NonInteractive {
text.Warning(out, "This will delete ALL entries from your store!")
text.Break(out)
cont, err := text.AskYesNo(out, "Are you sure you want to continue? [yes/no]: ", in)
if err != nil {
return err
}
if !cont {
return nil
}
text.Break(out)
}
return c.deleteAllKeys(out)
}

err := c.Globals.APIClient.DeleteConfigStoreItem(&c.input)
if err != nil {
Expand All @@ -78,3 +120,70 @@ func (c *DeleteCommand) Exec(_ io.Reader, out io.Writer) error {

return nil
}

func (c *DeleteCommand) deleteAllKeys(out io.Writer) error {
// NOTE: The Config Store returns ALL items (there is no pagination).
items, err := c.Globals.APIClient.ListConfigStoreItems(&fastly.ListConfigStoreItemsInput{
StoreID: c.input.StoreID,
})
if err != nil {
return fmt.Errorf("failed to acquire list of Config Store items: %w", err)
}

var (
mu sync.Mutex
wg sync.WaitGroup
)
poolSize := deleteKeysConcurrencyLimit
if c.concurrency.WasSet {
poolSize = c.concurrency.Value
}
semaphore := make(chan struct{}, poolSize)

total := len(items)
failedKeys := []string{}

batchSize := batchLimit
if c.batchSize.WasSet {
batchSize = c.batchSize.Value
}

// With KV Store we have pagination support and so that natively provides us a
// predefined 'batch' size. Because we don't have pagination with the Config
// Store it means we'll define our own batch size which the user can override.
for i := 0; i < total; i += batchSize {
end := i + batchSize
if end > total {
end = total
}
seg := items[i:end]

wg.Add(1)
go func(items []*fastly.ConfigStoreItem) {
semaphore <- struct{}{}
defer func() { <-semaphore }()
defer wg.Done()

for _, item := range items {
text.Output(out, "Deleting key: %s", item.Key)
err := c.Globals.APIClient.DeleteConfigStoreItem(&fastly.DeleteConfigStoreItemInput{StoreID: c.input.StoreID, Key: item.Key})
if err != nil {
c.Globals.ErrLog.Add(fmt.Errorf("failed to delete key '%s': %s", item.Key, err))
mu.Lock()
failedKeys = append(failedKeys, item.Key)
mu.Unlock()
}
}
}(seg)
}

wg.Wait()
close(semaphore)

if len(failedKeys) > 0 {
return fmt.Errorf("failed to delete keys: %s", strings.Join(failedKeys, ", "))
}

text.Success(out, "Deleted all keys from Config Store '%s'", c.input.StoreID)
return nil
}
7 changes: 6 additions & 1 deletion pkg/commands/kvstoreentry/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import (
)

// deleteKeysConcurrencyLimit is used to limit the concurrency when deleting ALL keys.
const deleteKeysConcurrencyLimit int = 1000
// This is effectively the 'thread pool' size.
const deleteKeysConcurrencyLimit int = 100

// DeleteCommand calls the Fastly API to delete an kv store.
type DeleteCommand struct {
Expand Down Expand Up @@ -58,6 +59,10 @@ func (c *DeleteCommand) Exec(in io.Reader, out io.Writer) error {
if c.Globals.Verbose() && c.JSONOutput.Enabled {
return fsterr.ErrInvalidVerboseJSONCombo
}
// TODO: Support --json for bulk deletions.
if c.deleteAll && c.JSONOutput.Enabled {
return fsterr.ErrInvalidDeleteAllJSONKeyCombo
}
if c.deleteAll && c.key.WasSet {
return fsterr.ErrInvalidDeleteAllKeyCombo
}
Expand Down
4 changes: 4 additions & 0 deletions pkg/commands/kvstoreentry/kvstoreentry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ func TestDeleteCommand(t *testing.T) {
Args: testutil.Args(kvstoreentry.RootName + " delete --store-id " + storeID),
WantError: "invalid command, neither --all or --key provided",
},
{
Args: testutil.Args(kvstoreentry.RootName + " delete --json --all --store-id " + storeID),
WantError: "invalid flag combination, --all and --json",
},
{
Args: testutil.Args(kvstoreentry.RootName + " delete --key a-key --all --store-id " + storeID),
WantError: "invalid flag combination, --all and --key",
Expand Down
7 changes: 7 additions & 0 deletions pkg/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ var ErrInvalidVerboseJSONCombo = RemediationError{
Remediation: "Use either --verbose or --json, not both.",
}

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

// ErrInvalidDeleteAllKeyCombo means the user provided both a --all and --key
// flag which are mutually exclusive behaviours.
var ErrInvalidDeleteAllKeyCombo = RemediationError{
Expand Down