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
 
-- 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
+<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 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.
 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)

+ 15 - 8
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)
 	}
 }

+ 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.
 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")
 }

+ 101 - 54
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)
+		}
+	}
+}