github.com/manicqin/nomad@v0.9.5/command/agent/job_endpoint_test.go (about) 1 package agent 2 3 import ( 4 "net/http" 5 "net/http/httptest" 6 "reflect" 7 "strings" 8 "testing" 9 "time" 10 11 "github.com/golang/snappy" 12 "github.com/hashicorp/nomad/api" 13 "github.com/hashicorp/nomad/helper" 14 "github.com/hashicorp/nomad/nomad/mock" 15 "github.com/hashicorp/nomad/nomad/structs" 16 "github.com/kr/pretty" 17 "github.com/stretchr/testify/assert" 18 "github.com/stretchr/testify/require" 19 ) 20 21 func TestHTTP_JobsList(t *testing.T) { 22 t.Parallel() 23 httpTest(t, nil, func(s *TestAgent) { 24 for i := 0; i < 3; i++ { 25 // Create the job 26 job := mock.Job() 27 args := structs.JobRegisterRequest{ 28 Job: job, 29 WriteRequest: structs.WriteRequest{ 30 Region: "global", 31 Namespace: structs.DefaultNamespace, 32 }, 33 } 34 var resp structs.JobRegisterResponse 35 if err := s.Agent.RPC("Job.Register", &args, &resp); err != nil { 36 t.Fatalf("err: %v", err) 37 } 38 } 39 40 // Make the HTTP request 41 req, err := http.NewRequest("GET", "/v1/jobs", nil) 42 if err != nil { 43 t.Fatalf("err: %v", err) 44 } 45 respW := httptest.NewRecorder() 46 47 // Make the request 48 obj, err := s.Server.JobsRequest(respW, req) 49 if err != nil { 50 t.Fatalf("err: %v", err) 51 } 52 53 // Check for the index 54 if respW.HeaderMap.Get("X-Nomad-Index") == "" { 55 t.Fatalf("missing index") 56 } 57 if respW.HeaderMap.Get("X-Nomad-KnownLeader") != "true" { 58 t.Fatalf("missing known leader") 59 } 60 if respW.HeaderMap.Get("X-Nomad-LastContact") == "" { 61 t.Fatalf("missing last contact") 62 } 63 64 // Check the job 65 j := obj.([]*structs.JobListStub) 66 if len(j) != 3 { 67 t.Fatalf("bad: %#v", j) 68 } 69 }) 70 } 71 72 func TestHTTP_PrefixJobsList(t *testing.T) { 73 ids := []string{ 74 "aaaaaaaa-e8f7-fd38-c855-ab94ceb89706", 75 "aabbbbbb-e8f7-fd38-c855-ab94ceb89706", 76 "aabbcccc-e8f7-fd38-c855-ab94ceb89706", 77 } 78 t.Parallel() 79 httpTest(t, nil, func(s *TestAgent) { 80 for i := 0; i < 3; i++ { 81 // Create the job 82 job := mock.Job() 83 job.ID = ids[i] 84 job.TaskGroups[0].Count = 1 85 args := structs.JobRegisterRequest{ 86 Job: job, 87 WriteRequest: structs.WriteRequest{ 88 Region: "global", 89 Namespace: structs.DefaultNamespace, 90 }, 91 } 92 var resp structs.JobRegisterResponse 93 if err := s.Agent.RPC("Job.Register", &args, &resp); err != nil { 94 t.Fatalf("err: %v", err) 95 } 96 } 97 98 // Make the HTTP request 99 req, err := http.NewRequest("GET", "/v1/jobs?prefix=aabb", nil) 100 if err != nil { 101 t.Fatalf("err: %v", err) 102 } 103 respW := httptest.NewRecorder() 104 105 // Make the request 106 obj, err := s.Server.JobsRequest(respW, req) 107 if err != nil { 108 t.Fatalf("err: %v", err) 109 } 110 111 // Check for the index 112 if respW.HeaderMap.Get("X-Nomad-Index") == "" { 113 t.Fatalf("missing index") 114 } 115 if respW.HeaderMap.Get("X-Nomad-KnownLeader") != "true" { 116 t.Fatalf("missing known leader") 117 } 118 if respW.HeaderMap.Get("X-Nomad-LastContact") == "" { 119 t.Fatalf("missing last contact") 120 } 121 122 // Check the job 123 j := obj.([]*structs.JobListStub) 124 if len(j) != 2 { 125 t.Fatalf("bad: %#v", j) 126 } 127 }) 128 } 129 130 func TestHTTP_JobsRegister(t *testing.T) { 131 t.Parallel() 132 httpTest(t, nil, func(s *TestAgent) { 133 // Create the job 134 job := MockJob() 135 args := api.JobRegisterRequest{ 136 Job: job, 137 WriteRequest: api.WriteRequest{Region: "global"}, 138 } 139 buf := encodeReq(args) 140 141 // Make the HTTP request 142 req, err := http.NewRequest("PUT", "/v1/jobs", buf) 143 if err != nil { 144 t.Fatalf("err: %v", err) 145 } 146 respW := httptest.NewRecorder() 147 148 // Make the request 149 obj, err := s.Server.JobsRequest(respW, req) 150 if err != nil { 151 t.Fatalf("err: %v", err) 152 } 153 154 // Check the response 155 dereg := obj.(structs.JobRegisterResponse) 156 if dereg.EvalID == "" { 157 t.Fatalf("bad: %v", dereg) 158 } 159 160 // Check for the index 161 if respW.HeaderMap.Get("X-Nomad-Index") == "" { 162 t.Fatalf("missing index") 163 } 164 165 // Check the job is registered 166 getReq := structs.JobSpecificRequest{ 167 JobID: *job.ID, 168 QueryOptions: structs.QueryOptions{ 169 Region: "global", 170 Namespace: structs.DefaultNamespace, 171 }, 172 } 173 var getResp structs.SingleJobResponse 174 if err := s.Agent.RPC("Job.GetJob", &getReq, &getResp); err != nil { 175 t.Fatalf("err: %v", err) 176 } 177 178 if getResp.Job == nil { 179 t.Fatalf("job does not exist") 180 } 181 }) 182 } 183 184 // Test that ACL token is properly threaded through to the RPC endpoint 185 func TestHTTP_JobsRegister_ACL(t *testing.T) { 186 t.Parallel() 187 httpACLTest(t, nil, func(s *TestAgent) { 188 // Create the job 189 job := MockJob() 190 args := api.JobRegisterRequest{ 191 Job: job, 192 WriteRequest: api.WriteRequest{ 193 Region: "global", 194 }, 195 } 196 buf := encodeReq(args) 197 198 // Make the HTTP request 199 req, err := http.NewRequest("PUT", "/v1/jobs", buf) 200 if err != nil { 201 t.Fatalf("err: %v", err) 202 } 203 respW := httptest.NewRecorder() 204 setToken(req, s.RootToken) 205 206 // Make the request 207 obj, err := s.Server.JobsRequest(respW, req) 208 if err != nil { 209 t.Fatalf("err: %v", err) 210 } 211 assert.NotNil(t, obj) 212 }) 213 } 214 215 func TestHTTP_JobsRegister_Defaulting(t *testing.T) { 216 t.Parallel() 217 httpTest(t, nil, func(s *TestAgent) { 218 // Create the job 219 job := MockJob() 220 221 // Do not set its priority 222 job.Priority = nil 223 224 args := api.JobRegisterRequest{ 225 Job: job, 226 WriteRequest: api.WriteRequest{Region: "global"}, 227 } 228 buf := encodeReq(args) 229 230 // Make the HTTP request 231 req, err := http.NewRequest("PUT", "/v1/jobs", buf) 232 if err != nil { 233 t.Fatalf("err: %v", err) 234 } 235 respW := httptest.NewRecorder() 236 237 // Make the request 238 obj, err := s.Server.JobsRequest(respW, req) 239 if err != nil { 240 t.Fatalf("err: %v", err) 241 } 242 243 // Check the response 244 dereg := obj.(structs.JobRegisterResponse) 245 if dereg.EvalID == "" { 246 t.Fatalf("bad: %v", dereg) 247 } 248 249 // Check for the index 250 if respW.HeaderMap.Get("X-Nomad-Index") == "" { 251 t.Fatalf("missing index") 252 } 253 254 // Check the job is registered 255 getReq := structs.JobSpecificRequest{ 256 JobID: *job.ID, 257 QueryOptions: structs.QueryOptions{ 258 Region: "global", 259 Namespace: structs.DefaultNamespace, 260 }, 261 } 262 var getResp structs.SingleJobResponse 263 if err := s.Agent.RPC("Job.GetJob", &getReq, &getResp); err != nil { 264 t.Fatalf("err: %v", err) 265 } 266 267 if getResp.Job == nil { 268 t.Fatalf("job does not exist") 269 } 270 if getResp.Job.Priority != 50 { 271 t.Fatalf("job didn't get defaulted") 272 } 273 }) 274 } 275 276 func TestHTTP_JobsParse(t *testing.T) { 277 t.Parallel() 278 httpTest(t, nil, func(s *TestAgent) { 279 buf := encodeReq(api.JobsParseRequest{JobHCL: mock.HCL()}) 280 req, err := http.NewRequest("POST", "/v1/jobs/parse", buf) 281 if err != nil { 282 t.Fatalf("err: %v", err) 283 } 284 285 respW := httptest.NewRecorder() 286 287 obj, err := s.Server.JobsParseRequest(respW, req) 288 if err != nil { 289 t.Fatalf("err: %v", err) 290 } 291 if obj == nil { 292 t.Fatal("response should not be nil") 293 } 294 295 job := obj.(*api.Job) 296 expected := mock.Job() 297 if job.Name == nil || *job.Name != expected.Name { 298 t.Fatalf("job name is '%s', expected '%s'", *job.Name, expected.Name) 299 } 300 301 if job.Datacenters == nil || 302 job.Datacenters[0] != expected.Datacenters[0] { 303 t.Fatalf("job datacenters is '%s', expected '%s'", 304 job.Datacenters[0], expected.Datacenters[0]) 305 } 306 }) 307 } 308 func TestHTTP_JobQuery(t *testing.T) { 309 t.Parallel() 310 httpTest(t, nil, func(s *TestAgent) { 311 // Create the job 312 job := mock.Job() 313 args := structs.JobRegisterRequest{ 314 Job: job, 315 WriteRequest: structs.WriteRequest{ 316 Region: "global", 317 Namespace: structs.DefaultNamespace, 318 }, 319 } 320 var resp structs.JobRegisterResponse 321 if err := s.Agent.RPC("Job.Register", &args, &resp); err != nil { 322 t.Fatalf("err: %v", err) 323 } 324 325 // Make the HTTP request 326 req, err := http.NewRequest("GET", "/v1/job/"+job.ID, nil) 327 if err != nil { 328 t.Fatalf("err: %v", err) 329 } 330 respW := httptest.NewRecorder() 331 332 // Make the request 333 obj, err := s.Server.JobSpecificRequest(respW, req) 334 if err != nil { 335 t.Fatalf("err: %v", err) 336 } 337 338 // Check for the index 339 if respW.HeaderMap.Get("X-Nomad-Index") == "" { 340 t.Fatalf("missing index") 341 } 342 if respW.HeaderMap.Get("X-Nomad-KnownLeader") != "true" { 343 t.Fatalf("missing known leader") 344 } 345 if respW.HeaderMap.Get("X-Nomad-LastContact") == "" { 346 t.Fatalf("missing last contact") 347 } 348 349 // Check the job 350 j := obj.(*structs.Job) 351 if j.ID != job.ID { 352 t.Fatalf("bad: %#v", j) 353 } 354 }) 355 } 356 357 func TestHTTP_JobQuery_Payload(t *testing.T) { 358 t.Parallel() 359 httpTest(t, nil, func(s *TestAgent) { 360 // Create the job 361 job := mock.Job() 362 363 // Insert Payload compressed 364 expected := []byte("hello world") 365 compressed := snappy.Encode(nil, expected) 366 job.Payload = compressed 367 368 // Directly manipulate the state 369 state := s.Agent.server.State() 370 if err := state.UpsertJob(1000, job); err != nil { 371 t.Fatalf("Failed to upsert job: %v", err) 372 } 373 374 // Make the HTTP request 375 req, err := http.NewRequest("GET", "/v1/job/"+job.ID, nil) 376 if err != nil { 377 t.Fatalf("err: %v", err) 378 } 379 respW := httptest.NewRecorder() 380 381 // Make the request 382 obj, err := s.Server.JobSpecificRequest(respW, req) 383 if err != nil { 384 t.Fatalf("err: %v", err) 385 } 386 387 // Check for the index 388 if respW.HeaderMap.Get("X-Nomad-Index") == "" { 389 t.Fatalf("missing index") 390 } 391 if respW.HeaderMap.Get("X-Nomad-KnownLeader") != "true" { 392 t.Fatalf("missing known leader") 393 } 394 if respW.HeaderMap.Get("X-Nomad-LastContact") == "" { 395 t.Fatalf("missing last contact") 396 } 397 398 // Check the job 399 j := obj.(*structs.Job) 400 if j.ID != job.ID { 401 t.Fatalf("bad: %#v", j) 402 } 403 404 // Check the payload is decompressed 405 if !reflect.DeepEqual(j.Payload, expected) { 406 t.Fatalf("Payload not decompressed properly; got %#v; want %#v", j.Payload, expected) 407 } 408 }) 409 } 410 411 func TestHTTP_JobUpdate(t *testing.T) { 412 t.Parallel() 413 httpTest(t, nil, func(s *TestAgent) { 414 // Create the job 415 job := MockJob() 416 args := api.JobRegisterRequest{ 417 Job: job, 418 WriteRequest: api.WriteRequest{ 419 Region: "global", 420 Namespace: api.DefaultNamespace, 421 }, 422 } 423 buf := encodeReq(args) 424 425 // Make the HTTP request 426 req, err := http.NewRequest("PUT", "/v1/job/"+*job.ID, buf) 427 if err != nil { 428 t.Fatalf("err: %v", err) 429 } 430 respW := httptest.NewRecorder() 431 432 // Make the request 433 obj, err := s.Server.JobSpecificRequest(respW, req) 434 if err != nil { 435 t.Fatalf("err: %v", err) 436 } 437 438 // Check the response 439 dereg := obj.(structs.JobRegisterResponse) 440 if dereg.EvalID == "" { 441 t.Fatalf("bad: %v", dereg) 442 } 443 444 // Check for the index 445 if respW.HeaderMap.Get("X-Nomad-Index") == "" { 446 t.Fatalf("missing index") 447 } 448 449 // Check the job is registered 450 getReq := structs.JobSpecificRequest{ 451 JobID: *job.ID, 452 QueryOptions: structs.QueryOptions{ 453 Region: "global", 454 Namespace: structs.DefaultNamespace, 455 }, 456 } 457 var getResp structs.SingleJobResponse 458 if err := s.Agent.RPC("Job.GetJob", &getReq, &getResp); err != nil { 459 t.Fatalf("err: %v", err) 460 } 461 462 if getResp.Job == nil { 463 t.Fatalf("job does not exist") 464 } 465 }) 466 } 467 468 func TestHTTP_JobUpdateRegion(t *testing.T) { 469 t.Parallel() 470 471 cases := []struct { 472 Name string 473 ConfigRegion string 474 APIRegion string 475 ExpectedRegion string 476 }{ 477 { 478 Name: "api region takes precedence", 479 ConfigRegion: "not-global", 480 APIRegion: "north-america", 481 ExpectedRegion: "north-america", 482 }, 483 { 484 Name: "config region is set", 485 ConfigRegion: "north-america", 486 APIRegion: "", 487 ExpectedRegion: "north-america", 488 }, 489 { 490 Name: "api region is set", 491 ConfigRegion: "", 492 APIRegion: "north-america", 493 ExpectedRegion: "north-america", 494 }, 495 { 496 Name: "defaults to node region global if no region is provided", 497 ConfigRegion: "", 498 APIRegion: "", 499 ExpectedRegion: "global", 500 }, 501 { 502 Name: "defaults to node region not-global if no region is provided", 503 ConfigRegion: "", 504 APIRegion: "", 505 ExpectedRegion: "not-global", 506 }, 507 } 508 509 for _, tc := range cases { 510 t.Run(tc.Name, func(t *testing.T) { 511 httpTest(t, func(c *Config) { c.Region = tc.ExpectedRegion }, func(s *TestAgent) { 512 // Create the job 513 job := MockRegionalJob() 514 515 if tc.ConfigRegion == "" { 516 job.Region = nil 517 } else { 518 job.Region = &tc.ConfigRegion 519 } 520 521 args := api.JobRegisterRequest{ 522 Job: job, 523 WriteRequest: api.WriteRequest{ 524 Namespace: api.DefaultNamespace, 525 Region: tc.APIRegion, 526 }, 527 } 528 529 buf := encodeReq(args) 530 531 // Make the HTTP request 532 url := "/v1/job/" + *job.ID 533 534 req, err := http.NewRequest("PUT", url, buf) 535 require.NoError(t, err) 536 respW := httptest.NewRecorder() 537 538 // Make the request 539 obj, err := s.Server.JobSpecificRequest(respW, req) 540 require.NoError(t, err) 541 542 // Check the response 543 dereg := obj.(structs.JobRegisterResponse) 544 require.NotEmpty(t, dereg.EvalID) 545 546 // Check for the index 547 require.NotEmpty(t, respW.HeaderMap.Get("X-Nomad-Index"), "missing index") 548 549 // Check the job is registered 550 getReq := structs.JobSpecificRequest{ 551 JobID: *job.ID, 552 QueryOptions: structs.QueryOptions{ 553 Region: tc.ExpectedRegion, 554 Namespace: structs.DefaultNamespace, 555 }, 556 } 557 var getResp structs.SingleJobResponse 558 err = s.Agent.RPC("Job.GetJob", &getReq, &getResp) 559 require.NoError(t, err) 560 require.NotNil(t, getResp.Job, "job does not exist") 561 require.Equal(t, tc.ExpectedRegion, getResp.Job.Region) 562 }) 563 }) 564 } 565 } 566 567 func TestHTTP_JobDelete(t *testing.T) { 568 t.Parallel() 569 httpTest(t, nil, func(s *TestAgent) { 570 // Create the job 571 job := mock.Job() 572 args := structs.JobRegisterRequest{ 573 Job: job, 574 WriteRequest: structs.WriteRequest{ 575 Region: "global", 576 Namespace: structs.DefaultNamespace, 577 }, 578 } 579 var resp structs.JobRegisterResponse 580 if err := s.Agent.RPC("Job.Register", &args, &resp); err != nil { 581 t.Fatalf("err: %v", err) 582 } 583 584 // Make the HTTP request to do a soft delete 585 req, err := http.NewRequest("DELETE", "/v1/job/"+job.ID, nil) 586 if err != nil { 587 t.Fatalf("err: %v", err) 588 } 589 respW := httptest.NewRecorder() 590 591 // Make the request 592 obj, err := s.Server.JobSpecificRequest(respW, req) 593 if err != nil { 594 t.Fatalf("err: %v", err) 595 } 596 597 // Check the response 598 dereg := obj.(structs.JobDeregisterResponse) 599 if dereg.EvalID == "" { 600 t.Fatalf("bad: %v", dereg) 601 } 602 603 // Check for the index 604 if respW.HeaderMap.Get("X-Nomad-Index") == "" { 605 t.Fatalf("missing index") 606 } 607 608 // Check the job is still queryable 609 getReq1 := structs.JobSpecificRequest{ 610 JobID: job.ID, 611 QueryOptions: structs.QueryOptions{ 612 Region: "global", 613 Namespace: structs.DefaultNamespace, 614 }, 615 } 616 var getResp1 structs.SingleJobResponse 617 if err := s.Agent.RPC("Job.GetJob", &getReq1, &getResp1); err != nil { 618 t.Fatalf("err: %v", err) 619 } 620 if getResp1.Job == nil { 621 t.Fatalf("job doesn't exists") 622 } 623 if !getResp1.Job.Stop { 624 t.Fatalf("job should be marked as stop") 625 } 626 627 // Make the HTTP request to do a purge delete 628 req2, err := http.NewRequest("DELETE", "/v1/job/"+job.ID+"?purge=true", nil) 629 if err != nil { 630 t.Fatalf("err: %v", err) 631 } 632 respW.Flush() 633 634 // Make the request 635 obj, err = s.Server.JobSpecificRequest(respW, req2) 636 if err != nil { 637 t.Fatalf("err: %v", err) 638 } 639 640 // Check the response 641 dereg = obj.(structs.JobDeregisterResponse) 642 if dereg.EvalID == "" { 643 t.Fatalf("bad: %v", dereg) 644 } 645 646 // Check for the index 647 if respW.HeaderMap.Get("X-Nomad-Index") == "" { 648 t.Fatalf("missing index") 649 } 650 651 // Check the job is gone 652 getReq2 := structs.JobSpecificRequest{ 653 JobID: job.ID, 654 QueryOptions: structs.QueryOptions{ 655 Region: "global", 656 Namespace: structs.DefaultNamespace, 657 }, 658 } 659 var getResp2 structs.SingleJobResponse 660 if err := s.Agent.RPC("Job.GetJob", &getReq2, &getResp2); err != nil { 661 t.Fatalf("err: %v", err) 662 } 663 if getResp2.Job != nil { 664 t.Fatalf("job still exists") 665 } 666 }) 667 } 668 669 func TestHTTP_JobForceEvaluate(t *testing.T) { 670 t.Parallel() 671 httpTest(t, nil, func(s *TestAgent) { 672 // Create the job 673 job := mock.Job() 674 args := structs.JobRegisterRequest{ 675 Job: job, 676 WriteRequest: structs.WriteRequest{ 677 Region: "global", 678 Namespace: structs.DefaultNamespace, 679 }, 680 } 681 var resp structs.JobRegisterResponse 682 if err := s.Agent.RPC("Job.Register", &args, &resp); err != nil { 683 t.Fatalf("err: %v", err) 684 } 685 686 // Make the HTTP request 687 req, err := http.NewRequest("POST", "/v1/job/"+job.ID+"/evaluate", nil) 688 if err != nil { 689 t.Fatalf("err: %v", err) 690 } 691 respW := httptest.NewRecorder() 692 693 // Make the request 694 obj, err := s.Server.JobSpecificRequest(respW, req) 695 if err != nil { 696 t.Fatalf("err: %v", err) 697 } 698 699 // Check the response 700 reg := obj.(structs.JobRegisterResponse) 701 if reg.EvalID == "" { 702 t.Fatalf("bad: %v", reg) 703 } 704 705 // Check for the index 706 if respW.HeaderMap.Get("X-Nomad-Index") == "" { 707 t.Fatalf("missing index") 708 } 709 }) 710 } 711 712 func TestHTTP_JobEvaluate_ForceReschedule(t *testing.T) { 713 t.Parallel() 714 httpTest(t, nil, func(s *TestAgent) { 715 // Create the job 716 job := mock.Job() 717 args := structs.JobRegisterRequest{ 718 Job: job, 719 WriteRequest: structs.WriteRequest{ 720 Region: "global", 721 Namespace: structs.DefaultNamespace, 722 }, 723 } 724 var resp structs.JobRegisterResponse 725 if err := s.Agent.RPC("Job.Register", &args, &resp); err != nil { 726 t.Fatalf("err: %v", err) 727 } 728 jobEvalReq := api.JobEvaluateRequest{ 729 JobID: job.ID, 730 EvalOptions: api.EvalOptions{ 731 ForceReschedule: true, 732 }, 733 } 734 735 buf := encodeReq(jobEvalReq) 736 737 // Make the HTTP request 738 req, err := http.NewRequest("POST", "/v1/job/"+job.ID+"/evaluate", buf) 739 if err != nil { 740 t.Fatalf("err: %v", err) 741 } 742 respW := httptest.NewRecorder() 743 744 // Make the request 745 obj, err := s.Server.JobSpecificRequest(respW, req) 746 if err != nil { 747 t.Fatalf("err: %v", err) 748 } 749 750 // Check the response 751 reg := obj.(structs.JobRegisterResponse) 752 if reg.EvalID == "" { 753 t.Fatalf("bad: %v", reg) 754 } 755 756 // Check for the index 757 if respW.HeaderMap.Get("X-Nomad-Index") == "" { 758 t.Fatalf("missing index") 759 } 760 }) 761 } 762 763 func TestHTTP_JobEvaluations(t *testing.T) { 764 t.Parallel() 765 httpTest(t, nil, func(s *TestAgent) { 766 // Create the job 767 job := mock.Job() 768 args := structs.JobRegisterRequest{ 769 Job: job, 770 WriteRequest: structs.WriteRequest{ 771 Region: "global", 772 Namespace: structs.DefaultNamespace, 773 }, 774 } 775 var resp structs.JobRegisterResponse 776 if err := s.Agent.RPC("Job.Register", &args, &resp); err != nil { 777 t.Fatalf("err: %v", err) 778 } 779 780 // Make the HTTP request 781 req, err := http.NewRequest("GET", "/v1/job/"+job.ID+"/evaluations", nil) 782 if err != nil { 783 t.Fatalf("err: %v", err) 784 } 785 respW := httptest.NewRecorder() 786 787 // Make the request 788 obj, err := s.Server.JobSpecificRequest(respW, req) 789 if err != nil { 790 t.Fatalf("err: %v", err) 791 } 792 793 // Check the response 794 evals := obj.([]*structs.Evaluation) 795 // Can be multiple evals, use the last one, since they are in order 796 idx := len(evals) - 1 797 if len(evals) < 0 || evals[idx].ID != resp.EvalID { 798 t.Fatalf("bad: %v", evals) 799 } 800 801 // Check for the index 802 if respW.HeaderMap.Get("X-Nomad-Index") == "" { 803 t.Fatalf("missing index") 804 } 805 if respW.HeaderMap.Get("X-Nomad-KnownLeader") != "true" { 806 t.Fatalf("missing known leader") 807 } 808 if respW.HeaderMap.Get("X-Nomad-LastContact") == "" { 809 t.Fatalf("missing last contact") 810 } 811 }) 812 } 813 814 func TestHTTP_JobAllocations(t *testing.T) { 815 t.Parallel() 816 httpTest(t, nil, func(s *TestAgent) { 817 // Create the job 818 alloc1 := mock.Alloc() 819 args := structs.JobRegisterRequest{ 820 Job: alloc1.Job, 821 WriteRequest: structs.WriteRequest{ 822 Region: "global", 823 Namespace: structs.DefaultNamespace, 824 }, 825 } 826 var resp structs.JobRegisterResponse 827 if err := s.Agent.RPC("Job.Register", &args, &resp); err != nil { 828 t.Fatalf("err: %v", err) 829 } 830 831 // Directly manipulate the state 832 expectedDisplayMsg := "test message" 833 testEvent := structs.NewTaskEvent("test event").SetMessage(expectedDisplayMsg) 834 var events []*structs.TaskEvent 835 events = append(events, testEvent) 836 taskState := &structs.TaskState{Events: events} 837 alloc1.TaskStates = make(map[string]*structs.TaskState) 838 alloc1.TaskStates["test"] = taskState 839 state := s.Agent.server.State() 840 err := state.UpsertAllocs(1000, []*structs.Allocation{alloc1}) 841 if err != nil { 842 t.Fatalf("err: %v", err) 843 } 844 845 // Make the HTTP request 846 req, err := http.NewRequest("GET", "/v1/job/"+alloc1.Job.ID+"/allocations?all=true", nil) 847 if err != nil { 848 t.Fatalf("err: %v", err) 849 } 850 respW := httptest.NewRecorder() 851 852 // Make the request 853 obj, err := s.Server.JobSpecificRequest(respW, req) 854 if err != nil { 855 t.Fatalf("err: %v", err) 856 } 857 858 // Check the response 859 allocs := obj.([]*structs.AllocListStub) 860 if len(allocs) != 1 && allocs[0].ID != alloc1.ID { 861 t.Fatalf("bad: %v", allocs) 862 } 863 displayMsg := allocs[0].TaskStates["test"].Events[0].DisplayMessage 864 assert.Equal(t, expectedDisplayMsg, displayMsg) 865 866 // Check for the index 867 if respW.HeaderMap.Get("X-Nomad-Index") == "" { 868 t.Fatalf("missing index") 869 } 870 if respW.HeaderMap.Get("X-Nomad-KnownLeader") != "true" { 871 t.Fatalf("missing known leader") 872 } 873 if respW.HeaderMap.Get("X-Nomad-LastContact") == "" { 874 t.Fatalf("missing last contact") 875 } 876 }) 877 } 878 879 func TestHTTP_JobDeployments(t *testing.T) { 880 assert := assert.New(t) 881 t.Parallel() 882 httpTest(t, nil, func(s *TestAgent) { 883 // Create the job 884 j := mock.Job() 885 args := structs.JobRegisterRequest{ 886 Job: j, 887 WriteRequest: structs.WriteRequest{ 888 Region: "global", 889 Namespace: structs.DefaultNamespace, 890 }, 891 } 892 var resp structs.JobRegisterResponse 893 assert.Nil(s.Agent.RPC("Job.Register", &args, &resp), "JobRegister") 894 895 // Directly manipulate the state 896 state := s.Agent.server.State() 897 d := mock.Deployment() 898 d.JobID = j.ID 899 d.JobCreateIndex = resp.JobModifyIndex 900 901 assert.Nil(state.UpsertDeployment(1000, d), "UpsertDeployment") 902 903 // Make the HTTP request 904 req, err := http.NewRequest("GET", "/v1/job/"+j.ID+"/deployments", nil) 905 assert.Nil(err, "HTTP") 906 respW := httptest.NewRecorder() 907 908 // Make the request 909 obj, err := s.Server.JobSpecificRequest(respW, req) 910 assert.Nil(err, "JobSpecificRequest") 911 912 // Check the response 913 deploys := obj.([]*structs.Deployment) 914 assert.Len(deploys, 1, "deployments") 915 assert.Equal(d.ID, deploys[0].ID, "deployment id") 916 917 assert.NotZero(respW.HeaderMap.Get("X-Nomad-Index"), "missing index") 918 assert.Equal("true", respW.HeaderMap.Get("X-Nomad-KnownLeader"), "missing known leader") 919 assert.NotZero(respW.HeaderMap.Get("X-Nomad-LastContact"), "missing last contact") 920 }) 921 } 922 923 func TestHTTP_JobDeployment(t *testing.T) { 924 assert := assert.New(t) 925 t.Parallel() 926 httpTest(t, nil, func(s *TestAgent) { 927 // Create the job 928 j := mock.Job() 929 args := structs.JobRegisterRequest{ 930 Job: j, 931 WriteRequest: structs.WriteRequest{ 932 Region: "global", 933 Namespace: structs.DefaultNamespace, 934 }, 935 } 936 var resp structs.JobRegisterResponse 937 assert.Nil(s.Agent.RPC("Job.Register", &args, &resp), "JobRegister") 938 939 // Directly manipulate the state 940 state := s.Agent.server.State() 941 d := mock.Deployment() 942 d.JobID = j.ID 943 d.JobCreateIndex = resp.JobModifyIndex 944 assert.Nil(state.UpsertDeployment(1000, d), "UpsertDeployment") 945 946 // Make the HTTP request 947 req, err := http.NewRequest("GET", "/v1/job/"+j.ID+"/deployment", nil) 948 assert.Nil(err, "HTTP") 949 respW := httptest.NewRecorder() 950 951 // Make the request 952 obj, err := s.Server.JobSpecificRequest(respW, req) 953 assert.Nil(err, "JobSpecificRequest") 954 955 // Check the response 956 out := obj.(*structs.Deployment) 957 assert.NotNil(out, "deployment") 958 assert.Equal(d.ID, out.ID, "deployment id") 959 960 assert.NotZero(respW.HeaderMap.Get("X-Nomad-Index"), "missing index") 961 assert.Equal("true", respW.HeaderMap.Get("X-Nomad-KnownLeader"), "missing known leader") 962 assert.NotZero(respW.HeaderMap.Get("X-Nomad-LastContact"), "missing last contact") 963 }) 964 } 965 966 func TestHTTP_JobVersions(t *testing.T) { 967 t.Parallel() 968 httpTest(t, nil, func(s *TestAgent) { 969 // Create the job 970 job := mock.Job() 971 args := structs.JobRegisterRequest{ 972 Job: job, 973 WriteRequest: structs.WriteRequest{ 974 Region: "global", 975 Namespace: structs.DefaultNamespace, 976 }, 977 } 978 var resp structs.JobRegisterResponse 979 if err := s.Agent.RPC("Job.Register", &args, &resp); err != nil { 980 t.Fatalf("err: %v", err) 981 } 982 983 job2 := mock.Job() 984 job2.ID = job.ID 985 job2.Priority = 100 986 987 args2 := structs.JobRegisterRequest{ 988 Job: job2, 989 WriteRequest: structs.WriteRequest{ 990 Region: "global", 991 Namespace: structs.DefaultNamespace, 992 }, 993 } 994 var resp2 structs.JobRegisterResponse 995 if err := s.Agent.RPC("Job.Register", &args2, &resp2); err != nil { 996 t.Fatalf("err: %v", err) 997 } 998 999 // Make the HTTP request 1000 req, err := http.NewRequest("GET", "/v1/job/"+job.ID+"/versions?diffs=true", nil) 1001 if err != nil { 1002 t.Fatalf("err: %v", err) 1003 } 1004 respW := httptest.NewRecorder() 1005 1006 // Make the request 1007 obj, err := s.Server.JobSpecificRequest(respW, req) 1008 if err != nil { 1009 t.Fatalf("err: %v", err) 1010 } 1011 1012 // Check the response 1013 vResp := obj.(structs.JobVersionsResponse) 1014 versions := vResp.Versions 1015 if len(versions) != 2 { 1016 t.Fatalf("got %d versions; want 2", len(versions)) 1017 } 1018 1019 if v := versions[0]; v.Version != 1 || v.Priority != 100 { 1020 t.Fatalf("bad %v", v) 1021 } 1022 1023 if v := versions[1]; v.Version != 0 { 1024 t.Fatalf("bad %v", v) 1025 } 1026 1027 if len(vResp.Diffs) != 1 { 1028 t.Fatalf("bad %v", vResp) 1029 } 1030 1031 // Check for the index 1032 if respW.HeaderMap.Get("X-Nomad-Index") == "" { 1033 t.Fatalf("missing index") 1034 } 1035 if respW.HeaderMap.Get("X-Nomad-KnownLeader") != "true" { 1036 t.Fatalf("missing known leader") 1037 } 1038 if respW.HeaderMap.Get("X-Nomad-LastContact") == "" { 1039 t.Fatalf("missing last contact") 1040 } 1041 }) 1042 } 1043 1044 func TestHTTP_PeriodicForce(t *testing.T) { 1045 t.Parallel() 1046 httpTest(t, nil, func(s *TestAgent) { 1047 // Create and register a periodic job. 1048 job := mock.PeriodicJob() 1049 args := structs.JobRegisterRequest{ 1050 Job: job, 1051 WriteRequest: structs.WriteRequest{ 1052 Region: "global", 1053 Namespace: structs.DefaultNamespace, 1054 }, 1055 } 1056 var resp structs.JobRegisterResponse 1057 if err := s.Agent.RPC("Job.Register", &args, &resp); err != nil { 1058 t.Fatalf("err: %v", err) 1059 } 1060 1061 // Make the HTTP request 1062 req, err := http.NewRequest("POST", "/v1/job/"+job.ID+"/periodic/force", nil) 1063 if err != nil { 1064 t.Fatalf("err: %v", err) 1065 } 1066 respW := httptest.NewRecorder() 1067 1068 // Make the request 1069 obj, err := s.Server.JobSpecificRequest(respW, req) 1070 if err != nil { 1071 t.Fatalf("err: %v", err) 1072 } 1073 1074 // Check for the index 1075 if respW.HeaderMap.Get("X-Nomad-Index") == "" { 1076 t.Fatalf("missing index") 1077 } 1078 1079 // Check the response 1080 r := obj.(structs.PeriodicForceResponse) 1081 if r.EvalID == "" { 1082 t.Fatalf("bad: %#v", r) 1083 } 1084 }) 1085 } 1086 1087 func TestHTTP_JobPlan(t *testing.T) { 1088 t.Parallel() 1089 httpTest(t, nil, func(s *TestAgent) { 1090 // Create the job 1091 job := MockJob() 1092 args := api.JobPlanRequest{ 1093 Job: job, 1094 Diff: true, 1095 WriteRequest: api.WriteRequest{ 1096 Region: "global", 1097 Namespace: api.DefaultNamespace, 1098 }, 1099 } 1100 buf := encodeReq(args) 1101 1102 // Make the HTTP request 1103 req, err := http.NewRequest("PUT", "/v1/job/"+*job.ID+"/plan", buf) 1104 if err != nil { 1105 t.Fatalf("err: %v", err) 1106 } 1107 respW := httptest.NewRecorder() 1108 1109 // Make the request 1110 obj, err := s.Server.JobSpecificRequest(respW, req) 1111 if err != nil { 1112 t.Fatalf("err: %v", err) 1113 } 1114 1115 // Check the response 1116 plan := obj.(structs.JobPlanResponse) 1117 if plan.Annotations == nil { 1118 t.Fatalf("bad: %v", plan) 1119 } 1120 1121 if plan.Diff == nil { 1122 t.Fatalf("bad: %v", plan) 1123 } 1124 }) 1125 } 1126 1127 func TestHTTP_JobPlanRegion(t *testing.T) { 1128 t.Parallel() 1129 1130 cases := []struct { 1131 Name string 1132 ConfigRegion string 1133 APIRegion string 1134 ExpectedRegion string 1135 }{ 1136 { 1137 Name: "api region takes precedence", 1138 ConfigRegion: "not-global", 1139 APIRegion: "north-america", 1140 ExpectedRegion: "north-america", 1141 }, 1142 { 1143 Name: "config region is set", 1144 ConfigRegion: "north-america", 1145 APIRegion: "", 1146 ExpectedRegion: "north-america", 1147 }, 1148 { 1149 Name: "api region is set", 1150 ConfigRegion: "", 1151 APIRegion: "north-america", 1152 ExpectedRegion: "north-america", 1153 }, 1154 { 1155 Name: "falls back to default if no region is provided", 1156 ConfigRegion: "", 1157 APIRegion: "", 1158 ExpectedRegion: "global", 1159 }, 1160 } 1161 1162 for _, tc := range cases { 1163 t.Run(tc.Name, func(t *testing.T) { 1164 httpTest(t, func(c *Config) { c.Region = tc.ExpectedRegion }, func(s *TestAgent) { 1165 // Create the job 1166 job := MockRegionalJob() 1167 1168 if tc.ConfigRegion == "" { 1169 job.Region = nil 1170 } else { 1171 job.Region = &tc.ConfigRegion 1172 } 1173 1174 args := api.JobPlanRequest{ 1175 Job: job, 1176 Diff: true, 1177 WriteRequest: api.WriteRequest{ 1178 Region: tc.APIRegion, 1179 Namespace: api.DefaultNamespace, 1180 }, 1181 } 1182 buf := encodeReq(args) 1183 1184 // Make the HTTP request 1185 req, err := http.NewRequest("PUT", "/v1/job/"+*job.ID+"/plan", buf) 1186 require.NoError(t, err) 1187 respW := httptest.NewRecorder() 1188 1189 // Make the request 1190 obj, err := s.Server.JobSpecificRequest(respW, req) 1191 require.NoError(t, err) 1192 1193 // Check the response 1194 plan := obj.(structs.JobPlanResponse) 1195 require.NotNil(t, plan.Annotations) 1196 require.NotNil(t, plan.Diff) 1197 }) 1198 }) 1199 } 1200 } 1201 1202 func TestHTTP_JobDispatch(t *testing.T) { 1203 t.Parallel() 1204 httpTest(t, nil, func(s *TestAgent) { 1205 // Create the parameterized job 1206 job := mock.BatchJob() 1207 job.ParameterizedJob = &structs.ParameterizedJobConfig{} 1208 1209 args := structs.JobRegisterRequest{ 1210 Job: job, 1211 WriteRequest: structs.WriteRequest{ 1212 Region: "global", 1213 Namespace: structs.DefaultNamespace, 1214 }, 1215 } 1216 var resp structs.JobRegisterResponse 1217 if err := s.Agent.RPC("Job.Register", &args, &resp); err != nil { 1218 t.Fatalf("err: %v", err) 1219 } 1220 1221 // Make the request 1222 respW := httptest.NewRecorder() 1223 args2 := structs.JobDispatchRequest{ 1224 WriteRequest: structs.WriteRequest{ 1225 Region: "global", 1226 Namespace: structs.DefaultNamespace, 1227 }, 1228 } 1229 buf := encodeReq(args2) 1230 1231 // Make the HTTP request 1232 req2, err := http.NewRequest("PUT", "/v1/job/"+job.ID+"/dispatch", buf) 1233 if err != nil { 1234 t.Fatalf("err: %v", err) 1235 } 1236 respW.Flush() 1237 1238 // Make the request 1239 obj, err := s.Server.JobSpecificRequest(respW, req2) 1240 if err != nil { 1241 t.Fatalf("err: %v", err) 1242 } 1243 1244 // Check the response 1245 dispatch := obj.(structs.JobDispatchResponse) 1246 if dispatch.EvalID == "" { 1247 t.Fatalf("bad: %v", dispatch) 1248 } 1249 1250 if dispatch.DispatchedJobID == "" { 1251 t.Fatalf("bad: %v", dispatch) 1252 } 1253 }) 1254 } 1255 1256 func TestHTTP_JobRevert(t *testing.T) { 1257 t.Parallel() 1258 httpTest(t, nil, func(s *TestAgent) { 1259 // Create the job and register it twice 1260 job := mock.Job() 1261 regReq := structs.JobRegisterRequest{ 1262 Job: job, 1263 WriteRequest: structs.WriteRequest{ 1264 Region: "global", 1265 Namespace: structs.DefaultNamespace, 1266 }, 1267 } 1268 var regResp structs.JobRegisterResponse 1269 if err := s.Agent.RPC("Job.Register", ®Req, ®Resp); err != nil { 1270 t.Fatalf("err: %v", err) 1271 } 1272 1273 // Change the job to get a new version 1274 job.Datacenters = append(job.Datacenters, "foo") 1275 if err := s.Agent.RPC("Job.Register", ®Req, ®Resp); err != nil { 1276 t.Fatalf("err: %v", err) 1277 } 1278 1279 args := structs.JobRevertRequest{ 1280 JobID: job.ID, 1281 JobVersion: 0, 1282 WriteRequest: structs.WriteRequest{ 1283 Region: "global", 1284 Namespace: structs.DefaultNamespace, 1285 }, 1286 } 1287 buf := encodeReq(args) 1288 1289 // Make the HTTP request 1290 req, err := http.NewRequest("PUT", "/v1/job/"+job.ID+"/revert", buf) 1291 if err != nil { 1292 t.Fatalf("err: %v", err) 1293 } 1294 respW := httptest.NewRecorder() 1295 1296 // Make the request 1297 obj, err := s.Server.JobSpecificRequest(respW, req) 1298 if err != nil { 1299 t.Fatalf("err: %v", err) 1300 } 1301 1302 // Check the response 1303 revertResp := obj.(structs.JobRegisterResponse) 1304 if revertResp.EvalID == "" { 1305 t.Fatalf("bad: %v", revertResp) 1306 } 1307 1308 // Check for the index 1309 if respW.HeaderMap.Get("X-Nomad-Index") == "" { 1310 t.Fatalf("missing index") 1311 } 1312 }) 1313 } 1314 1315 func TestHTTP_JobStable(t *testing.T) { 1316 t.Parallel() 1317 httpTest(t, nil, func(s *TestAgent) { 1318 // Create the job and register it twice 1319 job := mock.Job() 1320 regReq := structs.JobRegisterRequest{ 1321 Job: job, 1322 WriteRequest: structs.WriteRequest{ 1323 Region: "global", 1324 Namespace: structs.DefaultNamespace, 1325 }, 1326 } 1327 var regResp structs.JobRegisterResponse 1328 if err := s.Agent.RPC("Job.Register", ®Req, ®Resp); err != nil { 1329 t.Fatalf("err: %v", err) 1330 } 1331 1332 if err := s.Agent.RPC("Job.Register", ®Req, ®Resp); err != nil { 1333 t.Fatalf("err: %v", err) 1334 } 1335 1336 args := structs.JobStabilityRequest{ 1337 JobID: job.ID, 1338 JobVersion: 0, 1339 Stable: true, 1340 WriteRequest: structs.WriteRequest{ 1341 Region: "global", 1342 Namespace: structs.DefaultNamespace, 1343 }, 1344 } 1345 buf := encodeReq(args) 1346 1347 // Make the HTTP request 1348 req, err := http.NewRequest("PUT", "/v1/job/"+job.ID+"/stable", buf) 1349 if err != nil { 1350 t.Fatalf("err: %v", err) 1351 } 1352 respW := httptest.NewRecorder() 1353 1354 // Make the request 1355 obj, err := s.Server.JobSpecificRequest(respW, req) 1356 if err != nil { 1357 t.Fatalf("err: %v", err) 1358 } 1359 1360 // Check the response 1361 stableResp := obj.(structs.JobStabilityResponse) 1362 if stableResp.Index == 0 { 1363 t.Fatalf("bad: %v", stableResp) 1364 } 1365 1366 // Check for the index 1367 if respW.HeaderMap.Get("X-Nomad-Index") == "" { 1368 t.Fatalf("missing index") 1369 } 1370 }) 1371 } 1372 1373 func TestJobs_ApiJobToStructsJob(t *testing.T) { 1374 apiJob := &api.Job{ 1375 Stop: helper.BoolToPtr(true), 1376 Region: helper.StringToPtr("global"), 1377 Namespace: helper.StringToPtr("foo"), 1378 ID: helper.StringToPtr("foo"), 1379 ParentID: helper.StringToPtr("lol"), 1380 Name: helper.StringToPtr("name"), 1381 Type: helper.StringToPtr("service"), 1382 Priority: helper.IntToPtr(50), 1383 AllAtOnce: helper.BoolToPtr(true), 1384 Datacenters: []string{"dc1", "dc2"}, 1385 Constraints: []*api.Constraint{ 1386 { 1387 LTarget: "a", 1388 RTarget: "b", 1389 Operand: "c", 1390 }, 1391 }, 1392 Affinities: []*api.Affinity{ 1393 { 1394 LTarget: "a", 1395 RTarget: "b", 1396 Operand: "c", 1397 Weight: helper.Int8ToPtr(50), 1398 }, 1399 }, 1400 Update: &api.UpdateStrategy{ 1401 Stagger: helper.TimeToPtr(1 * time.Second), 1402 MaxParallel: helper.IntToPtr(5), 1403 HealthCheck: helper.StringToPtr(structs.UpdateStrategyHealthCheck_Manual), 1404 MinHealthyTime: helper.TimeToPtr(1 * time.Minute), 1405 HealthyDeadline: helper.TimeToPtr(3 * time.Minute), 1406 ProgressDeadline: helper.TimeToPtr(3 * time.Minute), 1407 AutoRevert: helper.BoolToPtr(false), 1408 Canary: helper.IntToPtr(1), 1409 }, 1410 Spreads: []*api.Spread{ 1411 { 1412 Attribute: "${meta.rack}", 1413 Weight: helper.Int8ToPtr(100), 1414 SpreadTarget: []*api.SpreadTarget{ 1415 { 1416 Value: "r1", 1417 Percent: 50, 1418 }, 1419 }, 1420 }, 1421 }, 1422 Periodic: &api.PeriodicConfig{ 1423 Enabled: helper.BoolToPtr(true), 1424 Spec: helper.StringToPtr("spec"), 1425 SpecType: helper.StringToPtr("cron"), 1426 ProhibitOverlap: helper.BoolToPtr(true), 1427 TimeZone: helper.StringToPtr("test zone"), 1428 }, 1429 ParameterizedJob: &api.ParameterizedJobConfig{ 1430 Payload: "payload", 1431 MetaRequired: []string{"a", "b"}, 1432 MetaOptional: []string{"c", "d"}, 1433 }, 1434 Payload: []byte("payload"), 1435 Meta: map[string]string{ 1436 "foo": "bar", 1437 }, 1438 TaskGroups: []*api.TaskGroup{ 1439 { 1440 Name: helper.StringToPtr("group1"), 1441 Count: helper.IntToPtr(5), 1442 Constraints: []*api.Constraint{ 1443 { 1444 LTarget: "x", 1445 RTarget: "y", 1446 Operand: "z", 1447 }, 1448 }, 1449 Affinities: []*api.Affinity{ 1450 { 1451 LTarget: "x", 1452 RTarget: "y", 1453 Operand: "z", 1454 Weight: helper.Int8ToPtr(100), 1455 }, 1456 }, 1457 RestartPolicy: &api.RestartPolicy{ 1458 Interval: helper.TimeToPtr(1 * time.Second), 1459 Attempts: helper.IntToPtr(5), 1460 Delay: helper.TimeToPtr(10 * time.Second), 1461 Mode: helper.StringToPtr("delay"), 1462 }, 1463 ReschedulePolicy: &api.ReschedulePolicy{ 1464 Interval: helper.TimeToPtr(12 * time.Hour), 1465 Attempts: helper.IntToPtr(5), 1466 DelayFunction: helper.StringToPtr("constant"), 1467 Delay: helper.TimeToPtr(30 * time.Second), 1468 Unlimited: helper.BoolToPtr(true), 1469 MaxDelay: helper.TimeToPtr(20 * time.Minute), 1470 }, 1471 Migrate: &api.MigrateStrategy{ 1472 MaxParallel: helper.IntToPtr(12), 1473 HealthCheck: helper.StringToPtr("task_events"), 1474 MinHealthyTime: helper.TimeToPtr(12 * time.Hour), 1475 HealthyDeadline: helper.TimeToPtr(12 * time.Hour), 1476 }, 1477 Spreads: []*api.Spread{ 1478 { 1479 Attribute: "${node.datacenter}", 1480 Weight: helper.Int8ToPtr(100), 1481 SpreadTarget: []*api.SpreadTarget{ 1482 { 1483 Value: "dc1", 1484 Percent: 100, 1485 }, 1486 }, 1487 }, 1488 }, 1489 EphemeralDisk: &api.EphemeralDisk{ 1490 SizeMB: helper.IntToPtr(100), 1491 Sticky: helper.BoolToPtr(true), 1492 Migrate: helper.BoolToPtr(true), 1493 }, 1494 Update: &api.UpdateStrategy{ 1495 HealthCheck: helper.StringToPtr(structs.UpdateStrategyHealthCheck_Checks), 1496 MinHealthyTime: helper.TimeToPtr(2 * time.Minute), 1497 HealthyDeadline: helper.TimeToPtr(5 * time.Minute), 1498 ProgressDeadline: helper.TimeToPtr(5 * time.Minute), 1499 AutoRevert: helper.BoolToPtr(true), 1500 }, 1501 Meta: map[string]string{ 1502 "key": "value", 1503 }, 1504 Services: []*api.Service{ 1505 { 1506 Name: "groupserviceA", 1507 Tags: []string{"a", "b"}, 1508 CanaryTags: []string{"d", "e"}, 1509 PortLabel: "1234", 1510 Meta: map[string]string{ 1511 "servicemeta": "foobar", 1512 }, 1513 CheckRestart: &api.CheckRestart{ 1514 Limit: 4, 1515 Grace: helper.TimeToPtr(11 * time.Second), 1516 }, 1517 Checks: []api.ServiceCheck{ 1518 { 1519 Id: "hello", 1520 Name: "bar", 1521 Type: "http", 1522 Command: "foo", 1523 Args: []string{"a", "b"}, 1524 Path: "/check", 1525 Protocol: "http", 1526 PortLabel: "foo", 1527 AddressMode: "driver", 1528 GRPCService: "foo.Bar", 1529 GRPCUseTLS: true, 1530 Interval: 4 * time.Second, 1531 Timeout: 2 * time.Second, 1532 InitialStatus: "ok", 1533 CheckRestart: &api.CheckRestart{ 1534 Limit: 3, 1535 IgnoreWarnings: true, 1536 }, 1537 TaskName: "task1", 1538 }, 1539 }, 1540 Connect: &api.ConsulConnect{ 1541 Native: false, 1542 SidecarService: &api.ConsulSidecarService{ 1543 Tags: []string{"f", "g"}, 1544 Port: "9000", 1545 }, 1546 }, 1547 }, 1548 }, 1549 Tasks: []*api.Task{ 1550 { 1551 Name: "task1", 1552 Leader: true, 1553 Driver: "docker", 1554 User: "mary", 1555 Config: map[string]interface{}{ 1556 "lol": "code", 1557 }, 1558 Env: map[string]string{ 1559 "hello": "world", 1560 }, 1561 Constraints: []*api.Constraint{ 1562 { 1563 LTarget: "x", 1564 RTarget: "y", 1565 Operand: "z", 1566 }, 1567 }, 1568 Affinities: []*api.Affinity{ 1569 { 1570 LTarget: "a", 1571 RTarget: "b", 1572 Operand: "c", 1573 Weight: helper.Int8ToPtr(50), 1574 }, 1575 }, 1576 1577 Services: []*api.Service{ 1578 { 1579 Id: "id", 1580 Name: "serviceA", 1581 Tags: []string{"1", "2"}, 1582 CanaryTags: []string{"3", "4"}, 1583 PortLabel: "foo", 1584 Meta: map[string]string{ 1585 "servicemeta": "foobar", 1586 }, 1587 CheckRestart: &api.CheckRestart{ 1588 Limit: 4, 1589 Grace: helper.TimeToPtr(11 * time.Second), 1590 }, 1591 Checks: []api.ServiceCheck{ 1592 { 1593 Id: "hello", 1594 Name: "bar", 1595 Type: "http", 1596 Command: "foo", 1597 Args: []string{"a", "b"}, 1598 Path: "/check", 1599 Protocol: "http", 1600 PortLabel: "foo", 1601 AddressMode: "driver", 1602 GRPCService: "foo.Bar", 1603 GRPCUseTLS: true, 1604 Interval: 4 * time.Second, 1605 Timeout: 2 * time.Second, 1606 InitialStatus: "ok", 1607 CheckRestart: &api.CheckRestart{ 1608 Limit: 3, 1609 IgnoreWarnings: true, 1610 }, 1611 }, 1612 { 1613 Id: "check2id", 1614 Name: "check2", 1615 Type: "tcp", 1616 PortLabel: "foo", 1617 Interval: 4 * time.Second, 1618 Timeout: 2 * time.Second, 1619 }, 1620 }, 1621 }, 1622 }, 1623 Resources: &api.Resources{ 1624 CPU: helper.IntToPtr(100), 1625 MemoryMB: helper.IntToPtr(10), 1626 Networks: []*api.NetworkResource{ 1627 { 1628 IP: "10.10.11.1", 1629 MBits: helper.IntToPtr(10), 1630 ReservedPorts: []api.Port{ 1631 { 1632 Label: "http", 1633 Value: 80, 1634 }, 1635 }, 1636 DynamicPorts: []api.Port{ 1637 { 1638 Label: "ssh", 1639 Value: 2000, 1640 }, 1641 }, 1642 }, 1643 }, 1644 Devices: []*api.RequestedDevice{ 1645 { 1646 Name: "nvidia/gpu", 1647 Count: helper.Uint64ToPtr(4), 1648 Constraints: []*api.Constraint{ 1649 { 1650 LTarget: "x", 1651 RTarget: "y", 1652 Operand: "z", 1653 }, 1654 }, 1655 Affinities: []*api.Affinity{ 1656 { 1657 LTarget: "a", 1658 RTarget: "b", 1659 Operand: "c", 1660 Weight: helper.Int8ToPtr(50), 1661 }, 1662 }, 1663 }, 1664 { 1665 Name: "gpu", 1666 Count: nil, 1667 }, 1668 }, 1669 }, 1670 Meta: map[string]string{ 1671 "lol": "code", 1672 }, 1673 KillTimeout: helper.TimeToPtr(10 * time.Second), 1674 KillSignal: "SIGQUIT", 1675 LogConfig: &api.LogConfig{ 1676 MaxFiles: helper.IntToPtr(10), 1677 MaxFileSizeMB: helper.IntToPtr(100), 1678 }, 1679 Artifacts: []*api.TaskArtifact{ 1680 { 1681 GetterSource: helper.StringToPtr("source"), 1682 GetterOptions: map[string]string{ 1683 "a": "b", 1684 }, 1685 GetterMode: helper.StringToPtr("dir"), 1686 RelativeDest: helper.StringToPtr("dest"), 1687 }, 1688 }, 1689 Vault: &api.Vault{ 1690 Policies: []string{"a", "b", "c"}, 1691 Env: helper.BoolToPtr(true), 1692 ChangeMode: helper.StringToPtr("c"), 1693 ChangeSignal: helper.StringToPtr("sighup"), 1694 }, 1695 Templates: []*api.Template{ 1696 { 1697 SourcePath: helper.StringToPtr("source"), 1698 DestPath: helper.StringToPtr("dest"), 1699 EmbeddedTmpl: helper.StringToPtr("embedded"), 1700 ChangeMode: helper.StringToPtr("change"), 1701 ChangeSignal: helper.StringToPtr("signal"), 1702 Splay: helper.TimeToPtr(1 * time.Minute), 1703 Perms: helper.StringToPtr("666"), 1704 LeftDelim: helper.StringToPtr("abc"), 1705 RightDelim: helper.StringToPtr("def"), 1706 Envvars: helper.BoolToPtr(true), 1707 VaultGrace: helper.TimeToPtr(3 * time.Second), 1708 }, 1709 }, 1710 DispatchPayload: &api.DispatchPayloadConfig{ 1711 File: "fileA", 1712 }, 1713 }, 1714 }, 1715 }, 1716 }, 1717 VaultToken: helper.StringToPtr("token"), 1718 Status: helper.StringToPtr("status"), 1719 StatusDescription: helper.StringToPtr("status_desc"), 1720 Version: helper.Uint64ToPtr(10), 1721 CreateIndex: helper.Uint64ToPtr(1), 1722 ModifyIndex: helper.Uint64ToPtr(3), 1723 JobModifyIndex: helper.Uint64ToPtr(5), 1724 } 1725 1726 expected := &structs.Job{ 1727 Stop: true, 1728 Region: "global", 1729 Namespace: "foo", 1730 ID: "foo", 1731 ParentID: "lol", 1732 Name: "name", 1733 Type: "service", 1734 Priority: 50, 1735 AllAtOnce: true, 1736 Datacenters: []string{"dc1", "dc2"}, 1737 Constraints: []*structs.Constraint{ 1738 { 1739 LTarget: "a", 1740 RTarget: "b", 1741 Operand: "c", 1742 }, 1743 }, 1744 Affinities: []*structs.Affinity{ 1745 { 1746 LTarget: "a", 1747 RTarget: "b", 1748 Operand: "c", 1749 Weight: 50, 1750 }, 1751 }, 1752 Spreads: []*structs.Spread{ 1753 { 1754 Attribute: "${meta.rack}", 1755 Weight: 100, 1756 SpreadTarget: []*structs.SpreadTarget{ 1757 { 1758 Value: "r1", 1759 Percent: 50, 1760 }, 1761 }, 1762 }, 1763 }, 1764 Update: structs.UpdateStrategy{ 1765 Stagger: 1 * time.Second, 1766 MaxParallel: 5, 1767 }, 1768 Periodic: &structs.PeriodicConfig{ 1769 Enabled: true, 1770 Spec: "spec", 1771 SpecType: "cron", 1772 ProhibitOverlap: true, 1773 TimeZone: "test zone", 1774 }, 1775 ParameterizedJob: &structs.ParameterizedJobConfig{ 1776 Payload: "payload", 1777 MetaRequired: []string{"a", "b"}, 1778 MetaOptional: []string{"c", "d"}, 1779 }, 1780 Payload: []byte("payload"), 1781 Meta: map[string]string{ 1782 "foo": "bar", 1783 }, 1784 TaskGroups: []*structs.TaskGroup{ 1785 { 1786 Name: "group1", 1787 Count: 5, 1788 Constraints: []*structs.Constraint{ 1789 { 1790 LTarget: "x", 1791 RTarget: "y", 1792 Operand: "z", 1793 }, 1794 }, 1795 Affinities: []*structs.Affinity{ 1796 { 1797 LTarget: "x", 1798 RTarget: "y", 1799 Operand: "z", 1800 Weight: 100, 1801 }, 1802 }, 1803 RestartPolicy: &structs.RestartPolicy{ 1804 Interval: 1 * time.Second, 1805 Attempts: 5, 1806 Delay: 10 * time.Second, 1807 Mode: "delay", 1808 }, 1809 Spreads: []*structs.Spread{ 1810 { 1811 Attribute: "${node.datacenter}", 1812 Weight: 100, 1813 SpreadTarget: []*structs.SpreadTarget{ 1814 { 1815 Value: "dc1", 1816 Percent: 100, 1817 }, 1818 }, 1819 }, 1820 }, 1821 ReschedulePolicy: &structs.ReschedulePolicy{ 1822 Interval: 12 * time.Hour, 1823 Attempts: 5, 1824 DelayFunction: "constant", 1825 Delay: 30 * time.Second, 1826 Unlimited: true, 1827 MaxDelay: 20 * time.Minute, 1828 }, 1829 Migrate: &structs.MigrateStrategy{ 1830 MaxParallel: 12, 1831 HealthCheck: "task_events", 1832 MinHealthyTime: 12 * time.Hour, 1833 HealthyDeadline: 12 * time.Hour, 1834 }, 1835 EphemeralDisk: &structs.EphemeralDisk{ 1836 SizeMB: 100, 1837 Sticky: true, 1838 Migrate: true, 1839 }, 1840 Update: &structs.UpdateStrategy{ 1841 Stagger: 1 * time.Second, 1842 MaxParallel: 5, 1843 HealthCheck: structs.UpdateStrategyHealthCheck_Checks, 1844 MinHealthyTime: 2 * time.Minute, 1845 HealthyDeadline: 5 * time.Minute, 1846 ProgressDeadline: 5 * time.Minute, 1847 AutoRevert: true, 1848 AutoPromote: false, 1849 Canary: 1, 1850 }, 1851 Meta: map[string]string{ 1852 "key": "value", 1853 }, 1854 Services: []*structs.Service{ 1855 { 1856 Name: "groupserviceA", 1857 Tags: []string{"a", "b"}, 1858 CanaryTags: []string{"d", "e"}, 1859 PortLabel: "1234", 1860 AddressMode: "auto", 1861 Meta: map[string]string{ 1862 "servicemeta": "foobar", 1863 }, 1864 Checks: []*structs.ServiceCheck{ 1865 { 1866 Name: "bar", 1867 Type: "http", 1868 Command: "foo", 1869 Args: []string{"a", "b"}, 1870 Path: "/check", 1871 Protocol: "http", 1872 PortLabel: "foo", 1873 AddressMode: "driver", 1874 GRPCService: "foo.Bar", 1875 GRPCUseTLS: true, 1876 Interval: 4 * time.Second, 1877 Timeout: 2 * time.Second, 1878 InitialStatus: "ok", 1879 CheckRestart: &structs.CheckRestart{ 1880 Grace: 11 * time.Second, 1881 Limit: 3, 1882 IgnoreWarnings: true, 1883 }, 1884 TaskName: "task1", 1885 }, 1886 }, 1887 Connect: &structs.ConsulConnect{ 1888 Native: false, 1889 SidecarService: &structs.ConsulSidecarService{ 1890 Tags: []string{"f", "g"}, 1891 Port: "9000", 1892 }, 1893 }, 1894 }, 1895 }, 1896 Tasks: []*structs.Task{ 1897 { 1898 Name: "task1", 1899 Driver: "docker", 1900 Leader: true, 1901 User: "mary", 1902 Config: map[string]interface{}{ 1903 "lol": "code", 1904 }, 1905 Constraints: []*structs.Constraint{ 1906 { 1907 LTarget: "x", 1908 RTarget: "y", 1909 Operand: "z", 1910 }, 1911 }, 1912 Affinities: []*structs.Affinity{ 1913 { 1914 LTarget: "a", 1915 RTarget: "b", 1916 Operand: "c", 1917 Weight: 50, 1918 }, 1919 }, 1920 Env: map[string]string{ 1921 "hello": "world", 1922 }, 1923 Services: []*structs.Service{ 1924 { 1925 Name: "serviceA", 1926 Tags: []string{"1", "2"}, 1927 CanaryTags: []string{"3", "4"}, 1928 PortLabel: "foo", 1929 AddressMode: "auto", 1930 Meta: map[string]string{ 1931 "servicemeta": "foobar", 1932 }, 1933 Checks: []*structs.ServiceCheck{ 1934 { 1935 Name: "bar", 1936 Type: "http", 1937 Command: "foo", 1938 Args: []string{"a", "b"}, 1939 Path: "/check", 1940 Protocol: "http", 1941 PortLabel: "foo", 1942 AddressMode: "driver", 1943 Interval: 4 * time.Second, 1944 Timeout: 2 * time.Second, 1945 InitialStatus: "ok", 1946 GRPCService: "foo.Bar", 1947 GRPCUseTLS: true, 1948 CheckRestart: &structs.CheckRestart{ 1949 Limit: 3, 1950 Grace: 11 * time.Second, 1951 IgnoreWarnings: true, 1952 }, 1953 }, 1954 { 1955 Name: "check2", 1956 Type: "tcp", 1957 PortLabel: "foo", 1958 Interval: 4 * time.Second, 1959 Timeout: 2 * time.Second, 1960 CheckRestart: &structs.CheckRestart{ 1961 Limit: 4, 1962 Grace: 11 * time.Second, 1963 }, 1964 }, 1965 }, 1966 }, 1967 }, 1968 Resources: &structs.Resources{ 1969 CPU: 100, 1970 MemoryMB: 10, 1971 Networks: []*structs.NetworkResource{ 1972 { 1973 IP: "10.10.11.1", 1974 MBits: 10, 1975 ReservedPorts: []structs.Port{ 1976 { 1977 Label: "http", 1978 Value: 80, 1979 }, 1980 }, 1981 DynamicPorts: []structs.Port{ 1982 { 1983 Label: "ssh", 1984 Value: 2000, 1985 }, 1986 }, 1987 }, 1988 }, 1989 Devices: []*structs.RequestedDevice{ 1990 { 1991 Name: "nvidia/gpu", 1992 Count: 4, 1993 Constraints: []*structs.Constraint{ 1994 { 1995 LTarget: "x", 1996 RTarget: "y", 1997 Operand: "z", 1998 }, 1999 }, 2000 Affinities: []*structs.Affinity{ 2001 { 2002 LTarget: "a", 2003 RTarget: "b", 2004 Operand: "c", 2005 Weight: 50, 2006 }, 2007 }, 2008 }, 2009 { 2010 Name: "gpu", 2011 Count: 1, 2012 }, 2013 }, 2014 }, 2015 Meta: map[string]string{ 2016 "lol": "code", 2017 }, 2018 KillTimeout: 10 * time.Second, 2019 KillSignal: "SIGQUIT", 2020 LogConfig: &structs.LogConfig{ 2021 MaxFiles: 10, 2022 MaxFileSizeMB: 100, 2023 }, 2024 Artifacts: []*structs.TaskArtifact{ 2025 { 2026 GetterSource: "source", 2027 GetterOptions: map[string]string{ 2028 "a": "b", 2029 }, 2030 GetterMode: "dir", 2031 RelativeDest: "dest", 2032 }, 2033 }, 2034 Vault: &structs.Vault{ 2035 Policies: []string{"a", "b", "c"}, 2036 Env: true, 2037 ChangeMode: "c", 2038 ChangeSignal: "sighup", 2039 }, 2040 Templates: []*structs.Template{ 2041 { 2042 SourcePath: "source", 2043 DestPath: "dest", 2044 EmbeddedTmpl: "embedded", 2045 ChangeMode: "change", 2046 ChangeSignal: "SIGNAL", 2047 Splay: 1 * time.Minute, 2048 Perms: "666", 2049 LeftDelim: "abc", 2050 RightDelim: "def", 2051 Envvars: true, 2052 VaultGrace: 3 * time.Second, 2053 }, 2054 }, 2055 DispatchPayload: &structs.DispatchPayloadConfig{ 2056 File: "fileA", 2057 }, 2058 }, 2059 }, 2060 }, 2061 }, 2062 2063 VaultToken: "token", 2064 } 2065 2066 structsJob := ApiJobToStructJob(apiJob) 2067 2068 if diff := pretty.Diff(expected, structsJob); len(diff) > 0 { 2069 t.Fatalf("bad:\n%s", strings.Join(diff, "\n")) 2070 } 2071 2072 systemAPIJob := &api.Job{ 2073 Stop: helper.BoolToPtr(true), 2074 Region: helper.StringToPtr("global"), 2075 Namespace: helper.StringToPtr("foo"), 2076 ID: helper.StringToPtr("foo"), 2077 ParentID: helper.StringToPtr("lol"), 2078 Name: helper.StringToPtr("name"), 2079 Type: helper.StringToPtr("system"), 2080 Priority: helper.IntToPtr(50), 2081 AllAtOnce: helper.BoolToPtr(true), 2082 Datacenters: []string{"dc1", "dc2"}, 2083 Constraints: []*api.Constraint{ 2084 { 2085 LTarget: "a", 2086 RTarget: "b", 2087 Operand: "c", 2088 }, 2089 }, 2090 TaskGroups: []*api.TaskGroup{ 2091 { 2092 Name: helper.StringToPtr("group1"), 2093 Count: helper.IntToPtr(5), 2094 Constraints: []*api.Constraint{ 2095 { 2096 LTarget: "x", 2097 RTarget: "y", 2098 Operand: "z", 2099 }, 2100 }, 2101 RestartPolicy: &api.RestartPolicy{ 2102 Interval: helper.TimeToPtr(1 * time.Second), 2103 Attempts: helper.IntToPtr(5), 2104 Delay: helper.TimeToPtr(10 * time.Second), 2105 Mode: helper.StringToPtr("delay"), 2106 }, 2107 EphemeralDisk: &api.EphemeralDisk{ 2108 SizeMB: helper.IntToPtr(100), 2109 Sticky: helper.BoolToPtr(true), 2110 Migrate: helper.BoolToPtr(true), 2111 }, 2112 Meta: map[string]string{ 2113 "key": "value", 2114 }, 2115 Tasks: []*api.Task{ 2116 { 2117 Name: "task1", 2118 Leader: true, 2119 Driver: "docker", 2120 User: "mary", 2121 Config: map[string]interface{}{ 2122 "lol": "code", 2123 }, 2124 Env: map[string]string{ 2125 "hello": "world", 2126 }, 2127 Constraints: []*api.Constraint{ 2128 { 2129 LTarget: "x", 2130 RTarget: "y", 2131 Operand: "z", 2132 }, 2133 }, 2134 Resources: &api.Resources{ 2135 CPU: helper.IntToPtr(100), 2136 MemoryMB: helper.IntToPtr(10), 2137 Networks: []*api.NetworkResource{ 2138 { 2139 IP: "10.10.11.1", 2140 MBits: helper.IntToPtr(10), 2141 ReservedPorts: []api.Port{ 2142 { 2143 Label: "http", 2144 Value: 80, 2145 }, 2146 }, 2147 DynamicPorts: []api.Port{ 2148 { 2149 Label: "ssh", 2150 Value: 2000, 2151 }, 2152 }, 2153 }, 2154 }, 2155 }, 2156 Meta: map[string]string{ 2157 "lol": "code", 2158 }, 2159 KillTimeout: helper.TimeToPtr(10 * time.Second), 2160 KillSignal: "SIGQUIT", 2161 LogConfig: &api.LogConfig{ 2162 MaxFiles: helper.IntToPtr(10), 2163 MaxFileSizeMB: helper.IntToPtr(100), 2164 }, 2165 Artifacts: []*api.TaskArtifact{ 2166 { 2167 GetterSource: helper.StringToPtr("source"), 2168 GetterOptions: map[string]string{ 2169 "a": "b", 2170 }, 2171 GetterMode: helper.StringToPtr("dir"), 2172 RelativeDest: helper.StringToPtr("dest"), 2173 }, 2174 }, 2175 DispatchPayload: &api.DispatchPayloadConfig{ 2176 File: "fileA", 2177 }, 2178 }, 2179 }, 2180 }, 2181 }, 2182 Status: helper.StringToPtr("status"), 2183 StatusDescription: helper.StringToPtr("status_desc"), 2184 Version: helper.Uint64ToPtr(10), 2185 CreateIndex: helper.Uint64ToPtr(1), 2186 ModifyIndex: helper.Uint64ToPtr(3), 2187 JobModifyIndex: helper.Uint64ToPtr(5), 2188 } 2189 2190 expectedSystemJob := &structs.Job{ 2191 Stop: true, 2192 Region: "global", 2193 Namespace: "foo", 2194 ID: "foo", 2195 ParentID: "lol", 2196 Name: "name", 2197 Type: "system", 2198 Priority: 50, 2199 AllAtOnce: true, 2200 Datacenters: []string{"dc1", "dc2"}, 2201 Constraints: []*structs.Constraint{ 2202 { 2203 LTarget: "a", 2204 RTarget: "b", 2205 Operand: "c", 2206 }, 2207 }, 2208 TaskGroups: []*structs.TaskGroup{ 2209 { 2210 Name: "group1", 2211 Count: 5, 2212 Constraints: []*structs.Constraint{ 2213 { 2214 LTarget: "x", 2215 RTarget: "y", 2216 Operand: "z", 2217 }, 2218 }, 2219 RestartPolicy: &structs.RestartPolicy{ 2220 Interval: 1 * time.Second, 2221 Attempts: 5, 2222 Delay: 10 * time.Second, 2223 Mode: "delay", 2224 }, 2225 EphemeralDisk: &structs.EphemeralDisk{ 2226 SizeMB: 100, 2227 Sticky: true, 2228 Migrate: true, 2229 }, 2230 Meta: map[string]string{ 2231 "key": "value", 2232 }, 2233 Tasks: []*structs.Task{ 2234 { 2235 Name: "task1", 2236 Driver: "docker", 2237 Leader: true, 2238 User: "mary", 2239 Config: map[string]interface{}{ 2240 "lol": "code", 2241 }, 2242 Constraints: []*structs.Constraint{ 2243 { 2244 LTarget: "x", 2245 RTarget: "y", 2246 Operand: "z", 2247 }, 2248 }, 2249 Env: map[string]string{ 2250 "hello": "world", 2251 }, 2252 Resources: &structs.Resources{ 2253 CPU: 100, 2254 MemoryMB: 10, 2255 Networks: []*structs.NetworkResource{ 2256 { 2257 IP: "10.10.11.1", 2258 MBits: 10, 2259 ReservedPorts: []structs.Port{ 2260 { 2261 Label: "http", 2262 Value: 80, 2263 }, 2264 }, 2265 DynamicPorts: []structs.Port{ 2266 { 2267 Label: "ssh", 2268 Value: 2000, 2269 }, 2270 }, 2271 }, 2272 }, 2273 }, 2274 Meta: map[string]string{ 2275 "lol": "code", 2276 }, 2277 KillTimeout: 10 * time.Second, 2278 KillSignal: "SIGQUIT", 2279 LogConfig: &structs.LogConfig{ 2280 MaxFiles: 10, 2281 MaxFileSizeMB: 100, 2282 }, 2283 Artifacts: []*structs.TaskArtifact{ 2284 { 2285 GetterSource: "source", 2286 GetterOptions: map[string]string{ 2287 "a": "b", 2288 }, 2289 GetterMode: "dir", 2290 RelativeDest: "dest", 2291 }, 2292 }, 2293 DispatchPayload: &structs.DispatchPayloadConfig{ 2294 File: "fileA", 2295 }, 2296 }, 2297 }, 2298 }, 2299 }, 2300 } 2301 2302 systemStructsJob := ApiJobToStructJob(systemAPIJob) 2303 2304 if diff := pretty.Diff(expectedSystemJob, systemStructsJob); len(diff) > 0 { 2305 t.Fatalf("bad:\n%s", strings.Join(diff, "\n")) 2306 } 2307 } 2308 2309 func TestJobs_ApiJobToStructsJobUpdate(t *testing.T) { 2310 apiJob := &api.Job{ 2311 Update: &api.UpdateStrategy{ 2312 Stagger: helper.TimeToPtr(1 * time.Second), 2313 MaxParallel: helper.IntToPtr(5), 2314 HealthCheck: helper.StringToPtr(structs.UpdateStrategyHealthCheck_Manual), 2315 MinHealthyTime: helper.TimeToPtr(1 * time.Minute), 2316 HealthyDeadline: helper.TimeToPtr(3 * time.Minute), 2317 ProgressDeadline: helper.TimeToPtr(3 * time.Minute), 2318 AutoRevert: helper.BoolToPtr(false), 2319 AutoPromote: nil, 2320 Canary: helper.IntToPtr(1), 2321 }, 2322 TaskGroups: []*api.TaskGroup{ 2323 { 2324 Update: &api.UpdateStrategy{ 2325 Canary: helper.IntToPtr(2), 2326 AutoRevert: helper.BoolToPtr(true), 2327 }, 2328 }, { 2329 Update: &api.UpdateStrategy{ 2330 Canary: helper.IntToPtr(3), 2331 AutoPromote: helper.BoolToPtr(true), 2332 }, 2333 }, 2334 }, 2335 } 2336 2337 structsJob := ApiJobToStructJob(apiJob) 2338 2339 // Update has been moved from job down to the groups 2340 jobUpdate := structs.UpdateStrategy{ 2341 Stagger: 1000000000, 2342 MaxParallel: 5, 2343 HealthCheck: "", 2344 MinHealthyTime: 0, 2345 HealthyDeadline: 0, 2346 ProgressDeadline: 0, 2347 AutoRevert: false, 2348 AutoPromote: false, 2349 Canary: 0, 2350 } 2351 2352 // But the groups inherit settings from the job update 2353 group1 := structs.UpdateStrategy{ 2354 Stagger: 1000000000, 2355 MaxParallel: 5, 2356 HealthCheck: "manual", 2357 MinHealthyTime: 60000000000, 2358 HealthyDeadline: 180000000000, 2359 ProgressDeadline: 180000000000, 2360 AutoRevert: true, 2361 AutoPromote: false, 2362 Canary: 2, 2363 } 2364 2365 group2 := structs.UpdateStrategy{ 2366 Stagger: 1000000000, 2367 MaxParallel: 5, 2368 HealthCheck: "manual", 2369 MinHealthyTime: 60000000000, 2370 HealthyDeadline: 180000000000, 2371 ProgressDeadline: 180000000000, 2372 AutoRevert: false, 2373 AutoPromote: true, 2374 Canary: 3, 2375 } 2376 2377 require.Equal(t, jobUpdate, structsJob.Update) 2378 require.Equal(t, group1, *structsJob.TaskGroups[0].Update) 2379 require.Equal(t, group2, *structsJob.TaskGroups[1].Update) 2380 } 2381 2382 // TestHTTP_JobValidate_SystemMigrate asserts that a system job with a migrate 2383 // stanza fails to validate but does not panic (see #5477). 2384 func TestHTTP_JobValidate_SystemMigrate(t *testing.T) { 2385 t.Parallel() 2386 httpTest(t, nil, func(s *TestAgent) { 2387 // Create the job 2388 job := &api.Job{ 2389 Region: helper.StringToPtr("global"), 2390 Datacenters: []string{"dc1"}, 2391 ID: helper.StringToPtr("systemmigrate"), 2392 Name: helper.StringToPtr("systemmigrate"), 2393 TaskGroups: []*api.TaskGroup{ 2394 {Name: helper.StringToPtr("web")}, 2395 }, 2396 2397 // System job... 2398 Type: helper.StringToPtr("system"), 2399 2400 // ...with an empty migrate stanza 2401 Migrate: &api.MigrateStrategy{}, 2402 } 2403 2404 args := api.JobValidateRequest{ 2405 Job: job, 2406 WriteRequest: api.WriteRequest{Region: "global"}, 2407 } 2408 buf := encodeReq(args) 2409 2410 // Make the HTTP request 2411 req, err := http.NewRequest("PUT", "/v1/validate/job", buf) 2412 require.NoError(t, err) 2413 respW := httptest.NewRecorder() 2414 2415 // Make the request 2416 obj, err := s.Server.ValidateJobRequest(respW, req) 2417 require.NoError(t, err) 2418 2419 // Check the response 2420 resp := obj.(structs.JobValidateResponse) 2421 require.Contains(t, resp.Error, `Job type "system" does not allow migrate block`) 2422 }) 2423 }