Browse Source

etcdctlv3: unify txn interactive mode input with get/put/delete

Anthony Romano 9 years ago
parent
commit
87dcb2adea

+ 68 - 2
etcdctlv3/README.md

@@ -15,7 +15,7 @@ PUT assigns the specified value with the specified key. If key already holds a v
 
 
 Simple reply
 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.
 - Error string if PUT failed. Exit code is non-zero.
 
 
@@ -93,7 +93,7 @@ TODO: --prefix, --from
 
 
 Simple reply
 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.
 - Error string if DEL failed. Exit code is non-zero.
 
 
@@ -109,6 +109,72 @@ OK
 ./etcdctl range foo
 ./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
+<Txn> ::= <CMP>* "\n" <THEN> "\n" <ELSE> "\n"
+<CMP> ::= (<CMPCREATE>|<CMPMOD>|<CMPVAL>|<CMPVER>) "\n"
+<CMPOP> ::= "<" | "=" | ">"
+<CMPCREATE> := ("c"|"create")"("<KEY>")" <REVISION>
+<CMPMOD> ::= ("m"|"mod")"("<KEY>")" <CMPOP> <REVISION>
+<CMPVAL> ::= ("val"|"value")"("<KEY>")" <CMPOP> <VALUE>
+<CMPVER> ::= ("ver"|"version")"("<KEY>")" <CMPOP> <VERSION>
+<THEN> ::= <OP>*
+<ELSE> ::= <OP>*
+<OP> ::= ((see put, get, del etcdctl command syntax)) "\n"
+<KEY> ::= (%q formatted string)
+<VALUE> ::= (%q formatted string)
+<REVISION> ::= "\""[0-9]+"\""
+<VERSION> ::= "\""[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 [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.
 Watch watches events stream on keys or prefixes. The watch command runs until it encounters an error or is terminated by the user.

+ 14 - 7
etcdctlv3/command/del_command.go

@@ -33,22 +33,29 @@ func NewDelCommand() *cobra.Command {
 
 
 // delCommandFunc executes the "del" command.
 // delCommandFunc executes the "del" command.
 func delCommandFunc(cmd *cobra.Command, args []string) {
 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 {
 	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."))
 		ExitWithError(ExitBadArgs, fmt.Errorf("del command needs one argument as key and an optional argument as range_end."))
 	}
 	}
-
 	opts := []clientv3.OpOption{}
 	opts := []clientv3.OpOption{}
 	key := args[0]
 	key := args[0]
 	if len(args) > 1 {
 	if len(args) > 1 {
 		opts = append(opts, clientv3.WithRange(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: add number of key removed into the response of delete.
 	// TODO: print out the number of removed keys.
 	// TODO: print out the number of removed keys.
 	fmt.Println(0)
 	fmt.Println(0)

+ 15 - 8
etcdctlv3/command/get_command.go

@@ -50,6 +50,17 @@ func NewGetCommand() *cobra.Command {
 
 
 // getCommandFunc executes the "get" command.
 // getCommandFunc executes the "get" command.
 func getCommandFunc(cmd *cobra.Command, args []string) {
 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 {
 	if len(args) == 0 {
 		ExitWithError(ExitBadArgs, fmt.Errorf("range command needs arguments."))
 		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))
 	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 {
 	for _, kv := range resp.Kvs {
-		printKV(getHex, kv)
+		printKV(isHex, kv)
 	}
 	}
 }
 }

+ 20 - 5
etcdctlv3/command/put_command.go

@@ -56,6 +56,18 @@ will store the content of the file to <key>.
 
 
 // putCommandFunc executes the "put" command.
 // putCommandFunc executes the "put" command.
 func putCommandFunc(cmd *cobra.Command, args []string) {
 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 {
 	if len(args) == 0 {
 		ExitWithError(ExitBadArgs, fmt.Errorf("put command needs 1 argument and input from stdin or 2 arguments."))
 		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))
 		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")
 	fmt.Println("OK")
 }
 }

+ 101 - 54
etcdctlv3/command/txn_command.go

@@ -18,21 +18,31 @@ import (
 	"bufio"
 	"bufio"
 	"fmt"
 	"fmt"
 	"os"
 	"os"
+	"regexp"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 
 
 	"github.com/coreos/etcd/Godeps/_workspace/src/github.com/spf13/cobra"
 	"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/Godeps/_workspace/src/golang.org/x/net/context"
 	"github.com/coreos/etcd/clientv3"
 	"github.com/coreos/etcd/clientv3"
+	pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
+)
+
+var (
+	txnInteractive bool
+	txnHex         bool
 )
 )
 
 
 // NewTxnCommand returns the cobra command for "txn".
 // NewTxnCommand returns the cobra command for "txn".
 func NewTxnCommand() *cobra.Command {
 func NewTxnCommand() *cobra.Command {
-	return &cobra.Command{
-		Use:   "txn",
+	cmd := &cobra.Command{
+		Use:   "txn [options]",
 		Short: "Txn processes all the requests in one transaction.",
 		Short: "Txn processes all the requests in one transaction.",
 		Run:   txnCommandFunc,
 		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.
 // 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."))
 		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)
 	reader := bufio.NewReader(os.Stdin)
 
 
 	txn := clientv3.NewKV(mustClientFromCmd(cmd)).Txn(context.Background())
 	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)...)
 	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)...)
 	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)...)
 	txn.Else(readOps(reader)...)
 
 
 	resp, err := txn.Commit()
 	resp, err := txn.Commit()
 	if err != nil {
 	if err != nil {
 		ExitWithError(ExitError, err)
 		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) {
 func readCompares(r *bufio.Reader) (cmps []clientv3.Cmp) {
@@ -106,51 +117,65 @@ func readOps(r *bufio.Reader) (ops []clientv3.Op) {
 	return ops
 	return ops
 }
 }
 
 
+func argify(s string) []string {
+	r := regexp.MustCompile("'.+'|\".+\"|\\S+")
+	return r.FindAllString(s, -1)
+}
+
 func parseRequestUnion(line string) (*clientv3.Op, error) {
 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)
 		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 nil, fmt.Errorf("invalid txn request: %s", line)
 	}
 	}
-	return op, nil
+
+	op := <-opc
+	return &op, nil
 }
 }
 
 
 func parseCompare(line string) (*clientv3.Cmp, error) {
 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 (
 	var (
@@ -158,23 +183,23 @@ func parseCompare(line string) (*clientv3.Cmp, error) {
 		err error
 		err error
 		cmp clientv3.Cmp
 		cmp clientv3.Cmp
 	)
 	)
-
-	key := parts[0]
-	switch parts[1] {
+	switch target {
 	case "ver", "version":
 	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":
 	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":
 	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":
 	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 {
 	if err != nil {
@@ -183,3 +208,25 @@ func parseCompare(line string) (*clientv3.Cmp, error) {
 
 
 	return &cmp, nil
 	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)
+		}
+	}
+}