diff --git a/e2e/ctl_v3_move_leader_test.go b/e2e/ctl_v3_move_leader_test.go new file mode 100644 index 000000000000..ec2f134d5e34 --- /dev/null +++ b/e2e/ctl_v3_move_leader_test.go @@ -0,0 +1,92 @@ +// Copyright 2017 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 ( + "context" + "fmt" + "os" + "testing" + "time" + + "github.com/coreos/etcd/clientv3" + "github.com/coreos/etcd/pkg/testutil" + "github.com/coreos/etcd/pkg/types" +) + +func TestCtlV3MoveLeader(t *testing.T) { + defer testutil.AfterTest(t) + + epc := setupEtcdctlTest(t, &configNoTLS, true) + defer func() { + if errC := epc.Close(); errC != nil { + t.Fatalf("error closing etcd processes (%v)", errC) + } + }() + + var leadIdx int + var leaderID uint64 + var transferee uint64 + for i, ep := range epc.grpcEndpoints() { + cli, err := clientv3.New(clientv3.Config{ + Endpoints: []string{ep}, + DialTimeout: 3 * time.Second, + }) + if err != nil { + t.Fatal(err) + } + resp, err := cli.Status(context.Background(), ep) + if err != nil { + t.Fatal(err) + } + cli.Close() + + if resp.Header.GetMemberId() == resp.Leader { + leadIdx = i + leaderID = resp.Leader + } else { + transferee = resp.Header.GetMemberId() + } + } + + os.Setenv("ETCDCTL_API", "3") + defer os.Unsetenv("ETCDCTL_API") + cx := ctlCtx{ + t: t, + cfg: configNoTLS, + dialTimeout: 7 * time.Second, + epc: epc, + } + + tests := []struct { + prefixes []string + expect string + }{ + { // request to non-leader + cx.prefixArgs([]string{cx.epc.grpcEndpoints()[(leadIdx+1)%3]}), + "no leader endpoint given at ", + }, + { // request to leader + cx.prefixArgs([]string{cx.epc.grpcEndpoints()[leadIdx]}), + fmt.Sprintf("Leadership transferred from %s to %s", types.ID(leaderID), types.ID(transferee)), + }, + } + for i, tc := range tests { + cmdArgs := append(tc.prefixes, "move-leader", types.ID(transferee).String()) + if err := spawnWithExpect(cmdArgs, tc.expect); err != nil { + t.Fatalf("#%d: %v", i, err) + } + } +} diff --git a/etcdctl/README.md b/etcdctl/README.md index 84ad1dce2892..371085f1462a 100644 --- a/etcdctl/README.md +++ b/etcdctl/README.md @@ -805,6 +805,29 @@ Prints a line of JSON encoding the database hash, revision, total keys, and size +----------+----------+------------+------------+ ``` +### MOVE-LEADER \ + +MOVE-LEADER manually transfers a leadership to another node in the cluster. + +#### Example + +```bash +# to choose transferee +transferee_id=$(./etcdctl \ + --endpoints localhost:12379,localhost:22379,localhost:32379 \ + endpoint status | grep -m 1 "false" | awk -F', ' '{print $2}') +echo ${transferee_id} +# c89feb932daef420 + +# endpoints should include leader node +./etcdctl --endpoints ${transferee_ep} move-leader ${transferee_id} +# Error: no leader endpoint given at [localhost:22379 localhost:32379] + +# request to leader with target node ID +./etcdctl --endpoints ${leader_ep} move-leader ${transferee_id} +# Leadership transferred from 45ddc0e800e20b93 to c89feb932daef420 +``` + ## Concurrency commands ### LOCK \ [command arg1 arg2 ...] diff --git a/etcdctl/ctlv3/command/move_leader_command.go b/etcdctl/ctlv3/command/move_leader_command.go new file mode 100644 index 000000000000..b0b7dd315e87 --- /dev/null +++ b/etcdctl/ctlv3/command/move_leader_command.go @@ -0,0 +1,90 @@ +// Copyright 2017 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 command + +import ( + "fmt" + "strconv" + "time" + + "github.com/coreos/etcd/clientv3" + "github.com/spf13/cobra" +) + +// NewMoveLeaderCommand returns the cobra command for "move-leader". +func NewMoveLeaderCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "move-leader ", + Short: "Transfers leadership.", + Run: transferLeadershipCommandFunc, + } + return cmd +} + +// transferLeadershipCommandFunc executes the "compaction" command. +func transferLeadershipCommandFunc(cmd *cobra.Command, args []string) { + if len(args) != 1 { + ExitWithError(ExitBadArgs, fmt.Errorf("move-leader command needs 1 argument")) + } + target, err := strconv.ParseUint(args[0], 16, 64) + if err != nil { + ExitWithError(ExitBadArgs, err) + } + + c := mustClientFromCmd(cmd) + eps := c.Endpoints() + c.Close() + + ctx, cancel := commandCtx(cmd) + + // find current leader + var leaderCli *clientv3.Client + var leaderID uint64 + for _, ep := range eps { + cli, err := clientv3.New(clientv3.Config{ + Endpoints: []string{ep}, + DialTimeout: 3 * time.Second, + }) + if err != nil { + if err != nil { + ExitWithError(ExitError, err) + } + } + resp, err := cli.Status(ctx, ep) + if err != nil { + if err != nil { + ExitWithError(ExitError, err) + } + } + + if resp.Header.GetMemberId() == resp.Leader { + leaderCli = cli + leaderID = resp.Leader + } else { + cli.Close() + } + } + if leaderCli == nil { + ExitWithError(ExitBadArgs, fmt.Errorf("no leader endpoint given at %v", eps)) + } + + cerr := leaderCli.MoveLeader(ctx, target) + cancel() + if cerr != nil { + ExitWithError(ExitError, cerr) + } + + display.MoveLeader(leaderID, target) +} diff --git a/etcdctl/ctlv3/command/printer.go b/etcdctl/ctlv3/command/printer.go index ad29e75cd1b0..267649488646 100644 --- a/etcdctl/ctlv3/command/printer.go +++ b/etcdctl/ctlv3/command/printer.go @@ -43,6 +43,7 @@ type printer interface { MemberList(v3.MemberListResponse) EndpointStatus([]epStatus) + MoveLeader(leader, target uint64) Alarm(v3.AlarmResponse) DBStatus(dbstatus) @@ -143,7 +144,10 @@ func newPrinterUnsupported(n string) printer { } func (p *printerUnsupported) EndpointStatus([]epStatus) { p.p(nil) } -func (p *printerUnsupported) DBStatus(dbstatus) { p.p(nil) } +func (p *printerUnsupported) MoveLeader(leader, target uint64) { + p.p(nil) +} +func (p *printerUnsupported) DBStatus(dbstatus) { p.p(nil) } func makeMemberListTable(r v3.MemberListResponse) (hdr []string, rows [][]string) { hdr = []string{"ID", "Status", "Name", "Peer Addrs", "Client Addrs"} diff --git a/etcdctl/ctlv3/command/printer_simple.go b/etcdctl/ctlv3/command/printer_simple.go index baa79e23d7cb..f6abf84c69b8 100644 --- a/etcdctl/ctlv3/command/printer_simple.go +++ b/etcdctl/ctlv3/command/printer_simple.go @@ -20,6 +20,7 @@ import ( v3 "github.com/coreos/etcd/clientv3" pb "github.com/coreos/etcd/etcdserver/etcdserverpb" + "github.com/coreos/etcd/pkg/types" ) type simplePrinter struct { @@ -135,6 +136,10 @@ func (s *simplePrinter) EndpointStatus(statusList []epStatus) { } } +func (s *simplePrinter) MoveLeader(leader, target uint64) { + fmt.Printf("Leadership transferred from %s to %s\n", types.ID(leader), types.ID(target)) +} + func (s *simplePrinter) DBStatus(ds dbstatus) { _, rows := makeDBStatusTable(ds) for _, row := range rows { diff --git a/etcdctl/ctlv3/ctl.go b/etcdctl/ctlv3/ctl.go index 92e715d97d51..affadc3c3259 100644 --- a/etcdctl/ctlv3/ctl.go +++ b/etcdctl/ctlv3/ctl.go @@ -82,6 +82,7 @@ func init() { command.NewUserCommand(), command.NewRoleCommand(), command.NewCheckCommand(), + command.NewMoveLeaderCommand(), ) }