diff --git a/build b/build index 0c74f4f24..459ae8036 100755 --- a/build +++ b/build @@ -12,3 +12,4 @@ ln -s ${PWD} $GOPATH/src/${REPO_PATH} eval $(go env) go build -o bin/etcd ${REPO_PATH} +go build -o bin/etcdctl ${REPO_PATH}/etcdctl diff --git a/etcdctl/README.md b/etcdctl/README.md new file mode 100644 index 000000000..7762ee70a --- /dev/null +++ b/etcdctl/README.md @@ -0,0 +1,267 @@ +etcdctl +======== + +[![Build Status](https://travis-ci.org/coreos/etcdctl.png)](https://travis-ci.org/coreos/etcdctl) + +`etcdctl` is a command line client for [etcd][etcd]. +It can be used in scripts or for administrators to explore an etcd cluster. + +[etcd]: https://github.com/coreos/etcd + + +## Getting etcdctl + +The latest release is available as a binary at [Github][github-release] along with etcd. + +[github-release]: https://github.com/coreos/etcd/releases/ + +You can also build etcdctl from source: + +``` +./build +``` + + +## Usage + +### Setting Key Values + +Set a value on the `/foo/bar` key: + +``` +$ etcdctl set /foo/bar "Hello world" +Hello world +``` + +Set a value on the `/foo/bar` key with a value that expires in 60 seconds: + +``` +$ etcdctl set /foo/bar "Hello world" --ttl 60 +Hello world +``` + +Conditionally set a value on `/foo/bar` if the previous value was "Hello world": + +``` +$ etcdctl set /foo/bar "Goodbye world" --swap-with-value "Hello world" +Goodbye world +``` + +Conditionally set a value on `/foo/bar` if the previous etcd index was 12: + +``` +$ etcdctl set /foo/bar "Goodbye world" --swap-with-index 12 +Goodbye world +``` + +Create a new key `/foo/bar`, only if the key did not previously exist: + +``` +$ etcdctl mk /foo/new_bar "Hello world" +Hello world +``` + +Create a new dir `/fooDir`, only if the key did not previously exist: + +``` +$ etcdctl mkdir /fooDir +``` + +Update an existing key `/foo/bar`, only if the key already existed: + +``` +$ etcdctl update /foo/bar "Hola mundo" +Hola mundo +``` + +Create or update a directory called `/mydir`: + +``` +$ etcdctl setDir /mydir +``` + + +### Retrieving a key value + +Get the current value for a single key in the local etcd node: + +``` +$ etcdctl get /foo/bar +Hello world +``` + +Get the current value for a key within the cluster: + +``` +$ etcdctl get /foo/bar --consistent +Hello world +``` + +Get the value of a key with additional metadata in a parseable format: + +``` +$ etcdctl -o extended get /foo/bar +Key: /foo/bar +Modified-Index: 72 +TTL: 0 +Etcd-Index: 72 +Raft-Index: 5611 +Raft-Term: 1 + +Hello World +``` + +### Listing a directory + +Explore the keyspace using the `ls` command + +``` +$ etcdctl ls +/akey +/adir +$ etcdctl ls /adir +/adir/key1 +/adir/key2 +``` + +Add `--recursive` to recursively list subdirectories encountered. + +``` +$ etcdctl ls --recursive +/akey +/adir +/adir/key1 +/adir/key2 +``` + + +### Deleting a key + +Delete a key: + +``` +$ etcdctl rm /foo/bar +``` + +Delete an empty directory or a key-value pair + +``` +$ etcdctl rmdir /path/to/dir +``` + +or + +``` +$ etcdctl rm /path/to/dir --dir +``` + +Recursively delete a key and all child keys: + +``` +$ etcdctl rm /path/to/dir --recursive +``` + +Conditionally delete `/foo/bar` if the previous value was "Hello world": + +``` +$ etcdctl rm /foo/bar --with-value "Hello world" +``` + +Conditionally delete `/foo/bar` if the previous etcd index was 12: + +``` +$ etcdctl rm /foo/bar --with-index 12 +``` + +### Watching for changes + +Watch for only the next change on a key: + +``` +$ etcdctl watch /foo/bar +Hello world +``` + +Continuously watch a key: + +``` +$ etcdctl watch /foo/bar --forever +Hello world +.... client hangs forever until ctrl+C printing values as key change +``` + +Continuously watch a key, starting with a given etcd index: + +``` +$ etcdctl watch /foo/bar --forever --index 12 +Hello world +.... client hangs forever until ctrl+C printing values as key change +``` + +Continuously watch a key and exec a program: + +``` +$ etcdctl exec-watch /foo/bar -- sh -c "env | grep ETCD" +ETCD_WATCH_ACTION=set +ETCD_VALUE=My configuration stuff +ETCD_MODIFIED_INDEX=1999 +ETCD_KEY=/foo/bar +ETCD_WATCH_ACTION=set +ETCD_VALUE=My new configuration stuff +ETCD_MODIFIED_INDEX=2000 +ETCD_KEY=/foo/bar +``` + +Continuously and recursively watch a key and exec a program: +``` +$ etcdctl exec-watch --recursive /foo -- sh -c "env | grep ETCD" +ETCD_WATCH_ACTION=set +ETCD_VALUE=My configuration stuff +ETCD_MODIFIED_INDEX=1999 +ETCD_KEY=/foo/bar +ETCD_WATCH_ACTION=set +ETCD_VALUE=My new configuration stuff +ETCD_MODIFIED_INDEX=2000 +ETCD_KEY=/foo/barbar +``` + +## Return Codes + +The following exit codes can be returned from etcdctl: + +``` +0 Success +1 Malformed etcdctl arguments +2 Failed to connect to host +3 Failed to auth (client cert rejected, ca validation failure, etc) +4 400 error from etcd +5 500 error from etcd +``` + +## Peers + +If your etcd cluster isn't available on `http://127.0.0.1:4001` you can specify +a `--peers` flag or `ETCDCTL_PEERS` environment variable. You can list one peer, +or a comma-separated list of peers. + +``` +ETCDCTL_PEERS="http://10.0.28.1:4002" etcdctl set my-key to-a-value +ETCDCTL_PEERS="http://10.0.28.1:4002,http://10.0.28.2:4002,http://10.0.28.3:4002" etcdctl set my-key to-a-value +etcdctl --peers http://10.0.28.1:4002 my-key to-a-value +etcdctl --peers http://10.0.28.1:4002,http://10.0.28.2:4002,http://10.0.28.3:4002 etcdctl set my-key to-a-value +``` + +## Project Details + +### Versioning + +etcdctl uses [semantic versioning][semver]. +Releases will follow lockstep with the etcd release cycle. + +[semver]: http://semver.org/ + +### License + +etcdctl is under the Apache 2.0 license. See the [LICENSE][license] file for details. + +[license]: https://github.com/coreos/etcdctl/blob/master/LICENSE diff --git a/etcdctl/command/error.go b/etcdctl/command/error.go new file mode 100644 index 000000000..b9ff2d046 --- /dev/null +++ b/etcdctl/command/error.go @@ -0,0 +1,19 @@ +package command + +import ( + "fmt" + "os" +) + +const ( + SUCCESS = iota + MalformedEtcdctlArguments + FailedToConnectToHost + FailedToAuth + ErrorFromEtcd +) + +func handleError(code int, err error) { + fmt.Fprintln(os.Stderr, "Error: ", err) + os.Exit(code) +} diff --git a/etcdctl/command/exec_watch_command.go b/etcdctl/command/exec_watch_command.go new file mode 100644 index 000000000..d96ffde62 --- /dev/null +++ b/etcdctl/command/exec_watch_command.go @@ -0,0 +1,105 @@ +package command + +import ( + "errors" + "fmt" + "io" + "os" + "os/exec" + "os/signal" + + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli" + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd" +) + +// NewExecWatchCommand returns the CLI command for "exec-watch". +func NewExecWatchCommand() cli.Command { + return cli.Command{ + Name: "exec-watch", + Usage: "watch a key for changes and exec an executable", + Flags: []cli.Flag{ + cli.IntFlag{"after-index", 0, "watch after the given index"}, + cli.BoolFlag{"recursive", "watch all values for key and child keys"}, + }, + Action: func(c *cli.Context) { + handleKey(c, execWatchCommandFunc) + }, + } +} + +// execWatchCommandFunc executes the "exec-watch" command. +func execWatchCommandFunc(c *cli.Context, client *etcd.Client) (*etcd.Response, error) { + _ = io.Copy + _ = exec.Command + args := c.Args() + argsLen := len(args) + + if argsLen < 2 { + return nil, errors.New("Key and command to exec required") + } + + key := args[argsLen-1] + cmdArgs := args[:argsLen-1] + + index := 0 + if c.Int("after-index") != 0 { + index = c.Int("after-index") + 1 + key = args[0] + cmdArgs = args[2:] + } + + recursive := c.Bool("recursive") + if recursive != false { + key = args[0] + cmdArgs = args[2:] + } + + sigch := make(chan os.Signal, 1) + signal.Notify(sigch, os.Interrupt) + stop := make(chan bool) + + go func() { + <-sigch + stop <- true + os.Exit(0) + }() + + receiver := make(chan *etcd.Response) + client.SetConsistency(etcd.WEAK_CONSISTENCY) + go client.Watch(key, uint64(index), recursive, receiver, stop) + + for { + resp := <-receiver + cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) + cmd.Env = environResponse(resp, os.Environ()) + + stdout, err := cmd.StdoutPipe() + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + stderr, err := cmd.StderrPipe() + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + err = cmd.Start() + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + go io.Copy(os.Stdout, stdout) + go io.Copy(os.Stderr, stderr) + cmd.Wait() + } + + return nil, nil +} + +func environResponse(resp *etcd.Response, env []string) []string { + env = append(env, "ETCD_WATCH_ACTION="+resp.Action) + env = append(env, "ETCD_WATCH_MODIFIED_INDEX="+fmt.Sprintf("%d", resp.Node.ModifiedIndex)) + env = append(env, "ETCD_WATCH_KEY="+resp.Node.Key) + env = append(env, "ETCD_WATCH_VALUE="+resp.Node.Value) + return env +} diff --git a/etcdctl/command/get_command.go b/etcdctl/command/get_command.go new file mode 100644 index 000000000..696cd3751 --- /dev/null +++ b/etcdctl/command/get_command.go @@ -0,0 +1,60 @@ +package command + +import ( + "errors" + "fmt" + "os" + + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli" + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd" +) + +// NewGetCommand returns the CLI command for "get". +func NewGetCommand() cli.Command { + return cli.Command{ + Name: "get", + Usage: "retrieve the value of a key", + Flags: []cli.Flag{ + cli.BoolFlag{"sort", "returns result in sorted order"}, + cli.BoolFlag{"consistent", "send request to the leader, thereby guranteeing that any earlier writes will be seen by the read"}, + }, + Action: func(c *cli.Context) { + handleGet(c, getCommandFunc) + }, + } +} + +// handleGet handles a request that intends to do get-like operations. +func handleGet(c *cli.Context, fn handlerFunc) { + handlePrint(c, fn, printGet) +} + +// printGet writes error message when getting the value of a directory. +func printGet(resp *etcd.Response, format string) { + if resp.Node.Dir { + fmt.Fprintln(os.Stderr, fmt.Sprintf("%s: is a directory", resp.Node.Key)) + os.Exit(1) + } + + printKey(resp, format) +} + +// getCommandFunc executes the "get" command. +func getCommandFunc(c *cli.Context, client *etcd.Client) (*etcd.Response, error) { + if len(c.Args()) == 0 { + return nil, errors.New("Key required") + } + key := c.Args()[0] + consistent := c.Bool("consistent") + sorted := c.Bool("sort") + + // Setup consistency on the client. + if consistent { + client.SetConsistency(etcd.STRONG_CONSISTENCY) + } else { + client.SetConsistency(etcd.WEAK_CONSISTENCY) + } + + // Retrieve the value from the server. + return client.Get(key, sorted, false) +} diff --git a/etcdctl/command/handle.go b/etcdctl/command/handle.go new file mode 100644 index 000000000..7e1f981d3 --- /dev/null +++ b/etcdctl/command/handle.go @@ -0,0 +1,173 @@ +package command + +import ( + "encoding/json" + "errors" + "fmt" + "net/url" + "os" + "strings" + + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli" + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd" +) + +type handlerFunc func(*cli.Context, *etcd.Client) (*etcd.Response, error) +type printFunc func(*etcd.Response, string) + +// dumpCURL blindly dumps all curl output to os.Stderr +func dumpCURL(client *etcd.Client) { + client.OpenCURL() + for { + fmt.Fprintf(os.Stderr, "Curl-Example: %s\n", client.RecvCURL()) + } +} + +// createHttpPath attaches http scheme to the given address if needed +func createHttpPath(addr string) (string, error) { + u, err := url.Parse(addr) + if err != nil { + return "", err + } + + if u.Scheme == "" { + u.Scheme = "http" + } + return u.String(), nil +} + +// rawhandle wraps the command function handlers and sets up the +// environment but performs no output formatting. +func rawhandle(c *cli.Context, fn handlerFunc) (*etcd.Response, error) { + sync := !c.GlobalBool("no-sync") + + peerstr := c.GlobalString("peers") + + // Use an environment variable if nothing was supplied on the + // command line + if peerstr == "" { + peerstr = os.Getenv("ETCDCTL_PEERS") + } + + // If we still don't have peers, use a default + if peerstr == "" { + peerstr = "127.0.0.1:4001" + } + + peers := strings.Split(peerstr, ",") + + // If no sync, create http path for each peer address + if !sync { + revisedPeers := make([]string, 0) + for _, peer := range peers { + if revisedPeer, err := createHttpPath(peer); err != nil { + fmt.Fprintf(os.Stderr, "Unsupported url %v: %v\n", peer, err) + } else { + revisedPeers = append(revisedPeers, revisedPeer) + } + } + peers = revisedPeers + } + + client := etcd.NewClient(peers) + + if c.GlobalBool("debug") { + go dumpCURL(client) + } + + // Sync cluster. + if sync { + if ok := client.SyncCluster(); !ok { + handleError(FailedToConnectToHost, errors.New("Cannot sync with the cluster using peers "+strings.Join(peers, ", "))) + } + } + + if c.GlobalBool("debug") { + fmt.Fprintf(os.Stderr, "Cluster-Peers: %s\n", + strings.Join(client.GetCluster(), " ")) + } + + // Execute handler function. + return fn(c, client) +} + +// handlePrint wraps the command function handlers to parse global flags +// into a client and to properly format the response objects. +func handlePrint(c *cli.Context, fn handlerFunc, pFn printFunc) { + resp, err := rawhandle(c, fn) + + // Print error and exit, if necessary. + if err != nil { + handleError(ErrorFromEtcd, err) + } + + if resp != nil && pFn != nil { + pFn(resp, c.GlobalString("output")) + } +} + +// handleDir handles a request that wants to do operations on a single dir. +// Dir cannot be printed out, so we set NIL print function here. +func handleDir(c *cli.Context, fn handlerFunc) { + handlePrint(c, fn, nil) +} + +// handleKey handles a request that wants to do operations on a single key. +func handleKey(c *cli.Context, fn handlerFunc) { + handlePrint(c, fn, printKey) +} + +func handleAll(c *cli.Context, fn handlerFunc) { + handlePrint(c, fn, printAll) +} + +// printKey writes the etcd response to STDOUT in the given format. +func printKey(resp *etcd.Response, format string) { + // printKey is only for keys, error on directories + if resp.Node.Dir == true { + fmt.Fprintln(os.Stderr, fmt.Sprintf("Cannot print key [%s: Is a directory]", resp.Node.Key)) + os.Exit(1) + } + printKeyOnly(resp, format) +} + +// printAll prints the etcd response in the given format in its best efforts. +func printAll(resp *etcd.Response, format string) { + if resp.Node.Dir == true { + return + } + printKeyOnly(resp, format) +} + +// printKeyOnly only supports to print key correctly. +func printKeyOnly(resp *etcd.Response, format string) { + // Format the result. + switch format { + case "simple": + fmt.Println(resp.Node.Value) + case "extended": + // Extended prints in a rfc2822 style format + fmt.Println("Key:", resp.Node.Key) + fmt.Println("Created-Index:", resp.Node.CreatedIndex) + fmt.Println("Modified-Index:", resp.Node.ModifiedIndex) + + if resp.PrevNode != nil { + fmt.Println("PrevNode.Value:", resp.PrevNode.Value) + } + + fmt.Println("TTL:", resp.Node.TTL) + fmt.Println("Etcd-Index:", resp.EtcdIndex) + fmt.Println("Raft-Index:", resp.RaftIndex) + fmt.Println("Raft-Term:", resp.RaftTerm) + fmt.Println("") + fmt.Println(resp.Node.Value) + case "json": + b, err := json.Marshal(resp) + if err != nil { + panic(err) + } + fmt.Println(string(b)) + default: + fmt.Fprintln(os.Stderr, "Unsupported output format:", format) + } +} diff --git a/etcdctl/command/ls_command.go b/etcdctl/command/ls_command.go new file mode 100644 index 000000000..12bd89f9f --- /dev/null +++ b/etcdctl/command/ls_command.go @@ -0,0 +1,57 @@ +package command + +import ( + "fmt" + + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli" + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd" +) + +func NewLsCommand() cli.Command { + return cli.Command{ + Name: "ls", + Usage: "retrieve a directory", + Flags: []cli.Flag{ + cli.BoolFlag{"recursive", "returns all values for key and child keys"}, + }, + Action: func(c *cli.Context) { + handleLs(c, lsCommandFunc) + }, + } +} + +// handleLs handles a request that intends to do ls-like operations. +func handleLs(c *cli.Context, fn handlerFunc) { + handlePrint(c, fn, printLs) +} + +// printLs writes a response out in a manner similar to the `ls` command in unix. +// Non-empty directories list their contents and files list their name. +func printLs(resp *etcd.Response, format string) { + if !resp.Node.Dir { + fmt.Println(resp.Node.Key) + } + for _, node := range resp.Node.Nodes { + rPrint(node) + } +} + +// lsCommandFunc executes the "ls" command. +func lsCommandFunc(c *cli.Context, client *etcd.Client) (*etcd.Response, error) { + key := "/" + if len(c.Args()) != 0 { + key = c.Args()[0] + } + recursive := c.Bool("recursive") + + // Retrieve the value from the server. + return client.Get(key, false, recursive) +} + +// rPrint recursively prints out the nodes in the node structure. +func rPrint(n *etcd.Node) { + fmt.Println(n.Key) + for _, node := range n.Nodes { + rPrint(node) + } +} diff --git a/etcdctl/command/mk_command.go b/etcdctl/command/mk_command.go new file mode 100644 index 000000000..7050c013b --- /dev/null +++ b/etcdctl/command/mk_command.go @@ -0,0 +1,39 @@ +package command + +import ( + "errors" + "os" + + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli" + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd" +) + +// NewMakeCommand returns the CLI command for "mk". +func NewMakeCommand() cli.Command { + return cli.Command{ + Name: "mk", + Usage: "make a new key with a given value", + Flags: []cli.Flag{ + cli.IntFlag{"ttl", 0, "key time-to-live"}, + }, + Action: func(c *cli.Context) { + handleKey(c, makeCommandFunc) + }, + } +} + +// makeCommandFunc executes the "make" command. +func makeCommandFunc(c *cli.Context, client *etcd.Client) (*etcd.Response, error) { + if len(c.Args()) == 0 { + return nil, errors.New("Key required") + } + key := c.Args()[0] + value, err := argOrStdin(c.Args(), os.Stdin, 1) + if err != nil { + return nil, errors.New("Value required") + } + + ttl := c.Int("ttl") + + return client.Create(key, value, uint64(ttl)) +} diff --git a/etcdctl/command/mkdir_command.go b/etcdctl/command/mkdir_command.go new file mode 100644 index 000000000..3b096d7fa --- /dev/null +++ b/etcdctl/command/mkdir_command.go @@ -0,0 +1,33 @@ +package command + +import ( + "errors" + + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli" + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd" +) + +// NewMakeDirCommand returns the CLI command for "mkdir". +func NewMakeDirCommand() cli.Command { + return cli.Command{ + Name: "mkdir", + Usage: "make a new directory", + Flags: []cli.Flag{ + cli.IntFlag{"ttl", 0, "key time-to-live"}, + }, + Action: func(c *cli.Context) { + handleDir(c, makeDirCommandFunc) + }, + } +} + +// makeDirCommandFunc executes the "mkdir" command. +func makeDirCommandFunc(c *cli.Context, client *etcd.Client) (*etcd.Response, error) { + if len(c.Args()) == 0 { + return nil, errors.New("Key required") + } + key := c.Args()[0] + ttl := c.Int("ttl") + + return client.CreateDir(key, uint64(ttl)) +} diff --git a/etcdctl/command/rm_command.go b/etcdctl/command/rm_command.go new file mode 100644 index 000000000..d5ba3a9a5 --- /dev/null +++ b/etcdctl/command/rm_command.go @@ -0,0 +1,50 @@ +package command + +import ( + "errors" + + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli" + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd" +) + +// NewRemoveCommand returns the CLI command for "rm". +func NewRemoveCommand() cli.Command { + return cli.Command{ + Name: "rm", + Usage: "remove a key", + Flags: []cli.Flag{ + cli.BoolFlag{"dir", "removes the key if it is an empty directory or a key-value pair"}, + cli.BoolFlag{"recursive", "removes the key and all child keys(if it is a directory)"}, + cli.StringFlag{"with-value", "", "previous value"}, + cli.IntFlag{"with-index", 0, "previous index"}, + }, + Action: func(c *cli.Context) { + handleAll(c, removeCommandFunc) + }, + } +} + +// removeCommandFunc executes the "rm" command. +func removeCommandFunc(c *cli.Context, client *etcd.Client) (*etcd.Response, error) { + if len(c.Args()) == 0 { + return nil, errors.New("Key required") + } + key := c.Args()[0] + recursive := c.Bool("recursive") + dir := c.Bool("dir") + + // TODO: distinguish with flag is not set and empty flag + // the cli pkg need to provide this feature + prevValue := c.String("with-value") + prevIndex := uint64(c.Int("with-index")) + + if prevValue != "" || prevIndex != 0 { + return client.CompareAndDelete(key, prevValue, prevIndex) + } + + if recursive || !dir { + return client.Delete(key, recursive) + } + + return client.DeleteDir(key) +} diff --git a/etcdctl/command/rmdir_command.go b/etcdctl/command/rmdir_command.go new file mode 100644 index 000000000..9f6ca1574 --- /dev/null +++ b/etcdctl/command/rmdir_command.go @@ -0,0 +1,29 @@ +package command + +import ( + "errors" + + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli" + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd" +) + +// NewRemoveCommand returns the CLI command for "rmdir". +func NewRemoveDirCommand() cli.Command { + return cli.Command{ + Name: "rmdir", + Usage: "removes the key if it is an empty directory or a key-value pair", + Action: func(c *cli.Context) { + handleDir(c, removeDirCommandFunc) + }, + } +} + +// removeDirCommandFunc executes the "rmdir" command. +func removeDirCommandFunc(c *cli.Context, client *etcd.Client) (*etcd.Response, error) { + if len(c.Args()) == 0 { + return nil, errors.New("Key required") + } + key := c.Args()[0] + + return client.DeleteDir(key) +} diff --git a/etcdctl/command/set_command.go b/etcdctl/command/set_command.go new file mode 100644 index 000000000..0ca2525fe --- /dev/null +++ b/etcdctl/command/set_command.go @@ -0,0 +1,47 @@ +package command + +import ( + "errors" + "os" + + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli" + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd" +) + +// NewSetCommand returns the CLI command for "set". +func NewSetCommand() cli.Command { + return cli.Command{ + Name: "set", + Usage: "set the value of a key", + Flags: []cli.Flag{ + cli.IntFlag{"ttl", 0, "key time-to-live"}, + cli.StringFlag{"swap-with-value", "", "previous value"}, + cli.IntFlag{"swap-with-index", 0, "previous index"}, + }, + Action: func(c *cli.Context) { + handleKey(c, setCommandFunc) + }, + } +} + +// setCommandFunc executes the "set" command. +func setCommandFunc(c *cli.Context, client *etcd.Client) (*etcd.Response, error) { + if len(c.Args()) == 0 { + return nil, errors.New("Key required") + } + key := c.Args()[0] + value, err := argOrStdin(c.Args(), os.Stdin, 1) + if err != nil { + return nil, errors.New("Value required") + } + + ttl := c.Int("ttl") + prevValue := c.String("swap-with-value") + prevIndex := c.Int("swap-with-index") + + if prevValue == "" && prevIndex == 0 { + return client.Set(key, value, uint64(ttl)) + } else { + return client.CompareAndSwap(key, value, uint64(ttl), prevValue, uint64(prevIndex)) + } +} diff --git a/etcdctl/command/set_dir_command.go b/etcdctl/command/set_dir_command.go new file mode 100644 index 000000000..101b6908a --- /dev/null +++ b/etcdctl/command/set_dir_command.go @@ -0,0 +1,33 @@ +package command + +import ( + "errors" + + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli" + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd" +) + +// NewSetDirCommand returns the CLI command for "setDir". +func NewSetDirCommand() cli.Command { + return cli.Command{ + Name: "setdir", + Usage: "create a new or existing directory", + Flags: []cli.Flag{ + cli.IntFlag{"ttl", 0, "key time-to-live"}, + }, + Action: func(c *cli.Context) { + handleDir(c, setDirCommandFunc) + }, + } +} + +// setDirCommandFunc executes the "setDir" command. +func setDirCommandFunc(c *cli.Context, client *etcd.Client) (*etcd.Response, error) { + if len(c.Args()) == 0 { + return nil, errors.New("Key required") + } + key := c.Args()[0] + ttl := c.Int("ttl") + + return client.SetDir(key, uint64(ttl)) +} diff --git a/etcdctl/command/update_command.go b/etcdctl/command/update_command.go new file mode 100644 index 000000000..47594b73a --- /dev/null +++ b/etcdctl/command/update_command.go @@ -0,0 +1,39 @@ +package command + +import ( + "errors" + "os" + + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli" + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd" +) + +// NewUpdateCommand returns the CLI command for "update". +func NewUpdateCommand() cli.Command { + return cli.Command{ + Name: "update", + Usage: "update an existing key with a given value", + Flags: []cli.Flag{ + cli.IntFlag{"ttl", 0, "key time-to-live"}, + }, + Action: func(c *cli.Context) { + handleKey(c, updateCommandFunc) + }, + } +} + +// updateCommandFunc executes the "update" command. +func updateCommandFunc(c *cli.Context, client *etcd.Client) (*etcd.Response, error) { + if len(c.Args()) == 0 { + return nil, errors.New("Key required") + } + key := c.Args()[0] + value, err := argOrStdin(c.Args(), os.Stdin, 1) + if err != nil { + return nil, errors.New("Value required") + } + + ttl := c.Int("ttl") + + return client.Update(key, value, uint64(ttl)) +} diff --git a/etcdctl/command/update_dir_command.go b/etcdctl/command/update_dir_command.go new file mode 100644 index 000000000..f13333d91 --- /dev/null +++ b/etcdctl/command/update_dir_command.go @@ -0,0 +1,33 @@ +package command + +import ( + "errors" + + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli" + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd" +) + +// NewUpdateDirCommand returns the CLI command for "updateDir". +func NewUpdateDirCommand() cli.Command { + return cli.Command{ + Name: "updatedir", + Usage: "update an existing directory", + Flags: []cli.Flag{ + cli.IntFlag{"ttl", 0, "key time-to-live"}, + }, + Action: func(c *cli.Context) { + handleDir(c, updateDirCommandFunc) + }, + } +} + +// updateDirCommandFunc executes the "updateDir" command. +func updateDirCommandFunc(c *cli.Context, client *etcd.Client) (*etcd.Response, error) { + if len(c.Args()) == 0 { + return nil, errors.New("Key required") + } + key := c.Args()[0] + ttl := c.Int("ttl") + + return client.UpdateDir(key, uint64(ttl)) +} diff --git a/etcdctl/command/util.go b/etcdctl/command/util.go new file mode 100644 index 000000000..7dc651088 --- /dev/null +++ b/etcdctl/command/util.go @@ -0,0 +1,35 @@ +package command + +import ( + "errors" + "io" + "io/ioutil" + "strings" +) + +var ( + ErrNoAvailSrc = errors.New("no available argument and stdin") +) + +// trimsplit slices s into all substrings separated by sep and returns a +// slice of the substrings between the separator with all leading and trailing +// white space removed, as defined by Unicode. +func trimsplit(s, sep string) []string { + raw := strings.Split(s, ",") + trimmed := make([]string, 0) + for _, r := range raw { + trimmed = append(trimmed, strings.TrimSpace(r)) + } + return trimmed +} + +func argOrStdin(args []string, stdin io.Reader, i int) (string, error) { + if i < len(args) { + return args[i], nil + } + bytes, err := ioutil.ReadAll(stdin) + if string(bytes) == "" || err != nil { + return "", ErrNoAvailSrc + } + return string(bytes), nil +} diff --git a/etcdctl/command/util_test.go b/etcdctl/command/util_test.go new file mode 100644 index 000000000..f47dd9faa --- /dev/null +++ b/etcdctl/command/util_test.go @@ -0,0 +1,56 @@ +package command + +import ( + "bytes" + "testing" +) + +func TestArgOrStdin(t *testing.T) { + tests := []struct { + args []string + stdin string + i int + w string + we error + }{ + { + args: []string{ + "a", + }, + stdin: "b", + i: 0, + w: "a", + we: nil, + }, + { + args: []string{ + "a", + }, + stdin: "b", + i: 1, + w: "b", + we: nil, + }, + { + args: []string{ + "a", + }, + stdin: "", + i: 1, + w: "", + we: ErrNoAvailSrc, + }, + } + + for i, tt := range tests { + var b bytes.Buffer + b.Write([]byte(tt.stdin)) + g, ge := argOrStdin(tt.args, &b, tt.i) + if g != tt.w { + t.Errorf("#%d: expect %v, not %v", i, tt.w, g) + } + if ge != tt.we { + t.Errorf("#%d: expect %v, not %v", i, tt.we, ge) + } + } +} diff --git a/etcdctl/command/watch_command.go b/etcdctl/command/watch_command.go new file mode 100644 index 000000000..8f7f873d2 --- /dev/null +++ b/etcdctl/command/watch_command.go @@ -0,0 +1,85 @@ +package command + +import ( + "errors" + "os" + "os/signal" + + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli" + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/coreos/go-etcd/etcd" +) + +// NewWatchCommand returns the CLI command for "watch". +func NewWatchCommand() cli.Command { + return cli.Command{ + Name: "watch", + Usage: "watch a key for changes", + Flags: []cli.Flag{ + cli.BoolFlag{"forever", "forever watch a key until CTRL+C"}, + cli.IntFlag{"after-index", 0, "watch after the given index"}, + cli.BoolFlag{"recursive", "returns all values for key and child keys"}, + }, + Action: func(c *cli.Context) { + handleKey(c, watchCommandFunc) + }, + } +} + +// watchCommandFunc executes the "watch" command. +func watchCommandFunc(c *cli.Context, client *etcd.Client) (*etcd.Response, error) { + if len(c.Args()) == 0 { + return nil, errors.New("Key required") + } + key := c.Args()[0] + recursive := c.Bool("recursive") + forever := c.Bool("forever") + + index := 0 + if c.Int("after-index") != 0 { + index = c.Int("after-index") + 1 + } + + if forever { + sigch := make(chan os.Signal, 1) + signal.Notify(sigch, os.Interrupt) + stop := make(chan bool) + + go func() { + <-sigch + os.Exit(0) + }() + + receiver := make(chan *etcd.Response) + errCh := make(chan error, 1) + + go func() { + _, err := client.Watch(key, uint64(index), recursive, receiver, stop) + errCh <- err + }() + + for { + select { + case resp := <-receiver: + printAll(resp, c.GlobalString("output")) + case err := <-errCh: + handleError(-1, err) + } + } + + } else { + var resp *etcd.Response + var err error + resp, err = client.Watch(key, uint64(index), recursive, nil, nil) + + if err != nil { + handleError(ErrorFromEtcd, err) + } + + if err != nil { + return nil, err + } + printAll(resp, c.GlobalString("output")) + } + + return nil, nil +} diff --git a/etcdctl/main.go b/etcdctl/main.go new file mode 100644 index 000000000..552a208b7 --- /dev/null +++ b/etcdctl/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "os" + + "github.com/coreos/etcd/Godeps/_workspace/src/github.com/codegangsta/cli" + + "github.com/coreos/etcd/etcdctl/command" + "github.com/coreos/etcd/version" +) + +func main() { + app := cli.NewApp() + app.Name = "etcdctl" + app.Version = version.Version + app.Usage = "A simple command line client for etcd." + app.Flags = []cli.Flag{ + cli.BoolFlag{"debug", "output cURL commands which can be used to reproduce the request"}, + cli.BoolFlag{"no-sync", "don't synchronize cluster information before sending request"}, + cli.StringFlag{"output, o", "simple", "output response in the given format (`simple` or `json`)"}, + cli.StringFlag{"peers, C", "", "a comma-delimited list of machine addresses in the cluster (default: \"127.0.0.1:4001\")"}, + } + app.Commands = []cli.Command{ + command.NewMakeCommand(), + command.NewMakeDirCommand(), + command.NewRemoveCommand(), + command.NewRemoveDirCommand(), + command.NewGetCommand(), + command.NewLsCommand(), + command.NewSetCommand(), + command.NewSetDirCommand(), + command.NewUpdateCommand(), + command.NewUpdateDirCommand(), + command.NewWatchCommand(), + command.NewExecWatchCommand(), + } + app.Run(os.Args) +} diff --git a/test b/test index f8f12a1ec..53813e788 100755 --- a/test +++ b/test @@ -15,9 +15,9 @@ COVER=${COVER:-"-cover"} source ./build # Hack: gofmt ./ will recursively check the .git directory. So use *.go for gofmt. -TESTABLE_AND_FORMATTABLE="client discovery etcdserver etcdserver/etcdhttp etcdserver/etcdserverpb integration pkg pkg/flags pkg/transport proxy raft snap store wait wal" +TESTABLE_AND_FORMATTABLE="client discovery etcdctl/command etcdserver etcdserver/etcdhttp etcdserver/etcdserverpb integration pkg pkg/flags pkg/transport proxy raft snap store wait wal" TESTABLE="$TESTABLE_AND_FORMATTABLE ./" -FORMATTABLE="$TESTABLE_AND_FORMATTABLE *.go" +FORMATTABLE="$TESTABLE_AND_FORMATTABLE *.go etcdctl/" # user has not provided PKG override if [ -z "$PKG" ]; then