mirror of https://github.com/status-im/consul.git
Merge pull request #2360 from hashicorp/sethvargo/kv_cli
Add kv subcommand and CLI
This commit is contained in:
commit
be17741617
22
api/kv.go
22
api/kv.go
|
@ -11,12 +11,34 @@ import (
|
|||
|
||||
// KVPair is used to represent a single K/V entry
|
||||
type KVPair struct {
|
||||
// Key is the name of the key. It is also part of the URL path when accessed
|
||||
// via the API.
|
||||
Key string
|
||||
|
||||
// CreateIndex holds the index corresponding the creation of this KVPair. This
|
||||
// is a read-only field.
|
||||
CreateIndex uint64
|
||||
|
||||
// ModifyIndex is used for the Check-And-Set operations and can also be fed
|
||||
// back into the WaitIndex of the QueryOptions in order to perform blocking
|
||||
// queries.
|
||||
ModifyIndex uint64
|
||||
|
||||
// LockIndex holds the index corresponding to a lock on this key, if any. This
|
||||
// is a read-only field.
|
||||
LockIndex uint64
|
||||
|
||||
// Flags are any user-defined flags on the key. It is up to the implementer
|
||||
// to check these values, since Consul does not treat them specially.
|
||||
Flags uint64
|
||||
|
||||
// Value is the value for the key. This can be any value, but it will be
|
||||
// base64 encoded upon transport.
|
||||
Value []byte
|
||||
|
||||
// Session is a string representing the ID of the session. Any other
|
||||
// interactions with this key over the same session must specify the same
|
||||
// session ID.
|
||||
Session string
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
// KVCommand is a Command implementation that just shows help for
|
||||
// the subcommands nested below it.
|
||||
type KVCommand struct {
|
||||
Ui cli.Ui
|
||||
}
|
||||
|
||||
func (c *KVCommand) Run(args []string) int {
|
||||
return cli.RunResultHelp
|
||||
}
|
||||
|
||||
func (c *KVCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: consul kv <subcommand> [options] [args]
|
||||
|
||||
This command has subcommands for interacting with Consul's key-value
|
||||
store. Here are some simple examples, and more detailed examples are
|
||||
available in the subcommands or the documentation.
|
||||
|
||||
Create or update the key named "redis/config/connections" with the value "5":
|
||||
|
||||
$ consul kv put redis/config/connections 5
|
||||
|
||||
Read this value back:
|
||||
|
||||
$ consul kv get redis/config/connections
|
||||
|
||||
Or get detailed key information:
|
||||
|
||||
$ consul kv get -detailed redis/config/connections
|
||||
|
||||
Finally, delete the key:
|
||||
|
||||
$ consul kv delete redis/config/connections
|
||||
|
||||
For more examples, ask for subcommand help or view the documentation.
|
||||
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *KVCommand) Synopsis() string {
|
||||
return "Interact with the key-value store"
|
||||
}
|
||||
|
||||
var apiOptsText = strings.TrimSpace(`
|
||||
API Options:
|
||||
|
||||
-http-addr=<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> Name of the datacenter to query. If unspecified, the
|
||||
query will default to the datacenter of the Consul
|
||||
agent at the HTTP address.
|
||||
|
||||
-token=<value> 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.
|
||||
`)
|
|
@ -0,0 +1,15 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func TestKVCommand_implements(t *testing.T) {
|
||||
var _ cli.Command = &KVCommand{}
|
||||
}
|
||||
|
||||
func TestKVCommand_noTabs(t *testing.T) {
|
||||
assertNoTabs(t, new(KVCommand))
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
// KVDeleteCommand is a Command implementation that is used to delete a key or
|
||||
// prefix of keys from the key-value store.
|
||||
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. Specifying this
|
||||
value also requires the -modify-index flag to be set.
|
||||
The default value is false.
|
||||
|
||||
-modify-index=<int> Unsigned integer representing the ModifyIndex of the
|
||||
key. This is used in combination with the -cas flag.
|
||||
|
||||
-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", "", "")
|
||||
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
|
||||
}
|
||||
|
||||
// ModifyIndex is required for CAS
|
||||
if *cas && *modifyIndex == 0 {
|
||||
c.Ui.Error("Must specify -modify-index with -cas!")
|
||||
return 1
|
||||
}
|
||||
|
||||
// Specifying a ModifyIndex for a non-CAS operation is not possible.
|
||||
if *modifyIndex != 0 && !*cas {
|
||||
c.Ui.Error("Cannot specify -modify-index without -cas!")
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"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", "-modify-index", "2", "-recurse", "foo"},
|
||||
"Cannot specify both",
|
||||
},
|
||||
"-cas no -modify-index": {
|
||||
[]string{"-cas", "foo"},
|
||||
"Must specify -modify-index",
|
||||
},
|
||||
"-modify-index no -cas": {
|
||||
[]string{"-modify-index", "2", "foo"},
|
||||
"Cannot specify -modify-index without",
|
||||
},
|
||||
"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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestKVDeleteCommand_CAS(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,
|
||||
"-cas",
|
||||
"-modify-index", "1",
|
||||
"foo",
|
||||
}
|
||||
|
||||
code := c.Run(args)
|
||||
if code == 0 {
|
||||
t.Fatalf("bad: expected error")
|
||||
}
|
||||
|
||||
data, _, err := client.KV().Get("foo", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Reset buffers
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
args = []string{
|
||||
"-http-addr=" + srv.httpAddr,
|
||||
"-cas",
|
||||
"-modify-index", strconv.FormatUint(data.ModifyIndex, 10),
|
||||
"foo",
|
||||
}
|
||||
|
||||
code = c.Run(args)
|
||||
if code != 0 {
|
||||
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
|
||||
}
|
||||
|
||||
data, _, err = client.KV().Get("foo", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if data != nil {
|
||||
t.Fatalf("bad: %#v", data)
|
||||
}
|
||||
}
|
|
@ -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 fetch the value of
|
||||
// a key from the key-value store.
|
||||
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> 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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,239 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
// KVPutCommand is a Command implementation that is used to write data to the
|
||||
// key-value store.
|
||||
type KVPutCommand struct {
|
||||
Ui cli.Ui
|
||||
|
||||
// testStdin is the input for testing.
|
||||
testStdin io.Reader
|
||||
}
|
||||
|
||||
func (c *KVPutCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: consul kv put [options] KEY [DATA]
|
||||
|
||||
Writes the data to the given path in the key-value store. The data can be of
|
||||
any type.
|
||||
|
||||
$ consul kv put config/redis/maxconns 5
|
||||
|
||||
The data can also be consumed from a file on disk by prefixing with the "@"
|
||||
symbol. For example:
|
||||
|
||||
$ consul kv put config/program/license @license.lic
|
||||
|
||||
Or it can be read from stdin using the "-" symbol:
|
||||
|
||||
$ echo "abcd1234" | consul kv put config/program/license -
|
||||
|
||||
The DATA argument itself is optional. If omitted, this will create an empty
|
||||
key-value pair at the specified path:
|
||||
|
||||
$ consul kv put webapp/beta/active
|
||||
|
||||
To perform a Check-And-Set operation, specify the -cas flag with the
|
||||
appropriate -modify-index flag corresponding to the key you want to perform
|
||||
the CAS operation on:
|
||||
|
||||
$ consul kv put -cas -modify-index=844 config/redis/maxconns 5
|
||||
|
||||
Additional flags and more advanced use cases are detailed below.
|
||||
|
||||
` + apiOptsText + `
|
||||
|
||||
KV Put Options:
|
||||
|
||||
-acquire Obtain a lock on the key. If the key does not exist,
|
||||
this operation will create the key and obtain the
|
||||
lock. The session must already exist and be specified
|
||||
via the -session flag. The default value is false.
|
||||
|
||||
-cas Perform a Check-And-Set operation. Specifying this
|
||||
value also requires the -modify-index flag to be set.
|
||||
The default value is false.
|
||||
|
||||
-flags=<int> Unsigned integer value to assign to this key-value
|
||||
pair. This value is not read by Consul, so clients can
|
||||
use this value however makes sense for their use case.
|
||||
The default value is 0 (no flags).
|
||||
|
||||
-modify-index=<int> Unsigned integer representing the ModifyIndex of the
|
||||
key. This is used in combination with the -cas flag.
|
||||
|
||||
-release Forfeit the lock on the key at the givne path. This
|
||||
requires the -session flag to be set. The key must be
|
||||
held by the session in order to be unlocked. The
|
||||
default value is false.
|
||||
|
||||
-session=<string> User-defined identifer for this session as a string.
|
||||
This is commonly used with the -acquire and -release
|
||||
operations to build robust locking, but it can be set
|
||||
on any key. The default value is empty (no session).
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *KVPutCommand) Run(args []string) int {
|
||||
cmdFlags := flag.NewFlagSet("get", flag.ContinueOnError)
|
||||
cmdFlags.Usage = func() { c.Ui.Output(c.Help()) }
|
||||
httpAddr := HTTPAddrFlag(cmdFlags)
|
||||
datacenter := cmdFlags.String("datacenter", "", "")
|
||||
token := cmdFlags.String("token", "", "")
|
||||
cas := cmdFlags.Bool("cas", false, "")
|
||||
flags := cmdFlags.Uint64("flags", 0, "")
|
||||
modifyIndex := cmdFlags.Uint64("modify-index", 0, "")
|
||||
session := cmdFlags.String("session", "", "")
|
||||
acquire := cmdFlags.Bool("acquire", false, "")
|
||||
release := cmdFlags.Bool("release", false, "")
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check for arg validation
|
||||
args = cmdFlags.Args()
|
||||
key, data, err := c.dataFromArgs(args)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error! %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Session is reauired for release or acquire
|
||||
if (*release || *acquire) && *session == "" {
|
||||
c.Ui.Error("Error! Missing -session (required with -acquire and -release)")
|
||||
return 1
|
||||
}
|
||||
|
||||
// ModifyIndex is required for CAS
|
||||
if *cas && *modifyIndex == 0 {
|
||||
c.Ui.Error("Must specify -modify-index with -cas!")
|
||||
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
|
||||
}
|
||||
|
||||
pair := &api.KVPair{
|
||||
Key: key,
|
||||
ModifyIndex: *modifyIndex,
|
||||
Flags: *flags,
|
||||
Value: []byte(data),
|
||||
Session: *session,
|
||||
}
|
||||
|
||||
wo := &api.WriteOptions{
|
||||
Datacenter: *datacenter,
|
||||
Token: *token,
|
||||
}
|
||||
|
||||
switch {
|
||||
case *cas:
|
||||
ok, _, err := client.KV().CAS(pair, wo)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error! Did not write to %s: %s", key, err))
|
||||
return 1
|
||||
}
|
||||
if !ok {
|
||||
c.Ui.Error(fmt.Sprintf("Error! Did not write to %s: CAS failed", key))
|
||||
return 1
|
||||
}
|
||||
|
||||
c.Ui.Info(fmt.Sprintf("Success! Data written to: %s", key))
|
||||
return 0
|
||||
case *acquire:
|
||||
ok, _, err := client.KV().Acquire(pair, wo)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error! Failed writing data: %s", err))
|
||||
return 1
|
||||
}
|
||||
if !ok {
|
||||
c.Ui.Error("Error! Did not acquire lock")
|
||||
return 1
|
||||
}
|
||||
|
||||
c.Ui.Info(fmt.Sprintf("Success! Lock acquired on: %s", key))
|
||||
return 0
|
||||
case *release:
|
||||
ok, _, err := client.KV().Release(pair, wo)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error! Failed writing data: %s", key))
|
||||
return 1
|
||||
}
|
||||
if !ok {
|
||||
c.Ui.Error("Error! Did not release lock")
|
||||
return 1
|
||||
}
|
||||
|
||||
c.Ui.Info(fmt.Sprintf("Success! Lock released on: %s", key))
|
||||
return 0
|
||||
default:
|
||||
if _, err := client.KV().Put(pair, wo); err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error! Failed writing data: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
c.Ui.Info(fmt.Sprintf("Success! Data written to: %s", key))
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func (c *KVPutCommand) Synopsis() string {
|
||||
return "Sets or updates data in the KV store"
|
||||
}
|
||||
|
||||
func (c *KVPutCommand) dataFromArgs(args []string) (string, string, error) {
|
||||
var stdin io.Reader = os.Stdin
|
||||
if c.testStdin != nil {
|
||||
stdin = c.testStdin
|
||||
}
|
||||
|
||||
switch len(args) {
|
||||
case 0:
|
||||
return "", "", fmt.Errorf("Missing KEY argument")
|
||||
case 1:
|
||||
return args[0], "", nil
|
||||
case 2:
|
||||
default:
|
||||
return "", "", fmt.Errorf("Too many arguments (expected 1 or 2, got %d)", len(args))
|
||||
}
|
||||
|
||||
key := args[0]
|
||||
data := args[1]
|
||||
|
||||
switch data[0] {
|
||||
case '@':
|
||||
data, err := ioutil.ReadFile(data[1:])
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("Failed to read file: %s", err)
|
||||
}
|
||||
return key, string(data), nil
|
||||
case '-':
|
||||
var b bytes.Buffer
|
||||
if _, err := io.Copy(&b, stdin); err != nil {
|
||||
return "", "", fmt.Errorf("Failed to read stdin: %s", err)
|
||||
}
|
||||
return key, b.String(), nil
|
||||
default:
|
||||
return key, data, nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,284 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func TestKVPutCommand_implements(t *testing.T) {
|
||||
var _ cli.Command = &KVPutCommand{}
|
||||
}
|
||||
|
||||
func TestKVPutCommand_noTabs(t *testing.T) {
|
||||
assertNoTabs(t, new(KVPutCommand))
|
||||
}
|
||||
|
||||
func TestKVPutCommand_Validation(t *testing.T) {
|
||||
ui := new(cli.MockUi)
|
||||
c := &KVPutCommand{Ui: ui}
|
||||
|
||||
cases := map[string]struct {
|
||||
args []string
|
||||
output string
|
||||
}{
|
||||
"-acquire without -session": {
|
||||
[]string{"-acquire", "foo"},
|
||||
"Missing -session",
|
||||
},
|
||||
"-release without -session": {
|
||||
[]string{"-release", "foo"},
|
||||
"Missing -session",
|
||||
},
|
||||
"-cas no -modify-index": {
|
||||
[]string{"-cas", "foo"},
|
||||
"Must specify -modify-index",
|
||||
},
|
||||
"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 TestKVPutCommand_Run(t *testing.T) {
|
||||
srv, client := testAgentWithAPIClient(t)
|
||||
defer srv.Shutdown()
|
||||
waitForLeader(t, srv.httpAddr)
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
c := &KVPutCommand{Ui: ui}
|
||||
|
||||
args := []string{
|
||||
"-http-addr=" + srv.httpAddr,
|
||||
"foo", "bar",
|
||||
}
|
||||
|
||||
code := c.Run(args)
|
||||
if code != 0 {
|
||||
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
|
||||
}
|
||||
|
||||
data, _, err := client.KV().Get("foo", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(data.Value, []byte("bar")) {
|
||||
t.Errorf("bad: %#v", data.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKVPutCommand_File(t *testing.T) {
|
||||
srv, client := testAgentWithAPIClient(t)
|
||||
defer srv.Shutdown()
|
||||
waitForLeader(t, srv.httpAddr)
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
c := &KVPutCommand{Ui: ui}
|
||||
|
||||
f, err := ioutil.TempFile("", "kv-put-command-file")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %#v", err)
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
if _, err := f.WriteString("bar"); err != nil {
|
||||
t.Fatalf("err: %#v", err)
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-http-addr=" + srv.httpAddr,
|
||||
"foo", "@" + f.Name(),
|
||||
}
|
||||
|
||||
code := c.Run(args)
|
||||
if code != 0 {
|
||||
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
|
||||
}
|
||||
|
||||
data, _, err := client.KV().Get("foo", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(data.Value, []byte("bar")) {
|
||||
t.Errorf("bad: %#v", data.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKVPutCommand_FileNoExist(t *testing.T) {
|
||||
ui := new(cli.MockUi)
|
||||
c := &KVPutCommand{Ui: ui}
|
||||
|
||||
args := []string{
|
||||
"foo", "@/nope/definitely/not-a-real-file.txt",
|
||||
}
|
||||
|
||||
code := c.Run(args)
|
||||
if code == 0 {
|
||||
t.Fatal("bad: expected error")
|
||||
}
|
||||
|
||||
output := ui.ErrorWriter.String()
|
||||
if !strings.Contains(output, "Failed to read file") {
|
||||
t.Errorf("bad: %#v", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKVPutCommand_Stdin(t *testing.T) {
|
||||
srv, client := testAgentWithAPIClient(t)
|
||||
defer srv.Shutdown()
|
||||
waitForLeader(t, srv.httpAddr)
|
||||
|
||||
stdinR, stdinW := io.Pipe()
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
c := &KVPutCommand{
|
||||
Ui: ui,
|
||||
testStdin: stdinR,
|
||||
}
|
||||
|
||||
go func() {
|
||||
stdinW.Write([]byte("bar"))
|
||||
stdinW.Close()
|
||||
}()
|
||||
|
||||
args := []string{
|
||||
"-http-addr=" + srv.httpAddr,
|
||||
"foo", "-",
|
||||
}
|
||||
|
||||
code := c.Run(args)
|
||||
if code != 0 {
|
||||
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
|
||||
}
|
||||
|
||||
data, _, err := client.KV().Get("foo", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(data.Value, []byte("bar")) {
|
||||
t.Errorf("bad: %#v", data.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKVPutCommand_Flags(t *testing.T) {
|
||||
srv, client := testAgentWithAPIClient(t)
|
||||
defer srv.Shutdown()
|
||||
waitForLeader(t, srv.httpAddr)
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
c := &KVPutCommand{Ui: ui}
|
||||
|
||||
args := []string{
|
||||
"-http-addr=" + srv.httpAddr,
|
||||
"-flags", "12345",
|
||||
"foo",
|
||||
}
|
||||
|
||||
code := c.Run(args)
|
||||
if code != 0 {
|
||||
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
|
||||
}
|
||||
|
||||
data, _, err := client.KV().Get("foo", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if data.Flags != 12345 {
|
||||
t.Errorf("bad: %#v", data.Flags)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKVPutCommand_CAS(t *testing.T) {
|
||||
srv, client := testAgentWithAPIClient(t)
|
||||
defer srv.Shutdown()
|
||||
waitForLeader(t, srv.httpAddr)
|
||||
|
||||
// Create the initial pair so it has a ModifyIndex.
|
||||
pair := &api.KVPair{
|
||||
Key: "foo",
|
||||
Value: []byte("bar"),
|
||||
}
|
||||
if _, err := client.KV().Put(pair, nil); err != nil {
|
||||
t.Fatalf("err: %#v", err)
|
||||
}
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
c := &KVPutCommand{Ui: ui}
|
||||
|
||||
args := []string{
|
||||
"-http-addr=" + srv.httpAddr,
|
||||
"-cas",
|
||||
"-modify-index", "123",
|
||||
"foo", "a",
|
||||
}
|
||||
|
||||
code := c.Run(args)
|
||||
if code == 0 {
|
||||
t.Fatalf("bad: expected error")
|
||||
}
|
||||
|
||||
data, _, err := client.KV().Get("foo", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Reset buffers
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
args = []string{
|
||||
"-http-addr=" + srv.httpAddr,
|
||||
"-cas",
|
||||
"-modify-index", strconv.FormatUint(data.ModifyIndex, 10),
|
||||
"foo", "a",
|
||||
}
|
||||
|
||||
code = c.Run(args)
|
||||
if code != 0 {
|
||||
t.Fatalf("bad: %d. %#v", code, ui.ErrorWriter.String())
|
||||
}
|
||||
|
||||
data, _, err = client.KV().Get("foo", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(data.Value, []byte("a")) {
|
||||
t.Errorf("bad: %#v", data.Value)
|
||||
}
|
||||
}
|
|
@ -2,16 +2,20 @@ package command
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/hashicorp/consul/command/agent"
|
||||
"github.com/hashicorp/consul/consul"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/consul/command/agent"
|
||||
"github.com/hashicorp/consul/consul"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
var offset uint64
|
||||
|
@ -42,6 +46,15 @@ func testAgent(t *testing.T) *agentWrapper {
|
|||
return testAgentWithConfig(t, func(c *agent.Config) {})
|
||||
}
|
||||
|
||||
func testAgentWithAPIClient(t *testing.T) (*agentWrapper, *api.Client) {
|
||||
agent := testAgentWithConfig(t, func(c *agent.Config) {})
|
||||
client, err := api.NewClient(&api.Config{Address: agent.httpAddr})
|
||||
if err != nil {
|
||||
t.Fatalf("consul client: %#v", err)
|
||||
}
|
||||
return agent, client
|
||||
}
|
||||
|
||||
func testAgentWithConfig(t *testing.T, cb func(c *agent.Config)) *agentWrapper {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
|
@ -126,3 +139,9 @@ func nextConfig() *agent.Config {
|
|||
|
||||
return conf
|
||||
}
|
||||
|
||||
func assertNoTabs(t *testing.T, c cli.Command) {
|
||||
if strings.ContainsRune(c.Help(), '\t') {
|
||||
t.Errorf("%#v help output contains tabs", c)
|
||||
}
|
||||
}
|
||||
|
|
24
commands.go
24
commands.go
|
@ -53,6 +53,30 @@ func init() {
|
|||
}, nil
|
||||
},
|
||||
|
||||
"kv": func() (cli.Command, error) {
|
||||
return &command.KVCommand{
|
||||
Ui: ui,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"kv delete": func() (cli.Command, error) {
|
||||
return &command.KVDeleteCommand{
|
||||
Ui: ui,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"kv get": func() (cli.Command, error) {
|
||||
return &command.KVGetCommand{
|
||||
Ui: ui,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"kv put": func() (cli.Command, error) {
|
||||
return &command.KVPutCommand{
|
||||
Ui: ui,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"join": func() (cli.Command, error) {
|
||||
return &command.JoinCommand{
|
||||
Ui: ui,
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
source "https://rubygems.org"
|
||||
|
||||
gem "middleman-hashicorp", github: "hashicorp/middleman-hashicorp"
|
||||
gem "middleman-hashicorp", git: "https://github.com/hashicorp/middleman-hashicorp.git"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
GIT
|
||||
remote: git://github.com/hashicorp/middleman-hashicorp.git
|
||||
remote: https://github.com/hashicorp/middleman-hashicorp.git
|
||||
revision: 80ddc227b26cbbb3742d14396f26172174222080
|
||||
specs:
|
||||
middleman-hashicorp (0.2.0)
|
||||
|
@ -124,7 +124,7 @@ GEM
|
|||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2016.0521)
|
||||
mini_portile2 (2.1.0)
|
||||
minitest (5.9.0)
|
||||
minitest (5.9.1)
|
||||
multi_json (1.12.1)
|
||||
nokogiri (1.6.8)
|
||||
mini_portile2 (~> 2.1.0)
|
||||
|
@ -191,4 +191,4 @@ DEPENDENCIES
|
|||
middleman-hashicorp!
|
||||
|
||||
BUNDLED WITH
|
||||
1.11.2
|
||||
1.13.1
|
||||
|
|
|
@ -171,6 +171,14 @@ body.layout-intro{
|
|||
padding-left: 3%;
|
||||
padding-bottom: 80px;
|
||||
|
||||
.alert {
|
||||
a {
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.lead{
|
||||
margin-bottom: 48px
|
||||
}
|
||||
|
|
|
@ -51,6 +51,10 @@ pre {
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
tt {
|
||||
font-size: 18px;
|
||||
font-family: "Menlo", "Monaco", "Courier New", monospace;
|
||||
}
|
||||
|
||||
//fixed grid below 992 to prevent smaller responsive sizes
|
||||
@media (max-width: 992px) {
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
* `-http-addr=<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>` - Name of the datacenter to query. If unspecified, the
|
||||
query will default to the datacenter of the Consul agent at the HTTP address.
|
||||
|
||||
* `-token=<value>` - 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.
|
|
@ -27,6 +27,7 @@ usage: consul [--version] [--help] <command> [<args>]
|
|||
|
||||
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
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
---
|
||||
layout: "docs"
|
||||
page_title: "Commands: KV"
|
||||
sidebar_current: "docs-commands-kv"
|
||||
---
|
||||
|
||||
# Consul KV
|
||||
|
||||
Command: `consul kv`
|
||||
|
||||
The `kv` command is used to interact with Consul's key-value store via the
|
||||
command line. It exposes top-level commands for inserting, updating, reading,
|
||||
and deleting from the store.
|
||||
|
||||
The key-value store is also accessible via the
|
||||
[HTTP API](docs/agent/http/kv.html).
|
||||
|
||||
## Usage
|
||||
|
||||
Usage: `consul kv <subcommand>`
|
||||
|
||||
For the exact documentation for your Consul version, run `consul kv -h` to view
|
||||
the complete list of subcommands.
|
||||
|
||||
```text
|
||||
Usage: consul kv <subcommand> [options] [args]
|
||||
|
||||
# ...
|
||||
|
||||
Subcommands:
|
||||
|
||||
delete Removes data from the KV store
|
||||
get Retrieves or lists data from the KV store
|
||||
put Sets or updates data in the KV store
|
||||
```
|
||||
|
||||
For more information, examples, and usage about a subcommand, click on the name
|
||||
of the subcommand in the sidebar or one of the links below:
|
||||
|
||||
- [delete](/docs/commands/kv/delete.html)
|
||||
- [get](/docs/commands/kv/get.html)
|
||||
- [put](/docs/commands/kv/put.html)
|
||||
|
||||
## Basic Examples
|
||||
|
||||
To create or update the key named "redis/config/connections" to the value "5" in
|
||||
Consul's key-value store:
|
||||
|
||||
```text
|
||||
$ consul kv put redis/config/connections 5
|
||||
Success! Data written to: redis/config/connections
|
||||
```
|
||||
|
||||
To read a value back from Consul:
|
||||
|
||||
```text
|
||||
$ consul kv get redis/config/connections
|
||||
5
|
||||
```
|
||||
|
||||
Or you can query for detailed information:
|
||||
|
||||
```text
|
||||
$ consul kv get -detailed redis/config/connections
|
||||
CreateIndex 336
|
||||
Flags 0
|
||||
Key redis/config/connections
|
||||
LockIndex 0
|
||||
ModifyIndex 336
|
||||
Session -
|
||||
Value 5
|
||||
```
|
||||
|
||||
Finally, deleting a key is just as easy:
|
||||
|
||||
```text
|
||||
$ consul kv delete redis/config/connections
|
||||
Success! Data deleted at key: redis/config/connections
|
||||
```
|
||||
|
||||
For more examples, ask for subcommand help or view the subcommand documentation
|
||||
by clicking on one of the links in the sidebar.
|
|
@ -0,0 +1,85 @@
|
|||
---
|
||||
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. Specifying this value also
|
||||
requires the -modify-index flag to be set. The default value is false.
|
||||
|
||||
* `-modify-index=<int>` - Unsigned integer representing the ModifyIndex of the
|
||||
key. This is used in combination with the -cas flag.
|
||||
|
||||
* `-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
|
||||
```
|
||||
|
||||
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!
|
||||
```
|
|
@ -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 retrieve 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>` - 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="<string>"`.
|
||||
|
||||
```
|
||||
$ 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/
|
||||
```
|
|
@ -0,0 +1,130 @@
|
|||
---
|
||||
layout: "docs"
|
||||
page_title: "Commands: KV Put"
|
||||
sidebar_current: "docs-commands-kv-put"
|
||||
---
|
||||
|
||||
# Consul KV Put
|
||||
|
||||
Command: `consul kv put`
|
||||
|
||||
The `kv put` command writes the data to the given path in the key-value store.
|
||||
|
||||
## Usage
|
||||
|
||||
Usage: `consul kv put [options] KEY [DATA]`
|
||||
|
||||
#### API Options
|
||||
|
||||
<%= partial "docs/commands/http_api_options" %>
|
||||
|
||||
#### KV Put Options
|
||||
|
||||
* `-acquire` - Obtain a lock on the key. If the key does not exist, this
|
||||
operation will create the key and obtain the lock. The session must already
|
||||
exist and be specified via the -session flag. The default value is false.
|
||||
|
||||
* `-cas` - Perform a Check-And-Set operation. Specifying this value also
|
||||
requires the -modify-index flag to be set. The default value is false.
|
||||
|
||||
* `-flags=<int>` - Unsigned integer value to assign to this key-value pair. This
|
||||
value is not read by Consul, so clients can use this value however makes sense
|
||||
for their use case. The default value is 0 (no flags).
|
||||
|
||||
* `-modify-index=<int>` - Unsigned integer representing the ModifyIndex of the
|
||||
key. This is used in combination with the -cas flag.
|
||||
|
||||
* `-release` - Forfeit the lock on the key at the givne path. This requires the
|
||||
-session flag to be set. The key must be held by the session in order to be
|
||||
unlocked. The default value is false.
|
||||
|
||||
* `-session=<string>` - User-defined identifer for this session as a string.
|
||||
This is commonly used with the -acquire and -release operations to build
|
||||
robust locking, but it can be set on any key. The default value is empty (no
|
||||
session).
|
||||
|
||||
## Examples
|
||||
|
||||
To insert a value of "5" for the key named "redis/config/connections" in the
|
||||
key-value store:
|
||||
|
||||
```
|
||||
$ consul kv put redis/config/connections 5
|
||||
Success! Data written to: redis/config/connections
|
||||
```
|
||||
|
||||
If no data is specified, the key will be created with empty data:
|
||||
|
||||
```
|
||||
$ consul kv put redis/config/connections
|
||||
Success! Data written to: redis/config/connections
|
||||
```
|
||||
|
||||
!> **Be careful when overwriting data!** The above operation would overwrite
|
||||
the value at the key to the empty value.
|
||||
|
||||
For longer or sensitive values, it is possible to read from a file by prefixing
|
||||
with the `@` symbol:
|
||||
|
||||
```
|
||||
$ consul kv put redis/config/password @password.txt
|
||||
Success! Data written to: redis/config/connections
|
||||
```
|
||||
|
||||
Or read values from stdin by specifying the `-` symbol:
|
||||
|
||||
```
|
||||
$ echo "5" | consul kv put redis/config/password -
|
||||
Success! Data written to: redis/config/connections
|
||||
|
||||
$ consul kv put redis/config/password -
|
||||
5
|
||||
<CTRL+D>
|
||||
Success! Data written to: redis/config/connections
|
||||
```
|
||||
|
||||
~> For secret and sensitive values, you should consider using a secret
|
||||
management solution like **[HashiCorp's Vault](https://www.vaultproject.io/)**.
|
||||
While it is possible to secure values in Consul's KV store, Vault provides a
|
||||
more robust interface for secret management.
|
||||
|
||||
To only update 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 put -cas -modify-index=123 redis/config/connections 10
|
||||
Error! Did not write to redis/config/connections: CAS failed
|
||||
|
||||
$ consul kv put -cas -modify-index=456 redis/config/connections 10
|
||||
Success! Data written to: redis/config/connections
|
||||
```
|
||||
|
||||
To specify flags on the key, use the `-flags` option. These flags are completely
|
||||
controlled by the user:
|
||||
|
||||
```
|
||||
$ consul kv put -flags=42 redis/config/password s3cr3t
|
||||
Success! Data written to: redis/config/password
|
||||
```
|
||||
|
||||
To create or tune a lock, use the `-acquire` and `-session` flags. Note that the session must already exist (this command will not create it or manage it):
|
||||
|
||||
```
|
||||
$ consul kv put -acquire -session=abc123 redis/lock/update
|
||||
Success! Lock acquired on: redis/lock/update
|
||||
```
|
||||
|
||||
When you are finished, release the lock:
|
||||
|
||||
```
|
||||
$ consul kv put -release -session=acb123 redis/lock/update
|
||||
Success! Lock released on: redis/lock/update
|
||||
```
|
||||
|
||||
~> **Warning!** If you are trying to build a locking mechanism with these
|
||||
low-level primitives, you may want to look at the [<tt>consul
|
||||
lock</tt>](/docs/commands/lock.html) command. It provides higher-level
|
||||
functionality without exposing the internal APIs of Consul.
|
|
@ -99,6 +99,21 @@
|
|||
<a href="/docs/commands/keyring.html">keyring</a>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-commands-kv") %>>
|
||||
<a href="/docs/commands/kv.html">kv</a>
|
||||
<ul class="subnav">
|
||||
<li<%= sidebar_current("docs-commands-kv-delete") %>>
|
||||
<a href="/docs/commands/kv/delete.html">delete</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-commands-kv-get") %>>
|
||||
<a href="/docs/commands/kv/get.html">get</a>
|
||||
</li>
|
||||
<li<%= sidebar_current("docs-commands-kv-put") %>>
|
||||
<a href="/docs/commands/kv/put.html">put</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li<%= sidebar_current("docs-commands-leave") %>>
|
||||
<a href="/docs/commands/leave.html">leave</a></li>
|
||||
|
||||
|
|
Loading…
Reference in New Issue