From 297a22383f96c3bb770a39d407b2bfc2f126b676 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 26 Sep 2016 10:12:40 -0500 Subject: [PATCH] Add kv delete command --- command/kv_delete.go | 178 ++++++++++++++++++ command/kv_delete_test.go | 143 ++++++++++++++ commands.go | 6 + .../docs/commands/kv/delete.html.markdown.erb | 96 ++++++++++ website/source/layouts/docs.erb | 3 + 5 files changed, 426 insertions(+) create mode 100644 command/kv_delete.go create mode 100644 command/kv_delete_test.go create mode 100644 website/source/docs/commands/kv/delete.html.markdown.erb diff --git a/command/kv_delete.go b/command/kv_delete.go new file mode 100644 index 0000000000..6857f1f209 --- /dev/null +++ b/command/kv_delete.go @@ -0,0 +1,178 @@ +package command + +import ( + "flag" + "fmt" + "strings" + + "github.com/hashicorp/consul/api" + "github.com/mitchellh/cli" +) + +// KVDeleteCommand is a Command implementation that is used to setup +// a "watch" which uses a sub-process +type KVDeleteCommand struct { + Ui cli.Ui +} + +func (c *KVDeleteCommand) Help() string { + helpText := ` +Usage: consul kv delete [options] KEY_OR_PREFIX + + Removes the value from Consul's key-value store at the given path. If no + key exists at the path, no action is taken. + + To delete the value for the key named "foo" in the key-value store: + + $ consul kv delete foo + + To delete all keys which start with "foo", specify the -recurse option: + + $ consul kv delete -recurse foo + + This will delete the keys named "foo", "food", and "foo/bar/zip" if they + existed. + +` + apiOptsText + ` + +KV Delete Options: + + -cas Perform a Check-And-Set operation. If this value is + specified without -modify-index, the key will first be + fetched and the resulting ModifyIndex will be used on + the next query. The default value is false. + + -modify-index= Unsigned integer representing the ModifyIndex of the + key. This is often combined with the -cas flag, but it + can be specified for any key. The default value is 0. + + -recurse Recursively delete all keys with the path. The default + value is false. +` + return strings.TrimSpace(helpText) +} + +func (c *KVDeleteCommand) Run(args []string) int { + cmdFlags := flag.NewFlagSet("get", flag.ContinueOnError) + cmdFlags.Usage = func() { c.Ui.Output(c.Help()) } + datacenter := cmdFlags.String("datacenter", "", "") + token := cmdFlags.String("token", "", "") + stale := cmdFlags.Bool("stale", false, "") + cas := cmdFlags.Bool("cas", false, "") + modifyIndex := cmdFlags.Uint64("modify-index", 0, "") + recurse := cmdFlags.Bool("recurse", false, "") + httpAddr := HTTPAddrFlag(cmdFlags) + if err := cmdFlags.Parse(args); err != nil { + return 1 + } + + key := "" + + // Check for arg validation + args = cmdFlags.Args() + switch len(args) { + case 0: + key = "" + case 1: + key = args[0] + default: + c.Ui.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + // This is just a "nice" thing to do. Since pairs cannot start with a /, but + // users will likely put "/" or "/foo", lets go ahead and strip that for them + // here. + if len(key) > 0 && key[0] == '/' { + key = key[1:] + } + + // If the key is empty and we are not doing a recursive delete, this is an + // error. + if key == "" && !*recurse { + c.Ui.Error("Error! Missing KEY argument") + return 1 + } + + // It is not valid to use a CAS and recurse in the same call + if *recurse && *cas { + c.Ui.Error("Cannot specify both -cas and -recurse!") + return 1 + } + if *recurse && *modifyIndex != 0 { + c.Ui.Error("Cannot specify both -modify-index and -recurse!") + return 1 + } + + // Create and test the HTTP client + conf := api.DefaultConfig() + conf.Address = *httpAddr + conf.Token = *token + client, err := api.NewClient(conf) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err)) + return 1 + } + + wo := &api.WriteOptions{ + Datacenter: *datacenter, + } + + switch { + case *recurse: + if _, err := client.KV().DeleteTree(key, wo); err != nil { + c.Ui.Error(fmt.Sprintf("Error! Did not delete prefix %s: %s", key, err)) + return 1 + } + + c.Ui.Info(fmt.Sprintf("Success! Deleted keys with prefix: %s", key)) + return 0 + case *cas: + pair := &api.KVPair{ + Key: key, + ModifyIndex: *modifyIndex, + } + + // If the user did not supply a -modify-index, but wants a check-and-set, + // grab the current modify index and store that on the key. + if pair.ModifyIndex == 0 { + currentPair, _, err := client.KV().Get(key, &api.QueryOptions{ + Datacenter: *datacenter, + Token: *token, + AllowStale: *stale, + }) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error! Could not get current key: %s", err)) + return 1 + } + if currentPair != nil { + pair.ModifyIndex = currentPair.ModifyIndex + } + } + + success, _, err := client.KV().DeleteCAS(pair, wo) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error! Did not delete key %s: %s", key, err)) + return 1 + } + if !success { + c.Ui.Error(fmt.Sprintf("Error! Did not delete key %s: CAS failed", key)) + return 1 + } + + c.Ui.Info(fmt.Sprintf("Success! Deleted key: %s", key)) + return 0 + default: + if _, err := client.KV().Delete(key, wo); err != nil { + c.Ui.Error(fmt.Sprintf("Error deleting key %s: %s", key, err)) + return 1 + } + + c.Ui.Info(fmt.Sprintf("Success! Deleted key: %s", key)) + return 0 + } +} + +func (c *KVDeleteCommand) Synopsis() string { + return "Removes data from the KV store" +} diff --git a/command/kv_delete_test.go b/command/kv_delete_test.go new file mode 100644 index 0000000000..8768538ca2 --- /dev/null +++ b/command/kv_delete_test.go @@ -0,0 +1,143 @@ +package command + +import ( + "strings" + "testing" + + "github.com/hashicorp/consul/api" + "github.com/mitchellh/cli" +) + +func TestKVDeleteCommand_implements(t *testing.T) { + var _ cli.Command = &KVDeleteCommand{} +} + +func TestKVDeleteCommand_noTabs(t *testing.T) { + assertNoTabs(t, new(KVDeleteCommand)) +} + +func TestKVDeleteCommand_Validation(t *testing.T) { + ui := new(cli.MockUi) + c := &KVDeleteCommand{Ui: ui} + + cases := map[string]struct { + args []string + output string + }{ + "-cas and -recurse": { + []string{"-cas", "-recurse"}, + "Cannot specify both", + }, + "-modify-index and -recurse": { + []string{"-modify-index", "2", "-recurse"}, + "Cannot specify both", + }, + "no key": { + []string{}, + "Missing KEY argument", + }, + "extra args": { + []string{"foo", "bar", "baz"}, + "Too many arguments", + }, + } + + for name, tc := range cases { + // Ensure our buffer is always clear + if ui.ErrorWriter != nil { + ui.ErrorWriter.Reset() + } + if ui.OutputWriter != nil { + ui.OutputWriter.Reset() + } + + code := c.Run(tc.args) + if code == 0 { + t.Errorf("%s: expected non-zero exit", name) + } + + output := ui.ErrorWriter.String() + if !strings.Contains(output, tc.output) { + t.Errorf("%s: expected %q to contain %q", name, output, tc.output) + } + } +} + +func TestKVDeleteCommand_Run(t *testing.T) { + srv, client := testAgentWithAPIClient(t) + defer srv.Shutdown() + waitForLeader(t, srv.httpAddr) + + ui := new(cli.MockUi) + c := &KVDeleteCommand{Ui: ui} + + pair := &api.KVPair{ + Key: "foo", + Value: []byte("bar"), + } + _, err := client.KV().Put(pair, nil) + if err != nil { + t.Fatalf("err: %#v", err) + } + + args := []string{ + "-http-addr=" + srv.httpAddr, + "foo", + } + + code := c.Run(args) + if code != 0 { + t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) + } + + pair, _, err = client.KV().Get("foo", nil) + if err != nil { + t.Fatalf("err: %#v", err) + } + if pair != nil { + t.Fatalf("bad: %#v", pair) + } +} + +func TestKVDeleteCommand_Recurse(t *testing.T) { + srv, client := testAgentWithAPIClient(t) + defer srv.Shutdown() + waitForLeader(t, srv.httpAddr) + + ui := new(cli.MockUi) + c := &KVDeleteCommand{Ui: ui} + + keys := []string{"foo/a", "foo/b", "food"} + + for _, k := range keys { + pair := &api.KVPair{ + Key: k, + Value: []byte("bar"), + } + _, err := client.KV().Put(pair, nil) + if err != nil { + t.Fatalf("err: %#v", err) + } + } + + args := []string{ + "-http-addr=" + srv.httpAddr, + "-recurse", + "foo", + } + + code := c.Run(args) + if code != 0 { + t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) + } + + for _, k := range keys { + pair, _, err := client.KV().Get(k, nil) + if err != nil { + t.Fatalf("err: %#v", err) + } + if pair != nil { + t.Fatalf("bad: %#v", pair) + } + } +} diff --git a/commands.go b/commands.go index 6875c5f26c..41f60b3c09 100644 --- a/commands.go +++ b/commands.go @@ -59,6 +59,12 @@ func init() { }, nil }, + "kv delete": func() (cli.Command, error) { + return &command.KVDeleteCommand{ + Ui: ui, + }, nil + }, + "kv get": func() (cli.Command, error) { return &command.KVGetCommand{ Ui: ui, diff --git a/website/source/docs/commands/kv/delete.html.markdown.erb b/website/source/docs/commands/kv/delete.html.markdown.erb new file mode 100644 index 0000000000..e9ec96590a --- /dev/null +++ b/website/source/docs/commands/kv/delete.html.markdown.erb @@ -0,0 +1,96 @@ +--- +layout: "docs" +page_title: "Commands: KV Delete" +sidebar_current: "docs-commands-kv-delete" +--- + +# Consul KV Delete + +Command: `consul kv delete` + +The `kv delete` command removes the value from Consul's key-value store at the +given path. If no key exists at the path, no action is taken. + +## Usage + +Usage: `consul kv delete [options] KEY_OR_PREFIX` + +#### API Options + +<%= partial "docs/commands/http_api_options" %> + +#### KV Delete Options + +* `-cas` - Perform a Check-And-Set operation. If this value is specified without + -modify-index, the key will first be fetched and the resulting ModifyIndex + will be used on the next query. The default value is false. + +* `-modify-index=` - Unsigned integer representing the ModifyIndex of the + key. This is often combined with the -cas flag, but it can be specified for + any key. The default value is 0. + +* `-recurse` - Recursively delete all keys with the path. The default value is + false. + +## Examples + +To remove the value for the key named "redis/config/connections" in the +key-value store: + +``` +$ consul kv delete redis/config/connections +Success! Deleted key: redis/config/connections +``` + +If the key does not exist, the command will not error, and a success message +will be returned: + +``` +$ consul kv delete not-a-real-key +Success! Deleted key: not-a-real-key +``` + +To only delete a key if it has not been modified since a given index, specify +the `-cas` and `-modify-index` flags: + +``` +$ consul kv get -detailed redis/config/connections | grep ModifyIndex +ModifyIndex 456 + +$ consul kv delete -cas -modify-index=123 redis/config/connections +Error! Did not delete key redis/config/connections: CAS failed + +$ consul kv delete -cas -modify-index=456 redis/config/connections +Success! Deleted key: redis/config/connections +``` + +It is also possible to have Consul fetch the current ModifyIndex before making +the query, by omitting the `-modify-index` flag. If the data is changed between +the initial read and the write, the operation will fail. + +``` +$ consul kv delete -cas redis/config/connections +Success! Deleted key: redis/config/connections +``` + +To recursively delete all keys that start with a given prefix, specify the +`-recurse` flag: + +``` +$ consul kv delete -recurse redis/ +Success! Deleted keys with prefix: redis/ +``` + +!> **Trailing slashes are important** in the recursive delete operation, since +Consul performs a greedy match on the provided prefix. If you were to use "foo" +as the key, this would recursively delete any key starting with those letters +such as "foo", "food", and "football" not just "foo". To ensure you are deleting +a folder, always use a trailing slash. + +It is not valid to combine the `-cas` option with `-recurse`, since you are +deleting multiple keys under a prefix in a single operation: + +``` +$ consul kv delete -cas -recurse redis/ +Cannot specify both -cas and -recurse! +``` diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 1668ad77fc..7c56891080 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -102,6 +102,9 @@ > kv