From a8dc0f86b614c51514ef368adde9a2a337c2fec4 Mon Sep 17 00:00:00 2001 From: Seth Vargo Date: Mon, 26 Sep 2016 10:12:14 -0500 Subject: [PATCH] Add kv get --- command/kv_get.go | 226 ++++++++++++++++ command/kv_get_test.go | 252 ++++++++++++++++++ commands.go | 6 + .../commands/_http_api_options.html.markdown | 16 ++ .../source/docs/commands/index.html.markdown | 3 + .../docs/commands/kv/get.html.markdown.erb | 150 +++++++++++ website/source/layouts/docs.erb | 9 + 7 files changed, 662 insertions(+) create mode 100644 command/kv_get.go create mode 100644 command/kv_get_test.go create mode 100644 website/source/docs/commands/_http_api_options.html.markdown create mode 100644 website/source/docs/commands/kv/get.html.markdown.erb diff --git a/command/kv_get.go b/command/kv_get.go new file mode 100644 index 000000000..0358d7085 --- /dev/null +++ b/command/kv_get.go @@ -0,0 +1,226 @@ +package command + +import ( + "bytes" + "flag" + "fmt" + "io" + "strings" + "text/tabwriter" + + "github.com/hashicorp/consul/api" + "github.com/mitchellh/cli" +) + +// KVGetCommand is a Command implementation that is used to setup +// a "watch" which uses a sub-process +type KVGetCommand struct { + Ui cli.Ui +} + +func (c *KVGetCommand) Help() string { + helpText := ` +Usage: consul kv get [options] [KEY_OR_PREFIX] + + Retrieves the value from Consul's key-value store at the given key name. If no + key exists with that name, an error is returned. If a key exists with that + name but has no data, nothing is returned. If the name or prefix is omitted, + it defaults to "" which is the root of the key-value store. + + To retrieve the value for the key named "foo" in the key-value store: + + $ consul kv get foo + + This will return the original, raw value stored in Consul. To view detailed + information about the key, specify the "-detailed" flag. This will output all + known metadata about the key including ModifyIndex and any user-supplied + flags: + + $ consul kv get -detailed foo + + To treat the path as a prefix and list all keys which start with the given + prefix, specify the "-recurse" flag: + + $ consul kv get -recurse foo + + This will return all key-vlaue pairs. To just list the keys which start with + the specified prefix, use the "-keys" option instead: + + $ consul kv get -keys foo + + For a full list of options and examples, please see the Consul documentation. + +` + apiOptsText + ` + +KV Get Options: + + -detailed Provide additional metadata about the key in addition + to the value such as the ModifyIndex and any flags + that may have been set on the key. The default value + is false. + + -keys List keys which start with the given prefix, but not + their values. This is especially useful if you only + need the key names themselves. This option is commonly + combined with the -separator option. The default value + is false. + + -recurse Recursively look at all keys prefixed with the given + path. The default value is false. + + -separator= String to use as a separator between keys. The default + value is "/", but this option is only taken into + account when paired with the -keys flag. + +` + return strings.TrimSpace(helpText) +} + +func (c *KVGetCommand) 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, "") + detailed := cmdFlags.Bool("detailed", false, "") + keys := cmdFlags.Bool("keys", false, "") + recurse := cmdFlags.Bool("recurse", false, "") + separator := cmdFlags.String("separator", "/", "") + 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 or key-based lookup, + // this is an error. + if key == "" && !(*recurse || *keys) { + c.Ui.Error("Error! Missing KEY argument") + 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 + } + + switch { + case *keys: + keys, _, err := client.KV().Keys(key, *separator, &api.QueryOptions{ + Datacenter: *datacenter, + AllowStale: *stale, + }) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error querying Consul agent: %s", err)) + return 1 + } + + for _, k := range keys { + c.Ui.Info(string(k)) + } + + return 0 + case *recurse: + pairs, _, err := client.KV().List(key, &api.QueryOptions{ + Datacenter: *datacenter, + AllowStale: *stale, + }) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error querying Consul agent: %s", err)) + return 1 + } + + for i, pair := range pairs { + if *detailed { + var b bytes.Buffer + if err := prettyKVPair(&b, pair); err != nil { + c.Ui.Error(fmt.Sprintf("Error rendering KV pair: %s", err)) + return 1 + } + + c.Ui.Info(b.String()) + + if i < len(pairs)-1 { + c.Ui.Info("") + } + } else { + c.Ui.Info(fmt.Sprintf("%s:%s", pair.Key, pair.Value)) + } + } + + return 0 + default: + pair, _, err := client.KV().Get(key, &api.QueryOptions{ + Datacenter: *datacenter, + AllowStale: *stale, + }) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error querying Consul agent: %s", err)) + return 1 + } + + if pair == nil { + c.Ui.Error(fmt.Sprintf("Error! No key exists at: %s", key)) + return 1 + } + + if *detailed { + var b bytes.Buffer + if err := prettyKVPair(&b, pair); err != nil { + c.Ui.Error(fmt.Sprintf("Error rendering KV pair: %s", err)) + return 1 + } + + c.Ui.Info(b.String()) + return 0 + } else { + c.Ui.Info(string(pair.Value)) + return 0 + } + } +} + +func (c *KVGetCommand) Synopsis() string { + return "Retrieves or lists data from the KV store" +} + +func prettyKVPair(w io.Writer, pair *api.KVPair) error { + tw := tabwriter.NewWriter(w, 0, 2, 6, ' ', 0) + fmt.Fprintf(tw, "CreateIndex\t%d\n", pair.CreateIndex) + fmt.Fprintf(tw, "Flags\t%d\n", pair.Flags) + fmt.Fprintf(tw, "Key\t%s\n", pair.Key) + fmt.Fprintf(tw, "LockIndex\t%d\n", pair.LockIndex) + fmt.Fprintf(tw, "ModifyIndex\t%d\n", pair.ModifyIndex) + if pair.Session == "" { + fmt.Fprintf(tw, "Session\t-\n") + } else { + fmt.Fprintf(tw, "Session\t%s\n", pair.Session) + } + fmt.Fprintf(tw, "Value\t%s", pair.Value) + return tw.Flush() +} diff --git a/command/kv_get_test.go b/command/kv_get_test.go new file mode 100644 index 000000000..e0dc8ce31 --- /dev/null +++ b/command/kv_get_test.go @@ -0,0 +1,252 @@ +package command + +import ( + "strings" + "testing" + + "github.com/hashicorp/consul/api" + "github.com/mitchellh/cli" +) + +func TestKVGetCommand_implements(t *testing.T) { + var _ cli.Command = &KVGetCommand{} +} + +func TestKVGetCommand_noTabs(t *testing.T) { + assertNoTabs(t, new(KVGetCommand)) +} + +func TestKVGetCommand_Validation(t *testing.T) { + ui := new(cli.MockUi) + c := &KVGetCommand{Ui: ui} + + cases := map[string]struct { + args []string + output string + }{ + "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 TestKVGetCommand_Run(t *testing.T) { + srv, client := testAgentWithAPIClient(t) + defer srv.Shutdown() + waitForLeader(t, srv.httpAddr) + + ui := new(cli.MockUi) + c := &KVGetCommand{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()) + } + + output := ui.OutputWriter.String() + if !strings.Contains(output, "bar") { + t.Errorf("bad: %#v", output) + } +} + +func TestKVGetCommand_Missing(t *testing.T) { + srv, _ := testAgentWithAPIClient(t) + defer srv.Shutdown() + waitForLeader(t, srv.httpAddr) + + ui := new(cli.MockUi) + c := &KVGetCommand{Ui: ui} + + args := []string{ + "-http-addr=" + srv.httpAddr, + "not-a-real-key", + } + + code := c.Run(args) + if code == 0 { + t.Fatalf("expected bad code") + } +} + +func TestKVGetCommand_Empty(t *testing.T) { + srv, client := testAgentWithAPIClient(t) + defer srv.Shutdown() + waitForLeader(t, srv.httpAddr) + + ui := new(cli.MockUi) + c := &KVGetCommand{Ui: ui} + + pair := &api.KVPair{ + Key: "empty", + Value: []byte(""), + } + _, err := client.KV().Put(pair, nil) + if err != nil { + t.Fatalf("err: %#v", err) + } + + args := []string{ + "-http-addr=" + srv.httpAddr, + "empty", + } + + code := c.Run(args) + if code != 0 { + t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) + } +} + +func TestKVGetCommand_Detailed(t *testing.T) { + srv, client := testAgentWithAPIClient(t) + defer srv.Shutdown() + waitForLeader(t, srv.httpAddr) + + ui := new(cli.MockUi) + c := &KVGetCommand{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, + "-detailed", + "foo", + } + + code := c.Run(args) + if code != 0 { + t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) + } + + output := ui.OutputWriter.String() + for _, key := range []string{ + "CreateIndex", + "LockIndex", + "ModifyIndex", + "Flags", + "Session", + "Value", + } { + if !strings.Contains(output, key) { + t.Fatalf("bad %#v, missing %q", output, key) + } + } +} + +func TestKVGetCommand_Keys(t *testing.T) { + srv, client := testAgentWithAPIClient(t) + defer srv.Shutdown() + waitForLeader(t, srv.httpAddr) + + ui := new(cli.MockUi) + c := &KVGetCommand{Ui: ui} + + keys := []string{"foo/bar", "foo/baz", "foo/zip"} + for _, key := range keys { + if _, err := client.KV().Put(&api.KVPair{Key: key}, nil); err != nil { + t.Fatalf("err: %#v", err) + } + } + + args := []string{ + "-http-addr=" + srv.httpAddr, + "-keys", + "foo/", + } + + code := c.Run(args) + if code != 0 { + t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String()) + } + + output := ui.OutputWriter.String() + for _, key := range keys { + if !strings.Contains(output, key) { + t.Fatalf("bad %#v missing %q", output, key) + } + } +} + +func TestKVGetCommand_Recurse(t *testing.T) { + srv, client := testAgentWithAPIClient(t) + defer srv.Shutdown() + waitForLeader(t, srv.httpAddr) + + ui := new(cli.MockUi) + c := &KVGetCommand{Ui: ui} + + keys := map[string]string{ + "foo/a": "a", + "foo/b": "b", + "foo/c": "c", + } + for k, v := range keys { + pair := &api.KVPair{Key: k, Value: []byte(v)} + if _, err := client.KV().Put(pair, nil); 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()) + } + + output := ui.OutputWriter.String() + for key, value := range keys { + if !strings.Contains(output, key+":"+value) { + t.Fatalf("bad %#v missing %q", output, key+"adafa") + } + } +} diff --git a/commands.go b/commands.go index 691ac8de6..7b8c37775 100644 --- a/commands.go +++ b/commands.go @@ -59,6 +59,12 @@ func init() { }, nil }, + "kv get": func() (cli.Command, error) { + return &command.KVGetCommand{ + Ui: ui, + }, nil + }, + "join": func() (cli.Command, error) { return &command.JoinCommand{ Ui: ui, diff --git a/website/source/docs/commands/_http_api_options.html.markdown b/website/source/docs/commands/_http_api_options.html.markdown new file mode 100644 index 000000000..ad5e02a0c --- /dev/null +++ b/website/source/docs/commands/_http_api_options.html.markdown @@ -0,0 +1,16 @@ +* `-http-addr=` - Address of the Consul agent with the port. This can be + an IP address or DNS address, but it must include the port. This can also be + specified via the CONSUL_HTTP_ADDR environment variable. The default value is + 127.0.0.1:8500. + +* `-datacenter=` - Name of the datacenter to query. If unspecified, the + query will default to the datacenter of the Consul agent at the HTTP address. + +* `-token=` - ACL token to use in the request. This can also be specified + via the `CONSUL_HTTP_TOKEN` environment variable. If unspecified, the query + will default to the token of the Consul agent at the HTTP address. + +* `-stale` - Permit any Consul server (non-leader) to respond to this request. + This allows for lower latency and higher throughput, but can result in stale + data. This option has no effect on non-read operations. The default value is + false. diff --git a/website/source/docs/commands/index.html.markdown b/website/source/docs/commands/index.html.markdown index 7e0302754..7ee03658f 100644 --- a/website/source/docs/commands/index.html.markdown +++ b/website/source/docs/commands/index.html.markdown @@ -27,6 +27,7 @@ usage: consul [--version] [--help] [] Available commands are: agent Runs a Consul agent + configtest Validate config file event Fire a new event exec Executes a command on Consul nodes force-leave Forces a member of the cluster to enter the "left" state @@ -34,8 +35,10 @@ Available commands are: join Tell Consul agent to join cluster keygen Generates a new encryption key keyring Manages gossip layer encryption keys + kv Interact with the key-value store leave Gracefully leaves the Consul cluster and shuts down lock Execute a command holding a lock + maint Controls node or service maintenance mode members Lists the members of a Consul cluster monitor Stream logs from a Consul agent operator Provides cluster-level tools for Consul operators diff --git a/website/source/docs/commands/kv/get.html.markdown.erb b/website/source/docs/commands/kv/get.html.markdown.erb new file mode 100644 index 000000000..29b36959d --- /dev/null +++ b/website/source/docs/commands/kv/get.html.markdown.erb @@ -0,0 +1,150 @@ +--- +layout: "docs" +page_title: "Commands: KV Get" +sidebar_current: "docs-commands-kv-get" +--- + +# Consul KV Get + +Command: `consul kv get` + +The `kv get` command is used to retrieves the value from Consul's key-value +store at the given key name. If no key exists with that name, an error is +returned. If a key exists with that name but has no data, nothing is returned. +If the name or prefix is omitted, it defaults to "" which is the root of the +key-value store. + +## Usage + +Usage: `consul kv get [options] [KEY_OR_PREFIX]` + +#### API Options + +<%= partial "docs/commands/http_api_options" %> + +#### KV Get Options + +* `-detailed` - Provide additional metadata about the key in addition to the + value such as the ModifyIndex and any flags that may have been set on the key. + The default value is false. + +* `-keys` - List keys which start with the given prefix, but not their values. + This is especially useful if you only need the key names themselves. This + option is commonly combined with the -separator option. The default value is + false. + +* `-recurse` - Recursively look at all keys prefixed with the given path. The + default value is false. + +* `-separator=` - String to use as a separator between keys. The default + value is "/", but this option is only taken into account when paired with the + -keys flag. + +## Examples + +To retrieve the value for the key named "redis/config/connections" in the +key-value store: + +``` +$ consul kv get redis/config/connections +5 +``` + +This will return the original, raw value stored in Consul. To view detailed +information about the key, specify the "-detailed" flag. This will output all +known metadata about the key including ModifyIndex and any user-supplied +flags: + +``` +$ consul kv get -detailed redis/config/connections +CreateIndex 336 +Flags 0 +Key redis/config/connections +LockIndex 0 +ModifyIndex 336 +Session - +Value 5 +``` + +If the key with the given name does not exist, an error is returned: + +``` +$ consul kv get not-a-real-key +Error! No key exists at: not-a-real-key +``` + +To treat the path as a prefix and list all keys which start with the given +prefix, specify the "-recurse" flag: + +``` +$ consul kv get -recurse redis/ +redis/config/connections:5 +redis/config/cpu:128 +redis/config/memory:512 +``` + +Or list detailed information about all pairs under a prefix: + +``` +$ consul kv get -recurse -detailed redis +CreateIndex 336 +Flags 0 +Key redis/config/connections +LockIndex 0 +ModifyIndex 336 +Session - +Value 5 + +CreateIndex 472 +Flags 0 +Key redis/config/cpu +LockIndex 0 +ModifyIndex 472 +Session - +Value 128 + +CreateIndex 471 +Flags 0 +Key redis/config/memory +LockIndex 0 +ModifyIndex 471 +Session - +Value 512 +``` + +To just list the keys which start with the specified prefix, use the "-keys" +option instead. This is more performant and results in a smaller payload: + +``` +$ consul kv get -keys redis/config/ +redis/config/connections +redis/config/cpu +redis/config/memory +``` + +By default, the `-keys` operation uses a separator of "/", meaning it will not +recurse beyond that separator. You can choose a different separator by setting +`-separator=""`. + +``` +$ consul kv get -keys -separator="s" redis +redis/c +``` + +Alternatively, you can disable the separator altogether by setting it to the +empty string: + +``` +$ consul kv get -keys -separator="" redis +redis/config/connections +redis/config/cpu +redis/config/memory +``` + +To list all keys at the root, simply omit the prefix parameter: + +``` +$ consul kv get -keys +memcached/ +redis/ +``` diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 5c02bc7de..2738d14b4 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -99,6 +99,15 @@ keyring + > + kv + + + > leave