From 87dcb2adea5b168242ec62a135fae56b8a62e5ce Mon Sep 17 00:00:00 2001 From: Anthony Romano Date: Fri, 19 Feb 2016 15:48:29 -0800 Subject: [PATCH] etcdctlv3: unify txn interactive mode input with get/put/delete --- etcdctlv3/README.md | 70 +++++++++++++- etcdctlv3/command/del_command.go | 21 +++-- etcdctlv3/command/get_command.go | 23 +++-- etcdctlv3/command/put_command.go | 25 ++++- etcdctlv3/command/txn_command.go | 155 ++++++++++++++++++++----------- 5 files changed, 218 insertions(+), 76 deletions(-) diff --git a/etcdctlv3/README.md b/etcdctlv3/README.md index d3b132532..99611d9fd 100644 --- a/etcdctlv3/README.md +++ b/etcdctlv3/README.md @@ -15,7 +15,7 @@ PUT assigns the specified value with the specified key. If key already holds a v Simple reply -- OK if PUT executed correctly. Exit code is zero. +- OK if PUT executed correctly. Exit code is zero. - Error string if PUT failed. Exit code is non-zero. @@ -93,7 +93,7 @@ TODO: --prefix, --from Simple reply -- The number of keys that were removed in decimal if DEL executed correctly. Exit code is zero. +- The number of keys that were removed in decimal if DEL executed correctly. Exit code is zero. - Error string if DEL failed. Exit code is non-zero. @@ -109,6 +109,72 @@ OK ./etcdctl range foo ``` +### TXN [options] + +TXN applies multiple etcd requests as a single atomic transaction. A transaction consists of list of conditions, a list of requests to apply if all the conditions are true, and a list of requests to apply if any condition is false. + +#### Options + +- hex -- print out keys and values as hex encoded string + +- interactive -- input transaction with interactive mode + +#### Input Format +Interactive mode: +```ebnf + ::= * "\n" "\n" "\n" + ::= (|||) "\n" + ::= "<" | "=" | ">" + := ("c"|"create")"("")" + ::= ("m"|"mod")"("")" + ::= ("val"|"value")"("")" + ::= ("ver"|"version")"("")" + ::= * + ::= * + ::= ((see put, get, del etcdctl command syntax)) "\n" + ::= (%q formatted string) + ::= (%q formatted string) + ::= "\""[0-9]+"\"" + ::= "\""[0-9]+"\"" +``` + +TODO: non-interactive mode + +#### Return value + +Simple reply + +- SUCCESS if etcd processed the transaction success list, FAILURE if etcd processed the transaction failure list. + +- Simple reply for each command executed request list, each separated by a blank line. + +- Additional error string if TXN failed. Exit code is non-zero. + +TODO: probably json and binary encoded proto + +#### Examples + +``` bash +./etcdctl txn -i +mod("key1") > "0" + +put key1 "overwrote-key1" + +put key1 "created-key1" +put key2 "some extra key" + +FAILURE + +OK + +OK +``` + +#### Notes + +TODO: non-interactive mode + + ### WATCH [options] [key or prefix] Watch watches events stream on keys or prefixes. The watch command runs until it encounters an error or is terminated by the user. diff --git a/etcdctlv3/command/del_command.go b/etcdctlv3/command/del_command.go index dca38bb3d..3193602d1 100644 --- a/etcdctlv3/command/del_command.go +++ b/etcdctlv3/command/del_command.go @@ -33,22 +33,29 @@ func NewDelCommand() *cobra.Command { // delCommandFunc executes the "del" command. func delCommandFunc(cmd *cobra.Command, args []string) { + key, opts := getDelOp(cmd, args) + c := mustClientFromCmd(cmd) + kvapi := clientv3.NewKV(c) + resp, err := kvapi.Delete(context.TODO(), key, opts...) + if err != nil { + ExitWithError(ExitError, err) + } + printDeleteResponse(*resp) +} + +func getDelOp(cmd *cobra.Command, args []string) (string, []clientv3.OpOption) { if len(args) == 0 || len(args) > 2 { ExitWithError(ExitBadArgs, fmt.Errorf("del command needs one argument as key and an optional argument as range_end.")) } - opts := []clientv3.OpOption{} key := args[0] if len(args) > 1 { opts = append(opts, clientv3.WithRange(args[1])) } + return key, opts +} - c := mustClientFromCmd(cmd) - kvapi := clientv3.NewKV(c) - _, err := kvapi.Delete(context.TODO(), key, opts...) - if err != nil { - ExitWithError(ExitError, err) - } +func printDeleteResponse(resp clientv3.DeleteResponse) { // TODO: add number of key removed into the response of delete. // TODO: print out the number of removed keys. fmt.Println(0) diff --git a/etcdctlv3/command/get_command.go b/etcdctlv3/command/get_command.go index 167e8db06..12b41948a 100644 --- a/etcdctlv3/command/get_command.go +++ b/etcdctlv3/command/get_command.go @@ -50,6 +50,17 @@ func NewGetCommand() *cobra.Command { // getCommandFunc executes the "get" command. func getCommandFunc(cmd *cobra.Command, args []string) { + key, opts := getGetOp(cmd, args) + c := mustClientFromCmd(cmd) + kvapi := clientv3.NewKV(c) + resp, err := kvapi.Get(context.TODO(), key, opts...) + if err != nil { + ExitWithError(ExitError, err) + } + printGetResponse(*resp, getHex) +} + +func getGetOp(cmd *cobra.Command, args []string) (string, []clientv3.OpOption) { if len(args) == 0 { ExitWithError(ExitBadArgs, fmt.Errorf("range command needs arguments.")) } @@ -94,15 +105,11 @@ func getCommandFunc(cmd *cobra.Command, args []string) { } opts = append(opts, clientv3.WithSort(sortByTarget, sortByOrder)) + return key, opts +} - c := mustClientFromCmd(cmd) - kvapi := clientv3.NewKV(c) - resp, err := kvapi.Get(context.TODO(), key, opts...) - if err != nil { - ExitWithError(ExitError, err) - } - +func printGetResponse(resp clientv3.GetResponse, isHex bool) { for _, kv := range resp.Kvs { - printKV(getHex, kv) + printKV(isHex, kv) } } diff --git a/etcdctlv3/command/put_command.go b/etcdctlv3/command/put_command.go index d36689d58..5f21351de 100644 --- a/etcdctlv3/command/put_command.go +++ b/etcdctlv3/command/put_command.go @@ -56,6 +56,18 @@ will store the content of the file to . // putCommandFunc executes the "put" command. func putCommandFunc(cmd *cobra.Command, args []string) { + key, value, opts := getPutOp(cmd, args) + + c := mustClientFromCmd(cmd) + kvapi := clientv3.NewKV(c) + resp, err := kvapi.Put(context.TODO(), key, value, opts...) + if err != nil { + ExitWithError(ExitError, err) + } + printPutResponse(*resp) +} + +func getPutOp(cmd *cobra.Command, args []string) (string, string, []clientv3.OpOption) { if len(args) == 0 { ExitWithError(ExitBadArgs, fmt.Errorf("put command needs 1 argument and input from stdin or 2 arguments.")) } @@ -71,11 +83,14 @@ func putCommandFunc(cmd *cobra.Command, args []string) { ExitWithError(ExitBadArgs, fmt.Errorf("bad lease ID (%v), expecting ID in Hex", err)) } - c := mustClientFromCmd(cmd) - kvapi := clientv3.NewKV(c) - _, err = kvapi.Put(context.TODO(), key, value, clientv3.WithLease(lease.LeaseID(id))) - if err != nil { - ExitWithError(ExitError, err) + opts := []clientv3.OpOption{} + if id != 0 { + opts = append(opts, clientv3.WithLease(lease.LeaseID(id))) } + + return key, value, opts +} + +func printPutResponse(resp clientv3.PutResponse) { fmt.Println("OK") } diff --git a/etcdctlv3/command/txn_command.go b/etcdctlv3/command/txn_command.go index 61a0c1a87..91acf99a7 100644 --- a/etcdctlv3/command/txn_command.go +++ b/etcdctlv3/command/txn_command.go @@ -18,21 +18,31 @@ import ( "bufio" "fmt" "os" + "regexp" "strconv" "strings" "github.com/coreos/etcd/Godeps/_workspace/src/github.com/spf13/cobra" "github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context" "github.com/coreos/etcd/clientv3" + pb "github.com/coreos/etcd/etcdserver/etcdserverpb" +) + +var ( + txnInteractive bool + txnHex bool ) // NewTxnCommand returns the cobra command for "txn". func NewTxnCommand() *cobra.Command { - return &cobra.Command{ - Use: "txn", + cmd := &cobra.Command{ + Use: "txn [options]", Short: "Txn processes all the requests in one transaction.", Run: txnCommandFunc, } + cmd.Flags().BoolVarP(&txnInteractive, "interactive", "i", false, "input transaction in interactive mode") + cmd.Flags().BoolVar(&txnHex, "hex", false, "print out key-values as hex encoded strings") + return cmd } // txnCommandFunc executes the "txn" command. @@ -41,25 +51,26 @@ func txnCommandFunc(cmd *cobra.Command, args []string) { ExitWithError(ExitBadArgs, fmt.Errorf("txn command does not accept argument.")) } + if !txnInteractive { + ExitWithError(ExitBadFeature, fmt.Errorf("txn command only supports interactive mode")) + } + reader := bufio.NewReader(os.Stdin) txn := clientv3.NewKV(mustClientFromCmd(cmd)).Txn(context.Background()) - fmt.Println("entry comparison[key target expected_result compare_value] (end with empty line):") + fmt.Println("compares:") txn.If(readCompares(reader)...) - fmt.Println("entry success request[method key value(end_range)] (end with empty line):") + fmt.Println("success requests (get, put, delete):") txn.Then(readOps(reader)...) - fmt.Println("entry failure request[method key value(end_range)] (end with empty line):") + fmt.Println("failure requests (get, put, delete):") txn.Else(readOps(reader)...) resp, err := txn.Commit() if err != nil { ExitWithError(ExitError, err) } - if resp.Succeeded { - fmt.Println("executed success request list") - } else { - fmt.Println("executed failure request list") - } + + printTxnResponse(*resp, txnHex) } func readCompares(r *bufio.Reader) (cmps []clientv3.Cmp) { @@ -106,51 +117,65 @@ func readOps(r *bufio.Reader) (ops []clientv3.Op) { return ops } +func argify(s string) []string { + r := regexp.MustCompile("'.+'|\".+\"|\\S+") + return r.FindAllString(s, -1) +} + func parseRequestUnion(line string) (*clientv3.Op, error) { - parts := strings.Split(line, " ") - if len(parts) < 2 { + args := argify(line) + if len(args) < 2 { return nil, fmt.Errorf("invalid txn compare request: %s", line) } - op := &clientv3.Op{} - key := parts[1] - switch parts[0] { - case "r", "range": - if len(parts) == 3 { - *op = clientv3.OpGet(key, clientv3.WithRange(parts[2])) - } else { - *op = clientv3.OpGet(key) - } - case "p", "put": - *op = clientv3.OpPut(key, parts[2]) - case "d", "deleteRange": - if len(parts) == 3 { - *op = clientv3.OpDelete(key, clientv3.WithRange(parts[2])) - } else { - *op = clientv3.OpDelete(key) - } - default: + opc := make(chan clientv3.Op, 1) + + put := NewPutCommand() + put.Run = func(cmd *cobra.Command, args []string) { + key, value, opts := getPutOp(cmd, args) + opc <- clientv3.OpPut(key, value, opts...) + } + get := NewGetCommand() + get.Run = func(cmd *cobra.Command, args []string) { + key, opts := getGetOp(cmd, args) + opc <- clientv3.OpGet(key, opts...) + } + del := NewDelCommand() + del.Run = func(cmd *cobra.Command, args []string) { + key, opts := getDelOp(cmd, args) + opc <- clientv3.OpDelete(key, opts...) + } + cmds := &cobra.Command{SilenceErrors: true} + cmds.AddCommand(put, get, del) + + cmds.SetArgs(args) + if err := cmds.Execute(); err != nil { return nil, fmt.Errorf("invalid txn request: %s", line) } - return op, nil + + op := <-opc + return &op, nil } func parseCompare(line string) (*clientv3.Cmp, error) { - parts := strings.Split(line, " ") - if len(parts) != 4 { - return nil, fmt.Errorf("invalid txn compare request: %s", line) + var ( + key string + op string + val string + ) + + lparenSplit := strings.SplitN(line, "(", 2) + if len(lparenSplit) != 2 { + return nil, fmt.Errorf("malformed comparison: %s", line) } - cmpType := "" - switch parts[2] { - case "g", "greater": - cmpType = ">" - case "e", "equal": - cmpType = "=" - case "l", "less": - cmpType = "<" - default: - return nil, fmt.Errorf("invalid txn compare request: %s", line) + target := lparenSplit[0] + n, serr := fmt.Sscanf(lparenSplit[1], "%q) %s %q", &key, &op, &val) + if n != 3 { + return nil, fmt.Errorf("malformed comparison: %s; got %s(%q) %s %q", line, target, key, op, val) + } + if serr != nil { + return nil, fmt.Errorf("malformed comparison: %s (%v)", line, serr) } var ( @@ -158,23 +183,23 @@ func parseCompare(line string) (*clientv3.Cmp, error) { err error cmp clientv3.Cmp ) - - key := parts[0] - switch parts[1] { + switch target { case "ver", "version": - if v, err = strconv.ParseInt(parts[3], 10, 64); err != nil { - cmp = clientv3.Compare(clientv3.Version(key), cmpType, v) + if v, err = strconv.ParseInt(val, 10, 64); err == nil { + cmp = clientv3.Compare(clientv3.Version(key), op, v) } case "c", "create": - if v, err = strconv.ParseInt(parts[3], 10, 64); err != nil { - cmp = clientv3.Compare(clientv3.CreatedRevision(key), cmpType, v) + if v, err = strconv.ParseInt(val, 10, 64); err == nil { + cmp = clientv3.Compare(clientv3.CreatedRevision(key), op, v) } case "m", "mod": - if v, err = strconv.ParseInt(parts[3], 10, 64); err != nil { - cmp = clientv3.Compare(clientv3.ModifiedRevision(key), cmpType, v) + if v, err = strconv.ParseInt(val, 10, 64); err == nil { + cmp = clientv3.Compare(clientv3.ModifiedRevision(key), op, v) } case "val", "value": - cmp = clientv3.Compare(clientv3.Value(key), cmpType, parts[3]) + cmp = clientv3.Compare(clientv3.Value(key), op, val) + default: + return nil, fmt.Errorf("malformed comparison: %s (unknown target %s)", line, target) } if err != nil { @@ -183,3 +208,25 @@ func parseCompare(line string) (*clientv3.Cmp, error) { return &cmp, nil } + +func printTxnResponse(resp clientv3.TxnResponse, isHex bool) { + if resp.Succeeded { + fmt.Println("SUCCESS") + } else { + fmt.Println("FAILURE") + } + + for _, r := range resp.Responses { + fmt.Println("") + switch v := r.Response.(type) { + case *pb.ResponseUnion_ResponseDeleteRange: + printDeleteResponse((clientv3.DeleteResponse)(*v.ResponseDeleteRange)) + case *pb.ResponseUnion_ResponsePut: + printPutResponse((clientv3.PutResponse)(*v.ResponsePut)) + case *pb.ResponseUnion_ResponseRange: + printGetResponse(((clientv3.GetResponse)(*v.ResponseRange)), isHex) + default: + fmt.Printf("unexpected response %+v\n", r) + } + } +}