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 }