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  }