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