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 }