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