From caaf53ad3e42aadcbddee5eec543438dcbf80f86 Mon Sep 17 00:00:00 2001 From: Diogo Monica Date: Wed, 27 Jul 2016 21:17:00 -0700 Subject: [PATCH] Add --force to node removal Signed-off-by: Diogo Monica (cherry picked from commit a327c231b5c68c13b7dcde2fdc83b8e4cec59c43) Signed-off-by: Tibor Vass --- api/client/node/remove.go | 20 +++++++++++---- api/server/router/swarm/backend.go | 2 +- api/server/router/swarm/cluster_routes.go | 8 +++++- daemon/cluster/cluster.go | 4 +-- docs/reference/commandline/node_rm.md | 21 ++++++++++++++- integration-cli/daemon_swarm.go | 11 ++++++++ integration-cli/docker_api_swarm_test.go | 31 +++++++++++++++++++++++ 7 files changed, 87 insertions(+), 10 deletions(-) diff --git a/api/client/node/remove.go b/api/client/node/remove.go index 4966f26675..bb54a831e8 100644 --- a/api/client/node/remove.go +++ b/api/client/node/remove.go @@ -7,26 +7,36 @@ import ( "github.com/docker/docker/api/client" "github.com/docker/docker/cli" + "github.com/docker/engine-api/types" "github.com/spf13/cobra" ) +type removeOptions struct { + force bool +} + func newRemoveCommand(dockerCli *client.DockerCli) *cobra.Command { - return &cobra.Command{ - Use: "rm NODE [NODE...]", + opts := removeOptions{} + + cmd := &cobra.Command{ + Use: "rm [OPTIONS] NODE [NODE...]", Aliases: []string{"remove"}, Short: "Remove one or more nodes from the swarm", Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runRemove(dockerCli, args) + return runRemove(dockerCli, args, opts) }, } + flags := cmd.Flags() + flags.BoolVar(&opts.force, "force", false, "Force remove an active node") + return cmd } -func runRemove(dockerCli *client.DockerCli, args []string) error { +func runRemove(dockerCli *client.DockerCli, args []string, opts removeOptions) error { client := dockerCli.Client() ctx := context.Background() for _, nodeID := range args { - err := client.NodeRemove(ctx, nodeID) + err := client.NodeRemove(ctx, nodeID, types.NodeRemoveOptions{Force: opts.force}) if err != nil { return err } diff --git a/api/server/router/swarm/backend.go b/api/server/router/swarm/backend.go index 36ede04b84..e51ce7ba6b 100644 --- a/api/server/router/swarm/backend.go +++ b/api/server/router/swarm/backend.go @@ -20,7 +20,7 @@ type Backend interface { GetNodes(basictypes.NodeListOptions) ([]types.Node, error) GetNode(string) (types.Node, error) UpdateNode(string, uint64, types.NodeSpec) error - RemoveNode(string) error + RemoveNode(string, bool) error GetTasks(basictypes.TaskListOptions) ([]types.Task, error) GetTask(string) (types.Task, error) } diff --git a/api/server/router/swarm/cluster_routes.go b/api/server/router/swarm/cluster_routes.go index 911284b40d..53b49b7d59 100644 --- a/api/server/router/swarm/cluster_routes.go +++ b/api/server/router/swarm/cluster_routes.go @@ -219,7 +219,13 @@ func (sr *swarmRouter) updateNode(ctx context.Context, w http.ResponseWriter, r } func (sr *swarmRouter) removeNode(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { - if err := sr.backend.RemoveNode(vars["id"]); err != nil { + if err := httputils.ParseForm(r); err != nil { + return err + } + + force := httputils.BoolValue(r, "force") + + if err := sr.backend.RemoveNode(vars["id"], force); err != nil { logrus.Errorf("Error removing node %s: %v", vars["id"], err) return err } diff --git a/daemon/cluster/cluster.go b/daemon/cluster/cluster.go index 3dfbeaa89a..a0816316b1 100644 --- a/daemon/cluster/cluster.go +++ b/daemon/cluster/cluster.go @@ -1023,7 +1023,7 @@ func (c *Cluster) UpdateNode(nodeID string, version uint64, spec types.NodeSpec) } // RemoveNode removes a node from a cluster -func (c *Cluster) RemoveNode(input string) error { +func (c *Cluster) RemoveNode(input string, force bool) error { c.RLock() defer c.RUnlock() @@ -1039,7 +1039,7 @@ func (c *Cluster) RemoveNode(input string) error { return err } - if _, err := c.client.RemoveNode(ctx, &swarmapi.RemoveNodeRequest{NodeID: node.ID}); err != nil { + if _, err := c.client.RemoveNode(ctx, &swarmapi.RemoveNodeRequest{NodeID: node.ID, Force: force}); err != nil { return err } return nil diff --git a/docs/reference/commandline/node_rm.md b/docs/reference/commandline/node_rm.md index 9d6de5727c..6f1d13cd2b 100644 --- a/docs/reference/commandline/node_rm.md +++ b/docs/reference/commandline/node_rm.md @@ -13,7 +13,7 @@ parent = "smn_cli" # node rm ```markdown -Usage: docker node rm NODE [NODE...] +Usage: docker node rm [OPTIONS] NODE [NODE...] Remove one or more nodes from the swarm @@ -21,6 +21,7 @@ Aliases: rm, remove Options: + --force Force remove an active node --help Print usage ``` @@ -32,6 +33,24 @@ Example output: $ docker node rm swarm-node-02 Node swarm-node-02 removed from swarm +Removes nodes from the swarm that are in the down state. Attempting to remove +an active node will result in an error: + +```bash +$ docker node rm swarm-node-03 +Error response from daemon: rpc error: code = 9 desc = node swarm-node-03 is not down and can't be removed +``` + +If a worker node becomes compromised, exhibits unexpected or unwanted behavior, or if you lose access to it so +that a clean shutdown is impossible, you can use the force option. + +```bash +$ docker node rm --force swarm-node-03 +Node swarm-node-03 removed from swarm +``` + +Note that manager nodes have to be demoted to worker nodes before they can be removed +from the cluster. ## Related information diff --git a/integration-cli/daemon_swarm.go b/integration-cli/daemon_swarm.go index 39505a8dbf..9194fc05e8 100644 --- a/integration-cli/daemon_swarm.go +++ b/integration-cli/daemon_swarm.go @@ -208,6 +208,17 @@ func (d *SwarmDaemon) getNode(c *check.C, id string) *swarm.Node { return &node } +func (d *SwarmDaemon) removeNode(c *check.C, id string, force bool) { + url := "/nodes/" + id + if force { + url += "?force=1" + } + + status, out, err := d.SockRequest("DELETE", url, nil) + c.Assert(status, checker.Equals, http.StatusOK, check.Commentf("output: %q", string(out))) + c.Assert(err, checker.IsNil) +} + func (d *SwarmDaemon) updateNode(c *check.C, id string, f ...nodeConstructor) { for i := 0; ; i++ { node := d.getNode(c, id) diff --git a/integration-cli/docker_api_swarm_test.go b/integration-cli/docker_api_swarm_test.go index 364241ba35..ec04f2bd71 100644 --- a/integration-cli/docker_api_swarm_test.go +++ b/integration-cli/docker_api_swarm_test.go @@ -510,6 +510,37 @@ func (s *DockerSwarmSuite) TestApiSwarmNodeUpdate(c *check.C) { c.Assert(n.Spec.Availability, checker.Equals, swarm.NodeAvailabilityPause) } +func (s *DockerSwarmSuite) TestApiSwarmNodeRemove(c *check.C) { + testRequires(c, Network) + d1 := s.AddDaemon(c, true, true) + d2 := s.AddDaemon(c, true, false) + _ = s.AddDaemon(c, true, false) + + nodes := d1.listNodes(c) + c.Assert(len(nodes), checker.Equals, 3, check.Commentf("nodes: %#v", nodes)) + + // Getting the info so we can take the NodeID + d2Info, err := d2.info() + c.Assert(err, checker.IsNil) + + // forceful removal of d2 should work + d1.removeNode(c, d2Info.NodeID, true) + + nodes = d1.listNodes(c) + c.Assert(len(nodes), checker.Equals, 2, check.Commentf("nodes: %#v", nodes)) + + // Restart the node that was removed + err = d2.Restart() + c.Assert(err, checker.IsNil) + + // Give some time for the node to rejoin + time.Sleep(1 * time.Second) + + // Make sure the node didn't rejoin + nodes = d1.listNodes(c) + c.Assert(len(nodes), checker.Equals, 2, check.Commentf("nodes: %#v", nodes)) +} + func (s *DockerSwarmSuite) TestApiSwarmNodeDrainPause(c *check.C) { testRequires(c, Network) d1 := s.AddDaemon(c, true, true)