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 }