open-nomad/command/node_drain_test.go

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

452 lines
13 KiB
Go
Raw Normal View History

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
2015-09-13 00:09:03 +00:00
package command
import (
2018-03-30 18:07:40 +00:00
"bytes"
2017-08-22 20:13:44 +00:00
"fmt"
2015-09-13 00:09:03 +00:00
"strings"
"testing"
"time"
2015-09-13 00:09:03 +00:00
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/command/agent"
"github.com/hashicorp/nomad/helper/pointer"
2017-08-22 20:13:44 +00:00
"github.com/hashicorp/nomad/testutil"
2015-09-13 00:09:03 +00:00
"github.com/mitchellh/cli"
2017-08-22 20:13:44 +00:00
"github.com/posener/complete"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
2015-09-13 00:09:03 +00:00
)
func TestNodeDrainCommand_Implements(t *testing.T) {
ci.Parallel(t)
2015-09-13 00:09:03 +00:00
var _ cli.Command = &NodeDrainCommand{}
}
func TestNodeDrainCommand_Detach(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
server, client, url := testServer(t, true, func(c *agent.Config) {
c.NodeName = "drain_detach_node"
})
defer server.Shutdown()
// Wait for a node to appear
var nodeID string
testutil.WaitForResult(func() (bool, error) {
nodes, _, err := client.Nodes().List(nil)
if err != nil {
return false, err
}
if len(nodes) == 0 {
return false, fmt.Errorf("missing node")
}
nodeID = nodes[0].ID
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)
})
// Register a job to create an alloc to drain that will block draining
job := &api.Job{
ID: pointer.Of("mock_service"),
Name: pointer.Of("mock_service"),
Datacenters: []string{"dc1"},
TaskGroups: []*api.TaskGroup{
{
Name: pointer.Of("mock_group"),
Tasks: []*api.Task{
{
Name: "mock_task",
Driver: "mock_driver",
Config: map[string]interface{}{
"run_for": "10m",
},
},
},
},
},
}
_, _, err := client.Jobs().Register(job, nil)
require.Nil(err)
testutil.WaitForResult(func() (bool, error) {
allocs, _, err := client.Nodes().Allocations(nodeID, nil)
if err != nil {
return false, err
}
return len(allocs) > 0, fmt.Errorf("no allocs")
}, func(err error) {
t.Fatalf("err: %v", err)
})
2020-10-05 14:07:41 +00:00
ui := cli.NewMockUi()
cmd := &NodeDrainCommand{Meta: Meta{Ui: ui}}
if code := cmd.Run([]string{"-address=" + url, "-self", "-enable", "-detach"}); code != 0 {
t.Fatalf("expected exit 0, got: %d", code)
}
out := ui.OutputWriter.String()
expected := "drain strategy set"
require.Contains(out, expected)
node, _, err := client.Nodes().Info(nodeID, nil)
require.Nil(err)
require.NotNil(node.DrainStrategy)
}
func TestNodeDrainCommand_Monitor(t *testing.T) {
ci.Parallel(t)
require := require.New(t)
server, client, url := testServer(t, true, func(c *agent.Config) {
c.NodeName = "drain_monitor_node"
})
defer server.Shutdown()
// Wait for a node to appear
var nodeID string
testutil.WaitForResult(func() (bool, error) {
nodes, _, err := client.Nodes().List(nil)
if err != nil {
return false, err
}
if len(nodes) == 0 {
return false, fmt.Errorf("missing node")
}
if _, ok := nodes[0].Drivers["mock_driver"]; !ok {
return false, fmt.Errorf("mock_driver not ready")
}
nodeID = nodes[0].ID
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)
})
// Register a service job to create allocs to drain
serviceCount := 3
job := &api.Job{
ID: pointer.Of("mock_service"),
Name: pointer.Of("mock_service"),
Datacenters: []string{"dc1"},
Type: pointer.Of("service"),
TaskGroups: []*api.TaskGroup{
{
Name: pointer.Of("mock_group"),
Count: &serviceCount,
Migrate: &api.MigrateStrategy{
MaxParallel: pointer.Of(1),
HealthCheck: pointer.Of("task_states"),
MinHealthyTime: pointer.Of(10 * time.Millisecond),
HealthyDeadline: pointer.Of(5 * time.Minute),
},
Tasks: []*api.Task{
{
Name: "mock_task",
Driver: "mock_driver",
Config: map[string]interface{}{
"run_for": "10m",
},
Resources: &api.Resources{
CPU: pointer.Of(50),
MemoryMB: pointer.Of(50),
},
},
},
},
},
}
_, _, err := client.Jobs().Register(job, nil)
require.Nil(err)
// Register a system job to ensure it is ignored during draining
sysjob := &api.Job{
ID: pointer.Of("mock_system"),
Name: pointer.Of("mock_system"),
Datacenters: []string{"dc1"},
Type: pointer.Of("system"),
TaskGroups: []*api.TaskGroup{
{
Name: pointer.Of("mock_sysgroup"),
Count: pointer.Of(1),
Tasks: []*api.Task{
{
Name: "mock_systask",
Driver: "mock_driver",
Config: map[string]interface{}{
"run_for": "10m",
},
Resources: &api.Resources{
CPU: pointer.Of(50),
MemoryMB: pointer.Of(50),
},
},
},
},
},
}
_, _, err = client.Jobs().Register(sysjob, nil)
require.Nil(err)
var allocs []*api.Allocation
testutil.WaitForResult(func() (bool, error) {
allocs, _, err = client.Nodes().Allocations(nodeID, nil)
if err != nil {
return false, err
}
if len(allocs) != serviceCount+1 {
return false, fmt.Errorf("number of allocs %d != count (%d)", len(allocs), serviceCount+1)
}
for _, a := range allocs {
if a.ClientStatus != "running" {
return false, fmt.Errorf("alloc %q still not running: %s", a.ID, a.ClientStatus)
}
}
return true, nil
}, func(err error) {
t.Fatalf("err: %v", err)
})
2018-03-30 18:07:40 +00:00
outBuf := bytes.NewBuffer(nil)
ui := &cli.BasicUi{
Reader: bytes.NewReader(nil),
Writer: outBuf,
ErrorWriter: outBuf,
}
cmd := &NodeDrainCommand{Meta: Meta{Ui: ui}}
args := []string{"-address=" + url, "-self", "-enable", "-deadline", "1s", "-ignore-system"}
t.Logf("Running: %v", args)
require.Zero(cmd.Run(args))
2018-03-30 18:07:40 +00:00
out := outBuf.String()
t.Logf("Output:\n%s", out)
2018-05-31 22:50:05 +00:00
// Unfortunately travis is too slow to reliably see the expected output. The
// monitor goroutines may start only after some or all the allocs have been
// migrated.
if !testutil.IsTravis() {
require.Contains(out, "Drain complete for node")
2018-05-31 22:50:05 +00:00
for _, a := range allocs {
if *a.Job.Type == "system" {
if strings.Contains(out, a.ID) {
t.Fatalf("output should not contain system alloc %q", a.ID)
}
continue
}
2018-05-31 22:50:05 +00:00
require.Contains(out, fmt.Sprintf("Alloc %q marked for migration", a.ID))
require.Contains(out, fmt.Sprintf("Alloc %q draining", a.ID))
}
2018-05-31 22:50:05 +00:00
expected := fmt.Sprintf("All allocations on node %q have stopped\n", nodeID)
2018-06-07 22:47:03 +00:00
if !strings.HasSuffix(out, expected) {
t.Fatalf("expected output to end with:\n%s", expected)
}
}
// Test -monitor flag
outBuf.Reset()
args = []string{"-address=" + url, "-self", "-monitor", "-ignore-system"}
t.Logf("Running: %v", args)
require.Zero(cmd.Run(args))
out = outBuf.String()
t.Logf("Output:\n%s", out)
2018-05-24 10:30:33 +00:00
require.Contains(out, "No drain strategy set")
}
2018-06-06 00:58:44 +00:00
func TestNodeDrainCommand_Monitor_NoDrainStrategy(t *testing.T) {
ci.Parallel(t)
2018-06-06 00:58:44 +00:00
require := require.New(t)
server, client, url := testServer(t, true, func(c *agent.Config) {
2018-06-07 22:47:03 +00:00
c.NodeName = "drain_monitor_node2"
2018-06-06 00:58:44 +00:00
})
defer server.Shutdown()
// Wait for a node to appear
testutil.WaitForResult(func() (bool, error) {
nodes, _, err := client.Nodes().List(nil)
if err != nil {
return false, err
}
if len(nodes) == 0 {
return false, fmt.Errorf("missing node")
}
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)
})
// Test -monitor flag
outBuf := bytes.NewBuffer(nil)
ui := &cli.BasicUi{
Reader: bytes.NewReader(nil),
Writer: outBuf,
ErrorWriter: outBuf,
}
cmd := &NodeDrainCommand{Meta: Meta{Ui: ui}}
args := []string{"-address=" + url, "-self", "-monitor", "-ignore-system"}
t.Logf("Running: %v", args)
if code := cmd.Run(args); code != 0 {
t.Fatalf("expected exit 0, got: %d\n%s", code, outBuf.String())
}
out := outBuf.String()
t.Logf("Output:\n%s", out)
require.Contains(out, "No drain strategy set")
}
2015-09-13 00:09:03 +00:00
func TestNodeDrainCommand_Fails(t *testing.T) {
ci.Parallel(t)
2017-07-21 04:07:32 +00:00
srv, _, url := testServer(t, false, nil)
defer srv.Shutdown()
2015-09-13 00:09:03 +00:00
2020-10-05 14:07:41 +00:00
ui := cli.NewMockUi()
cmd := &NodeDrainCommand{Meta: Meta{Ui: ui}}
2015-09-13 00:09:03 +00:00
// Fails on misuse
if code := cmd.Run([]string{"some", "bad", "args"}); code != 1 {
t.Fatalf("expected exit code 1, got: %d", code)
}
if out := ui.ErrorWriter.String(); !strings.Contains(out, commandErrorText(cmd)) {
2015-09-13 00:09:03 +00:00
t.Fatalf("expected help output, got: %s", out)
}
ui.ErrorWriter.Reset()
// Fails on connection failure
if code := cmd.Run([]string{"-address=nope", "-enable", "12345678-abcd-efab-cdef-123456789abc"}); code != 1 {
2015-09-13 00:09:03 +00:00
t.Fatalf("expected exit code 1, got: %d", code)
}
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error toggling") {
2015-09-13 00:09:03 +00:00
t.Fatalf("expected failed toggle error, got: %s", out)
}
ui.ErrorWriter.Reset()
// Fails on nonexistent node
if code := cmd.Run([]string{"-address=" + url, "-enable", "12345678-abcd-efab-cdef-123456789abc"}); code != 1 {
2015-09-13 00:09:03 +00:00
t.Fatalf("expected exit 1, got: %d", code)
}
if out := ui.ErrorWriter.String(); !strings.Contains(out, "No node(s) with prefix or id") {
2015-09-13 00:09:03 +00:00
t.Fatalf("expected not exist error, got: %s", out)
}
ui.ErrorWriter.Reset()
// Fails if both enable and disable specified
if code := cmd.Run([]string{"-enable", "-disable", "12345678-abcd-efab-cdef-123456789abc"}); code != 1 {
2015-09-13 00:09:03 +00:00
t.Fatalf("expected exit 1, got: %d", code)
}
if out := ui.ErrorWriter.String(); !strings.Contains(out, commandErrorText(cmd)) {
2015-09-13 00:09:03 +00:00
t.Fatalf("expected help output, got: %s", out)
}
ui.ErrorWriter.Reset()
// Fails if neither enable or disable specified
if code := cmd.Run([]string{"12345678-abcd-efab-cdef-123456789abc"}); code != 1 {
2015-09-13 00:09:03 +00:00
t.Fatalf("expected exit 1, got: %d", code)
}
if out := ui.ErrorWriter.String(); !strings.Contains(out, commandErrorText(cmd)) {
2015-09-13 00:09:03 +00:00
t.Fatalf("expected help output, got: %s", out)
}
ui.ErrorWriter.Reset()
// Fail on identifier with too few characters
if code := cmd.Run([]string{"-address=" + url, "-enable", "1"}); code != 1 {
t.Fatalf("expected exit 1, got: %d", code)
}
if out := ui.ErrorWriter.String(); !strings.Contains(out, "must contain at least two characters.") {
t.Fatalf("expected too few characters error, got: %s", out)
}
ui.ErrorWriter.Reset()
// Identifiers with uneven length should produce a query result
if code := cmd.Run([]string{"-address=" + url, "-enable", "123"}); code != 1 {
t.Fatalf("expected exit 1, got: %d", code)
}
if out := ui.ErrorWriter.String(); !strings.Contains(out, "No node(s) with prefix or id") {
t.Fatalf("expected not exist error, got: %s", out)
}
2018-02-23 23:56:36 +00:00
ui.ErrorWriter.Reset()
// Fail on disable being used with drain strategy flags
for _, flag := range []string{"-force", "-no-deadline", "-ignore-system"} {
if code := cmd.Run([]string{"-address=" + url, "-disable", flag, "12345678-abcd-efab-cdef-123456789abc"}); code != 1 {
t.Fatalf("expected exit 1, got: %d", code)
}
if out := ui.ErrorWriter.String(); !strings.Contains(out, "combined with flags configuring drain strategy") {
t.Fatalf("got: %s", out)
}
ui.ErrorWriter.Reset()
}
// Fail on setting a deadline plus deadline modifying flags
for _, flag := range []string{"-force", "-no-deadline"} {
if code := cmd.Run([]string{"-address=" + url, "-enable", "-deadline=10s", flag, "12345678-abcd-efab-cdef-123456789abc"}); code != 1 {
t.Fatalf("expected exit 1, got: %d", code)
}
if out := ui.ErrorWriter.String(); !strings.Contains(out, "deadline can't be combined with") {
t.Fatalf("got: %s", out)
}
ui.ErrorWriter.Reset()
}
// Fail on setting a force and no deadline
if code := cmd.Run([]string{"-address=" + url, "-enable", "-force", "-no-deadline", "12345678-abcd-efab-cdef-123456789abc"}); code != 1 {
t.Fatalf("expected exit 1, got: %d", code)
}
if out := ui.ErrorWriter.String(); !strings.Contains(out, "mutually exclusive") {
t.Fatalf("got: %s", out)
}
ui.ErrorWriter.Reset()
// Fail on setting a bad deadline
for _, flag := range []string{"-deadline=0s", "-deadline=-1s"} {
if code := cmd.Run([]string{"-address=" + url, "-enable", flag, "12345678-abcd-efab-cdef-123456789abc"}); code != 1 {
t.Fatalf("expected exit 1, got: %d", code)
}
if out := ui.ErrorWriter.String(); !strings.Contains(out, "positive") {
t.Fatalf("got: %s", out)
}
ui.ErrorWriter.Reset()
}
2015-09-13 00:09:03 +00:00
}
2017-08-22 20:13:44 +00:00
func TestNodeDrainCommand_AutocompleteArgs(t *testing.T) {
ci.Parallel(t)
2017-08-22 20:13:44 +00:00
assert := assert.New(t)
srv, client, url := testServer(t, true, nil)
defer srv.Shutdown()
// Wait for a node to appear
var nodeID string
testutil.WaitForResult(func() (bool, error) {
nodes, _, err := client.Nodes().List(nil)
if err != nil {
return false, err
}
if len(nodes) == 0 {
return false, fmt.Errorf("missing node")
}
nodeID = nodes[0].ID
return true, nil
}, func(err error) {
t.Fatalf("err: %s", err)
})
2020-10-05 14:07:41 +00:00
ui := cli.NewMockUi()
2017-08-22 20:13:44 +00:00
cmd := &NodeDrainCommand{Meta: Meta{Ui: ui, flagAddress: url}}
prefix := nodeID[:len(nodeID)-5]
args := complete.Args{Last: prefix}
predictor := cmd.AutocompleteArgs()
res := predictor.Predict(args)
assert.Equal(1, len(res))
assert.Equal(nodeID, res[0])
}