github.com/lfch/etcd-io/tests/v3@v3.0.0-20221004140520-eac99acd3e9d/framework/e2e/etcdctl.go (about) 1 // Copyright 2022 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 e2e 16 17 import ( 18 "context" 19 "encoding/json" 20 "fmt" 21 "io" 22 "strconv" 23 "strings" 24 25 "github.com/lfch/etcd-io/api/v3/authpb" 26 "github.com/lfch/etcd-io/api/v3/etcdserverpb" 27 clientv3 "github.com/lfch/etcd-io/client/v3" 28 "github.com/lfch/etcd-io/tests/v3/framework/config" 29 ) 30 31 type EtcdctlV3 struct { 32 cfg *EtcdProcessClusterConfig 33 endpoints []string 34 userName string 35 password string 36 } 37 38 func NewEtcdctl(cfg *EtcdProcessClusterConfig, endpoints []string) *EtcdctlV3 { 39 return &EtcdctlV3{ 40 cfg: cfg, 41 endpoints: endpoints, 42 } 43 } 44 45 func (ctl *EtcdctlV3) WithAuth(userName, password string) *EtcdctlV3 { 46 ctl.userName = userName 47 ctl.password = password 48 return ctl 49 } 50 51 func (ctl *EtcdctlV3) DowngradeEnable(ctx context.Context, version string) error { 52 _, err := SpawnWithExpectLines(ctx, ctl.cmdArgs("downgrade", "enable", version), nil, "Downgrade enable success") 53 return err 54 } 55 56 func (ctl *EtcdctlV3) Get(ctx context.Context, key string, o config.GetOptions) (*clientv3.GetResponse, error) { 57 resp := clientv3.GetResponse{} 58 var args []string 59 if o.Timeout != 0 { 60 args = append(args, fmt.Sprintf("--command-timeout=%s", o.Timeout)) 61 } 62 if o.Serializable { 63 args = append(args, "--consistency", "s") 64 } 65 args = append(args, "get", key, "-w", "json") 66 if o.End != "" { 67 args = append(args, o.End) 68 } 69 if o.Revision != 0 { 70 args = append(args, fmt.Sprintf("--rev=%d", o.Revision)) 71 } 72 if o.Prefix { 73 args = append(args, "--prefix") 74 } 75 if o.Limit != 0 { 76 args = append(args, fmt.Sprintf("--limit=%d", o.Limit)) 77 } 78 if o.FromKey { 79 args = append(args, "--from-key") 80 } 81 if o.CountOnly { 82 args = append(args, "-w", "fields", "--count-only") 83 } else { 84 args = append(args, "-w", "json") 85 } 86 switch o.SortBy { 87 case clientv3.SortByCreateRevision: 88 args = append(args, "--sort-by=CREATE") 89 case clientv3.SortByModRevision: 90 args = append(args, "--sort-by=MODIFY") 91 case clientv3.SortByValue: 92 args = append(args, "--sort-by=VALUE") 93 case clientv3.SortByVersion: 94 args = append(args, "--sort-by=VERSION") 95 case clientv3.SortByKey: 96 // nothing 97 default: 98 return nil, fmt.Errorf("bad sort target %v", o.SortBy) 99 } 100 switch o.Order { 101 case clientv3.SortAscend: 102 args = append(args, "--order=ASCEND") 103 case clientv3.SortDescend: 104 args = append(args, "--order=DESCEND") 105 case clientv3.SortNone: 106 // nothing 107 default: 108 return nil, fmt.Errorf("bad sort order %v", o.Order) 109 } 110 if o.CountOnly { 111 cmd, err := SpawnCmd(ctl.cmdArgs(args...), nil) 112 if err != nil { 113 return nil, err 114 } 115 defer cmd.Close() 116 _, err = cmd.ExpectWithContext(ctx, "Count") 117 return &resp, err 118 } 119 err := ctl.spawnJsonCmd(ctx, &resp, args...) 120 return &resp, err 121 } 122 123 func (ctl *EtcdctlV3) Put(ctx context.Context, key, value string, opts config.PutOptions) error { 124 args := ctl.cmdArgs() 125 args = append(args, "put", key, value) 126 if opts.LeaseID != 0 { 127 args = append(args, "--lease", strconv.FormatInt(int64(opts.LeaseID), 16)) 128 } 129 _, err := SpawnWithExpectLines(ctx, args, nil, "OK") 130 return err 131 } 132 133 func (ctl *EtcdctlV3) Delete(ctx context.Context, key string, o config.DeleteOptions) (*clientv3.DeleteResponse, error) { 134 args := []string{"del", key} 135 if o.End != "" { 136 args = append(args, o.End) 137 } 138 if o.Prefix { 139 args = append(args, "--prefix") 140 } 141 if o.FromKey { 142 args = append(args, "--from-key") 143 } 144 var resp clientv3.DeleteResponse 145 err := ctl.spawnJsonCmd(ctx, &resp, args...) 146 return &resp, err 147 } 148 149 func (ctl *EtcdctlV3) Txn(ctx context.Context, compares, ifSucess, ifFail []string, o config.TxnOptions) (*clientv3.TxnResponse, error) { 150 args := ctl.cmdArgs() 151 args = append(args, "txn") 152 if o.Interactive { 153 args = append(args, "--interactive") 154 } 155 args = append(args, "-w", "json", "--hex=true") 156 cmd, err := SpawnCmd(args, nil) 157 if err != nil { 158 return nil, err 159 } 160 defer cmd.Close() 161 _, err = cmd.ExpectWithContext(ctx, "compares:") 162 if err != nil { 163 return nil, err 164 } 165 for _, cmp := range compares { 166 if err := cmd.Send(cmp + "\r"); err != nil { 167 return nil, err 168 } 169 } 170 if err := cmd.Send("\r"); err != nil { 171 return nil, err 172 } 173 _, err = cmd.ExpectWithContext(ctx, "success requests (get, put, del):") 174 if err != nil { 175 return nil, err 176 } 177 for _, req := range ifSucess { 178 if err = cmd.Send(req + "\r"); err != nil { 179 return nil, err 180 } 181 } 182 if err = cmd.Send("\r"); err != nil { 183 return nil, err 184 } 185 186 _, err = cmd.ExpectWithContext(ctx, "failure requests (get, put, del):") 187 if err != nil { 188 return nil, err 189 } 190 for _, req := range ifFail { 191 if err = cmd.Send(req + "\r"); err != nil { 192 return nil, err 193 } 194 } 195 if err = cmd.Send("\r"); err != nil { 196 return nil, err 197 } 198 var line string 199 line, err = cmd.ExpectWithContext(ctx, "header") 200 if err != nil { 201 return nil, err 202 } 203 var resp clientv3.TxnResponse 204 AddTxnResponse(&resp, line) 205 err = json.Unmarshal([]byte(line), &resp) 206 return &resp, err 207 } 208 209 // AddTxnResponse looks for ResponseOp json tags and adds the objects for json decoding 210 func AddTxnResponse(resp *clientv3.TxnResponse, jsonData string) { 211 if resp == nil { 212 return 213 } 214 if resp.Responses == nil { 215 resp.Responses = []*etcdserverpb.ResponseOp{} 216 } 217 jd := json.NewDecoder(strings.NewReader(jsonData)) 218 for { 219 t, e := jd.Token() 220 if e == io.EOF { 221 break 222 } 223 if t == "response_range" { 224 resp.Responses = append(resp.Responses, &etcdserverpb.ResponseOp{ 225 Response: &etcdserverpb.ResponseOp_ResponseRange{}, 226 }) 227 } 228 if t == "response_put" { 229 resp.Responses = append(resp.Responses, &etcdserverpb.ResponseOp{ 230 Response: &etcdserverpb.ResponseOp_ResponsePut{}, 231 }) 232 } 233 if t == "response_delete_range" { 234 resp.Responses = append(resp.Responses, &etcdserverpb.ResponseOp{ 235 Response: &etcdserverpb.ResponseOp_ResponseDeleteRange{}, 236 }) 237 } 238 if t == "response_txn" { 239 resp.Responses = append(resp.Responses, &etcdserverpb.ResponseOp{ 240 Response: &etcdserverpb.ResponseOp_ResponseTxn{}, 241 }) 242 } 243 } 244 } 245 246 func (ctl *EtcdctlV3) MemberList(ctx context.Context) (*clientv3.MemberListResponse, error) { 247 var resp clientv3.MemberListResponse 248 err := ctl.spawnJsonCmd(ctx, &resp, "member", "list") 249 return &resp, err 250 } 251 252 func (ctl *EtcdctlV3) MemberAdd(ctx context.Context, name string, peerAddrs []string) (*clientv3.MemberAddResponse, error) { 253 var resp clientv3.MemberAddResponse 254 err := ctl.spawnJsonCmd(ctx, &resp, "member", "add", name, "--peer-urls", strings.Join(peerAddrs, ",")) 255 return &resp, err 256 } 257 258 func (ctl *EtcdctlV3) MemberAddAsLearner(ctx context.Context, name string, peerAddrs []string) (*clientv3.MemberAddResponse, error) { 259 var resp clientv3.MemberAddResponse 260 err := ctl.spawnJsonCmd(ctx, &resp, "member", "add", name, "--learner", "--peer-urls", strings.Join(peerAddrs, ",")) 261 return &resp, err 262 } 263 264 func (ctl *EtcdctlV3) MemberRemove(ctx context.Context, id uint64) (*clientv3.MemberRemoveResponse, error) { 265 var resp clientv3.MemberRemoveResponse 266 err := ctl.spawnJsonCmd(ctx, &resp, "member", "remove", fmt.Sprintf("%x", id)) 267 return &resp, err 268 } 269 270 func (ctl *EtcdctlV3) cmdArgs(args ...string) []string { 271 cmdArgs := []string{CtlBinPath + "3"} 272 for k, v := range ctl.flags() { 273 cmdArgs = append(cmdArgs, fmt.Sprintf("--%s=%s", k, v)) 274 } 275 return append(cmdArgs, args...) 276 } 277 278 func (ctl *EtcdctlV3) flags() map[string]string { 279 fmap := make(map[string]string) 280 if ctl.cfg.ClientTLS == ClientTLS { 281 if ctl.cfg.IsClientAutoTLS { 282 fmap["insecure-transport"] = "false" 283 fmap["insecure-skip-tls-verify"] = "true" 284 } else if ctl.cfg.IsClientCRL { 285 fmap["cacert"] = CaPath 286 fmap["cert"] = RevokedCertPath 287 fmap["key"] = RevokedPrivateKeyPath 288 } else { 289 fmap["cacert"] = CaPath 290 fmap["cert"] = CertPath 291 fmap["key"] = PrivateKeyPath 292 } 293 } 294 fmap["endpoints"] = strings.Join(ctl.endpoints, ",") 295 if ctl.userName != "" && ctl.password != "" { 296 fmap["user"] = ctl.userName + ":" + ctl.password 297 } 298 return fmap 299 } 300 301 func (ctl *EtcdctlV3) Compact(ctx context.Context, rev int64, o config.CompactOption) (*clientv3.CompactResponse, error) { 302 args := ctl.cmdArgs("compact", fmt.Sprint(rev)) 303 if o.Timeout != 0 { 304 args = append(args, fmt.Sprintf("--command-timeout=%s", o.Timeout)) 305 } 306 if o.Physical { 307 args = append(args, "--physical") 308 } 309 310 _, err := SpawnWithExpectLines(ctx, args, nil, fmt.Sprintf("compacted revision %v", rev)) 311 return nil, err 312 } 313 314 func (ctl *EtcdctlV3) Status(ctx context.Context) ([]*clientv3.StatusResponse, error) { 315 var epStatus []*struct { 316 Endpoint string 317 Status *clientv3.StatusResponse 318 } 319 err := ctl.spawnJsonCmd(ctx, &epStatus, "endpoint", "status") 320 if err != nil { 321 return nil, err 322 } 323 resp := make([]*clientv3.StatusResponse, len(epStatus)) 324 for i, e := range epStatus { 325 resp[i] = e.Status 326 } 327 return resp, err 328 } 329 330 func (ctl *EtcdctlV3) HashKV(ctx context.Context, rev int64) ([]*clientv3.HashKVResponse, error) { 331 var epHashKVs []*struct { 332 Endpoint string 333 HashKV *clientv3.HashKVResponse 334 } 335 err := ctl.spawnJsonCmd(ctx, &epHashKVs, "endpoint", "hashkv", "--endpoints", strings.Join(ctl.endpoints, ","), "--rev", fmt.Sprint(rev)) 336 if err != nil { 337 return nil, err 338 } 339 resp := make([]*clientv3.HashKVResponse, len(epHashKVs)) 340 for _, e := range epHashKVs { 341 resp = append(resp, e.HashKV) 342 } 343 return resp, err 344 } 345 346 func (ctl *EtcdctlV3) Health(ctx context.Context) error { 347 args := ctl.cmdArgs() 348 args = append(args, "endpoint", "health") 349 lines := make([]string, len(ctl.endpoints)) 350 for i := range lines { 351 lines[i] = "is healthy" 352 } 353 _, err := SpawnWithExpectLines(ctx, args, nil, lines...) 354 return err 355 } 356 357 func (ctl *EtcdctlV3) Grant(ctx context.Context, ttl int64) (*clientv3.LeaseGrantResponse, error) { 358 args := ctl.cmdArgs() 359 args = append(args, "lease", "grant", strconv.FormatInt(ttl, 10), "-w", "json") 360 cmd, err := SpawnCmd(args, nil) 361 if err != nil { 362 return nil, err 363 } 364 defer cmd.Close() 365 var resp clientv3.LeaseGrantResponse 366 line, err := cmd.ExpectWithContext(ctx, "ID") 367 if err != nil { 368 return nil, err 369 } 370 err = json.Unmarshal([]byte(line), &resp) 371 return &resp, err 372 } 373 374 func (ctl *EtcdctlV3) TimeToLive(ctx context.Context, id clientv3.LeaseID, o config.LeaseOption) (*clientv3.LeaseTimeToLiveResponse, error) { 375 args := ctl.cmdArgs() 376 args = append(args, "lease", "timetolive", strconv.FormatInt(int64(id), 16), "-w", "json") 377 if o.WithAttachedKeys { 378 args = append(args, "--keys") 379 } 380 cmd, err := SpawnCmd(args, nil) 381 if err != nil { 382 return nil, err 383 } 384 defer cmd.Close() 385 var resp clientv3.LeaseTimeToLiveResponse 386 line, err := cmd.ExpectWithContext(ctx, "id") 387 if err != nil { 388 return nil, err 389 } 390 err = json.Unmarshal([]byte(line), &resp) 391 return &resp, err 392 } 393 394 func (ctl *EtcdctlV3) Defragment(ctx context.Context, o config.DefragOption) error { 395 args := append(ctl.cmdArgs(), "defrag") 396 if o.Timeout != 0 { 397 args = append(args, fmt.Sprintf("--command-timeout=%s", o.Timeout)) 398 } 399 lines := make([]string, len(ctl.endpoints)) 400 for i := range lines { 401 lines[i] = "Finished defragmenting etcd member" 402 } 403 _, err := SpawnWithExpectLines(ctx, args, map[string]string{}, lines...) 404 return err 405 } 406 407 func (ctl *EtcdctlV3) Leases(ctx context.Context) (*clientv3.LeaseLeasesResponse, error) { 408 args := ctl.cmdArgs("lease", "list", "-w", "json") 409 cmd, err := SpawnCmd(args, nil) 410 if err != nil { 411 return nil, err 412 } 413 defer cmd.Close() 414 var resp clientv3.LeaseLeasesResponse 415 line, err := cmd.ExpectWithContext(ctx, "id") 416 if err != nil { 417 return nil, err 418 } 419 err = json.Unmarshal([]byte(line), &resp) 420 return &resp, err 421 } 422 423 func (ctl *EtcdctlV3) KeepAliveOnce(ctx context.Context, id clientv3.LeaseID) (*clientv3.LeaseKeepAliveResponse, error) { 424 args := ctl.cmdArgs("lease", "keep-alive", strconv.FormatInt(int64(id), 16), "--once", "-w", "json") 425 cmd, err := SpawnCmd(args, nil) 426 if err != nil { 427 return nil, err 428 } 429 defer cmd.Close() 430 var resp clientv3.LeaseKeepAliveResponse 431 line, err := cmd.ExpectWithContext(ctx, "ID") 432 if err != nil { 433 return nil, err 434 } 435 err = json.Unmarshal([]byte(line), &resp) 436 return &resp, err 437 } 438 439 func (ctl *EtcdctlV3) Revoke(ctx context.Context, id clientv3.LeaseID) (*clientv3.LeaseRevokeResponse, error) { 440 var resp clientv3.LeaseRevokeResponse 441 err := ctl.spawnJsonCmd(ctx, &resp, "lease", "revoke", strconv.FormatInt(int64(id), 16)) 442 return &resp, err 443 } 444 445 func (ctl *EtcdctlV3) AlarmList(ctx context.Context) (*clientv3.AlarmResponse, error) { 446 var resp clientv3.AlarmResponse 447 err := ctl.spawnJsonCmd(ctx, &resp, "alarm", "list") 448 return &resp, err 449 } 450 451 func (ctl *EtcdctlV3) AlarmDisarm(ctx context.Context, _ *clientv3.AlarmMember) (*clientv3.AlarmResponse, error) { 452 args := ctl.cmdArgs() 453 args = append(args, "alarm", "disarm", "-w", "json") 454 ep, err := SpawnCmd(args, nil) 455 if err != nil { 456 return nil, err 457 } 458 defer ep.Close() 459 var resp clientv3.AlarmResponse 460 line, err := ep.ExpectWithContext(ctx, "alarm") 461 if err != nil { 462 return nil, err 463 } 464 err = json.Unmarshal([]byte(line), &resp) 465 return &resp, err 466 } 467 468 func (ctl *EtcdctlV3) AuthEnable(ctx context.Context) (*clientv3.AuthEnableResponse, error) { 469 var resp clientv3.AuthEnableResponse 470 err := ctl.spawnJsonCmd(ctx, &resp, "auth", "enable") 471 return &resp, err 472 } 473 474 func (ctl *EtcdctlV3) AuthDisable(ctx context.Context) (*clientv3.AuthDisableResponse, error) { 475 var resp clientv3.AuthDisableResponse 476 err := ctl.spawnJsonCmd(ctx, &resp, "auth", "disable") 477 return &resp, err 478 } 479 480 func (ctl *EtcdctlV3) AuthStatus(ctx context.Context) (*clientv3.AuthStatusResponse, error) { 481 var resp clientv3.AuthStatusResponse 482 err := ctl.spawnJsonCmd(ctx, &resp, "auth", "status") 483 return &resp, err 484 } 485 486 func (ctl *EtcdctlV3) UserAdd(ctx context.Context, name, password string, opts config.UserAddOptions) (*clientv3.AuthUserAddResponse, error) { 487 args := ctl.cmdArgs() 488 args = append(args, "user", "add") 489 if password == "" { 490 args = append(args, name) 491 } else { 492 args = append(args, fmt.Sprintf("%s:%s", name, password)) 493 } 494 495 if opts.NoPassword { 496 args = append(args, "--no-password") 497 } 498 499 args = append(args, "--interactive=false", "-w", "json") 500 501 cmd, err := SpawnCmd(args, nil) 502 if err != nil { 503 return nil, err 504 } 505 defer cmd.Close() 506 507 // If no password is provided, and NoPassword isn't set, the CLI will always 508 // wait for a password, send an enter in this case for an "empty" password. 509 if !opts.NoPassword && password == "" { 510 err := cmd.Send("\n") 511 if err != nil { 512 return nil, err 513 } 514 } 515 516 var resp clientv3.AuthUserAddResponse 517 line, err := cmd.ExpectWithContext(ctx, "header") 518 if err != nil { 519 return nil, err 520 } 521 err = json.Unmarshal([]byte(line), &resp) 522 return &resp, err 523 } 524 525 func (ctl *EtcdctlV3) UserGet(ctx context.Context, name string) (*clientv3.AuthUserGetResponse, error) { 526 var resp clientv3.AuthUserGetResponse 527 err := ctl.spawnJsonCmd(ctx, &resp, "user", "get", name) 528 return &resp, err 529 } 530 531 func (ctl *EtcdctlV3) UserList(ctx context.Context) (*clientv3.AuthUserListResponse, error) { 532 var resp clientv3.AuthUserListResponse 533 err := ctl.spawnJsonCmd(ctx, &resp, "user", "list") 534 return &resp, err 535 } 536 537 func (ctl *EtcdctlV3) UserDelete(ctx context.Context, name string) (*clientv3.AuthUserDeleteResponse, error) { 538 var resp clientv3.AuthUserDeleteResponse 539 err := ctl.spawnJsonCmd(ctx, &resp, "user", "delete", name) 540 return &resp, err 541 } 542 543 func (ctl *EtcdctlV3) UserChangePass(ctx context.Context, user, newPass string) error { 544 args := ctl.cmdArgs() 545 args = append(args, "user", "passwd", user, "--interactive=false") 546 cmd, err := SpawnCmd(args, nil) 547 if err != nil { 548 return err 549 } 550 defer cmd.Close() 551 err = cmd.Send(newPass + "\n") 552 if err != nil { 553 return err 554 } 555 556 _, err = cmd.ExpectWithContext(ctx, "Password updated") 557 return err 558 } 559 560 func (ctl *EtcdctlV3) UserGrantRole(ctx context.Context, user string, role string) (*clientv3.AuthUserGrantRoleResponse, error) { 561 var resp clientv3.AuthUserGrantRoleResponse 562 err := ctl.spawnJsonCmd(ctx, &resp, "user", "grant-role", user, role) 563 return &resp, err 564 } 565 566 func (ctl *EtcdctlV3) UserRevokeRole(ctx context.Context, user string, role string) (*clientv3.AuthUserRevokeRoleResponse, error) { 567 var resp clientv3.AuthUserRevokeRoleResponse 568 err := ctl.spawnJsonCmd(ctx, &resp, "user", "revoke-role", user, role) 569 return &resp, err 570 } 571 572 func (ctl *EtcdctlV3) RoleAdd(ctx context.Context, name string) (*clientv3.AuthRoleAddResponse, error) { 573 var resp clientv3.AuthRoleAddResponse 574 err := ctl.spawnJsonCmd(ctx, &resp, "role", "add", name) 575 return &resp, err 576 } 577 578 func (ctl *EtcdctlV3) RoleGrantPermission(ctx context.Context, name string, key, rangeEnd string, permType clientv3.PermissionType) (*clientv3.AuthRoleGrantPermissionResponse, error) { 579 permissionType := authpb.Permission_Type_name[int32(permType)] 580 var resp clientv3.AuthRoleGrantPermissionResponse 581 err := ctl.spawnJsonCmd(ctx, &resp, "role", "grant-permission", name, permissionType, key, rangeEnd) 582 return &resp, err 583 } 584 585 func (ctl *EtcdctlV3) RoleGet(ctx context.Context, role string) (*clientv3.AuthRoleGetResponse, error) { 586 var resp clientv3.AuthRoleGetResponse 587 err := ctl.spawnJsonCmd(ctx, &resp, "role", "get", role) 588 return &resp, err 589 } 590 591 func (ctl *EtcdctlV3) RoleList(ctx context.Context) (*clientv3.AuthRoleListResponse, error) { 592 var resp clientv3.AuthRoleListResponse 593 err := ctl.spawnJsonCmd(ctx, &resp, "role", "list") 594 return &resp, err 595 } 596 597 func (ctl *EtcdctlV3) RoleRevokePermission(ctx context.Context, role string, key, rangeEnd string) (*clientv3.AuthRoleRevokePermissionResponse, error) { 598 var resp clientv3.AuthRoleRevokePermissionResponse 599 err := ctl.spawnJsonCmd(ctx, &resp, "role", "revoke-permission", role, key, rangeEnd) 600 return &resp, err 601 } 602 603 func (ctl *EtcdctlV3) RoleDelete(ctx context.Context, role string) (*clientv3.AuthRoleDeleteResponse, error) { 604 var resp clientv3.AuthRoleDeleteResponse 605 err := ctl.spawnJsonCmd(ctx, &resp, "role", "delete", role) 606 return &resp, err 607 } 608 609 func (ctl *EtcdctlV3) spawnJsonCmd(ctx context.Context, output interface{}, args ...string) error { 610 args = append(args, "-w", "json") 611 cmd, err := SpawnCmd(append(ctl.cmdArgs(), args...), nil) 612 if err != nil { 613 return err 614 } 615 defer cmd.Close() 616 line, err := cmd.ExpectWithContext(ctx, "header") 617 if err != nil { 618 return err 619 } 620 return json.Unmarshal([]byte(line), output) 621 } 622 623 func (ctl *EtcdctlV3) Watch(ctx context.Context, key string, opts config.WatchOptions) clientv3.WatchChan { 624 args := ctl.cmdArgs() 625 args = append(args, "watch", key) 626 if opts.RangeEnd != "" { 627 args = append(args, opts.RangeEnd) 628 } 629 args = append(args, "-w", "json") 630 if opts.Prefix { 631 args = append(args, "--prefix") 632 } 633 if opts.Revision != 0 { 634 args = append(args, "--rev", fmt.Sprint(opts.Revision)) 635 } 636 proc, err := SpawnCmd(args, nil) 637 if err != nil { 638 return nil 639 } 640 641 ch := make(chan clientv3.WatchResponse) 642 go func() { 643 defer proc.Stop() 644 for { 645 select { 646 case <-ctx.Done(): 647 close(ch) 648 return 649 default: 650 if line := proc.ReadLine(); line != "" { 651 var resp clientv3.WatchResponse 652 json.Unmarshal([]byte(line), &resp) 653 if resp.Canceled { 654 close(ch) 655 return 656 } 657 if len(resp.Events) > 0 { 658 ch <- resp 659 } 660 } 661 } 662 } 663 }() 664 665 return ch 666 }