github.com/hernad/nomad@v1.6.112/command/alloc_status_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package command
     5  
     6  import (
     7  	"fmt"
     8  	"regexp"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/hernad/nomad/ci"
    13  	"github.com/hernad/nomad/command/agent"
    14  	"github.com/hernad/nomad/helper/uuid"
    15  	"github.com/hernad/nomad/nomad/mock"
    16  	"github.com/hernad/nomad/nomad/structs"
    17  	"github.com/mitchellh/cli"
    18  	"github.com/posener/complete"
    19  	"github.com/shoenig/test/must"
    20  )
    21  
    22  func TestAllocStatusCommand_Implements(t *testing.T) {
    23  	ci.Parallel(t)
    24  	var _ cli.Command = &AllocStatusCommand{}
    25  }
    26  
    27  func TestAllocStatusCommand_Fails(t *testing.T) {
    28  	ci.Parallel(t)
    29  	srv, _, url := testServer(t, false, nil)
    30  	defer srv.Shutdown()
    31  
    32  	ui := cli.NewMockUi()
    33  	cmd := &AllocStatusCommand{Meta: Meta{Ui: ui}}
    34  
    35  	// Fails on misuse
    36  	code := cmd.Run([]string{"some", "bad", "args"})
    37  	must.One(t, code)
    38  
    39  	out := ui.ErrorWriter.String()
    40  	must.StrContains(t, out, commandErrorText(cmd))
    41  
    42  	ui.ErrorWriter.Reset()
    43  
    44  	// Fails on connection failure
    45  	code = cmd.Run([]string{"-address=nope", "foobar"})
    46  	must.One(t, code)
    47  
    48  	out = ui.ErrorWriter.String()
    49  	must.StrContains(t, out, "Error querying allocation")
    50  
    51  	ui.ErrorWriter.Reset()
    52  
    53  	// Fails on missing alloc
    54  	code = cmd.Run([]string{"-address=" + url, "26470238-5CF2-438F-8772-DC67CFB0705C"})
    55  	must.One(t, code)
    56  
    57  	out = ui.ErrorWriter.String()
    58  	must.StrContains(t, out, "No allocation(s) with prefix or id")
    59  
    60  	ui.ErrorWriter.Reset()
    61  
    62  	// Fail on identifier with too few characters
    63  	code = cmd.Run([]string{"-address=" + url, "2"})
    64  	must.One(t, code)
    65  
    66  	out = ui.ErrorWriter.String()
    67  	must.StrContains(t, out, "must contain at least two characters.")
    68  
    69  	ui.ErrorWriter.Reset()
    70  
    71  	// Identifiers with uneven length should produce a query result
    72  	code = cmd.Run([]string{"-address=" + url, "123"})
    73  	must.One(t, code)
    74  
    75  	out = ui.ErrorWriter.String()
    76  	must.StrContains(t, out, "No allocation(s) with prefix or id")
    77  
    78  	ui.ErrorWriter.Reset()
    79  
    80  	// Failed on both -json and -t options are specified
    81  	code = cmd.Run([]string{"-address=" + url, "-json", "-t", "{{.ID}}"})
    82  	must.One(t, code)
    83  
    84  	out = ui.ErrorWriter.String()
    85  	must.StrContains(t, out, "Both json and template formatting are not allowed")
    86  }
    87  
    88  func TestAllocStatusCommand_LifecycleInfo(t *testing.T) {
    89  	ci.Parallel(t)
    90  
    91  	srv, client, url := testServer(t, true, nil)
    92  	defer srv.Shutdown()
    93  
    94  	waitForNodes(t, client)
    95  
    96  	ui := cli.NewMockUi()
    97  	cmd := &AllocStatusCommand{Meta: Meta{Ui: ui}}
    98  	state := srv.Agent.Server().State()
    99  
   100  	a := mock.Alloc()
   101  	a.Metrics = &structs.AllocMetric{}
   102  	tg := a.Job.LookupTaskGroup(a.TaskGroup)
   103  
   104  	initTask := tg.Tasks[0].Copy()
   105  	initTask.Name = "init_task"
   106  	initTask.Lifecycle = &structs.TaskLifecycleConfig{
   107  		Hook: "prestart",
   108  	}
   109  
   110  	prestartSidecarTask := tg.Tasks[0].Copy()
   111  	prestartSidecarTask.Name = "prestart_sidecar"
   112  	prestartSidecarTask.Lifecycle = &structs.TaskLifecycleConfig{
   113  		Hook:    "prestart",
   114  		Sidecar: true,
   115  	}
   116  
   117  	tg.Tasks = append(tg.Tasks, initTask, prestartSidecarTask)
   118  	a.TaskResources["init_task"] = a.TaskResources["web"]
   119  	a.TaskResources["prestart_sidecar"] = a.TaskResources["web"]
   120  	a.TaskStates = map[string]*structs.TaskState{
   121  		"web":              {State: "pending"},
   122  		"init_task":        {State: "running"},
   123  		"prestart_sidecar": {State: "running"},
   124  	}
   125  
   126  	must.NoError(t, state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{a}))
   127  
   128  	code := cmd.Run([]string{"-address=" + url, a.ID})
   129  	must.Zero(t, code)
   130  
   131  	out := ui.OutputWriter.String()
   132  	must.StrContains(t, out, `Task "init_task" (prestart) is "running"`)
   133  	must.StrContains(t, out, `Task "prestart_sidecar" (prestart sidecar) is "running"`)
   134  	must.StrContains(t, out, `Task "web" is "pending"`)
   135  }
   136  
   137  func TestAllocStatusCommand_Run(t *testing.T) {
   138  	ci.Parallel(t)
   139  	srv, client, url := testServer(t, true, nil)
   140  	defer srv.Shutdown()
   141  
   142  	waitForNodes(t, client)
   143  
   144  	ui := cli.NewMockUi()
   145  	cmd := &AllocStatusCommand{Meta: Meta{Ui: ui}}
   146  
   147  	jobID := "job1_sfx"
   148  	job1 := testJob(jobID)
   149  	resp, _, err := client.Jobs().Register(job1, nil)
   150  	must.NoError(t, err)
   151  
   152  	code := waitForSuccess(ui, client, fullId, t, resp.EvalID)
   153  	must.Zero(t, code)
   154  
   155  	// get an alloc id
   156  	allocID := ""
   157  	nodeName := ""
   158  	if allocs, _, err := client.Jobs().Allocations(jobID, false, nil); err == nil {
   159  		if len(allocs) > 0 {
   160  			allocID = allocs[0].ID
   161  			nodeName = allocs[0].NodeName
   162  		}
   163  	}
   164  	must.NotEq(t, "", allocID)
   165  
   166  	code = cmd.Run([]string{"-address=" + url, allocID})
   167  	must.Zero(t, code)
   168  
   169  	out := ui.OutputWriter.String()
   170  	must.StrContains(t, out, "Created")
   171  	must.StrContains(t, out, "Modified")
   172  
   173  	nodeNameRegexpStr := fmt.Sprintf(`\nNode Name\s+= %s\n`, regexp.QuoteMeta(nodeName))
   174  	must.RegexMatch(t, regexp.MustCompile(nodeNameRegexpStr), out)
   175  
   176  	ui.OutputWriter.Reset()
   177  
   178  	code = cmd.Run([]string{"-address=" + url, "-verbose", allocID})
   179  	must.Zero(t, code)
   180  
   181  	out = ui.OutputWriter.String()
   182  	must.StrContains(t, out, allocID)
   183  	must.StrContains(t, out, "Created")
   184  
   185  	ui.OutputWriter.Reset()
   186  
   187  	// Try the query with an even prefix that includes the hyphen
   188  	code = cmd.Run([]string{"-address=" + url, allocID[:13]})
   189  	must.Zero(t, code)
   190  
   191  	out = ui.OutputWriter.String()
   192  	must.StrContains(t, out, "Created")
   193  	ui.OutputWriter.Reset()
   194  
   195  	code = cmd.Run([]string{"-address=" + url, "-verbose", allocID})
   196  	must.Zero(t, code)
   197  
   198  	out = ui.OutputWriter.String()
   199  	must.StrContains(t, out, allocID)
   200  
   201  	// make sure nsd checks status output is elided if none exist
   202  	must.StrNotContains(t, out, `Nomad Service Checks:`)
   203  }
   204  
   205  func TestAllocStatusCommand_RescheduleInfo(t *testing.T) {
   206  	ci.Parallel(t)
   207  	srv, client, url := testServer(t, true, nil)
   208  	defer srv.Shutdown()
   209  
   210  	waitForNodes(t, client)
   211  
   212  	ui := cli.NewMockUi()
   213  	cmd := &AllocStatusCommand{Meta: Meta{Ui: ui}}
   214  	// Test reschedule attempt info
   215  	state := srv.Agent.Server().State()
   216  	a := mock.Alloc()
   217  	a.Metrics = &structs.AllocMetric{}
   218  	nextAllocId := uuid.Generate()
   219  	a.NextAllocation = nextAllocId
   220  	a.RescheduleTracker = &structs.RescheduleTracker{
   221  		Events: []*structs.RescheduleEvent{
   222  			{
   223  				RescheduleTime: time.Now().Add(-2 * time.Minute).UTC().UnixNano(),
   224  				PrevAllocID:    uuid.Generate(),
   225  				PrevNodeID:     uuid.Generate(),
   226  			},
   227  		},
   228  	}
   229  	must.NoError(t, state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{a}))
   230  
   231  	if code := cmd.Run([]string{"-address=" + url, a.ID}); code != 0 {
   232  		t.Fatalf("expected exit 0, got: %d", code)
   233  	}
   234  	out := ui.OutputWriter.String()
   235  	must.StrContains(t, out, "Replacement Alloc ID")
   236  	must.RegexMatch(t, regexp.MustCompile(".*Reschedule Attempts\\s*=\\s*1/2"), out)
   237  }
   238  
   239  func TestAllocStatusCommand_ScoreMetrics(t *testing.T) {
   240  	ci.Parallel(t)
   241  	srv, client, url := testServer(t, true, nil)
   242  	defer srv.Shutdown()
   243  
   244  	waitForNodes(t, client)
   245  
   246  	ui := cli.NewMockUi()
   247  	cmd := &AllocStatusCommand{Meta: Meta{Ui: ui}}
   248  
   249  	// Test node metrics
   250  	state := srv.Agent.Server().State()
   251  	a := mock.Alloc()
   252  	mockNode1 := mock.Node()
   253  	mockNode2 := mock.Node()
   254  	a.Metrics = &structs.AllocMetric{
   255  		ScoreMetaData: []*structs.NodeScoreMeta{
   256  			{
   257  				NodeID: mockNode1.ID,
   258  				Scores: map[string]float64{
   259  					"binpack":       0.77,
   260  					"node-affinity": 0.5,
   261  				},
   262  			},
   263  			{
   264  				NodeID: mockNode2.ID,
   265  				Scores: map[string]float64{
   266  					"binpack":       0.75,
   267  					"node-affinity": 0.33,
   268  				},
   269  			},
   270  		},
   271  	}
   272  	must.NoError(t, state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{a}))
   273  
   274  	code := cmd.Run([]string{"-address=" + url, "-verbose", a.ID})
   275  	must.Zero(t, code)
   276  
   277  	out := ui.OutputWriter.String()
   278  	must.StrContains(t, out, "Placement Metrics")
   279  	must.StrContains(t, out, mockNode1.ID)
   280  	must.StrContains(t, out, mockNode2.ID)
   281  
   282  	// assert we sort headers alphabetically
   283  	must.StrContains(t, out, "binpack  node-affinity")
   284  	must.StrContains(t, out, "final score")
   285  }
   286  
   287  func TestAllocStatusCommand_AutocompleteArgs(t *testing.T) {
   288  	ci.Parallel(t)
   289  
   290  	srv, _, url := testServer(t, true, nil)
   291  	defer srv.Shutdown()
   292  
   293  	ui := cli.NewMockUi()
   294  	cmd := &AllocStatusCommand{Meta: Meta{Ui: ui, flagAddress: url}}
   295  
   296  	// Create a fake alloc
   297  	state := srv.Agent.Server().State()
   298  	a := mock.Alloc()
   299  	must.NoError(t, state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{a}))
   300  
   301  	prefix := a.ID[:5]
   302  	args := complete.Args{Last: prefix}
   303  	predictor := cmd.AutocompleteArgs()
   304  
   305  	res := predictor.Predict(args)
   306  	must.Len(t, 1, res)
   307  	must.Eq(t, a.ID, res[0])
   308  }
   309  
   310  func TestAllocStatusCommand_HostVolumes(t *testing.T) {
   311  	ci.Parallel(t)
   312  	// We have to create a tempdir for the host volume even though we're
   313  	// not going to use it b/c the server validates the config on startup
   314  	tmpDir := t.TempDir()
   315  
   316  	vol0 := uuid.Generate()
   317  	srv, _, url := testServer(t, true, func(c *agent.Config) {
   318  		c.Client.HostVolumes = []*structs.ClientHostVolumeConfig{
   319  			{
   320  				Name:     vol0,
   321  				Path:     tmpDir,
   322  				ReadOnly: false,
   323  			},
   324  		}
   325  	})
   326  	defer srv.Shutdown()
   327  
   328  	state := srv.Agent.Server().State()
   329  
   330  	// Upsert the job and alloc
   331  	node := mock.Node()
   332  	alloc := mock.Alloc()
   333  	alloc.Metrics = &structs.AllocMetric{}
   334  	alloc.NodeID = node.ID
   335  	job := alloc.Job
   336  	job.TaskGroups[0].Volumes = map[string]*structs.VolumeRequest{
   337  		vol0: {
   338  			Name:   vol0,
   339  			Type:   structs.VolumeTypeHost,
   340  			Source: tmpDir,
   341  		},
   342  	}
   343  	job.TaskGroups[0].Tasks[0].VolumeMounts = []*structs.VolumeMount{
   344  		{
   345  			Volume:          vol0,
   346  			Destination:     "/var/www",
   347  			ReadOnly:        true,
   348  			PropagationMode: "private",
   349  		},
   350  	}
   351  	// fakes the placement enough so that we have something to iterate
   352  	// on in 'nomad alloc status'
   353  	alloc.TaskStates = map[string]*structs.TaskState{
   354  		"web": {
   355  			Events: []*structs.TaskEvent{
   356  				structs.NewTaskEvent("test event").SetMessage("test msg"),
   357  			},
   358  		},
   359  	}
   360  	summary := mock.JobSummary(alloc.JobID)
   361  	must.NoError(t, state.UpsertJobSummary(1004, summary))
   362  	must.NoError(t, state.UpsertAllocs(structs.MsgTypeTestSetup, 1005, []*structs.Allocation{alloc}))
   363  
   364  	ui := cli.NewMockUi()
   365  	cmd := &AllocStatusCommand{Meta: Meta{Ui: ui}}
   366  	code := cmd.Run([]string{"-address=" + url, "-verbose", alloc.ID})
   367  	must.Zero(t, code)
   368  
   369  	out := ui.OutputWriter.String()
   370  	must.StrContains(t, out, "Host Volumes")
   371  	must.StrContains(t, out, fmt.Sprintf("%s  true", vol0))
   372  	must.StrNotContains(t, out, "CSI Volumes")
   373  }
   374  
   375  func TestAllocStatusCommand_CSIVolumes(t *testing.T) {
   376  	ci.Parallel(t)
   377  	srv, _, url := testServer(t, true, nil)
   378  	defer srv.Shutdown()
   379  
   380  	state := srv.Agent.Server().State()
   381  
   382  	// Upsert the node, plugin, and volume
   383  	vol0 := uuid.Generate()
   384  	node := mock.Node()
   385  	node.CSINodePlugins = map[string]*structs.CSIInfo{
   386  		"minnie": {
   387  			PluginID: "minnie",
   388  			Healthy:  true,
   389  			NodeInfo: &structs.CSINodeInfo{},
   390  		},
   391  	}
   392  	err := state.UpsertNode(structs.MsgTypeTestSetup, 1001, node)
   393  	must.NoError(t, err)
   394  
   395  	vols := []*structs.CSIVolume{{
   396  		ID:             vol0,
   397  		Namespace:      structs.DefaultNamespace,
   398  		PluginID:       "minnie",
   399  		AccessMode:     structs.CSIVolumeAccessModeMultiNodeSingleWriter,
   400  		AttachmentMode: structs.CSIVolumeAttachmentModeFilesystem,
   401  		Topologies: []*structs.CSITopology{{
   402  			Segments: map[string]string{"foo": "bar"},
   403  		}},
   404  	}}
   405  	err = state.UpsertCSIVolume(1002, vols)
   406  	must.NoError(t, err)
   407  
   408  	// Upsert the job and alloc
   409  	alloc := mock.Alloc()
   410  	alloc.Metrics = &structs.AllocMetric{}
   411  	alloc.NodeID = node.ID
   412  	job := alloc.Job
   413  	job.TaskGroups[0].Volumes = map[string]*structs.VolumeRequest{
   414  		vol0: {
   415  			Name:   vol0,
   416  			Type:   structs.VolumeTypeCSI,
   417  			Source: vol0,
   418  		},
   419  	}
   420  	job.TaskGroups[0].Tasks[0].VolumeMounts = []*structs.VolumeMount{
   421  		{
   422  			Volume:          vol0,
   423  			Destination:     "/var/www",
   424  			ReadOnly:        true,
   425  			PropagationMode: "private",
   426  		},
   427  	}
   428  	// if we don't set a task state, there's nothing to iterate on alloc status
   429  	alloc.TaskStates = map[string]*structs.TaskState{
   430  		"web": {
   431  			Events: []*structs.TaskEvent{
   432  				structs.NewTaskEvent("test event").SetMessage("test msg"),
   433  			},
   434  		},
   435  	}
   436  	summary := mock.JobSummary(alloc.JobID)
   437  	must.NoError(t, state.UpsertJobSummary(1004, summary))
   438  	must.NoError(t, state.UpsertAllocs(structs.MsgTypeTestSetup, 1005, []*structs.Allocation{alloc}))
   439  
   440  	ui := cli.NewMockUi()
   441  	cmd := &AllocStatusCommand{Meta: Meta{Ui: ui}}
   442  	if code := cmd.Run([]string{"-address=" + url, "-verbose", alloc.ID}); code != 0 {
   443  		t.Fatalf("expected exit 0, got: %d", code)
   444  	}
   445  	out := ui.OutputWriter.String()
   446  	must.StrContains(t, out, "CSI Volumes")
   447  	must.StrContains(t, out, fmt.Sprintf("%s  minnie", vol0))
   448  	must.StrNotContains(t, out, "Host Volumes")
   449  }
   450  
   451  func TestAllocStatusCommand_NSD_Checks(t *testing.T) {
   452  	ci.Parallel(t)
   453  	srv, client, url := testServer(t, true, nil)
   454  	defer srv.Shutdown()
   455  
   456  	// wait for nodes
   457  	waitForNodes(t, client)
   458  
   459  	jobID := "job1_checks"
   460  	job1 := testNomadServiceJob(jobID)
   461  
   462  	resp, _, err := client.Jobs().Register(job1, nil)
   463  	must.NoError(t, err)
   464  
   465  	// wait for registration success
   466  	ui := cli.NewMockUi()
   467  	code := waitForSuccess(ui, client, fullId, t, resp.EvalID)
   468  	must.Zero(t, code)
   469  
   470  	// Get an alloc id
   471  	allocID := getAllocFromJob(t, client, jobID)
   472  
   473  	// wait for the check to be marked failure
   474  	waitForCheckStatus(t, client, allocID, "failure")
   475  
   476  	// Run command
   477  	cmd := &AllocStatusCommand{Meta: Meta{Ui: ui, flagAddress: url}}
   478  	code = cmd.Run([]string{"-address=" + url, allocID})
   479  	must.Zero(t, code)
   480  
   481  	// check output
   482  	out := ui.OutputWriter.String()
   483  	must.StrContains(t, out, `Nomad Service Checks:`)
   484  	must.RegexMatch(t, regexp.MustCompile(`Service\s+Task\s+Name\s+Mode\s+Status`), out)
   485  	must.RegexMatch(t, regexp.MustCompile(`service1\s+\(group\)\s+check1\s+healthiness\s+(pending|failure)`), out)
   486  }