diff --git a/tests/e2e/v3_curl_cluster_test.go b/tests/e2e/v3_curl_cluster_test.go new file mode 100644 index 00000000000..9d3b8197f79 --- /dev/null +++ b/tests/e2e/v3_curl_cluster_test.go @@ -0,0 +1,123 @@ +// Copyright 2023 The etcd Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package e2e + +import ( + "encoding/json" + "fmt" + "strconv" + "testing" + + "github.com/stretchr/testify/require" + + pb "go.etcd.io/etcd/api/v3/etcdserverpb" + "go.etcd.io/etcd/pkg/v3/expect" + "go.etcd.io/etcd/tests/v3/framework/e2e" +) + +func TestCurlV3ClusterOperations(t *testing.T) { + testCtl(t, testCurlV3ClusterOperations, withCfg(*e2e.NewConfig(e2e.WithClusterSize(1)))) +} + +func testCurlV3ClusterOperations(cx ctlCtx) { + var ( + peerURL = "http://127.0.0.1:22380" + updatedPeerURL = "http://127.0.0.1:32380" + ) + + // add member + cx.t.Logf("Adding member %q", peerURL) + addMemberReq, err := json.Marshal(&pb.MemberAddRequest{PeerURLs: []string{peerURL}, IsLearner: true}) + require.NoError(cx.t, err) + + if err = e2e.CURLPost(cx.epc, e2e.CURLReq{ + Endpoint: "/v3/cluster/member/add", + Value: string(addMemberReq), + Expected: expect.ExpectedResponse{Value: peerURL}, + }); err != nil { + cx.t.Fatalf("testCurlV3ClusterOperations failed to add member (%v)", err) + } + + // list members and get the new member's ID + cx.t.Log("Listing members after adding a member") + members := mustListMembers(cx) + require.Equal(cx.t, 2, len(members)) + cx.t.Logf("members: %+v", members) + + var newMemberIDStr string + for _, m := range members { + mObj := m.(map[string]interface{}) + pURLs, _ := mObj["peerURLs"] + pURL := pURLs.([]interface{})[0].(string) + if pURL == peerURL { + newMemberIDStr = mObj["ID"].(string) + break + } + } + require.True(cx.t, len(newMemberIDStr) > 0) + + // update member + cx.t.Logf("Update peerURL from %q to %q for member %q", peerURL, updatedPeerURL, newMemberIDStr) + newMemberID, err := strconv.ParseUint(newMemberIDStr, 10, 64) + updateMemberReq, err := json.Marshal(&pb.MemberUpdateRequest{ID: newMemberID, PeerURLs: []string{updatedPeerURL}}) + require.NoError(cx.t, err) + + if err = e2e.CURLPost(cx.epc, e2e.CURLReq{ + Endpoint: "/v3/cluster/member/update", + Value: string(updateMemberReq), + Expected: expect.ExpectedResponse{Value: updatedPeerURL}, + }); err != nil { + cx.t.Fatalf("testCurlV3ClusterOperations failed to update member (%v)", err) + } + + // promote member + cx.t.Logf("Promoting the member %d", newMemberID) + if err = e2e.CURLPost(cx.epc, e2e.CURLReq{ + Endpoint: "/v3/cluster/member/promote", + Value: fmt.Sprintf(`{"ID": %d}`, newMemberID), + Expected: expect.ExpectedResponse{Value: "etcdserver: can only promote a learner member which is in sync with leader"}, + }); err != nil { + cx.t.Fatalf("testCurlV3ClusterOperations failed to promote member (%v)", err) + } + + // remove member + cx.t.Logf("Removing the member %d", newMemberID) + if err = e2e.CURLPost(cx.epc, e2e.CURLReq{ + Endpoint: "/v3/cluster/member/remove", + Value: fmt.Sprintf(`{"ID": %d}`, newMemberID), + Expected: expect.ExpectedResponse{Value: "members"}, + }); err != nil { + cx.t.Fatalf("testCurlV3ClusterOperations failed to remove member (%v)", err) + } + + // list members again after deleting a member + cx.t.Log("Listing members again after deleting a member") + members = mustListMembers(cx) + require.Equal(cx.t, 1, len(members)) +} + +func mustListMembers(cx ctlCtx) []interface{} { + clus := cx.epc + args := e2e.CURLPrefixArgsCluster(clus.Cfg, clus.Procs[0], "POST", e2e.CURLReq{ + Endpoint: "/v3/cluster/member/list", + Value: "{}", + }) + resp, err := runCommandAndReadJsonOutput(args) + require.NoError(cx.t, err) + + members, ok := resp["members"] + require.True(cx.t, ok) + return members.([]interface{}) +}