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