github.com/rohankumardubey/nomad@v0.11.8/command/job_status_test.go (about)

     1  package command
     2  
     3  import (
     4  	"fmt"
     5  	"regexp"
     6  	"strings"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/hashicorp/nomad/api"
    11  	"github.com/hashicorp/nomad/command/agent"
    12  	"github.com/hashicorp/nomad/nomad/mock"
    13  	"github.com/hashicorp/nomad/nomad/structs"
    14  	"github.com/hashicorp/nomad/testutil"
    15  	"github.com/mitchellh/cli"
    16  	"github.com/posener/complete"
    17  	"github.com/stretchr/testify/assert"
    18  	"github.com/stretchr/testify/require"
    19  )
    20  
    21  func TestJobStatusCommand_Implements(t *testing.T) {
    22  	t.Parallel()
    23  	var _ cli.Command = &JobStatusCommand{}
    24  }
    25  
    26  func TestJobStatusCommand_Run(t *testing.T) {
    27  	t.Parallel()
    28  	srv, client, url := testServer(t, true, nil)
    29  	defer srv.Shutdown()
    30  	testutil.WaitForResult(func() (bool, error) {
    31  		nodes, _, err := client.Nodes().List(nil)
    32  		if err != nil {
    33  			return false, err
    34  		}
    35  		if len(nodes) == 0 {
    36  			return false, fmt.Errorf("missing node")
    37  		}
    38  		if _, ok := nodes[0].Drivers["mock_driver"]; !ok {
    39  			return false, fmt.Errorf("mock_driver not ready")
    40  		}
    41  		return true, nil
    42  	}, func(err error) {
    43  		t.Fatalf("err: %s", err)
    44  	})
    45  
    46  	ui := new(cli.MockUi)
    47  	cmd := &JobStatusCommand{Meta: Meta{Ui: ui}}
    48  
    49  	// Should return blank for no jobs
    50  	if code := cmd.Run([]string{"-address=" + url}); code != 0 {
    51  		t.Fatalf("expected exit 0, got: %d", code)
    52  	}
    53  
    54  	// Check for this awkward nil string, since a nil bytes.Buffer
    55  	// returns this purposely, and mitchellh/cli has a nil pointer
    56  	// if nothing was ever output.
    57  	exp := "No running jobs"
    58  	if out := strings.TrimSpace(ui.OutputWriter.String()); out != exp {
    59  		t.Fatalf("expected %q; got: %q", exp, out)
    60  	}
    61  
    62  	// Register two jobs
    63  	job1 := testJob("job1_sfx")
    64  	resp, _, err := client.Jobs().Register(job1, nil)
    65  	if err != nil {
    66  		t.Fatalf("err: %s", err)
    67  	}
    68  	if code := waitForSuccess(ui, client, fullId, t, resp.EvalID); code != 0 {
    69  		t.Fatalf("status code non zero saw %d", code)
    70  	}
    71  
    72  	job2 := testJob("job2_sfx")
    73  	resp2, _, err := client.Jobs().Register(job2, nil)
    74  	if err != nil {
    75  		t.Fatalf("err: %s", err)
    76  	}
    77  	if code := waitForSuccess(ui, client, fullId, t, resp2.EvalID); code != 0 {
    78  		t.Fatalf("status code non zero saw %d", code)
    79  	}
    80  
    81  	// Query again and check the result
    82  	if code := cmd.Run([]string{"-address=" + url}); code != 0 {
    83  		t.Fatalf("expected exit 0, got: %d", code)
    84  	}
    85  	out := ui.OutputWriter.String()
    86  	if !strings.Contains(out, "job1_sfx") || !strings.Contains(out, "job2_sfx") {
    87  		t.Fatalf("expected job1_sfx and job2_sfx, got: %s", out)
    88  	}
    89  	ui.OutputWriter.Reset()
    90  
    91  	// Query a single job
    92  	if code := cmd.Run([]string{"-address=" + url, "job2_sfx"}); code != 0 {
    93  		t.Fatalf("expected exit 0, got: %d", code)
    94  	}
    95  	out = ui.OutputWriter.String()
    96  	if strings.Contains(out, "job1_sfx") || !strings.Contains(out, "job2_sfx") {
    97  		t.Fatalf("expected only job2_sfx, got: %s", out)
    98  	}
    99  	if !strings.Contains(out, "Allocations") {
   100  		t.Fatalf("should dump allocations")
   101  	}
   102  	if !strings.Contains(out, "Summary") {
   103  		t.Fatalf("should dump summary")
   104  	}
   105  	ui.OutputWriter.Reset()
   106  
   107  	// Query a single job showing evals
   108  	if code := cmd.Run([]string{"-address=" + url, "-evals", "job2_sfx"}); code != 0 {
   109  		t.Fatalf("expected exit 0, got: %d", code)
   110  	}
   111  	out = ui.OutputWriter.String()
   112  	if strings.Contains(out, "job1_sfx") || !strings.Contains(out, "job2_sfx") {
   113  		t.Fatalf("expected only job2_sfx, got: %s", out)
   114  	}
   115  	if !strings.Contains(out, "Evaluations") {
   116  		t.Fatalf("should dump evaluations")
   117  	}
   118  	if !strings.Contains(out, "Allocations") {
   119  		t.Fatalf("should dump allocations")
   120  	}
   121  	ui.OutputWriter.Reset()
   122  
   123  	// Query a single job in verbose mode
   124  	if code := cmd.Run([]string{"-address=" + url, "-verbose", "job2_sfx"}); code != 0 {
   125  		t.Fatalf("expected exit 0, got: %d", code)
   126  	}
   127  
   128  	nodeName := ""
   129  	if allocs, _, err := client.Jobs().Allocations("job2_sfx", false, nil); err == nil {
   130  		if len(allocs) > 0 {
   131  			nodeName = allocs[0].NodeName
   132  		}
   133  	}
   134  
   135  	out = ui.OutputWriter.String()
   136  	if strings.Contains(out, "job1_sfx") || !strings.Contains(out, "job2_sfx") {
   137  		t.Fatalf("expected only job2_sfx, got: %s", out)
   138  	}
   139  	if !strings.Contains(out, "Evaluations") {
   140  		t.Fatalf("should dump evaluations")
   141  	}
   142  	if !strings.Contains(out, "Allocations") {
   143  		t.Fatalf("should dump allocations")
   144  	}
   145  	if !strings.Contains(out, "Created") {
   146  		t.Fatal("should have created header")
   147  	}
   148  	if !strings.Contains(out, "Modified") {
   149  		t.Fatal("should have modified header")
   150  	}
   151  
   152  	// string calculations based on 1-byte chars, not using runes
   153  	allocationsTableName := "Allocations\n"
   154  	allocationsTableStr := strings.Split(out, allocationsTableName)[1]
   155  	nodeNameHeaderStr := "Node Name"
   156  	nodeNameHeaderIndex := strings.Index(allocationsTableStr, nodeNameHeaderStr)
   157  	nodeNameRegexpStr := fmt.Sprintf(`.*%s.*\n.{%d}%s`, nodeNameHeaderStr, nodeNameHeaderIndex, regexp.QuoteMeta(nodeName))
   158  	require.Regexp(t, regexp.MustCompile(nodeNameRegexpStr), out)
   159  
   160  	ui.ErrorWriter.Reset()
   161  	ui.OutputWriter.Reset()
   162  
   163  	// Query jobs with prefix match
   164  	if code := cmd.Run([]string{"-address=" + url, "job"}); code != 1 {
   165  		t.Fatalf("expected exit 1, got: %d", code)
   166  	}
   167  	out = ui.ErrorWriter.String()
   168  	if !strings.Contains(out, "job1_sfx") || !strings.Contains(out, "job2_sfx") {
   169  		t.Fatalf("expected job1_sfx and job2_sfx, got: %s", out)
   170  	}
   171  	ui.ErrorWriter.Reset()
   172  	ui.OutputWriter.Reset()
   173  
   174  	// Query a single job with prefix match
   175  	if code := cmd.Run([]string{"-address=" + url, "job1"}); code != 0 {
   176  		t.Fatalf("expected exit 0, got: %d", code)
   177  	}
   178  	out = ui.OutputWriter.String()
   179  	if !strings.Contains(out, "job1_sfx") || strings.Contains(out, "job2_sfx") {
   180  		t.Fatalf("expected only job1_sfx, got: %s", out)
   181  	}
   182  
   183  	if !strings.Contains(out, "Created") {
   184  		t.Fatal("should have created header")
   185  	}
   186  
   187  	if !strings.Contains(out, "Modified") {
   188  		t.Fatal("should have modified header")
   189  	}
   190  	ui.OutputWriter.Reset()
   191  
   192  	// Query in short view mode
   193  	if code := cmd.Run([]string{"-address=" + url, "-short", "job2"}); code != 0 {
   194  		t.Fatalf("expected exit 0, got: %d", code)
   195  	}
   196  	out = ui.OutputWriter.String()
   197  	if !strings.Contains(out, "job2") {
   198  		t.Fatalf("expected job2, got: %s", out)
   199  	}
   200  	if strings.Contains(out, "Evaluations") {
   201  		t.Fatalf("should not dump evaluations")
   202  	}
   203  	if strings.Contains(out, "Allocations") {
   204  		t.Fatalf("should not dump allocations")
   205  	}
   206  	if strings.Contains(out, resp.EvalID) {
   207  		t.Fatalf("should not contain full identifiers, got %s", out)
   208  	}
   209  	ui.OutputWriter.Reset()
   210  
   211  	// Request full identifiers
   212  	if code := cmd.Run([]string{"-address=" + url, "-verbose", "job1"}); code != 0 {
   213  		t.Fatalf("expected exit 0, got: %d", code)
   214  	}
   215  	out = ui.OutputWriter.String()
   216  	if !strings.Contains(out, resp.EvalID) {
   217  		t.Fatalf("should contain full identifiers, got %s", out)
   218  	}
   219  }
   220  
   221  func TestJobStatusCommand_Fails(t *testing.T) {
   222  	t.Parallel()
   223  	ui := new(cli.MockUi)
   224  	cmd := &JobStatusCommand{Meta: Meta{Ui: ui}}
   225  
   226  	// Fails on misuse
   227  	if code := cmd.Run([]string{"some", "bad", "args"}); code != 1 {
   228  		t.Fatalf("expected exit code 1, got: %d", code)
   229  	}
   230  	if out := ui.ErrorWriter.String(); !strings.Contains(out, commandErrorText(cmd)) {
   231  		t.Fatalf("expected help output, got: %s", out)
   232  	}
   233  	ui.ErrorWriter.Reset()
   234  
   235  	// Fails on connection failure
   236  	if code := cmd.Run([]string{"-address=nope"}); code != 1 {
   237  		t.Fatalf("expected exit code 1, got: %d", code)
   238  	}
   239  	if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error querying jobs") {
   240  		t.Fatalf("expected failed query error, got: %s", out)
   241  	}
   242  }
   243  
   244  func TestJobStatusCommand_AutocompleteArgs(t *testing.T) {
   245  	assert := assert.New(t)
   246  	t.Parallel()
   247  
   248  	srv, _, url := testServer(t, true, nil)
   249  	defer srv.Shutdown()
   250  
   251  	ui := new(cli.MockUi)
   252  	cmd := &JobStatusCommand{Meta: Meta{Ui: ui, flagAddress: url}}
   253  
   254  	// Create a fake job
   255  	state := srv.Agent.Server().State()
   256  	j := mock.Job()
   257  	assert.Nil(state.UpsertJob(1000, j))
   258  
   259  	prefix := j.ID[:len(j.ID)-5]
   260  	args := complete.Args{Last: prefix}
   261  	predictor := cmd.AutocompleteArgs()
   262  
   263  	res := predictor.Predict(args)
   264  	assert.Equal(1, len(res))
   265  	assert.Equal(j.ID, res[0])
   266  }
   267  
   268  func TestJobStatusCommand_WithAccessPolicy(t *testing.T) {
   269  	assert := assert.New(t)
   270  	t.Parallel()
   271  
   272  	config := func(c *agent.Config) {
   273  		c.ACL.Enabled = true
   274  	}
   275  
   276  	srv, client, url := testServer(t, true, config)
   277  	defer srv.Shutdown()
   278  
   279  	// Bootstrap an initial ACL token
   280  	token := srv.RootToken
   281  	assert.NotNil(token, "failed to bootstrap ACL token")
   282  
   283  	// Wait for client ready
   284  	client.SetSecretID(token.SecretID)
   285  	testutil.WaitForResult(func() (bool, error) {
   286  		nodes, _, err := client.Nodes().List(nil)
   287  		if err != nil {
   288  			return false, err
   289  		}
   290  		if len(nodes) == 0 {
   291  			return false, fmt.Errorf("missing node")
   292  		}
   293  		if _, ok := nodes[0].Drivers["mock_driver"]; !ok {
   294  			return false, fmt.Errorf("mock_driver not ready")
   295  		}
   296  		return true, nil
   297  	}, func(err error) {
   298  		t.Fatalf("err: %s", err)
   299  	})
   300  
   301  	// Register a job
   302  	j := testJob("job1_sfx")
   303  
   304  	invalidToken := mock.ACLToken()
   305  
   306  	ui := new(cli.MockUi)
   307  	cmd := &JobStatusCommand{Meta: Meta{Ui: ui, flagAddress: url}}
   308  
   309  	// registering a job without a token fails
   310  	client.SetSecretID(invalidToken.SecretID)
   311  	resp, _, err := client.Jobs().Register(j, nil)
   312  	assert.NotNil(err)
   313  
   314  	// registering a job with a valid token succeeds
   315  	client.SetSecretID(token.SecretID)
   316  	resp, _, err = client.Jobs().Register(j, nil)
   317  	assert.Nil(err)
   318  	code := waitForSuccess(ui, client, fullId, t, resp.EvalID)
   319  	assert.Equal(0, code)
   320  
   321  	// Request Job List without providing a valid token
   322  	code = cmd.Run([]string{"-address=" + url, "-token=" + invalidToken.SecretID, "-short"})
   323  	assert.Equal(1, code)
   324  
   325  	// Request Job List with a valid token
   326  	code = cmd.Run([]string{"-address=" + url, "-token=" + token.SecretID, "-short"})
   327  	assert.Equal(0, code)
   328  
   329  	out := ui.OutputWriter.String()
   330  	if !strings.Contains(out, *j.ID) {
   331  		t.Fatalf("should contain full identifiers, got %s", out)
   332  	}
   333  }
   334  
   335  func TestJobStatusCommand_RescheduleEvals(t *testing.T) {
   336  	t.Parallel()
   337  	srv, client, url := testServer(t, true, nil)
   338  	defer srv.Shutdown()
   339  
   340  	// Wait for a node to be ready
   341  	testutil.WaitForResult(func() (bool, error) {
   342  		nodes, _, err := client.Nodes().List(nil)
   343  		if err != nil {
   344  			return false, err
   345  		}
   346  		for _, node := range nodes {
   347  			if node.Status == structs.NodeStatusReady {
   348  				return true, nil
   349  			}
   350  		}
   351  		return false, fmt.Errorf("no ready nodes")
   352  	}, func(err error) {
   353  		t.Fatalf("err: %v", err)
   354  	})
   355  
   356  	ui := new(cli.MockUi)
   357  	cmd := &JobStatusCommand{Meta: Meta{Ui: ui, flagAddress: url}}
   358  
   359  	require := require.New(t)
   360  	state := srv.Agent.Server().State()
   361  
   362  	// Create state store objects for job, alloc and followup eval with a future WaitUntil value
   363  	j := mock.Job()
   364  	require.Nil(state.UpsertJob(900, j))
   365  
   366  	e := mock.Eval()
   367  	e.WaitUntil = time.Now().Add(1 * time.Hour)
   368  	require.Nil(state.UpsertEvals(902, []*structs.Evaluation{e}))
   369  	a := mock.Alloc()
   370  	a.Job = j
   371  	a.JobID = j.ID
   372  	a.TaskGroup = j.TaskGroups[0].Name
   373  	a.FollowupEvalID = e.ID
   374  	a.Metrics = &structs.AllocMetric{}
   375  	a.DesiredStatus = structs.AllocDesiredStatusRun
   376  	a.ClientStatus = structs.AllocClientStatusRunning
   377  	require.Nil(state.UpsertAllocs(1000, []*structs.Allocation{a}))
   378  
   379  	// Query jobs with prefix match
   380  	if code := cmd.Run([]string{"-address=" + url, j.ID}); code != 0 {
   381  		t.Fatalf("expected exit 0, got: %d", code)
   382  	}
   383  	out := ui.OutputWriter.String()
   384  	require.Contains(out, "Future Rescheduling Attempts")
   385  	require.Contains(out, e.ID[:8])
   386  }
   387  
   388  func waitForSuccess(ui cli.Ui, client *api.Client, length int, t *testing.T, evalId string) int {
   389  	mon := newMonitor(ui, client, length)
   390  	monErr := mon.monitor(evalId, false)
   391  	return monErr
   392  }