diff --git a/CHANGELOG/CHANGELOG-3.6.md b/CHANGELOG/CHANGELOG-3.6.md index 0d8924bf2..93252c059 100644 --- a/CHANGELOG/CHANGELOG-3.6.md +++ b/CHANGELOG/CHANGELOG-3.6.md @@ -32,6 +32,7 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.5.0...v3.6.0). - [Always print the raft_term in decimal](https://github.com/etcd-io/etcd/pull/13711) when displaying member list in json. - [Add one more field `storageVersion`](https://github.com/etcd-io/etcd/pull/13773) into the response of command `etcdctl endpoint status`. - Add [`--max-txn-ops`](https://github.com/etcd-io/etcd/pull/14340) flag to make-mirror command. +- Add [`--consistency`](https://github.com/etcd-io/etcd/pull/15261) flag to member list command. - Display [field `hash_revision`](https://github.com/etcd-io/etcd/pull/14812) for `etcdctl endpoint hash` command. ### etcdutl v3 @@ -39,6 +40,10 @@ See [code changes](https://github.com/etcd-io/etcd/compare/v3.5.0...v3.6.0). - Add command to generate [shell completion](https://github.com/etcd-io/etcd/pull/13142). - Add `migrate` command for downgrading/upgrading etcd data dir files. +### Package `clientv3` + +- [Support serializable `MemberList` operation](https://github.com/etcd-io/etcd/pull/15261). + ### Package `server` - Package `mvcc` was moved to `storage/mvcc` diff --git a/client/v3/client_test.go b/client/v3/client_test.go index 0f52ad5d3..c569d7a3b 100644 --- a/client/v3/client_test.go +++ b/client/v3/client_test.go @@ -426,7 +426,7 @@ type mockCluster struct { members []*etcdserverpb.Member } -func (mc *mockCluster) MemberList(ctx context.Context) (*MemberListResponse, error) { +func (mc *mockCluster) MemberList(ctx context.Context, opts ...OpOption) (*MemberListResponse, error) { return &MemberListResponse{Members: mc.members}, nil } diff --git a/client/v3/cluster.go b/client/v3/cluster.go index 92d7cdb56..e4bfbad81 100644 --- a/client/v3/cluster.go +++ b/client/v3/cluster.go @@ -34,7 +34,7 @@ type ( type Cluster interface { // MemberList lists the current cluster membership. - MemberList(ctx context.Context) (*MemberListResponse, error) + MemberList(ctx context.Context, opts ...OpOption) (*MemberListResponse, error) // MemberAdd adds a new member into the cluster. MemberAdd(ctx context.Context, peerAddrs []string) (*MemberAddResponse, error) @@ -122,9 +122,9 @@ func (c *cluster) MemberUpdate(ctx context.Context, id uint64, peerAddrs []strin return nil, toErr(ctx, err) } -func (c *cluster) MemberList(ctx context.Context) (*MemberListResponse, error) { - // it is safe to retry on list. - resp, err := c.remote.MemberList(ctx, &pb.MemberListRequest{Linearizable: true}, c.callOpts...) +func (c *cluster) MemberList(ctx context.Context, opts ...OpOption) (*MemberListResponse, error) { + opt := OpGet("", opts...) + resp, err := c.remote.MemberList(ctx, &pb.MemberListRequest{Linearizable: !opt.serializable}, c.callOpts...) if err == nil { return (*MemberListResponse)(resp), nil } diff --git a/etcdctl/README.md b/etcdctl/README.md index 39a5c56bc..a25e05132 100644 --- a/etcdctl/README.md +++ b/etcdctl/README.md @@ -711,6 +711,9 @@ MEMBER LIST prints the member details for all members associated with an etcd cl RPC: MemberList +#### Options +- consistency -- Linearizable(l) or Serializable(s) + #### Output Prints a humanized table of the member IDs, statuses, names, peer addresses, and client addresses. diff --git a/etcdctl/ctlv3/command/get_command.go b/etcdctl/ctlv3/command/get_command.go index a18cc32b9..86f281177 100644 --- a/etcdctl/ctlv3/command/get_command.go +++ b/etcdctl/ctlv3/command/get_command.go @@ -109,12 +109,8 @@ func getGetOp(args []string) (string, []clientv3.OpOption) { } var opts []clientv3.OpOption - switch getConsistency { - case "s": + if IsSerializable(getConsistency) { opts = append(opts, clientv3.WithSerializable()) - case "l": - default: - cobrautl.ExitWithError(cobrautl.ExitBadFeature, fmt.Errorf("unknown consistency flag %q", getConsistency)) } key := args[0] diff --git a/etcdctl/ctlv3/command/member_command.go b/etcdctl/ctlv3/command/member_command.go index 53b624b98..9cf95e840 100644 --- a/etcdctl/ctlv3/command/member_command.go +++ b/etcdctl/ctlv3/command/member_command.go @@ -27,8 +27,9 @@ import ( ) var ( - memberPeerURLs string - isLearner bool + memberPeerURLs string + isLearner bool + memberConsistency string ) // NewMemberCommand returns the cobra command for "member". @@ -100,6 +101,8 @@ The items in the lists are ID, Status, Name, Peer Addrs, Client Addrs, Is Learne Run: memberListCommandFunc, } + cc.Flags().StringVar(&memberConsistency, "consistency", "l", "Linearizable(l) or Serializable(s)") + return cc } @@ -226,8 +229,12 @@ func memberUpdateCommandFunc(cmd *cobra.Command, args []string) { // memberListCommandFunc executes the "member list" command. func memberListCommandFunc(cmd *cobra.Command, args []string) { + var opts []clientv3.OpOption + if IsSerializable(memberConsistency) { + opts = append(opts, clientv3.WithSerializable()) + } ctx, cancel := commandCtx(cmd) - resp, err := mustClientFromCmd(cmd).MemberList(ctx) + resp, err := mustClientFromCmd(cmd).MemberList(ctx, opts...) cancel() if err != nil { cobrautl.ExitWithError(cobrautl.ExitError, err) diff --git a/etcdctl/ctlv3/command/util.go b/etcdctl/ctlv3/command/util.go index 8338ef33d..52b882e52 100644 --- a/etcdctl/ctlv3/command/util.go +++ b/etcdctl/ctlv3/command/util.go @@ -166,3 +166,14 @@ func defrag(c *clientv3.Client, ep string) { } fmt.Printf("Defragmented %q\n", ep) } + +func IsSerializable(option string) bool { + switch option { + case "s": + return true + case "l": + default: + cobrautl.ExitWithError(cobrautl.ExitBadFeature, fmt.Errorf("unknown consistency flag %q", getConsistency)) + } + return false +} diff --git a/tests/common/member_test.go b/tests/common/member_test.go index ec2a15f62..ac5f234b3 100644 --- a/tests/common/member_test.go +++ b/tests/common/member_test.go @@ -40,7 +40,7 @@ func TestMemberList(t *testing.T) { cc := testutils.MustClient(clus.Client()) testutils.ExecuteUntil(ctx, t, func() { - resp, err := cc.MemberList(ctx) + resp, err := cc.MemberList(ctx, false) if err != nil { t.Fatalf("could not get member list, err: %s", err) } @@ -237,7 +237,7 @@ func TestMemberRemove(t *testing.T) { // Otherwise, return a member that client has not connected to. // It ensures that `MemberRemove` function does not return an "etcdserver: server stopped" error. func memberToRemove(ctx context.Context, t *testing.T, client intf.Client, clusterSize int) (memberId uint64, clusterId uint64) { - listResp, err := client.MemberList(ctx) + listResp, err := client.MemberList(ctx, false) if err != nil { t.Fatal(err) } diff --git a/tests/e2e/corrupt_test.go b/tests/e2e/corrupt_test.go index f4be8da45..d33f40ef1 100644 --- a/tests/e2e/corrupt_test.go +++ b/tests/e2e/corrupt_test.go @@ -124,7 +124,7 @@ func TestPeriodicCheckDetectsCorruption(t *testing.T) { assert.NoError(t, err, "error on put") } - members, err := cc.MemberList(ctx) + members, err := cc.MemberList(ctx, false) assert.NoError(t, err, "error on member list") var memberID uint64 for _, m := range members.Members { @@ -171,7 +171,7 @@ func TestCompactHashCheckDetectCorruption(t *testing.T) { err := cc.Put(ctx, testutil.PickKey(int64(i)), fmt.Sprint(i), config.PutOptions{}) assert.NoError(t, err, "error on put") } - members, err := cc.MemberList(ctx) + members, err := cc.MemberList(ctx, false) assert.NoError(t, err, "error on member list") var memberID uint64 for _, m := range members.Members { diff --git a/tests/e2e/ctl_v3_auth_test.go b/tests/e2e/ctl_v3_auth_test.go index 240347582..eeba9e617 100644 --- a/tests/e2e/ctl_v3_auth_test.go +++ b/tests/e2e/ctl_v3_auth_test.go @@ -202,7 +202,7 @@ func authTestMemberUpdate(cx ctlCtx) { cx.user, cx.pass = "root", "root" authSetupTestUser(cx) - mr, err := getMemberList(cx) + mr, err := getMemberList(cx, false) if err != nil { cx.t.Fatal(err) } diff --git a/tests/e2e/ctl_v3_member_test.go b/tests/e2e/ctl_v3_member_test.go index 6ebd73597..463ab2ca5 100644 --- a/tests/e2e/ctl_v3_member_test.go +++ b/tests/e2e/ctl_v3_member_test.go @@ -22,12 +22,20 @@ import ( "strings" "testing" + "github.com/stretchr/testify/require" + "go.etcd.io/etcd/api/v3/etcdserverpb" "go.etcd.io/etcd/tests/v3/framework/e2e" ) func TestCtlV3MemberList(t *testing.T) { testCtl(t, memberListTest) } func TestCtlV3MemberListWithHex(t *testing.T) { testCtl(t, memberListWithHexTest) } +func TestCtlV3MemberListSerializable(t *testing.T) { + cfg := e2e.NewConfig( + e2e.WithClusterSize(1), + ) + testCtl(t, memberListSerializableTest, withCfg(*cfg)) +} func TestCtlV3MemberAdd(t *testing.T) { testCtl(t, memberAddTest) } func TestCtlV3MemberAddAsLearner(t *testing.T) { testCtl(t, memberAddAsLearnerTest) } @@ -52,6 +60,19 @@ func memberListTest(cx ctlCtx) { } } +func memberListSerializableTest(cx ctlCtx) { + resp, err := getMemberList(cx, false) + require.NoError(cx.t, err) + require.Equal(cx.t, 1, len(resp.Members)) + + peerURL := fmt.Sprintf("http://localhost:%d", e2e.EtcdProcessBasePort+11) + err = ctlV3MemberAdd(cx, peerURL, false) + require.NoError(cx.t, err) + + resp, err = getMemberList(cx, true) + require.Equal(cx.t, 2, len(resp.Members)) +} + func ctlV3MemberList(cx ctlCtx) error { cmdArgs := append(cx.PrefixArgs(), "member", "list") lines := make([]string, cx.cfg.ClusterSize) @@ -61,8 +82,11 @@ func ctlV3MemberList(cx ctlCtx) error { return e2e.SpawnWithExpects(cmdArgs, cx.envMap, lines...) } -func getMemberList(cx ctlCtx) (etcdserverpb.MemberListResponse, error) { +func getMemberList(cx ctlCtx, serializable bool) (etcdserverpb.MemberListResponse, error) { cmdArgs := append(cx.PrefixArgs(), "--write-out", "json", "member", "list") + if serializable { + cmdArgs = append(cmdArgs, "--consistency", "s") + } proc, err := e2e.SpawnCmd(cmdArgs, cx.envMap) if err != nil { @@ -86,7 +110,7 @@ func getMemberList(cx ctlCtx) (etcdserverpb.MemberListResponse, error) { } func memberListWithHexTest(cx ctlCtx) { - resp, err := getMemberList(cx) + resp, err := getMemberList(cx, false) if err != nil { cx.t.Fatalf("getMemberList error (%v)", err) } @@ -166,7 +190,7 @@ func ctlV3MemberAdd(cx ctlCtx, peerURL string, isLearner bool) error { } func memberUpdateTest(cx ctlCtx) { - mr, err := getMemberList(cx) + mr, err := getMemberList(cx, false) if err != nil { cx.t.Fatal(err) } diff --git a/tests/e2e/ctl_v3_test.go b/tests/e2e/ctl_v3_test.go index 53a273098..02184f4c3 100644 --- a/tests/e2e/ctl_v3_test.go +++ b/tests/e2e/ctl_v3_test.go @@ -350,7 +350,7 @@ func (cx *ctlCtx) memberToRemove() (ep string, memberID string, clusterID string cx.t.Fatalf("%d-node is too small to test 'member remove'", n1) } - resp, err := getMemberList(*cx) + resp, err := getMemberList(*cx, false) if err != nil { cx.t.Fatal(err) } diff --git a/tests/e2e/v2store_deprecation_test.go b/tests/e2e/v2store_deprecation_test.go index 500e46149..a1480a9c5 100644 --- a/tests/e2e/v2store_deprecation_test.go +++ b/tests/e2e/v2store_deprecation_test.go @@ -158,7 +158,7 @@ func TestV2DeprecationSnapshotRecover(t *testing.T) { lastReleaseGetResponse, err := cc.Get(ctx, "", config.GetOptions{Prefix: true}) assert.NoError(t, err) - lastReleaseMemberListResponse, err := cc.MemberList(ctx) + lastReleaseMemberListResponse, err := cc.MemberList(ctx, false) assert.NoError(t, err) assert.NoError(t, epc.Close()) @@ -174,7 +174,7 @@ func TestV2DeprecationSnapshotRecover(t *testing.T) { currentReleaseGetResponse, err := cc.Get(ctx, "", config.GetOptions{Prefix: true}) assert.NoError(t, err) - currentReleaseMemberListResponse, err := cc.MemberList(ctx) + currentReleaseMemberListResponse, err := cc.MemberList(ctx, false) assert.NoError(t, err) assert.Equal(t, lastReleaseGetResponse.Kvs, currentReleaseGetResponse.Kvs) diff --git a/tests/framework/e2e/cluster.go b/tests/framework/e2e/cluster.go index e8a560ca8..17f992e99 100644 --- a/tests/framework/e2e/cluster.go +++ b/tests/framework/e2e/cluster.go @@ -721,7 +721,7 @@ func (epc *EtcdProcessCluster) CloseProc(ctx context.Context, finder func(EtcdPr // First remove member from the cluster memberCtl := epc.Client(opts...) - memberList, err := memberCtl.MemberList(ctx) + memberList, err := memberCtl.MemberList(ctx, false) if err != nil { return fmt.Errorf("failed to get member list: %w", err) } diff --git a/tests/framework/e2e/etcdctl.go b/tests/framework/e2e/etcdctl.go index f3e0f8112..031e42c9d 100644 --- a/tests/framework/e2e/etcdctl.go +++ b/tests/framework/e2e/etcdctl.go @@ -274,9 +274,13 @@ func AddTxnResponse(resp *clientv3.TxnResponse, jsonData string) { } } -func (ctl *EtcdctlV3) MemberList(ctx context.Context) (*clientv3.MemberListResponse, error) { +func (ctl *EtcdctlV3) MemberList(ctx context.Context, serializable bool) (*clientv3.MemberListResponse, error) { var resp clientv3.MemberListResponse - err := ctl.spawnJsonCmd(ctx, &resp, "member", "list") + args := []string{"member", "list"} + if serializable { + args = append(args, "--consistency", "s") + } + err := ctl.spawnJsonCmd(ctx, &resp, args...) return &resp, err } diff --git a/tests/framework/integration/integration.go b/tests/framework/integration/integration.go index df9db28b8..8d5f786e7 100644 --- a/tests/framework/integration/integration.go +++ b/tests/framework/integration/integration.go @@ -418,3 +418,10 @@ func (c integrationClient) MemberAddAsLearner(ctx context.Context, _ string, pee func (c integrationClient) MemberRemove(ctx context.Context, id uint64) (*clientv3.MemberRemoveResponse, error) { return c.Client.MemberRemove(ctx, id) } + +func (c integrationClient) MemberList(ctx context.Context, serializable bool) (*clientv3.MemberListResponse, error) { + if serializable { + return c.Client.MemberList(ctx, clientv3.WithSerializable()) + } + return c.Client.MemberList(ctx) +} diff --git a/tests/framework/interfaces/interface.go b/tests/framework/interfaces/interface.go index 6bafbd1e8..0477ea5f0 100644 --- a/tests/framework/interfaces/interface.go +++ b/tests/framework/interfaces/interface.go @@ -78,7 +78,7 @@ type Client interface { Txn(context context.Context, compares, ifSucess, ifFail []string, o config.TxnOptions) (*clientv3.TxnResponse, error) - MemberList(context context.Context) (*clientv3.MemberListResponse, error) + MemberList(context context.Context, serializable bool) (*clientv3.MemberListResponse, error) MemberAdd(context context.Context, name string, peerAddrs []string) (*clientv3.MemberAddResponse, error) MemberAddAsLearner(context context.Context, name string, peerAddrs []string) (*clientv3.MemberAddResponse, error) MemberRemove(ctx context.Context, id uint64) (*clientv3.MemberRemoveResponse, error)