go.etcd.io/etcd@v3.3.27+incompatible/etcdctl/ctlv3/command/watch_command.go (about)

     1  // Copyright 2015 The etcd Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package command
    16  
    17  import (
    18  	"bufio"
    19  	"context"
    20  	"errors"
    21  	"fmt"
    22  	"os"
    23  	"os/exec"
    24  	"strings"
    25  
    26  	"github.com/coreos/etcd/clientv3"
    27  
    28  	"github.com/spf13/cobra"
    29  )
    30  
    31  var (
    32  	errBadArgsNum              = errors.New("bad number of arguments")
    33  	errBadArgsNumConflictEnv   = errors.New("bad number of arguments (found conflicting environment key)")
    34  	errBadArgsNumSeparator     = errors.New("bad number of arguments (found separator --, but no commands)")
    35  	errBadArgsInteractiveWatch = errors.New("args[0] must be 'watch' for interactive calls")
    36  )
    37  
    38  var (
    39  	watchRev         int64
    40  	watchPrefix      bool
    41  	watchInteractive bool
    42  	watchPrevKey     bool
    43  )
    44  
    45  // NewWatchCommand returns the cobra command for "watch".
    46  func NewWatchCommand() *cobra.Command {
    47  	cmd := &cobra.Command{
    48  		Use:   "watch [options] [key or prefix] [range_end] [--] [exec-command arg1 arg2 ...]",
    49  		Short: "Watches events stream on keys or prefixes",
    50  		Run:   watchCommandFunc,
    51  	}
    52  
    53  	cmd.Flags().BoolVarP(&watchInteractive, "interactive", "i", false, "Interactive mode")
    54  	cmd.Flags().BoolVar(&watchPrefix, "prefix", false, "Watch on a prefix if prefix is set")
    55  	cmd.Flags().Int64Var(&watchRev, "rev", 0, "Revision to start watching")
    56  	cmd.Flags().BoolVar(&watchPrevKey, "prev-kv", false, "get the previous key-value pair before the event happens")
    57  
    58  	return cmd
    59  }
    60  
    61  // watchCommandFunc executes the "watch" command.
    62  func watchCommandFunc(cmd *cobra.Command, args []string) {
    63  	envKey, envRange := os.Getenv("ETCDCTL_WATCH_KEY"), os.Getenv("ETCDCTL_WATCH_RANGE_END")
    64  	if envKey == "" && envRange != "" {
    65  		ExitWithError(ExitBadArgs, fmt.Errorf("ETCDCTL_WATCH_KEY is empty but got ETCDCTL_WATCH_RANGE_END=%q", envRange))
    66  	}
    67  
    68  	if watchInteractive {
    69  		watchInteractiveFunc(cmd, os.Args, envKey, envRange)
    70  		return
    71  	}
    72  
    73  	watchArgs, execArgs, err := parseWatchArgs(os.Args, args, envKey, envRange, false)
    74  	if err != nil {
    75  		ExitWithError(ExitBadArgs, err)
    76  	}
    77  
    78  	c := mustClientFromCmd(cmd)
    79  	wc, err := getWatchChan(c, watchArgs)
    80  	if err != nil {
    81  		ExitWithError(ExitBadArgs, err)
    82  	}
    83  
    84  	printWatchCh(c, wc, execArgs)
    85  	if err = c.Close(); err != nil {
    86  		ExitWithError(ExitBadConnection, err)
    87  	}
    88  	ExitWithError(ExitInterrupted, fmt.Errorf("watch is canceled by the server"))
    89  }
    90  
    91  func watchInteractiveFunc(cmd *cobra.Command, osArgs []string, envKey, envRange string) {
    92  	c := mustClientFromCmd(cmd)
    93  
    94  	reader := bufio.NewReader(os.Stdin)
    95  
    96  	for {
    97  		l, err := reader.ReadString('\n')
    98  		if err != nil {
    99  			ExitWithError(ExitInvalidInput, fmt.Errorf("Error reading watch request line: %v", err))
   100  		}
   101  		l = strings.TrimSuffix(l, "\n")
   102  
   103  		args := argify(l)
   104  		if len(args) < 2 && envKey == "" {
   105  			fmt.Fprintf(os.Stderr, "Invalid command %s (command type or key is not provided)\n", l)
   106  			continue
   107  		}
   108  
   109  		if args[0] != "watch" {
   110  			fmt.Fprintf(os.Stderr, "Invalid command %s (only support watch)\n", l)
   111  			continue
   112  		}
   113  
   114  		watchArgs, execArgs, perr := parseWatchArgs(osArgs, args, envKey, envRange, true)
   115  		if perr != nil {
   116  			ExitWithError(ExitBadArgs, perr)
   117  		}
   118  
   119  		ch, err := getWatchChan(c, watchArgs)
   120  		if err != nil {
   121  			fmt.Fprintf(os.Stderr, "Invalid command %s (%v)\n", l, err)
   122  			continue
   123  		}
   124  		go printWatchCh(c, ch, execArgs)
   125  	}
   126  }
   127  
   128  func getWatchChan(c *clientv3.Client, args []string) (clientv3.WatchChan, error) {
   129  	if len(args) < 1 {
   130  		return nil, errBadArgsNum
   131  	}
   132  
   133  	key := args[0]
   134  	opts := []clientv3.OpOption{clientv3.WithRev(watchRev)}
   135  	if len(args) == 2 {
   136  		if watchPrefix {
   137  			return nil, fmt.Errorf("`range_end` and `--prefix` are mutually exclusive")
   138  		}
   139  		opts = append(opts, clientv3.WithRange(args[1]))
   140  	}
   141  	if watchPrefix {
   142  		opts = append(opts, clientv3.WithPrefix())
   143  	}
   144  	if watchPrevKey {
   145  		opts = append(opts, clientv3.WithPrevKV())
   146  	}
   147  	return c.Watch(clientv3.WithRequireLeader(context.Background()), key, opts...), nil
   148  }
   149  
   150  func printWatchCh(c *clientv3.Client, ch clientv3.WatchChan, execArgs []string) {
   151  	for resp := range ch {
   152  		if resp.Canceled {
   153  			fmt.Fprintf(os.Stderr, "watch was canceled (%v)\n", resp.Err())
   154  		}
   155  		display.Watch(resp)
   156  
   157  		if len(execArgs) > 0 {
   158  			for _, ev := range resp.Events {
   159  				cmd := exec.CommandContext(c.Ctx(), execArgs[0], execArgs[1:]...)
   160  				cmd.Env = os.Environ()
   161  				cmd.Env = append(cmd.Env, fmt.Sprintf("ETCD_WATCH_REVISION=%d", resp.Header.Revision))
   162  				cmd.Env = append(cmd.Env, fmt.Sprintf("ETCD_WATCH_EVENT_TYPE=%q", ev.Type))
   163  				cmd.Env = append(cmd.Env, fmt.Sprintf("ETCD_WATCH_KEY=%q", ev.Kv.Key))
   164  				cmd.Env = append(cmd.Env, fmt.Sprintf("ETCD_WATCH_VALUE=%q", ev.Kv.Value))
   165  				cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
   166  				if err := cmd.Run(); err != nil {
   167  					fmt.Fprintf(os.Stderr, "command %q error (%v)\n", execArgs, err)
   168  					os.Exit(1)
   169  				}
   170  			}
   171  		}
   172  	}
   173  }
   174  
   175  // "commandArgs" is the command arguments after "spf13/cobra" parses
   176  // all "watch" command flags, strips out special characters (e.g. "--").
   177  // "orArgs" is the raw arguments passed to "watch" command
   178  // (e.g. ./bin/etcdctl watch foo --rev 1 bar).
   179  // "--" characters are invalid arguments for "spf13/cobra" library,
   180  // so no need to handle such cases.
   181  func parseWatchArgs(osArgs, commandArgs []string, envKey, envRange string, interactive bool) (watchArgs []string, execArgs []string, err error) {
   182  	rawArgs := make([]string, len(osArgs))
   183  	copy(rawArgs, osArgs)
   184  	watchArgs = make([]string, len(commandArgs))
   185  	copy(watchArgs, commandArgs)
   186  
   187  	// remove preceding commands (e.g. ./bin/etcdctl watch)
   188  	// handle "./bin/etcdctl watch foo -- echo watch event"
   189  	for idx := range rawArgs {
   190  		if rawArgs[idx] == "watch" {
   191  			rawArgs = rawArgs[idx+1:]
   192  			break
   193  		}
   194  	}
   195  
   196  	// remove preceding commands (e.g. "watch foo bar" in interactive mode)
   197  	// handle "./bin/etcdctl watch foo -- echo watch event"
   198  	if interactive {
   199  		if watchArgs[0] != "watch" {
   200  			// "watch" not found
   201  			watchPrefix, watchRev, watchPrevKey = false, 0, false
   202  			return nil, nil, errBadArgsInteractiveWatch
   203  		}
   204  		watchArgs = watchArgs[1:]
   205  	}
   206  
   207  	execIdx, execExist := 0, false
   208  	if !interactive {
   209  		for execIdx = range rawArgs {
   210  			if rawArgs[execIdx] == "--" {
   211  				execExist = true
   212  				break
   213  			}
   214  		}
   215  		if execExist && execIdx == len(rawArgs)-1 {
   216  			// "watch foo bar --" should error
   217  			return nil, nil, errBadArgsNumSeparator
   218  		}
   219  		// "watch" with no argument should error
   220  		if !execExist && len(rawArgs) < 1 && envKey == "" {
   221  			return nil, nil, errBadArgsNum
   222  		}
   223  		if execExist && envKey != "" {
   224  			// "ETCDCTL_WATCH_KEY=foo watch foo -- echo 1" should error
   225  			// (watchArgs==["foo","echo","1"])
   226  			widx, ridx := len(watchArgs)-1, len(rawArgs)-1
   227  			for ; widx >= 0; widx-- {
   228  				if watchArgs[widx] == rawArgs[ridx] {
   229  					ridx--
   230  					continue
   231  				}
   232  				// watchArgs has extra:
   233  				// ETCDCTL_WATCH_KEY=foo watch foo  --  echo 1
   234  				// watchArgs:                       foo echo 1
   235  				if ridx == execIdx {
   236  					return nil, nil, errBadArgsNumConflictEnv
   237  				}
   238  			}
   239  		}
   240  		// check conflicting arguments
   241  		// e.g. "watch --rev 1 -- echo Hello World" has no conflict
   242  		if !execExist && len(watchArgs) > 0 && envKey != "" {
   243  			// "ETCDCTL_WATCH_KEY=foo watch foo" should error
   244  			// (watchArgs==["foo"])
   245  			return nil, nil, errBadArgsNumConflictEnv
   246  		}
   247  	} else {
   248  		for execIdx = range watchArgs {
   249  			if watchArgs[execIdx] == "--" {
   250  				execExist = true
   251  				break
   252  			}
   253  		}
   254  		if execExist && execIdx == len(watchArgs)-1 {
   255  			// "watch foo bar --" should error
   256  			watchPrefix, watchRev, watchPrevKey = false, 0, false
   257  			return nil, nil, errBadArgsNumSeparator
   258  		}
   259  
   260  		flagset := NewWatchCommand().Flags()
   261  		if err := flagset.Parse(watchArgs); err != nil {
   262  			watchPrefix, watchRev, watchPrevKey = false, 0, false
   263  			return nil, nil, err
   264  		}
   265  		pArgs := flagset.Args()
   266  
   267  		// "watch" with no argument should error
   268  		if !execExist && envKey == "" && len(pArgs) < 1 {
   269  			watchPrefix, watchRev, watchPrevKey = false, 0, false
   270  			return nil, nil, errBadArgsNum
   271  		}
   272  		// check conflicting arguments
   273  		// e.g. "watch --rev 1 -- echo Hello World" has no conflict
   274  		if !execExist && len(pArgs) > 0 && envKey != "" {
   275  			// "ETCDCTL_WATCH_KEY=foo watch foo" should error
   276  			// (watchArgs==["foo"])
   277  			watchPrefix, watchRev, watchPrevKey = false, 0, false
   278  			return nil, nil, errBadArgsNumConflictEnv
   279  		}
   280  	}
   281  
   282  	argsWithSep := rawArgs
   283  	if interactive {
   284  		// interactive mode directly passes "--" to the command args
   285  		argsWithSep = watchArgs
   286  	}
   287  
   288  	idx, foundSep := 0, false
   289  	for idx = range argsWithSep {
   290  		if argsWithSep[idx] == "--" {
   291  			foundSep = true
   292  			break
   293  		}
   294  	}
   295  	if foundSep {
   296  		execArgs = argsWithSep[idx+1:]
   297  	}
   298  
   299  	if interactive {
   300  		flagset := NewWatchCommand().Flags()
   301  		if err := flagset.Parse(argsWithSep); err != nil {
   302  			return nil, nil, err
   303  		}
   304  		watchArgs = flagset.Args()
   305  
   306  		watchPrefix, err = flagset.GetBool("prefix")
   307  		if err != nil {
   308  			return nil, nil, err
   309  		}
   310  		watchRev, err = flagset.GetInt64("rev")
   311  		if err != nil {
   312  			return nil, nil, err
   313  		}
   314  		watchPrevKey, err = flagset.GetBool("prev-kv")
   315  		if err != nil {
   316  			return nil, nil, err
   317  		}
   318  	}
   319  
   320  	// "ETCDCTL_WATCH_KEY=foo watch -- echo hello"
   321  	// should translate "watch foo -- echo hello"
   322  	// (watchArgs=["echo","hello"] should be ["foo","echo","hello"])
   323  	if envKey != "" {
   324  		ranges := []string{envKey}
   325  		if envRange != "" {
   326  			ranges = append(ranges, envRange)
   327  		}
   328  		watchArgs = append(ranges, watchArgs...)
   329  	}
   330  
   331  	if !foundSep {
   332  		return watchArgs, nil, nil
   333  	}
   334  
   335  	// "watch foo bar --rev 1 -- echo hello" or "watch foo --rev 1 bar -- echo hello",
   336  	// then "watchArgs" is "foo bar echo hello"
   337  	// so need ignore args after "argsWithSep[idx]", which is "--"
   338  	endIdx := 0
   339  	for endIdx = len(watchArgs) - 1; endIdx >= 0; endIdx-- {
   340  		if watchArgs[endIdx] == argsWithSep[idx+1] {
   341  			break
   342  		}
   343  	}
   344  	watchArgs = watchArgs[:endIdx]
   345  
   346  	return watchArgs, execArgs, nil
   347  }