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