package agent import ( "fmt" "net/http" "net/http/httptest" "reflect" "strconv" "testing" "time" "github.com/golang/snappy" "github.com/hashicorp/nomad/acl" api "github.com/hashicorp/nomad/api" "github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/helper/pointer" "github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/structs" "github.com/shoenig/test/must" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestHTTP_JobsList(t *testing.T) { ci.Parallel(t) httpTest(t, nil, func(s *TestAgent) { for i := 0; i < 3; i++ { // Create the job job := mock.Job() args := structs.JobRegisterRequest{ Job: job, WriteRequest: structs.WriteRequest{ Region: "global", Namespace: structs.DefaultNamespace, }, } var resp structs.JobRegisterResponse if err := s.Agent.RPC("Job.Register", &args, &resp); err != nil { t.Fatalf("err: %v", err) } } // Make the HTTP request req, err := http.NewRequest("GET", "/v1/jobs", nil) if err != nil { t.Fatalf("err: %v", err) } respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobsRequest(respW, req) if err != nil { t.Fatalf("err: %v", err) } // Check for the index if respW.Result().Header.Get("X-Nomad-Index") == "" { t.Fatalf("missing index") } if respW.Result().Header.Get("X-Nomad-KnownLeader") != "true" { t.Fatalf("missing known leader") } if respW.Result().Header.Get("X-Nomad-LastContact") == "" { t.Fatalf("missing last contact") } // Check the job j := obj.([]*structs.JobListStub) if len(j) != 3 { t.Fatalf("bad: %#v", j) } }) } func TestHTTP_PrefixJobsList(t *testing.T) { ci.Parallel(t) ids := []string{ "aaaaaaaa-e8f7-fd38-c855-ab94ceb89706", "aabbbbbb-e8f7-fd38-c855-ab94ceb89706", "aabbcccc-e8f7-fd38-c855-ab94ceb89706", } httpTest(t, nil, func(s *TestAgent) { for i := 0; i < 3; i++ { // Create the job job := mock.Job() job.ID = ids[i] job.TaskGroups[0].Count = 1 args := structs.JobRegisterRequest{ Job: job, WriteRequest: structs.WriteRequest{ Region: "global", Namespace: structs.DefaultNamespace, }, } var resp structs.JobRegisterResponse if err := s.Agent.RPC("Job.Register", &args, &resp); err != nil { t.Fatalf("err: %v", err) } } // Make the HTTP request req, err := http.NewRequest("GET", "/v1/jobs?prefix=aabb", nil) if err != nil { t.Fatalf("err: %v", err) } respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobsRequest(respW, req) if err != nil { t.Fatalf("err: %v", err) } // Check for the index if respW.Result().Header.Get("X-Nomad-Index") == "" { t.Fatalf("missing index") } if respW.Result().Header.Get("X-Nomad-KnownLeader") != "true" { t.Fatalf("missing known leader") } if respW.Result().Header.Get("X-Nomad-LastContact") == "" { t.Fatalf("missing last contact") } // Check the job j := obj.([]*structs.JobListStub) if len(j) != 2 { t.Fatalf("bad: %#v", j) } }) } func TestHTTP_JobsList_AllNamespaces_OSS(t *testing.T) { ci.Parallel(t) httpTest(t, nil, func(s *TestAgent) { for i := 0; i < 3; i++ { // Create the job job := mock.Job() args := structs.JobRegisterRequest{ Job: job, WriteRequest: structs.WriteRequest{ Region: "global", Namespace: structs.DefaultNamespace, }, } var resp structs.JobRegisterResponse err := s.Agent.RPC("Job.Register", &args, &resp) require.NoError(t, err) } // Make the HTTP request req, err := http.NewRequest("GET", "/v1/jobs?namespace=*", nil) require.NoError(t, err) respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobsRequest(respW, req) require.NoError(t, err) // Check for the index require.NotEmpty(t, respW.Result().Header.Get("X-Nomad-Index"), "missing index") require.Equal(t, "true", respW.Result().Header.Get("X-Nomad-KnownLeader"), "missing known leader") require.NotEmpty(t, respW.Result().Header.Get("X-Nomad-LastContact"), "missing last contact") // Check the job j := obj.([]*structs.JobListStub) require.Len(t, j, 3) require.Equal(t, "default", j[0].Namespace) }) } func TestHTTP_JobsRegister(t *testing.T) { ci.Parallel(t) httpTest(t, nil, func(s *TestAgent) { // Create the job job := MockJob() args := api.JobRegisterRequest{ Job: job, WriteRequest: api.WriteRequest{Region: "global"}, } buf := encodeReq(args) // Make the HTTP request req, err := http.NewRequest("PUT", "/v1/jobs", buf) if err != nil { t.Fatalf("err: %v", err) } respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobsRequest(respW, req) if err != nil { t.Fatalf("err: %v", err) } // Check the response dereg := obj.(structs.JobRegisterResponse) if dereg.EvalID == "" { t.Fatalf("bad: %v", dereg) } // Check for the index if respW.Result().Header.Get("X-Nomad-Index") == "" { t.Fatalf("missing index") } // Check the job is registered getReq := structs.JobSpecificRequest{ JobID: *job.ID, QueryOptions: structs.QueryOptions{ Region: "global", Namespace: structs.DefaultNamespace, }, } var getResp structs.SingleJobResponse if err := s.Agent.RPC("Job.GetJob", &getReq, &getResp); err != nil { t.Fatalf("err: %v", err) } if getResp.Job == nil { t.Fatalf("job does not exist") } }) } func TestHTTP_JobsRegister_IgnoresParentID(t *testing.T) { ci.Parallel(t) httpTest(t, nil, func(s *TestAgent) { // Create the job job := MockJob() parentID := "somebadparentid" job.ParentID = &parentID args := api.JobRegisterRequest{ Job: job, WriteRequest: api.WriteRequest{Region: "global"}, } buf := encodeReq(args) // Make the HTTP request req, err := http.NewRequest("PUT", "/v1/jobs", buf) require.NoError(t, err) respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobsRequest(respW, req) require.NoError(t, err) // Check the response reg := obj.(structs.JobRegisterResponse) require.NotEmpty(t, reg.EvalID) // Check for the index require.NotEmpty(t, respW.Result().Header.Get("X-Nomad-Index")) // Check the job is registered getReq := structs.JobSpecificRequest{ JobID: *job.ID, QueryOptions: structs.QueryOptions{ Region: "global", Namespace: structs.DefaultNamespace, }, } var getResp structs.SingleJobResponse err = s.Agent.RPC("Job.GetJob", &getReq, &getResp) require.NoError(t, err) require.NotNil(t, getResp.Job) require.Equal(t, *job.ID, getResp.Job.ID) require.Empty(t, getResp.Job.ParentID) // check the eval exists evalReq := structs.EvalSpecificRequest{ EvalID: reg.EvalID, QueryOptions: structs.QueryOptions{ Region: "global", Namespace: structs.DefaultNamespace, }, } var evalResp structs.SingleEvalResponse err = s.Agent.RPC("Eval.GetEval", &evalReq, &evalResp) require.NoError(t, err) require.NotNil(t, evalResp.Eval) require.Equal(t, reg.EvalID, evalResp.Eval.ID) }) } // Test that ACL token is properly threaded through to the RPC endpoint func TestHTTP_JobsRegister_ACL(t *testing.T) { ci.Parallel(t) httpACLTest(t, nil, func(s *TestAgent) { // Create the job job := MockJob() args := api.JobRegisterRequest{ Job: job, WriteRequest: api.WriteRequest{ Region: "global", }, } buf := encodeReq(args) // Make the HTTP request req, err := http.NewRequest("PUT", "/v1/jobs", buf) if err != nil { t.Fatalf("err: %v", err) } respW := httptest.NewRecorder() setToken(req, s.RootToken) // Make the request obj, err := s.Server.JobsRequest(respW, req) if err != nil { t.Fatalf("err: %v", err) } assert.NotNil(t, obj) }) } func TestHTTP_JobsRegister_Defaulting(t *testing.T) { ci.Parallel(t) httpTest(t, nil, func(s *TestAgent) { // Create the job job := MockJob() // Do not set its priority job.Priority = nil args := api.JobRegisterRequest{ Job: job, WriteRequest: api.WriteRequest{Region: "global"}, } buf := encodeReq(args) // Make the HTTP request req, err := http.NewRequest("PUT", "/v1/jobs", buf) if err != nil { t.Fatalf("err: %v", err) } respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobsRequest(respW, req) if err != nil { t.Fatalf("err: %v", err) } // Check the response dereg := obj.(structs.JobRegisterResponse) if dereg.EvalID == "" { t.Fatalf("bad: %v", dereg) } // Check for the index if respW.Result().Header.Get("X-Nomad-Index") == "" { t.Fatalf("missing index") } // Check the job is registered getReq := structs.JobSpecificRequest{ JobID: *job.ID, QueryOptions: structs.QueryOptions{ Region: "global", Namespace: structs.DefaultNamespace, }, } var getResp structs.SingleJobResponse if err := s.Agent.RPC("Job.GetJob", &getReq, &getResp); err != nil { t.Fatalf("err: %v", err) } if getResp.Job == nil { t.Fatalf("job does not exist") } if getResp.Job.Priority != 50 { t.Fatalf("job didn't get defaulted") } }) } func TestHTTP_JobsParse(t *testing.T) { ci.Parallel(t) httpTest(t, nil, func(s *TestAgent) { buf := encodeReq(api.JobsParseRequest{JobHCL: mock.HCL()}) req, err := http.NewRequest("POST", "/v1/jobs/parse", buf) if err != nil { t.Fatalf("err: %v", err) } respW := httptest.NewRecorder() obj, err := s.Server.JobsParseRequest(respW, req) if err != nil { t.Fatalf("err: %v", err) } if obj == nil { t.Fatal("response should not be nil") } job := obj.(*api.Job) expected := mock.Job() if job.Name == nil || *job.Name != expected.Name { t.Fatalf("job name is '%s', expected '%s'", *job.Name, expected.Name) } if job.Datacenters == nil || job.Datacenters[0] != expected.Datacenters[0] { t.Fatalf("job datacenters is '%s', expected '%s'", job.Datacenters[0], expected.Datacenters[0]) } }) } func TestHTTP_JobsParse_ACL(t *testing.T) { ci.Parallel(t) httpACLTest(t, nil, func(s *TestAgent) { state := s.Agent.server.State() // ACL tokens used in tests. nodeToken := mock.CreatePolicyAndToken( t, state, 1000, "node", mock.NodePolicy(acl.PolicyWrite), ) parseJobDevToken := mock.CreatePolicyAndToken( t, state, 1002, "parse-job-dev", mock.NamespacePolicy("dev", "", []string{"parse-job"}), ) readNsDevToken := mock.CreatePolicyAndToken( t, state, 1004, "read-dev", mock.NamespacePolicy("dev", "read", nil), ) parseJobDefaultToken := mock.CreatePolicyAndToken( t, state, 1006, "parse-job-default", mock.NamespacePolicy("default", "", []string{"parse-job"}), ) submitJobDefaultToken := mock.CreatePolicyAndToken( t, state, 1008, "submit-job-default", mock.NamespacePolicy("default", "", []string{"submit-job"}), ) readNsDefaultToken := mock.CreatePolicyAndToken( t, state, 1010, "read-default", mock.NamespacePolicy("default", "read", nil), ) testCases := []struct { name string token *structs.ACLToken namespace string expectError bool }{ { name: "missing ACL token", token: nil, expectError: true, }, { name: "wrong permissions", token: nodeToken, expectError: true, }, { name: "wrong namespace", token: readNsDevToken, expectError: true, }, { name: "wrong namespace capability", token: parseJobDevToken, expectError: true, }, { name: "default namespace read", token: readNsDefaultToken, expectError: false, }, { name: "non-default namespace read", token: readNsDevToken, namespace: "dev", expectError: false, }, { name: "default namespace parse-job capability", token: parseJobDefaultToken, expectError: false, }, { name: "default namespace submit-job capability", token: submitJobDefaultToken, expectError: false, }, { name: "non-default namespace capability", token: parseJobDevToken, namespace: "dev", expectError: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { buf := encodeReq(api.JobsParseRequest{JobHCL: mock.HCL()}) req, err := http.NewRequest("POST", "/v1/jobs/parse", buf) require.NoError(t, err) if tc.namespace != "" { setNamespace(req, tc.namespace) } if tc.token != nil { setToken(req, tc.token) } respW := httptest.NewRecorder() obj, err := s.Server.JobsParseRequest(respW, req) if tc.expectError { require.Error(t, err) require.Equal(t, structs.ErrPermissionDenied.Error(), err.Error()) } else { require.NoError(t, err) require.NotNil(t, obj) job := obj.(*api.Job) expected := mock.Job() require.Equal(t, expected.Name, *job.Name) require.ElementsMatch(t, expected.Datacenters, job.Datacenters) } }) } }) } func TestHTTP_JobQuery(t *testing.T) { ci.Parallel(t) httpTest(t, nil, func(s *TestAgent) { // Create the job job := mock.Job() args := structs.JobRegisterRequest{ Job: job, WriteRequest: structs.WriteRequest{ Region: "global", Namespace: structs.DefaultNamespace, }, } var resp structs.JobRegisterResponse if err := s.Agent.RPC("Job.Register", &args, &resp); err != nil { t.Fatalf("err: %v", err) } // Make the HTTP request req, err := http.NewRequest("GET", "/v1/job/"+job.ID, nil) if err != nil { t.Fatalf("err: %v", err) } respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobSpecificRequest(respW, req) if err != nil { t.Fatalf("err: %v", err) } // Check for the index if respW.Result().Header.Get("X-Nomad-Index") == "" { t.Fatalf("missing index") } if respW.Result().Header.Get("X-Nomad-KnownLeader") != "true" { t.Fatalf("missing known leader") } if respW.Result().Header.Get("X-Nomad-LastContact") == "" { t.Fatalf("missing last contact") } // Check the job j := obj.(*structs.Job) if j.ID != job.ID { t.Fatalf("bad: %#v", j) } }) } func TestHTTP_JobQuery_Payload(t *testing.T) { ci.Parallel(t) httpTest(t, nil, func(s *TestAgent) { // Create the job job := mock.Job() // Insert Payload compressed expected := []byte("hello world") compressed := snappy.Encode(nil, expected) job.Payload = compressed // Directly manipulate the state state := s.Agent.server.State() if err := state.UpsertJob(structs.MsgTypeTestSetup, 1000, job); err != nil { t.Fatalf("Failed to upsert job: %v", err) } // Make the HTTP request req, err := http.NewRequest("GET", "/v1/job/"+job.ID, nil) if err != nil { t.Fatalf("err: %v", err) } respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobSpecificRequest(respW, req) if err != nil { t.Fatalf("err: %v", err) } // Check for the index if respW.Result().Header.Get("X-Nomad-Index") == "" { t.Fatalf("missing index") } if respW.Result().Header.Get("X-Nomad-KnownLeader") != "true" { t.Fatalf("missing known leader") } if respW.Result().Header.Get("X-Nomad-LastContact") == "" { t.Fatalf("missing last contact") } // Check the job j := obj.(*structs.Job) if j.ID != job.ID { t.Fatalf("bad: %#v", j) } // Check the payload is decompressed if !reflect.DeepEqual(j.Payload, expected) { t.Fatalf("Payload not decompressed properly; got %#v; want %#v", j.Payload, expected) } }) } func TestHTTP_jobUpdate_systemScaling(t *testing.T) { ci.Parallel(t) httpTest(t, nil, func(s *TestAgent) { // Create the job job := MockJob() job.Type = pointer.Of("system") job.TaskGroups[0].Scaling = &api.ScalingPolicy{Enabled: pointer.Of(true)} args := api.JobRegisterRequest{ Job: job, WriteRequest: api.WriteRequest{ Region: "global", Namespace: api.DefaultNamespace, }, } buf := encodeReq(args) // Make the HTTP request req, err := http.NewRequest("PUT", "/v1/job/"+*job.ID, buf) if err != nil { t.Fatalf("err: %v", err) } respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobSpecificRequest(respW, req) assert.Nil(t, obj) assert.Equal(t, CodedError(400, "Task groups with job type system do not support scaling blocks"), err) }) } func TestHTTP_JobUpdate(t *testing.T) { ci.Parallel(t) httpTest(t, nil, func(s *TestAgent) { // Create the job job := MockJob() args := api.JobRegisterRequest{ Job: job, WriteRequest: api.WriteRequest{ Region: "global", Namespace: api.DefaultNamespace, }, } buf := encodeReq(args) // Make the HTTP request req, err := http.NewRequest("PUT", "/v1/job/"+*job.ID, buf) if err != nil { t.Fatalf("err: %v", err) } respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobSpecificRequest(respW, req) if err != nil { t.Fatalf("err: %v", err) } // Check the response dereg := obj.(structs.JobRegisterResponse) if dereg.EvalID == "" { t.Fatalf("bad: %v", dereg) } // Check for the index if respW.Result().Header.Get("X-Nomad-Index") == "" { t.Fatalf("missing index") } // Check the job is registered getReq := structs.JobSpecificRequest{ JobID: *job.ID, QueryOptions: structs.QueryOptions{ Region: "global", Namespace: structs.DefaultNamespace, }, } var getResp structs.SingleJobResponse if err := s.Agent.RPC("Job.GetJob", &getReq, &getResp); err != nil { t.Fatalf("err: %v", err) } if getResp.Job == nil { t.Fatalf("job does not exist") } }) } func TestHTTP_JobUpdate_EvalPriority(t *testing.T) { ci.Parallel(t) testCases := []struct { inputEvalPriority int expectedError bool name string }{ { inputEvalPriority: 95, expectedError: false, name: "valid input eval priority", }, { inputEvalPriority: 99999999999, expectedError: true, name: "invalid input eval priority", }, { inputEvalPriority: 0, expectedError: false, name: "no input eval priority", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { httpTest(t, nil, func(s *TestAgent) { // Create the job job := MockJob() args := api.JobRegisterRequest{ Job: job, WriteRequest: api.WriteRequest{ Region: "global", Namespace: api.DefaultNamespace, }, } // Add our eval priority query param if set. if tc.inputEvalPriority > 0 { args.EvalPriority = tc.inputEvalPriority } buf := encodeReq(args) // Make the HTTP request req, err := http.NewRequest("PUT", "/v1/job/"+*job.ID, buf) assert.Nil(t, err) respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobSpecificRequest(respW, req) if tc.expectedError { assert.NotNil(t, err) return } else { assert.Nil(t, err) } // Check the response regResp := obj.(structs.JobRegisterResponse) assert.NotEmpty(t, regResp.EvalID) assert.NotEmpty(t, respW.Result().Header.Get("X-Nomad-Index")) // Check the job is registered getReq := structs.JobSpecificRequest{ JobID: *job.ID, QueryOptions: structs.QueryOptions{ Region: "global", Namespace: structs.DefaultNamespace, }, } var getResp structs.SingleJobResponse assert.Nil(t, s.Agent.RPC("Job.GetJob", &getReq, &getResp)) assert.NotNil(t, getResp.Job) // Check the evaluation that resulted from the job register. evalInfoReq, err := http.NewRequest("GET", "/v1/evaluation/"+regResp.EvalID, nil) assert.Nil(t, err) respW.Flush() evalRaw, err := s.Server.EvalSpecificRequest(respW, evalInfoReq) assert.Nil(t, err) evalRespObj := evalRaw.(*structs.Evaluation) if tc.inputEvalPriority > 0 { assert.Equal(t, tc.inputEvalPriority, evalRespObj.Priority) } else { assert.Equal(t, *job.Priority, evalRespObj.Priority) } }) }) } } func TestHTTP_JobUpdateRegion(t *testing.T) { ci.Parallel(t) cases := []struct { Name string ConfigRegion string APIRegion string ExpectedRegion string }{ { Name: "api region takes precedence", ConfigRegion: "not-global", APIRegion: "north-america", ExpectedRegion: "north-america", }, { Name: "config region is set", ConfigRegion: "north-america", APIRegion: "", ExpectedRegion: "north-america", }, { Name: "api region is set", ConfigRegion: "", APIRegion: "north-america", ExpectedRegion: "north-america", }, { Name: "defaults to node region global if no region is provided", ConfigRegion: "", APIRegion: "", ExpectedRegion: "global", }, { Name: "defaults to node region not-global if no region is provided", ConfigRegion: "", APIRegion: "", ExpectedRegion: "not-global", }, } for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { httpTest(t, func(c *Config) { c.Region = tc.ExpectedRegion }, func(s *TestAgent) { // Create the job job := MockRegionalJob() if tc.ConfigRegion == "" { job.Region = nil } else { job.Region = &tc.ConfigRegion } args := api.JobRegisterRequest{ Job: job, WriteRequest: api.WriteRequest{ Namespace: api.DefaultNamespace, Region: tc.APIRegion, }, } buf := encodeReq(args) // Make the HTTP request url := "/v1/job/" + *job.ID req, err := http.NewRequest("PUT", url, buf) require.NoError(t, err) respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobSpecificRequest(respW, req) require.NoError(t, err) // Check the response dereg := obj.(structs.JobRegisterResponse) require.NotEmpty(t, dereg.EvalID) // Check for the index require.NotEmpty(t, respW.Result().Header.Get("X-Nomad-Index"), "missing index") // Check the job is registered getReq := structs.JobSpecificRequest{ JobID: *job.ID, QueryOptions: structs.QueryOptions{ Region: tc.ExpectedRegion, Namespace: structs.DefaultNamespace, }, } var getResp structs.SingleJobResponse err = s.Agent.RPC("Job.GetJob", &getReq, &getResp) require.NoError(t, err) require.NotNil(t, getResp.Job, "job does not exist") require.Equal(t, tc.ExpectedRegion, getResp.Job.Region) }) }) } } func TestHTTP_JobDelete(t *testing.T) { ci.Parallel(t) httpTest(t, nil, func(s *TestAgent) { // Create the job job := mock.Job() args := structs.JobRegisterRequest{ Job: job, WriteRequest: structs.WriteRequest{ Region: "global", Namespace: structs.DefaultNamespace, }, } var resp structs.JobRegisterResponse if err := s.Agent.RPC("Job.Register", &args, &resp); err != nil { t.Fatalf("err: %v", err) } // Make the HTTP request to do a soft delete req, err := http.NewRequest("DELETE", "/v1/job/"+job.ID, nil) if err != nil { t.Fatalf("err: %v", err) } respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobSpecificRequest(respW, req) if err != nil { t.Fatalf("err: %v", err) } // Check the response dereg := obj.(structs.JobDeregisterResponse) if dereg.EvalID == "" { t.Fatalf("bad: %v", dereg) } // Check for the index if respW.Result().Header.Get("X-Nomad-Index") == "" { t.Fatalf("missing index") } // Check the job is still queryable getReq1 := structs.JobSpecificRequest{ JobID: job.ID, QueryOptions: structs.QueryOptions{ Region: "global", Namespace: structs.DefaultNamespace, }, } var getResp1 structs.SingleJobResponse if err := s.Agent.RPC("Job.GetJob", &getReq1, &getResp1); err != nil { t.Fatalf("err: %v", err) } if getResp1.Job == nil { t.Fatalf("job doesn't exists") } if !getResp1.Job.Stop { t.Fatalf("job should be marked as stop") } // Make the HTTP request to do a purge delete req2, err := http.NewRequest("DELETE", "/v1/job/"+job.ID+"?purge=true", nil) if err != nil { t.Fatalf("err: %v", err) } respW.Flush() // Make the request obj, err = s.Server.JobSpecificRequest(respW, req2) if err != nil { t.Fatalf("err: %v", err) } // Check the response dereg = obj.(structs.JobDeregisterResponse) if dereg.EvalID == "" { t.Fatalf("bad: %v", dereg) } // Check for the index if respW.Result().Header.Get("X-Nomad-Index") == "" { t.Fatalf("missing index") } // Check the job is gone getReq2 := structs.JobSpecificRequest{ JobID: job.ID, QueryOptions: structs.QueryOptions{ Region: "global", Namespace: structs.DefaultNamespace, }, } var getResp2 structs.SingleJobResponse if err := s.Agent.RPC("Job.GetJob", &getReq2, &getResp2); err != nil { t.Fatalf("err: %v", err) } if getResp2.Job != nil { t.Fatalf("job still exists") } }) } func TestHTTP_JobDelete_EvalPriority(t *testing.T) { ci.Parallel(t) testCases := []struct { inputEvalPriority int expectedError bool name string }{ { inputEvalPriority: 95, expectedError: false, name: "valid input eval priority", }, { inputEvalPriority: 99999999999, expectedError: true, name: "invalid input eval priority", }, { inputEvalPriority: 0, expectedError: false, name: "no input eval priority", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { httpTest(t, nil, func(s *TestAgent) { // Create the job job := MockJob() args := api.JobRegisterRequest{ Job: job, WriteRequest: api.WriteRequest{ Region: "global", Namespace: api.DefaultNamespace, }, } buf := encodeReq(args) // Make the HTTP request regReq, err := http.NewRequest("PUT", "/v1/job/"+*job.ID, buf) assert.Nil(t, err) respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobSpecificRequest(respW, regReq) assert.Nil(t, err) // Check the response regResp := obj.(structs.JobRegisterResponse) assert.NotEmpty(t, regResp.EvalID) assert.NotEmpty(t, respW.Result().Header.Get("X-Nomad-Index")) // Check the job is registered getReq := structs.JobSpecificRequest{ JobID: *job.ID, QueryOptions: structs.QueryOptions{ Region: "global", Namespace: structs.DefaultNamespace, }, } var getResp structs.SingleJobResponse assert.Nil(t, s.Agent.RPC("Job.GetJob", &getReq, &getResp)) assert.NotNil(t, getResp.Job) // Delete the job. deleteReq, err := http.NewRequest("DELETE", "/v1/job/"+*job.ID+"?purge=true", nil) assert.Nil(t, err) respW.Flush() // Add our eval priority query param if set. if tc.inputEvalPriority > 0 { q := deleteReq.URL.Query() q.Add("eval_priority", strconv.Itoa(tc.inputEvalPriority)) deleteReq.URL.RawQuery = q.Encode() } // Make the request obj, err = s.Server.JobSpecificRequest(respW, deleteReq) if tc.expectedError { assert.NotNil(t, err) return } else { assert.Nil(t, err) } // Check the response dereg := obj.(structs.JobDeregisterResponse) assert.NotEmpty(t, dereg.EvalID) assert.NotEmpty(t, respW.Result().Header.Get("X-Nomad-Index")) // Check the evaluation that resulted from the job register. evalInfoReq, err := http.NewRequest("GET", "/v1/evaluation/"+dereg.EvalID, nil) assert.Nil(t, err) respW.Flush() evalRaw, err := s.Server.EvalSpecificRequest(respW, evalInfoReq) assert.Nil(t, err) evalRespObj := evalRaw.(*structs.Evaluation) if tc.inputEvalPriority > 0 { assert.Equal(t, tc.inputEvalPriority, evalRespObj.Priority) } else { assert.Equal(t, *job.Priority, evalRespObj.Priority) } }) }) } } func TestHTTP_Job_ScaleTaskGroup(t *testing.T) { ci.Parallel(t) require := require.New(t) httpTest(t, nil, func(s *TestAgent) { // Create the job job := mock.Job() args := structs.JobRegisterRequest{ Job: job, WriteRequest: structs.WriteRequest{ Region: "global", Namespace: structs.DefaultNamespace, }, } var resp structs.JobRegisterResponse require.NoError(s.Agent.RPC("Job.Register", &args, &resp)) newCount := job.TaskGroups[0].Count + 1 scaleReq := &api.ScalingRequest{ Count: pointer.Of(int64(newCount)), Message: "testing", Target: map[string]string{ "Job": job.ID, "Group": job.TaskGroups[0].Name, }, } buf := encodeReq(scaleReq) // Make the HTTP request to scale the job group req, err := http.NewRequest("POST", "/v1/job/"+job.ID+"/scale", buf) require.NoError(err) respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobSpecificRequest(respW, req) require.NoError(err) // Check the response resp = obj.(structs.JobRegisterResponse) require.NotEmpty(resp.EvalID) // Check for the index require.NotEmpty(respW.Header().Get("X-Nomad-Index")) // Check that the group count was changed getReq := structs.JobSpecificRequest{ JobID: job.ID, QueryOptions: structs.QueryOptions{ Region: "global", Namespace: structs.DefaultNamespace, }, } var getResp structs.SingleJobResponse err = s.Agent.RPC("Job.GetJob", &getReq, &getResp) require.NoError(err) require.NotNil(getResp.Job) require.Equal(newCount, getResp.Job.TaskGroups[0].Count) }) } func TestHTTP_Job_ScaleStatus(t *testing.T) { ci.Parallel(t) require := require.New(t) httpTest(t, nil, func(s *TestAgent) { // Create the job job := mock.Job() args := structs.JobRegisterRequest{ Job: job, WriteRequest: structs.WriteRequest{ Region: "global", Namespace: structs.DefaultNamespace, }, } var resp structs.JobRegisterResponse if err := s.Agent.RPC("Job.Register", &args, &resp); err != nil { t.Fatalf("err: %v", err) } // Make the HTTP request to scale the job group req, err := http.NewRequest("GET", "/v1/job/"+job.ID+"/scale", nil) require.NoError(err) respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobSpecificRequest(respW, req) require.NoError(err) // Check the response status := obj.(*structs.JobScaleStatus) require.NotEmpty(resp.EvalID) require.Equal(job.TaskGroups[0].Count, status.TaskGroups[job.TaskGroups[0].Name].Desired) // Check for the index require.NotEmpty(respW.Header().Get("X-Nomad-Index")) }) } func TestHTTP_JobForceEvaluate(t *testing.T) { ci.Parallel(t) httpTest(t, nil, func(s *TestAgent) { // Create the job job := mock.Job() args := structs.JobRegisterRequest{ Job: job, WriteRequest: structs.WriteRequest{ Region: "global", Namespace: structs.DefaultNamespace, }, } var resp structs.JobRegisterResponse if err := s.Agent.RPC("Job.Register", &args, &resp); err != nil { t.Fatalf("err: %v", err) } // Make the HTTP request req, err := http.NewRequest("POST", "/v1/job/"+job.ID+"/evaluate", nil) if err != nil { t.Fatalf("err: %v", err) } respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobSpecificRequest(respW, req) if err != nil { t.Fatalf("err: %v", err) } // Check the response reg := obj.(structs.JobRegisterResponse) if reg.EvalID == "" { t.Fatalf("bad: %v", reg) } // Check for the index if respW.Result().Header.Get("X-Nomad-Index") == "" { t.Fatalf("missing index") } }) } func TestHTTP_JobEvaluate_ForceReschedule(t *testing.T) { ci.Parallel(t) httpTest(t, nil, func(s *TestAgent) { // Create the job job := mock.Job() args := structs.JobRegisterRequest{ Job: job, WriteRequest: structs.WriteRequest{ Region: "global", Namespace: structs.DefaultNamespace, }, } var resp structs.JobRegisterResponse if err := s.Agent.RPC("Job.Register", &args, &resp); err != nil { t.Fatalf("err: %v", err) } jobEvalReq := api.JobEvaluateRequest{ JobID: job.ID, EvalOptions: api.EvalOptions{ ForceReschedule: true, }, } buf := encodeReq(jobEvalReq) // Make the HTTP request req, err := http.NewRequest("POST", "/v1/job/"+job.ID+"/evaluate", buf) if err != nil { t.Fatalf("err: %v", err) } respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobSpecificRequest(respW, req) if err != nil { t.Fatalf("err: %v", err) } // Check the response reg := obj.(structs.JobRegisterResponse) if reg.EvalID == "" { t.Fatalf("bad: %v", reg) } // Check for the index if respW.Result().Header.Get("X-Nomad-Index") == "" { t.Fatalf("missing index") } }) } func TestHTTP_JobEvaluations(t *testing.T) { ci.Parallel(t) httpTest(t, nil, func(s *TestAgent) { // Create the job job := mock.Job() args := structs.JobRegisterRequest{ Job: job, WriteRequest: structs.WriteRequest{ Region: "global", Namespace: structs.DefaultNamespace, }, } var resp structs.JobRegisterResponse if err := s.Agent.RPC("Job.Register", &args, &resp); err != nil { t.Fatalf("err: %v", err) } // Make the HTTP request req, err := http.NewRequest("GET", "/v1/job/"+job.ID+"/evaluations", nil) if err != nil { t.Fatalf("err: %v", err) } respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobSpecificRequest(respW, req) if err != nil { t.Fatalf("err: %v", err) } // Check the response evals := obj.([]*structs.Evaluation) // Can be multiple evals, use the last one, since they are in order idx := len(evals) - 1 if len(evals) < 0 || evals[idx].ID != resp.EvalID { t.Fatalf("bad: %v", evals) } // Check for the index if respW.Result().Header.Get("X-Nomad-Index") == "" { t.Fatalf("missing index") } if respW.Result().Header.Get("X-Nomad-KnownLeader") != "true" { t.Fatalf("missing known leader") } if respW.Result().Header.Get("X-Nomad-LastContact") == "" { t.Fatalf("missing last contact") } }) } func TestHTTP_JobAllocations(t *testing.T) { ci.Parallel(t) httpTest(t, nil, func(s *TestAgent) { // Create the job alloc1 := mock.Alloc() args := structs.JobRegisterRequest{ Job: alloc1.Job, WriteRequest: structs.WriteRequest{ Region: "global", Namespace: structs.DefaultNamespace, }, } var resp structs.JobRegisterResponse if err := s.Agent.RPC("Job.Register", &args, &resp); err != nil { t.Fatalf("err: %v", err) } // Directly manipulate the state expectedDisplayMsg := "test message" testEvent := structs.NewTaskEvent("test event").SetMessage(expectedDisplayMsg) var events []*structs.TaskEvent events = append(events, testEvent) taskState := &structs.TaskState{Events: events} alloc1.TaskStates = make(map[string]*structs.TaskState) alloc1.TaskStates["test"] = taskState state := s.Agent.server.State() err := state.UpsertAllocs(structs.MsgTypeTestSetup, 1000, []*structs.Allocation{alloc1}) if err != nil { t.Fatalf("err: %v", err) } // Make the HTTP request req, err := http.NewRequest("GET", "/v1/job/"+alloc1.Job.ID+"/allocations?all=true", nil) if err != nil { t.Fatalf("err: %v", err) } respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobSpecificRequest(respW, req) if err != nil { t.Fatalf("err: %v", err) } // Check the response allocs := obj.([]*structs.AllocListStub) if len(allocs) != 1 && allocs[0].ID != alloc1.ID { t.Fatalf("bad: %v", allocs) } displayMsg := allocs[0].TaskStates["test"].Events[0].DisplayMessage assert.Equal(t, expectedDisplayMsg, displayMsg) // Check for the index if respW.Result().Header.Get("X-Nomad-Index") == "" { t.Fatalf("missing index") } if respW.Result().Header.Get("X-Nomad-KnownLeader") != "true" { t.Fatalf("missing known leader") } if respW.Result().Header.Get("X-Nomad-LastContact") == "" { t.Fatalf("missing last contact") } }) } func TestHTTP_JobDeployments(t *testing.T) { ci.Parallel(t) assert := assert.New(t) httpTest(t, nil, func(s *TestAgent) { // Create the job j := mock.Job() args := structs.JobRegisterRequest{ Job: j, WriteRequest: structs.WriteRequest{ Region: "global", Namespace: structs.DefaultNamespace, }, } var resp structs.JobRegisterResponse assert.Nil(s.Agent.RPC("Job.Register", &args, &resp), "JobRegister") // Directly manipulate the state state := s.Agent.server.State() d := mock.Deployment() d.JobID = j.ID d.JobCreateIndex = resp.JobModifyIndex assert.Nil(state.UpsertDeployment(1000, d), "UpsertDeployment") // Make the HTTP request req, err := http.NewRequest("GET", "/v1/job/"+j.ID+"/deployments", nil) assert.Nil(err, "HTTP") respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobSpecificRequest(respW, req) assert.Nil(err, "JobSpecificRequest") // Check the response deploys := obj.([]*structs.Deployment) assert.Len(deploys, 1, "deployments") assert.Equal(d.ID, deploys[0].ID, "deployment id") assert.NotZero(respW.Result().Header.Get("X-Nomad-Index"), "missing index") assert.Equal("true", respW.Result().Header.Get("X-Nomad-KnownLeader"), "missing known leader") assert.NotZero(respW.Result().Header.Get("X-Nomad-LastContact"), "missing last contact") }) } func TestHTTP_JobDeployment(t *testing.T) { ci.Parallel(t) assert := assert.New(t) httpTest(t, nil, func(s *TestAgent) { // Create the job j := mock.Job() args := structs.JobRegisterRequest{ Job: j, WriteRequest: structs.WriteRequest{ Region: "global", Namespace: structs.DefaultNamespace, }, } var resp structs.JobRegisterResponse assert.Nil(s.Agent.RPC("Job.Register", &args, &resp), "JobRegister") // Directly manipulate the state state := s.Agent.server.State() d := mock.Deployment() d.JobID = j.ID d.JobCreateIndex = resp.JobModifyIndex assert.Nil(state.UpsertDeployment(1000, d), "UpsertDeployment") // Make the HTTP request req, err := http.NewRequest("GET", "/v1/job/"+j.ID+"/deployment", nil) assert.Nil(err, "HTTP") respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobSpecificRequest(respW, req) assert.Nil(err, "JobSpecificRequest") // Check the response out := obj.(*structs.Deployment) assert.NotNil(out, "deployment") assert.Equal(d.ID, out.ID, "deployment id") assert.NotZero(respW.Result().Header.Get("X-Nomad-Index"), "missing index") assert.Equal("true", respW.Result().Header.Get("X-Nomad-KnownLeader"), "missing known leader") assert.NotZero(respW.Result().Header.Get("X-Nomad-LastContact"), "missing last contact") }) } func TestHTTP_JobVersions(t *testing.T) { ci.Parallel(t) httpTest(t, nil, func(s *TestAgent) { // Create the job job := mock.Job() args := structs.JobRegisterRequest{ Job: job, WriteRequest: structs.WriteRequest{ Region: "global", Namespace: structs.DefaultNamespace, }, } var resp structs.JobRegisterResponse if err := s.Agent.RPC("Job.Register", &args, &resp); err != nil { t.Fatalf("err: %v", err) } job2 := mock.Job() job2.ID = job.ID job2.Priority = 100 args2 := structs.JobRegisterRequest{ Job: job2, WriteRequest: structs.WriteRequest{ Region: "global", Namespace: structs.DefaultNamespace, }, } var resp2 structs.JobRegisterResponse if err := s.Agent.RPC("Job.Register", &args2, &resp2); err != nil { t.Fatalf("err: %v", err) } // Make the HTTP request req, err := http.NewRequest("GET", "/v1/job/"+job.ID+"/versions?diffs=true", nil) if err != nil { t.Fatalf("err: %v", err) } respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobSpecificRequest(respW, req) if err != nil { t.Fatalf("err: %v", err) } // Check the response vResp := obj.(structs.JobVersionsResponse) versions := vResp.Versions if len(versions) != 2 { t.Fatalf("got %d versions; want 2", len(versions)) } if v := versions[0]; v.Version != 1 || v.Priority != 100 { t.Fatalf("bad %v", v) } if v := versions[1]; v.Version != 0 { t.Fatalf("bad %v", v) } if len(vResp.Diffs) != 1 { t.Fatalf("bad %v", vResp) } // Check for the index if respW.Result().Header.Get("X-Nomad-Index") == "" { t.Fatalf("missing index") } if respW.Result().Header.Get("X-Nomad-KnownLeader") != "true" { t.Fatalf("missing known leader") } if respW.Result().Header.Get("X-Nomad-LastContact") == "" { t.Fatalf("missing last contact") } }) } func TestHTTP_PeriodicForce(t *testing.T) { ci.Parallel(t) httpTest(t, nil, func(s *TestAgent) { // Create and register a periodic job. job := mock.PeriodicJob() args := structs.JobRegisterRequest{ Job: job, WriteRequest: structs.WriteRequest{ Region: "global", Namespace: structs.DefaultNamespace, }, } var resp structs.JobRegisterResponse if err := s.Agent.RPC("Job.Register", &args, &resp); err != nil { t.Fatalf("err: %v", err) } // Make the HTTP request req, err := http.NewRequest("POST", "/v1/job/"+job.ID+"/periodic/force", nil) if err != nil { t.Fatalf("err: %v", err) } respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobSpecificRequest(respW, req) if err != nil { t.Fatalf("err: %v", err) } // Check for the index if respW.Result().Header.Get("X-Nomad-Index") == "" { t.Fatalf("missing index") } // Check the response r := obj.(structs.PeriodicForceResponse) if r.EvalID == "" { t.Fatalf("bad: %#v", r) } }) } func TestHTTP_JobPlan(t *testing.T) { ci.Parallel(t) httpTest(t, nil, func(s *TestAgent) { // Create the job job := MockJob() args := api.JobPlanRequest{ Job: job, Diff: true, WriteRequest: api.WriteRequest{ Region: "global", Namespace: api.DefaultNamespace, }, } buf := encodeReq(args) // Make the HTTP request req, err := http.NewRequest("PUT", "/v1/job/"+*job.ID+"/plan", buf) if err != nil { t.Fatalf("err: %v", err) } respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobSpecificRequest(respW, req) if err != nil { t.Fatalf("err: %v", err) } // Check the response plan := obj.(structs.JobPlanResponse) if plan.Annotations == nil { t.Fatalf("bad: %v", plan) } if plan.Diff == nil { t.Fatalf("bad: %v", plan) } }) } func TestHTTP_JobPlanRegion(t *testing.T) { ci.Parallel(t) cases := []struct { Name string ConfigRegion string APIRegion string ExpectedRegion string }{ { Name: "api region takes precedence", ConfigRegion: "not-global", APIRegion: "north-america", ExpectedRegion: "north-america", }, { Name: "config region is set", ConfigRegion: "north-america", APIRegion: "", ExpectedRegion: "north-america", }, { Name: "api region is set", ConfigRegion: "", APIRegion: "north-america", ExpectedRegion: "north-america", }, { Name: "falls back to default if no region is provided", ConfigRegion: "", APIRegion: "", ExpectedRegion: "global", }, } for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { httpTest(t, func(c *Config) { c.Region = tc.ExpectedRegion }, func(s *TestAgent) { // Create the job job := MockRegionalJob() if tc.ConfigRegion == "" { job.Region = nil } else { job.Region = &tc.ConfigRegion } args := api.JobPlanRequest{ Job: job, Diff: true, WriteRequest: api.WriteRequest{ Region: tc.APIRegion, Namespace: api.DefaultNamespace, }, } buf := encodeReq(args) // Make the HTTP request req, err := http.NewRequest("PUT", "/v1/job/"+*job.ID+"/plan", buf) require.NoError(t, err) respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobSpecificRequest(respW, req) require.NoError(t, err) // Check the response plan := obj.(structs.JobPlanResponse) require.NotNil(t, plan.Annotations) require.NotNil(t, plan.Diff) }) }) } } func TestHTTP_JobDispatch(t *testing.T) { ci.Parallel(t) httpTest(t, nil, func(s *TestAgent) { // Create the parameterized job job := mock.BatchJob() job.ParameterizedJob = &structs.ParameterizedJobConfig{} args := structs.JobRegisterRequest{ Job: job, WriteRequest: structs.WriteRequest{ Region: "global", Namespace: structs.DefaultNamespace, }, } var resp structs.JobRegisterResponse if err := s.Agent.RPC("Job.Register", &args, &resp); err != nil { t.Fatalf("err: %v", err) } // Make the request respW := httptest.NewRecorder() args2 := structs.JobDispatchRequest{ WriteRequest: structs.WriteRequest{ Region: "global", Namespace: structs.DefaultNamespace, IdempotencyToken: "foo", }, } buf := encodeReq(args2) // Make the HTTP request req2, err := http.NewRequest("PUT", "/v1/job/"+job.ID+"/dispatch", buf) if err != nil { t.Fatalf("err: %v", err) } respW.Flush() // Make the request obj, err := s.Server.JobSpecificRequest(respW, req2) if err != nil { t.Fatalf("err: %v", err) } // Check the response dispatch := obj.(structs.JobDispatchResponse) if dispatch.EvalID == "" { t.Fatalf("bad: %v", dispatch) } if dispatch.DispatchedJobID == "" { t.Fatalf("bad: %v", dispatch) } }) } func TestHTTP_JobRevert(t *testing.T) { ci.Parallel(t) httpTest(t, nil, func(s *TestAgent) { // Create the job and register it twice job := mock.Job() regReq := structs.JobRegisterRequest{ Job: job, WriteRequest: structs.WriteRequest{ Region: "global", Namespace: structs.DefaultNamespace, }, } var regResp structs.JobRegisterResponse if err := s.Agent.RPC("Job.Register", ®Req, ®Resp); err != nil { t.Fatalf("err: %v", err) } // Change the job to get a new version job.Datacenters = append(job.Datacenters, "foo") if err := s.Agent.RPC("Job.Register", ®Req, ®Resp); err != nil { t.Fatalf("err: %v", err) } args := structs.JobRevertRequest{ JobID: job.ID, JobVersion: 0, WriteRequest: structs.WriteRequest{ Region: "global", Namespace: structs.DefaultNamespace, }, } buf := encodeReq(args) // Make the HTTP request req, err := http.NewRequest("PUT", "/v1/job/"+job.ID+"/revert", buf) if err != nil { t.Fatalf("err: %v", err) } respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobSpecificRequest(respW, req) if err != nil { t.Fatalf("err: %v", err) } // Check the response revertResp := obj.(structs.JobRegisterResponse) if revertResp.EvalID == "" { t.Fatalf("bad: %v", revertResp) } // Check for the index if respW.Result().Header.Get("X-Nomad-Index") == "" { t.Fatalf("missing index") } }) } func TestHTTP_JobStable(t *testing.T) { ci.Parallel(t) httpTest(t, nil, func(s *TestAgent) { // Create the job and register it twice job := mock.Job() regReq := structs.JobRegisterRequest{ Job: job, WriteRequest: structs.WriteRequest{ Region: "global", Namespace: structs.DefaultNamespace, }, } var regResp structs.JobRegisterResponse if err := s.Agent.RPC("Job.Register", ®Req, ®Resp); err != nil { t.Fatalf("err: %v", err) } if err := s.Agent.RPC("Job.Register", ®Req, ®Resp); err != nil { t.Fatalf("err: %v", err) } args := structs.JobStabilityRequest{ JobID: job.ID, JobVersion: 0, Stable: true, WriteRequest: structs.WriteRequest{ Region: "global", Namespace: structs.DefaultNamespace, }, } buf := encodeReq(args) // Make the HTTP request req, err := http.NewRequest("PUT", "/v1/job/"+job.ID+"/stable", buf) if err != nil { t.Fatalf("err: %v", err) } respW := httptest.NewRecorder() // Make the request obj, err := s.Server.JobSpecificRequest(respW, req) if err != nil { t.Fatalf("err: %v", err) } // Check the response stableResp := obj.(structs.JobStabilityResponse) if stableResp.Index == 0 { t.Fatalf("bad: %v", stableResp) } // Check for the index if respW.Result().Header.Get("X-Nomad-Index") == "" { t.Fatalf("missing index") } }) } func TestJobs_ParsingWriteRequest(t *testing.T) { ci.Parallel(t) // defaults agentRegion := "agentRegion" cases := []struct { name string jobRegion string multiregion *api.Multiregion queryRegion string queryNamespace string queryToken string apiRegion string apiNamespace string apiToken string expectedRequestRegion string expectedJobRegion string expectedToken string expectedNamespace string }{ { name: "no region provided at all", jobRegion: "", multiregion: nil, queryRegion: "", expectedRequestRegion: agentRegion, expectedJobRegion: agentRegion, expectedToken: "", expectedNamespace: "default", }, { name: "no region provided but multiregion safe", jobRegion: "", multiregion: &api.Multiregion{}, queryRegion: "", expectedRequestRegion: agentRegion, expectedJobRegion: api.GlobalRegion, expectedToken: "", expectedNamespace: "default", }, { name: "region flag provided", jobRegion: "", multiregion: nil, queryRegion: "west", expectedRequestRegion: "west", expectedJobRegion: "west", expectedToken: "", expectedNamespace: "default", }, { name: "job region provided", jobRegion: "west", multiregion: nil, queryRegion: "", expectedRequestRegion: "west", expectedJobRegion: "west", expectedToken: "", expectedNamespace: "default", }, { name: "job region overridden by region flag", jobRegion: "west", multiregion: nil, queryRegion: "east", expectedRequestRegion: "east", expectedJobRegion: "east", expectedToken: "", expectedNamespace: "default", }, { name: "multiregion to valid region", jobRegion: "", multiregion: &api.Multiregion{Regions: []*api.MultiregionRegion{ {Name: "west"}, {Name: "east"}, }}, queryRegion: "east", expectedRequestRegion: "east", expectedJobRegion: api.GlobalRegion, expectedToken: "", expectedNamespace: "default", }, { name: "multiregion sent to wrong region", jobRegion: "", multiregion: &api.Multiregion{Regions: []*api.MultiregionRegion{ {Name: "west"}, {Name: "east"}, }}, queryRegion: "north", expectedRequestRegion: "west", expectedJobRegion: api.GlobalRegion, expectedToken: "", expectedNamespace: "default", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { // we need a valid agent config but we don't want to start up // a real server for this srv := &HTTPServer{} srv.agent = &Agent{config: &Config{Region: agentRegion}} job := &api.Job{ Region: pointer.Of(tc.jobRegion), Multiregion: tc.multiregion, } req, _ := http.NewRequest("POST", "/", nil) if tc.queryToken != "" { req.Header.Set("X-Nomad-Token", tc.queryToken) } q := req.URL.Query() if tc.queryNamespace != "" { q.Add("namespace", tc.queryNamespace) } if tc.queryRegion != "" { q.Add("region", tc.queryRegion) } req.URL.RawQuery = q.Encode() apiReq := api.WriteRequest{ Region: tc.apiRegion, Namespace: tc.apiNamespace, SecretID: tc.apiToken, } sJob, sWriteReq := srv.apiJobAndRequestToStructs(job, req, apiReq) must.Eq(t, tc.expectedJobRegion, sJob.Region) must.Eq(t, tc.expectedNamespace, sJob.Namespace) must.Eq(t, tc.expectedNamespace, sWriteReq.Namespace) must.Eq(t, tc.expectedRequestRegion, sWriteReq.Region) must.Eq(t, tc.expectedToken, sWriteReq.AuthToken) }) } } func TestJobs_RegionForJob(t *testing.T) { ci.Parallel(t) // defaults agentRegion := "agentRegion" cases := []struct { name string jobRegion string multiregion *api.Multiregion queryRegion string apiRegion string agentRegion string expectedRequestRegion string expectedJobRegion string }{ { name: "no region provided", jobRegion: "", multiregion: nil, queryRegion: "", expectedRequestRegion: agentRegion, expectedJobRegion: agentRegion, }, { name: "no region provided but multiregion safe", jobRegion: "", multiregion: &api.Multiregion{}, queryRegion: "", expectedRequestRegion: agentRegion, expectedJobRegion: api.GlobalRegion, }, { name: "region flag provided", jobRegion: "", multiregion: nil, queryRegion: "west", expectedRequestRegion: "west", expectedJobRegion: "west", }, { name: "job region provided", jobRegion: "west", multiregion: nil, queryRegion: "", expectedRequestRegion: "west", expectedJobRegion: "west", }, { name: "job region overridden by region flag", jobRegion: "west", multiregion: nil, queryRegion: "east", expectedRequestRegion: "east", expectedJobRegion: "east", }, { name: "job region overridden by api body", jobRegion: "west", multiregion: nil, apiRegion: "east", expectedRequestRegion: "east", expectedJobRegion: "east", }, { name: "multiregion to valid region", jobRegion: "", multiregion: &api.Multiregion{Regions: []*api.MultiregionRegion{ {Name: "west"}, {Name: "east"}, }}, queryRegion: "east", expectedRequestRegion: "east", expectedJobRegion: api.GlobalRegion, }, { name: "multiregion sent to wrong region", jobRegion: "", multiregion: &api.Multiregion{Regions: []*api.MultiregionRegion{ {Name: "west"}, {Name: "east"}, }}, queryRegion: "north", expectedRequestRegion: "west", expectedJobRegion: api.GlobalRegion, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { job := &api.Job{ Region: pointer.Of(tc.jobRegion), Multiregion: tc.multiregion, } requestRegion, jobRegion := regionForJob( job, tc.queryRegion, tc.apiRegion, agentRegion) require.Equal(t, tc.expectedRequestRegion, requestRegion) require.Equal(t, tc.expectedJobRegion, jobRegion) }) } } func TestJobs_NamespaceForJob(t *testing.T) { ci.Parallel(t) // test namespace for pointer inputs ns := "dev" cases := []struct { name string job *api.Job queryNamespace string apiNamespace string expected string }{ { name: "no namespace provided", job: &api.Job{}, expected: structs.DefaultNamespace, }, { name: "jobspec has namespace", job: &api.Job{Namespace: &ns}, expected: "dev", }, { name: "-namespace flag overrides empty job namespace", job: &api.Job{}, queryNamespace: "prod", expected: "prod", }, { name: "-namespace flag overrides job namespace", job: &api.Job{Namespace: &ns}, queryNamespace: "prod", expected: "prod", }, { name: "-namespace flag overrides job namespace even if default", job: &api.Job{Namespace: &ns}, queryNamespace: structs.DefaultNamespace, expected: structs.DefaultNamespace, }, { name: "API param overrides empty job namespace", job: &api.Job{}, apiNamespace: "prod", expected: "prod", }, { name: "-namespace flag overrides API param", job: &api.Job{Namespace: &ns}, queryNamespace: "prod", apiNamespace: "whatever", expected: "prod", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { require.Equal(t, tc.expected, namespaceForJob(tc.job.Namespace, tc.queryNamespace, tc.apiNamespace), ) }) } } func TestHTTPServer_jobServiceRegistrations(t *testing.T) { ci.Parallel(t) testCases := []struct { testFn func(srv *TestAgent) name string }{ { testFn: func(s *TestAgent) { // Grab the state, so we can manipulate it and test against it. testState := s.Agent.server.State() // Generate a job and upsert this. job := mock.Job() require.NoError(t, testState.UpsertJob(structs.MsgTypeTestSetup, 10, job)) // Generate a service registration, assigned the jobID to the // mocked jobID, and upsert this. serviceReg := mock.ServiceRegistrations()[0] serviceReg.JobID = job.ID require.NoError(t, testState.UpsertServiceRegistrations( structs.MsgTypeTestSetup, 20, []*structs.ServiceRegistration{serviceReg})) // Build the HTTP request. path := fmt.Sprintf("/v1/job/%s/services", job.ID) req, err := http.NewRequest(http.MethodGet, path, nil) require.NoError(t, err) respW := httptest.NewRecorder() // Send the HTTP request. obj, err := s.Server.JobSpecificRequest(respW, req) require.NoError(t, err) // Check the response. require.Equal(t, "20", respW.Header().Get("X-Nomad-Index")) require.ElementsMatch(t, []*structs.ServiceRegistration{serviceReg}, obj.([]*structs.ServiceRegistration)) }, name: "job has registrations", }, { testFn: func(s *TestAgent) { // Grab the state, so we can manipulate it and test against it. testState := s.Agent.server.State() // Generate a job and upsert this. job := mock.Job() require.NoError(t, testState.UpsertJob(structs.MsgTypeTestSetup, 10, job)) // Build the HTTP request. path := fmt.Sprintf("/v1/job/%s/services", job.ID) req, err := http.NewRequest(http.MethodGet, path, nil) require.NoError(t, err) respW := httptest.NewRecorder() // Send the HTTP request. obj, err := s.Server.JobSpecificRequest(respW, req) require.NoError(t, err) // Check the response. require.Equal(t, "1", respW.Header().Get("X-Nomad-Index")) require.ElementsMatch(t, []*structs.ServiceRegistration{}, obj.([]*structs.ServiceRegistration)) }, name: "job without registrations", }, { testFn: func(s *TestAgent) { // Build the HTTP request. req, err := http.NewRequest(http.MethodGet, "/v1/job/example/services", nil) require.NoError(t, err) respW := httptest.NewRecorder() // Send the HTTP request. obj, err := s.Server.JobSpecificRequest(respW, req) require.Error(t, err) require.Contains(t, err.Error(), "job not found") require.Nil(t, obj) }, name: "job not found", }, { testFn: func(s *TestAgent) { // Build the HTTP request. req, err := http.NewRequest(http.MethodHead, "/v1/job/example/services", nil) require.NoError(t, err) respW := httptest.NewRecorder() // Send the HTTP request. obj, err := s.Server.JobSpecificRequest(respW, req) require.Error(t, err) require.Contains(t, err.Error(), "Invalid method") require.Nil(t, obj) }, name: "incorrect method", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { httpTest(t, nil, tc.testFn) }) } } func TestJobs_ApiJobToStructsJob(t *testing.T) { ci.Parallel(t) apiJob := &api.Job{ Stop: pointer.Of(true), Region: pointer.Of("global"), Namespace: pointer.Of("foo"), ID: pointer.Of("foo"), ParentID: pointer.Of("lol"), Name: pointer.Of("name"), Type: pointer.Of("service"), Priority: pointer.Of(50), AllAtOnce: pointer.Of(true), Datacenters: []string{"dc1", "dc2"}, Constraints: []*api.Constraint{ { LTarget: "a", RTarget: "b", Operand: "c", }, }, Affinities: []*api.Affinity{ { LTarget: "a", RTarget: "b", Operand: "c", Weight: pointer.Of(int8(50)), }, }, Update: &api.UpdateStrategy{ Stagger: pointer.Of(1 * time.Second), MaxParallel: pointer.Of(5), HealthCheck: pointer.Of(structs.UpdateStrategyHealthCheck_Manual), MinHealthyTime: pointer.Of(1 * time.Minute), HealthyDeadline: pointer.Of(3 * time.Minute), ProgressDeadline: pointer.Of(3 * time.Minute), AutoRevert: pointer.Of(false), Canary: pointer.Of(1), }, Spreads: []*api.Spread{ { Attribute: "${meta.rack}", Weight: pointer.Of(int8(100)), SpreadTarget: []*api.SpreadTarget{ { Value: "r1", Percent: 50, }, }, }, }, Periodic: &api.PeriodicConfig{ Enabled: pointer.Of(true), Spec: pointer.Of("spec"), SpecType: pointer.Of("cron"), ProhibitOverlap: pointer.Of(true), TimeZone: pointer.Of("test zone"), }, ParameterizedJob: &api.ParameterizedJobConfig{ Payload: "payload", MetaRequired: []string{"a", "b"}, MetaOptional: []string{"c", "d"}, }, Payload: []byte("payload"), Meta: map[string]string{ "foo": "bar", }, Multiregion: &api.Multiregion{ Strategy: &api.MultiregionStrategy{ MaxParallel: pointer.Of(2), OnFailure: pointer.Of("fail_all"), }, Regions: []*api.MultiregionRegion{ { Name: "west", Count: pointer.Of(1), Datacenters: []string{"dc1", "dc2"}, Meta: map[string]string{"region_code": "W"}, }, }, }, TaskGroups: []*api.TaskGroup{ { Name: pointer.Of("group1"), Count: pointer.Of(5), Constraints: []*api.Constraint{ { LTarget: "x", RTarget: "y", Operand: "z", }, }, Affinities: []*api.Affinity{ { LTarget: "x", RTarget: "y", Operand: "z", Weight: pointer.Of(int8(100)), }, }, RestartPolicy: &api.RestartPolicy{ Interval: pointer.Of(1 * time.Second), Attempts: pointer.Of(5), Delay: pointer.Of(10 * time.Second), Mode: pointer.Of("delay"), }, ReschedulePolicy: &api.ReschedulePolicy{ Interval: pointer.Of(12 * time.Hour), Attempts: pointer.Of(5), DelayFunction: pointer.Of("constant"), Delay: pointer.Of(30 * time.Second), Unlimited: pointer.Of(true), MaxDelay: pointer.Of(20 * time.Minute), }, Migrate: &api.MigrateStrategy{ MaxParallel: pointer.Of(12), HealthCheck: pointer.Of("task_events"), MinHealthyTime: pointer.Of(12 * time.Hour), HealthyDeadline: pointer.Of(12 * time.Hour), }, Spreads: []*api.Spread{ { Attribute: "${node.datacenter}", Weight: pointer.Of(int8(100)), SpreadTarget: []*api.SpreadTarget{ { Value: "dc1", Percent: 100, }, }, }, }, EphemeralDisk: &api.EphemeralDisk{ SizeMB: pointer.Of(100), Sticky: pointer.Of(true), Migrate: pointer.Of(true), }, Update: &api.UpdateStrategy{ HealthCheck: pointer.Of(structs.UpdateStrategyHealthCheck_Checks), MinHealthyTime: pointer.Of(2 * time.Minute), HealthyDeadline: pointer.Of(5 * time.Minute), ProgressDeadline: pointer.Of(5 * time.Minute), AutoRevert: pointer.Of(true), }, Meta: map[string]string{ "key": "value", }, Consul: &api.Consul{ Namespace: "team-foo", }, Services: []*api.Service{ { Name: "groupserviceA", Tags: []string{"a", "b"}, CanaryTags: []string{"d", "e"}, EnableTagOverride: true, PortLabel: "1234", Address: "group.example.com", Meta: map[string]string{ "servicemeta": "foobar", }, TaggedAddresses: map[string]string{ "wan": "1.2.3.4", }, CheckRestart: &api.CheckRestart{ Limit: 4, Grace: pointer.Of(11 * time.Second), }, Checks: []api.ServiceCheck{ { Name: "bar", Type: "http", Command: "foo", Args: []string{"a", "b"}, Path: "/check", Protocol: "http", Method: "POST", Body: "{\"check\":\"mem\"}", PortLabel: "foo", AddressMode: "driver", GRPCService: "foo.Bar", GRPCUseTLS: true, Interval: 4 * time.Second, Timeout: 2 * time.Second, InitialStatus: "ok", CheckRestart: &api.CheckRestart{ Limit: 3, IgnoreWarnings: true, }, TaskName: "task1", SuccessBeforePassing: 2, FailuresBeforeCritical: 3, }, }, Connect: &api.ConsulConnect{ Native: false, SidecarService: &api.ConsulSidecarService{ Tags: []string{"f", "g"}, Port: "9000", DisableDefaultTCPCheck: true, }, }, }, }, MaxClientDisconnect: pointer.Of(30 * time.Second), Tasks: []*api.Task{ { Name: "task1", Leader: true, Driver: "docker", User: "mary", Config: map[string]interface{}{ "lol": "code", }, Env: map[string]string{ "hello": "world", }, Constraints: []*api.Constraint{ { LTarget: "x", RTarget: "y", Operand: "z", }, }, Affinities: []*api.Affinity{ { LTarget: "a", RTarget: "b", Operand: "c", Weight: pointer.Of(int8(50)), }, }, VolumeMounts: []*api.VolumeMount{ { Volume: pointer.Of("vol"), Destination: pointer.Of("dest"), ReadOnly: pointer.Of(false), PropagationMode: pointer.Of("a"), }, }, RestartPolicy: &api.RestartPolicy{ Interval: pointer.Of(2 * time.Second), Attempts: pointer.Of(10), Delay: pointer.Of(20 * time.Second), Mode: pointer.Of("delay"), }, Services: []*api.Service{ { Name: "serviceA", Tags: []string{"1", "2"}, CanaryTags: []string{"3", "4"}, EnableTagOverride: true, PortLabel: "foo", Address: "task.example.com", Meta: map[string]string{ "servicemeta": "foobar", }, CheckRestart: &api.CheckRestart{ Limit: 4, Grace: pointer.Of(11 * time.Second), }, Checks: []api.ServiceCheck{ { Name: "bar", Type: "http", Command: "foo", Args: []string{"a", "b"}, Path: "/check", Protocol: "http", PortLabel: "foo", AddressMode: "driver", GRPCService: "foo.Bar", GRPCUseTLS: true, Interval: 4 * time.Second, Timeout: 2 * time.Second, InitialStatus: "ok", SuccessBeforePassing: 3, FailuresBeforeCritical: 4, CheckRestart: &api.CheckRestart{ Limit: 3, IgnoreWarnings: true, }, }, { Name: "check2", Type: "tcp", PortLabel: "foo", Interval: 4 * time.Second, Timeout: 2 * time.Second, }, }, }, }, Resources: &api.Resources{ CPU: pointer.Of(100), MemoryMB: pointer.Of(10), Networks: []*api.NetworkResource{ { IP: "10.10.11.1", MBits: pointer.Of(10), Hostname: "foobar", ReservedPorts: []api.Port{ { Label: "http", Value: 80, }, }, DynamicPorts: []api.Port{ { Label: "ssh", Value: 2000, }, }, }, }, Devices: []*api.RequestedDevice{ { Name: "nvidia/gpu", Count: pointer.Of(uint64(4)), Constraints: []*api.Constraint{ { LTarget: "x", RTarget: "y", Operand: "z", }, }, Affinities: []*api.Affinity{ { LTarget: "a", RTarget: "b", Operand: "c", Weight: pointer.Of(int8(50)), }, }, }, { Name: "gpu", Count: nil, }, }, }, Meta: map[string]string{ "lol": "code", }, KillTimeout: pointer.Of(10 * time.Second), KillSignal: "SIGQUIT", LogConfig: &api.LogConfig{ MaxFiles: pointer.Of(10), MaxFileSizeMB: pointer.Of(100), }, Artifacts: []*api.TaskArtifact{ { GetterSource: pointer.Of("source"), GetterOptions: map[string]string{ "a": "b", }, GetterMode: pointer.Of("dir"), RelativeDest: pointer.Of("dest"), }, }, Vault: &api.Vault{ Namespace: pointer.Of("ns1"), Policies: []string{"a", "b", "c"}, Env: pointer.Of(true), ChangeMode: pointer.Of("c"), ChangeSignal: pointer.Of("sighup"), }, Templates: []*api.Template{ { SourcePath: pointer.Of("source"), DestPath: pointer.Of("dest"), EmbeddedTmpl: pointer.Of("embedded"), ChangeMode: pointer.Of("change"), ChangeSignal: pointer.Of("signal"), ChangeScript: &api.ChangeScript{ Command: pointer.Of("/bin/foo"), Args: []string{"-h"}, Timeout: pointer.Of(5 * time.Second), FailOnError: pointer.Of(false), }, Splay: pointer.Of(1 * time.Minute), Perms: pointer.Of("666"), Uid: pointer.Of(1000), Gid: pointer.Of(1000), LeftDelim: pointer.Of("abc"), RightDelim: pointer.Of("def"), Envvars: pointer.Of(true), Wait: &api.WaitConfig{ Min: pointer.Of(5 * time.Second), Max: pointer.Of(10 * time.Second), }, ErrMissingKey: pointer.Of(true), }, }, DispatchPayload: &api.DispatchPayloadConfig{ File: "fileA", }, }, }, }, }, ConsulToken: pointer.Of("abc123"), VaultToken: pointer.Of("def456"), VaultNamespace: pointer.Of("ghi789"), Status: pointer.Of("status"), StatusDescription: pointer.Of("status_desc"), Version: pointer.Of(uint64(10)), CreateIndex: pointer.Of(uint64(1)), ModifyIndex: pointer.Of(uint64(3)), JobModifyIndex: pointer.Of(uint64(5)), } expected := &structs.Job{ Stop: true, Region: "global", Namespace: "foo", VaultNamespace: "ghi789", ID: "foo", Name: "name", Type: "service", Priority: 50, AllAtOnce: true, Datacenters: []string{"dc1", "dc2"}, Constraints: []*structs.Constraint{ { LTarget: "a", RTarget: "b", Operand: "c", }, }, Affinities: []*structs.Affinity{ { LTarget: "a", RTarget: "b", Operand: "c", Weight: 50, }, }, Spreads: []*structs.Spread{ { Attribute: "${meta.rack}", Weight: 100, SpreadTarget: []*structs.SpreadTarget{ { Value: "r1", Percent: 50, }, }, }, }, Update: structs.UpdateStrategy{ Stagger: 1 * time.Second, MaxParallel: 5, }, Periodic: &structs.PeriodicConfig{ Enabled: true, Spec: "spec", SpecType: "cron", ProhibitOverlap: true, TimeZone: "test zone", }, ParameterizedJob: &structs.ParameterizedJobConfig{ Payload: "payload", MetaRequired: []string{"a", "b"}, MetaOptional: []string{"c", "d"}, }, Payload: []byte("payload"), Meta: map[string]string{ "foo": "bar", }, Multiregion: &structs.Multiregion{ Strategy: &structs.MultiregionStrategy{ MaxParallel: 2, OnFailure: "fail_all", }, Regions: []*structs.MultiregionRegion{ { Name: "west", Count: 1, Datacenters: []string{"dc1", "dc2"}, Meta: map[string]string{"region_code": "W"}, }, }, }, TaskGroups: []*structs.TaskGroup{ { Name: "group1", Count: 5, Constraints: []*structs.Constraint{ { LTarget: "x", RTarget: "y", Operand: "z", }, }, Affinities: []*structs.Affinity{ { LTarget: "x", RTarget: "y", Operand: "z", Weight: 100, }, }, RestartPolicy: &structs.RestartPolicy{ Interval: 1 * time.Second, Attempts: 5, Delay: 10 * time.Second, Mode: "delay", }, Spreads: []*structs.Spread{ { Attribute: "${node.datacenter}", Weight: 100, SpreadTarget: []*structs.SpreadTarget{ { Value: "dc1", Percent: 100, }, }, }, }, ReschedulePolicy: &structs.ReschedulePolicy{ Interval: 12 * time.Hour, Attempts: 5, DelayFunction: "constant", Delay: 30 * time.Second, Unlimited: true, MaxDelay: 20 * time.Minute, }, Migrate: &structs.MigrateStrategy{ MaxParallel: 12, HealthCheck: "task_events", MinHealthyTime: 12 * time.Hour, HealthyDeadline: 12 * time.Hour, }, EphemeralDisk: &structs.EphemeralDisk{ SizeMB: 100, Sticky: true, Migrate: true, }, Update: &structs.UpdateStrategy{ Stagger: 1 * time.Second, MaxParallel: 5, HealthCheck: structs.UpdateStrategyHealthCheck_Checks, MinHealthyTime: 2 * time.Minute, HealthyDeadline: 5 * time.Minute, ProgressDeadline: 5 * time.Minute, AutoRevert: true, AutoPromote: false, Canary: 1, }, Meta: map[string]string{ "key": "value", }, Consul: &structs.Consul{ Namespace: "team-foo", }, Services: []*structs.Service{ { Name: "groupserviceA", Provider: "consul", Tags: []string{"a", "b"}, CanaryTags: []string{"d", "e"}, EnableTagOverride: true, PortLabel: "1234", AddressMode: "auto", Address: "group.example.com", Meta: map[string]string{ "servicemeta": "foobar", }, TaggedAddresses: map[string]string{ "wan": "1.2.3.4", }, OnUpdate: structs.OnUpdateRequireHealthy, Checks: []*structs.ServiceCheck{ { Name: "bar", Type: "http", Command: "foo", Args: []string{"a", "b"}, Path: "/check", Protocol: "http", Method: "POST", Body: "{\"check\":\"mem\"}", PortLabel: "foo", AddressMode: "driver", GRPCService: "foo.Bar", GRPCUseTLS: true, Interval: 4 * time.Second, Timeout: 2 * time.Second, InitialStatus: "ok", CheckRestart: &structs.CheckRestart{ Grace: 11 * time.Second, Limit: 3, IgnoreWarnings: true, }, TaskName: "task1", OnUpdate: structs.OnUpdateRequireHealthy, SuccessBeforePassing: 2, FailuresBeforeCritical: 3, }, }, Connect: &structs.ConsulConnect{ Native: false, SidecarService: &structs.ConsulSidecarService{ Tags: []string{"f", "g"}, Port: "9000", DisableDefaultTCPCheck: true, }, }, }, }, MaxClientDisconnect: pointer.Of(30 * time.Second), Tasks: []*structs.Task{ { Name: "task1", Driver: "docker", Leader: true, User: "mary", Config: map[string]interface{}{ "lol": "code", }, Constraints: []*structs.Constraint{ { LTarget: "x", RTarget: "y", Operand: "z", }, }, Affinities: []*structs.Affinity{ { LTarget: "a", RTarget: "b", Operand: "c", Weight: 50, }, }, Env: map[string]string{ "hello": "world", }, VolumeMounts: []*structs.VolumeMount{ { Volume: "vol", Destination: "dest", ReadOnly: false, PropagationMode: "a", }, }, RestartPolicy: &structs.RestartPolicy{ Interval: 2 * time.Second, Attempts: 10, Delay: 20 * time.Second, Mode: "delay", }, Services: []*structs.Service{ { Name: "serviceA", Provider: "consul", Tags: []string{"1", "2"}, CanaryTags: []string{"3", "4"}, EnableTagOverride: true, PortLabel: "foo", AddressMode: "auto", Address: "task.example.com", Meta: map[string]string{ "servicemeta": "foobar", }, OnUpdate: structs.OnUpdateRequireHealthy, Checks: []*structs.ServiceCheck{ { Name: "bar", Type: "http", Command: "foo", Args: []string{"a", "b"}, Path: "/check", Protocol: "http", PortLabel: "foo", AddressMode: "driver", Interval: 4 * time.Second, Timeout: 2 * time.Second, InitialStatus: "ok", GRPCService: "foo.Bar", GRPCUseTLS: true, SuccessBeforePassing: 3, FailuresBeforeCritical: 4, CheckRestart: &structs.CheckRestart{ Limit: 3, Grace: 11 * time.Second, IgnoreWarnings: true, }, OnUpdate: structs.OnUpdateRequireHealthy, }, { Name: "check2", Type: "tcp", PortLabel: "foo", Interval: 4 * time.Second, Timeout: 2 * time.Second, CheckRestart: &structs.CheckRestart{ Limit: 4, Grace: 11 * time.Second, }, OnUpdate: structs.OnUpdateRequireHealthy, }, }, }, }, Resources: &structs.Resources{ CPU: 100, MemoryMB: 10, Networks: []*structs.NetworkResource{ { IP: "10.10.11.1", MBits: 10, Hostname: "foobar", ReservedPorts: []structs.Port{ { Label: "http", Value: 80, }, }, DynamicPorts: []structs.Port{ { Label: "ssh", Value: 2000, }, }, }, }, Devices: []*structs.RequestedDevice{ { Name: "nvidia/gpu", Count: 4, Constraints: []*structs.Constraint{ { LTarget: "x", RTarget: "y", Operand: "z", }, }, Affinities: []*structs.Affinity{ { LTarget: "a", RTarget: "b", Operand: "c", Weight: 50, }, }, }, { Name: "gpu", Count: 1, }, }, }, Meta: map[string]string{ "lol": "code", }, KillTimeout: 10 * time.Second, KillSignal: "SIGQUIT", LogConfig: &structs.LogConfig{ MaxFiles: 10, MaxFileSizeMB: 100, }, Artifacts: []*structs.TaskArtifact{ { GetterSource: "source", GetterOptions: map[string]string{ "a": "b", }, GetterMode: "dir", RelativeDest: "dest", }, }, Vault: &structs.Vault{ Namespace: "ns1", Policies: []string{"a", "b", "c"}, Env: true, ChangeMode: "c", ChangeSignal: "sighup", }, Templates: []*structs.Template{ { SourcePath: "source", DestPath: "dest", EmbeddedTmpl: "embedded", ChangeMode: "change", ChangeSignal: "SIGNAL", ChangeScript: &structs.ChangeScript{ Command: "/bin/foo", Args: []string{"-h"}, Timeout: 5 * time.Second, FailOnError: false, }, Splay: 1 * time.Minute, Perms: "666", Uid: pointer.Of(1000), Gid: pointer.Of(1000), LeftDelim: "abc", RightDelim: "def", Envvars: true, Wait: &structs.WaitConfig{ Min: pointer.Of(5 * time.Second), Max: pointer.Of(10 * time.Second), }, ErrMissingKey: true, }, }, DispatchPayload: &structs.DispatchPayloadConfig{ File: "fileA", }, }, }, }, }, ConsulToken: "abc123", VaultToken: "def456", } structsJob := ApiJobToStructJob(apiJob) require.Equal(t, expected, structsJob) systemAPIJob := &api.Job{ Stop: pointer.Of(true), Region: pointer.Of("global"), Namespace: pointer.Of("foo"), ID: pointer.Of("foo"), ParentID: pointer.Of("lol"), Name: pointer.Of("name"), Type: pointer.Of("system"), Priority: pointer.Of(50), AllAtOnce: pointer.Of(true), Datacenters: []string{"dc1", "dc2"}, Constraints: []*api.Constraint{ { LTarget: "a", RTarget: "b", Operand: "c", }, }, TaskGroups: []*api.TaskGroup{ { Name: pointer.Of("group1"), Count: pointer.Of(5), Constraints: []*api.Constraint{ { LTarget: "x", RTarget: "y", Operand: "z", }, }, RestartPolicy: &api.RestartPolicy{ Interval: pointer.Of(1 * time.Second), Attempts: pointer.Of(5), Delay: pointer.Of(10 * time.Second), Mode: pointer.Of("delay"), }, EphemeralDisk: &api.EphemeralDisk{ SizeMB: pointer.Of(100), Sticky: pointer.Of(true), Migrate: pointer.Of(true), }, Meta: map[string]string{ "key": "value", }, Consul: &api.Consul{ Namespace: "foo", }, Tasks: []*api.Task{ { Name: "task1", Leader: true, Driver: "docker", User: "mary", Config: map[string]interface{}{ "lol": "code", }, Env: map[string]string{ "hello": "world", }, Constraints: []*api.Constraint{ { LTarget: "x", RTarget: "y", Operand: "z", }, }, Resources: &api.Resources{ CPU: pointer.Of(100), MemoryMB: pointer.Of(10), Networks: []*api.NetworkResource{ { IP: "10.10.11.1", MBits: pointer.Of(10), ReservedPorts: []api.Port{ { Label: "http", Value: 80, }, }, DynamicPorts: []api.Port{ { Label: "ssh", Value: 2000, }, }, }, }, }, Meta: map[string]string{ "lol": "code", }, KillTimeout: pointer.Of(10 * time.Second), KillSignal: "SIGQUIT", LogConfig: &api.LogConfig{ MaxFiles: pointer.Of(10), MaxFileSizeMB: pointer.Of(100), }, Artifacts: []*api.TaskArtifact{ { GetterSource: pointer.Of("source"), GetterOptions: map[string]string{"a": "b"}, GetterHeaders: map[string]string{"User-Agent": "nomad"}, GetterMode: pointer.Of("dir"), RelativeDest: pointer.Of("dest"), }, }, DispatchPayload: &api.DispatchPayloadConfig{ File: "fileA", }, }, }, }, }, Status: pointer.Of("status"), StatusDescription: pointer.Of("status_desc"), Version: pointer.Of(uint64(10)), CreateIndex: pointer.Of(uint64(1)), ModifyIndex: pointer.Of(uint64(3)), JobModifyIndex: pointer.Of(uint64(5)), } expectedSystemJob := &structs.Job{ Stop: true, Region: "global", Namespace: "foo", ID: "foo", Name: "name", Type: "system", Priority: 50, AllAtOnce: true, Datacenters: []string{"dc1", "dc2"}, Constraints: []*structs.Constraint{ { LTarget: "a", RTarget: "b", Operand: "c", }, }, TaskGroups: []*structs.TaskGroup{ { Name: "group1", Count: 5, Constraints: []*structs.Constraint{ { LTarget: "x", RTarget: "y", Operand: "z", }, }, RestartPolicy: &structs.RestartPolicy{ Interval: 1 * time.Second, Attempts: 5, Delay: 10 * time.Second, Mode: "delay", }, EphemeralDisk: &structs.EphemeralDisk{ SizeMB: 100, Sticky: true, Migrate: true, }, Meta: map[string]string{ "key": "value", }, Consul: &structs.Consul{ Namespace: "foo", }, Tasks: []*structs.Task{ { Name: "task1", Driver: "docker", Leader: true, User: "mary", Config: map[string]interface{}{ "lol": "code", }, Constraints: []*structs.Constraint{ { LTarget: "x", RTarget: "y", Operand: "z", }, }, Env: map[string]string{ "hello": "world", }, Resources: &structs.Resources{ CPU: 100, MemoryMB: 10, Networks: []*structs.NetworkResource{ { IP: "10.10.11.1", MBits: 10, ReservedPorts: []structs.Port{ { Label: "http", Value: 80, }, }, DynamicPorts: []structs.Port{ { Label: "ssh", Value: 2000, }, }, }, }, }, RestartPolicy: &structs.RestartPolicy{ Interval: 1 * time.Second, Attempts: 5, Delay: 10 * time.Second, Mode: "delay", }, Meta: map[string]string{ "lol": "code", }, KillTimeout: 10 * time.Second, KillSignal: "SIGQUIT", LogConfig: &structs.LogConfig{ MaxFiles: 10, MaxFileSizeMB: 100, }, Artifacts: []*structs.TaskArtifact{ { GetterSource: "source", GetterOptions: map[string]string{"a": "b"}, GetterHeaders: map[string]string{"User-Agent": "nomad"}, GetterMode: "dir", RelativeDest: "dest", }, }, DispatchPayload: &structs.DispatchPayloadConfig{ File: "fileA", }, }, }, }, }, } systemStructsJob := ApiJobToStructJob(systemAPIJob) require.Equal(t, expectedSystemJob, systemStructsJob) } func TestJobs_ApiJobToStructsJobUpdate(t *testing.T) { ci.Parallel(t) apiJob := &api.Job{ Update: &api.UpdateStrategy{ Stagger: pointer.Of(1 * time.Second), MaxParallel: pointer.Of(5), HealthCheck: pointer.Of(structs.UpdateStrategyHealthCheck_Manual), MinHealthyTime: pointer.Of(1 * time.Minute), HealthyDeadline: pointer.Of(3 * time.Minute), ProgressDeadline: pointer.Of(3 * time.Minute), AutoRevert: pointer.Of(false), AutoPromote: nil, Canary: pointer.Of(1), }, TaskGroups: []*api.TaskGroup{ { Update: &api.UpdateStrategy{ Canary: pointer.Of(2), AutoRevert: pointer.Of(true), }, }, { Update: &api.UpdateStrategy{ Canary: pointer.Of(3), AutoPromote: pointer.Of(true), }, }, }, } structsJob := ApiJobToStructJob(apiJob) // Update has been moved from job down to the groups jobUpdate := structs.UpdateStrategy{ Stagger: 1000000000, MaxParallel: 5, HealthCheck: "", MinHealthyTime: 0, HealthyDeadline: 0, ProgressDeadline: 0, AutoRevert: false, AutoPromote: false, Canary: 0, } // But the groups inherit settings from the job update group1 := structs.UpdateStrategy{ Stagger: 1000000000, MaxParallel: 5, HealthCheck: "manual", MinHealthyTime: 60000000000, HealthyDeadline: 180000000000, ProgressDeadline: 180000000000, AutoRevert: true, AutoPromote: false, Canary: 2, } group2 := structs.UpdateStrategy{ Stagger: 1000000000, MaxParallel: 5, HealthCheck: "manual", MinHealthyTime: 60000000000, HealthyDeadline: 180000000000, ProgressDeadline: 180000000000, AutoRevert: false, AutoPromote: true, Canary: 3, } require.Equal(t, jobUpdate, structsJob.Update) require.Equal(t, group1, *structsJob.TaskGroups[0].Update) require.Equal(t, group2, *structsJob.TaskGroups[1].Update) } // TestJobs_Matching_Resources asserts: // // api.{Default,Min}Resources == structs.{Default,Min}Resources // // While this is an odd place to test that, this is where both are imported, // validated, and converted. func TestJobs_Matching_Resources(t *testing.T) { ci.Parallel(t) // api.MinResources == structs.MinResources structsMinRes := ApiResourcesToStructs(api.MinResources()) assert.Equal(t, structs.MinResources(), structsMinRes) // api.DefaultResources == structs.DefaultResources structsDefaultRes := ApiResourcesToStructs(api.DefaultResources()) assert.Equal(t, structs.DefaultResources(), structsDefaultRes) } // TestHTTP_JobValidate_SystemMigrate asserts that a system job with a migrate // block fails to validate but does not panic (see #5477). func TestHTTP_JobValidate_SystemMigrate(t *testing.T) { ci.Parallel(t) httpTest(t, nil, func(s *TestAgent) { // Create the job job := &api.Job{ Region: pointer.Of("global"), Datacenters: []string{"dc1"}, ID: pointer.Of("systemmigrate"), Name: pointer.Of("systemmigrate"), TaskGroups: []*api.TaskGroup{ {Name: pointer.Of("web")}, }, // System job... Type: pointer.Of("system"), // ...with an empty migrate block Migrate: &api.MigrateStrategy{}, } args := api.JobValidateRequest{ Job: job, WriteRequest: api.WriteRequest{Region: "global"}, } buf := encodeReq(args) // Make the HTTP request req, err := http.NewRequest("PUT", "/v1/validate/job", buf) must.NoError(t, err) respW := httptest.NewRecorder() // Make the request obj, err := s.Server.ValidateJobRequest(respW, req) must.NoError(t, err) // Check the response resp := obj.(structs.JobValidateResponse) must.StrContains(t, resp.Error, `Job type "system" does not allow migrate block`) must.Len(t, 1, resp.ValidationErrors) }) } func TestConversion_dereferenceInt(t *testing.T) { ci.Parallel(t) require.Equal(t, 0, dereferenceInt(nil)) require.Equal(t, 42, dereferenceInt(pointer.Of(42))) } func TestConversion_apiLogConfigToStructs(t *testing.T) { ci.Parallel(t) require.Nil(t, apiLogConfigToStructs(nil)) require.Equal(t, &structs.LogConfig{ MaxFiles: 2, MaxFileSizeMB: 8, }, apiLogConfigToStructs(&api.LogConfig{ MaxFiles: pointer.Of(2), MaxFileSizeMB: pointer.Of(8), })) } func TestConversion_apiResourcesToStructs(t *testing.T) { ci.Parallel(t) cases := []struct { name string input *api.Resources expected *structs.Resources }{ { "nil", nil, nil, }, { "plain", &api.Resources{ CPU: pointer.Of(100), MemoryMB: pointer.Of(200), }, &structs.Resources{ CPU: 100, MemoryMB: 200, }, }, { "with memory max", &api.Resources{ CPU: pointer.Of(100), MemoryMB: pointer.Of(200), MemoryMaxMB: pointer.Of(300), }, &structs.Resources{ CPU: 100, MemoryMB: 200, MemoryMaxMB: 300, }, }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { found := ApiResourcesToStructs(c.input) require.Equal(t, c.expected, found) }) } } func TestConversion_apiConnectSidecarTaskToStructs(t *testing.T) { ci.Parallel(t) require.Nil(t, apiConnectSidecarTaskToStructs(nil)) delay := time.Duration(200) timeout := time.Duration(1000) config := make(map[string]interface{}) env := make(map[string]string) meta := make(map[string]string) require.Equal(t, &structs.SidecarTask{ Name: "name", Driver: "driver", User: "user", Config: config, Env: env, Resources: &structs.Resources{ CPU: 1, MemoryMB: 128, }, Meta: meta, KillTimeout: &timeout, LogConfig: &structs.LogConfig{ MaxFiles: 2, MaxFileSizeMB: 8, }, ShutdownDelay: &delay, KillSignal: "SIGTERM", }, apiConnectSidecarTaskToStructs(&api.SidecarTask{ Name: "name", Driver: "driver", User: "user", Config: config, Env: env, Resources: &api.Resources{ CPU: pointer.Of(1), MemoryMB: pointer.Of(128), }, Meta: meta, KillTimeout: &timeout, LogConfig: &api.LogConfig{ MaxFiles: pointer.Of(2), MaxFileSizeMB: pointer.Of(8), }, ShutdownDelay: &delay, KillSignal: "SIGTERM", })) } func TestConversion_apiConsulExposePathsToStructs(t *testing.T) { ci.Parallel(t) require.Nil(t, apiConsulExposePathsToStructs(nil)) require.Nil(t, apiConsulExposePathsToStructs(make([]*api.ConsulExposePath, 0))) require.Equal(t, []structs.ConsulExposePath{{ Path: "/health", Protocol: "http", LocalPathPort: 8080, ListenerPort: "hcPort", }}, apiConsulExposePathsToStructs([]*api.ConsulExposePath{{ Path: "/health", Protocol: "http", LocalPathPort: 8080, ListenerPort: "hcPort", }})) } func TestConversion_apiConsulExposeConfigToStructs(t *testing.T) { ci.Parallel(t) require.Nil(t, apiConsulExposeConfigToStructs(nil)) require.Equal(t, &structs.ConsulExposeConfig{ Paths: []structs.ConsulExposePath{{Path: "/health"}}, }, apiConsulExposeConfigToStructs(&api.ConsulExposeConfig{ Paths: []*api.ConsulExposePath{{Path: "/health"}}, })) } func TestConversion_apiUpstreamsToStructs(t *testing.T) { ci.Parallel(t) require.Nil(t, apiUpstreamsToStructs(nil)) require.Nil(t, apiUpstreamsToStructs(make([]*api.ConsulUpstream, 0))) require.Equal(t, []structs.ConsulUpstream{{ DestinationName: "upstream", DestinationNamespace: "ns2", LocalBindPort: 8000, Datacenter: "dc2", LocalBindAddress: "127.0.0.2", MeshGateway: structs.ConsulMeshGateway{Mode: "local"}, }}, apiUpstreamsToStructs([]*api.ConsulUpstream{{ DestinationName: "upstream", DestinationNamespace: "ns2", LocalBindPort: 8000, Datacenter: "dc2", LocalBindAddress: "127.0.0.2", MeshGateway: &api.ConsulMeshGateway{Mode: "local"}, }})) } func TestConversion_apiConsulMeshGatewayToStructs(t *testing.T) { ci.Parallel(t) require.Equal(t, structs.ConsulMeshGateway{}, apiMeshGatewayToStructs(nil)) require.Equal(t, structs.ConsulMeshGateway{Mode: "remote"}, apiMeshGatewayToStructs(&api.ConsulMeshGateway{Mode: "remote"})) } func TestConversion_apiConnectSidecarServiceProxyToStructs(t *testing.T) { ci.Parallel(t) require.Nil(t, apiConnectSidecarServiceProxyToStructs(nil)) config := make(map[string]interface{}) require.Equal(t, &structs.ConsulProxy{ LocalServiceAddress: "192.168.30.1", LocalServicePort: 9000, Config: map[string]any{}, Upstreams: []structs.ConsulUpstream{{ DestinationName: "upstream", }}, Expose: &structs.ConsulExposeConfig{ Paths: []structs.ConsulExposePath{{Path: "/health"}}, }, }, apiConnectSidecarServiceProxyToStructs(&api.ConsulProxy{ LocalServiceAddress: "192.168.30.1", LocalServicePort: 9000, Config: config, Upstreams: []*api.ConsulUpstream{{ DestinationName: "upstream", }}, Expose: &api.ConsulExposeConfig{ Paths: []*api.ConsulExposePath{{ Path: "/health", }}, }, })) } func TestConversion_apiConnectSidecarServiceToStructs(t *testing.T) { ci.Parallel(t) require.Nil(t, apiConnectSidecarTaskToStructs(nil)) require.Equal(t, &structs.ConsulSidecarService{ Tags: []string{"foo"}, Port: "myPort", Proxy: &structs.ConsulProxy{ LocalServiceAddress: "192.168.30.1", }, }, apiConnectSidecarServiceToStructs(&api.ConsulSidecarService{ Tags: []string{"foo"}, Port: "myPort", Proxy: &api.ConsulProxy{ LocalServiceAddress: "192.168.30.1", }, })) } func TestConversion_ApiConsulConnectToStructs(t *testing.T) { ci.Parallel(t) t.Run("nil", func(t *testing.T) { require.Nil(t, ApiConsulConnectToStructs(nil)) }) t.Run("sidecar", func(t *testing.T) { require.Equal(t, &structs.ConsulConnect{ Native: false, SidecarService: &structs.ConsulSidecarService{Port: "myPort"}, SidecarTask: &structs.SidecarTask{Name: "task"}, }, ApiConsulConnectToStructs(&api.ConsulConnect{ Native: false, SidecarService: &api.ConsulSidecarService{Port: "myPort"}, SidecarTask: &api.SidecarTask{Name: "task"}, })) }) t.Run("gateway proxy", func(t *testing.T) { require.Equal(t, &structs.ConsulConnect{ Gateway: &structs.ConsulGateway{ Proxy: &structs.ConsulGatewayProxy{ ConnectTimeout: pointer.Of(3 * time.Second), EnvoyGatewayBindTaggedAddresses: true, EnvoyGatewayBindAddresses: map[string]*structs.ConsulGatewayBindAddress{ "service": { Address: "10.0.0.1", Port: 9000, }}, EnvoyGatewayNoDefaultBind: true, EnvoyDNSDiscoveryType: "STRICT_DNS", Config: map[string]interface{}{ "foo": "bar", }, }, }, }, ApiConsulConnectToStructs(&api.ConsulConnect{ Gateway: &api.ConsulGateway{ Proxy: &api.ConsulGatewayProxy{ ConnectTimeout: pointer.Of(3 * time.Second), EnvoyGatewayBindTaggedAddresses: true, EnvoyGatewayBindAddresses: map[string]*api.ConsulGatewayBindAddress{ "service": { Address: "10.0.0.1", Port: 9000, }, }, EnvoyGatewayNoDefaultBind: true, EnvoyDNSDiscoveryType: "STRICT_DNS", Config: map[string]interface{}{ "foo": "bar", }, }, }, })) }) t.Run("gateway ingress", func(t *testing.T) { require.Equal(t, &structs.ConsulConnect{ Gateway: &structs.ConsulGateway{ Ingress: &structs.ConsulIngressConfigEntry{ TLS: &structs.ConsulGatewayTLSConfig{ Enabled: true, TLSMinVersion: "TLSv1_2", TLSMaxVersion: "TLSv1_3", CipherSuites: []string{"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"}, }, Listeners: []*structs.ConsulIngressListener{{ Port: 1111, Protocol: "http", Services: []*structs.ConsulIngressService{{ Name: "ingress1", Hosts: []string{"host1"}, }}, }}, }, }, }, ApiConsulConnectToStructs( &api.ConsulConnect{ Gateway: &api.ConsulGateway{ Ingress: &api.ConsulIngressConfigEntry{ TLS: &api.ConsulGatewayTLSConfig{ Enabled: true, TLSMinVersion: "TLSv1_2", TLSMaxVersion: "TLSv1_3", CipherSuites: []string{"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"}, }, Listeners: []*api.ConsulIngressListener{{ Port: 1111, Protocol: "http", Services: []*api.ConsulIngressService{{ Name: "ingress1", Hosts: []string{"host1"}, }}, }}, }, }, }, )) }) t.Run("gateway terminating", func(t *testing.T) { require.Equal(t, &structs.ConsulConnect{ Gateway: &structs.ConsulGateway{ Terminating: &structs.ConsulTerminatingConfigEntry{ Services: []*structs.ConsulLinkedService{{ Name: "linked-service", CAFile: "ca.pem", CertFile: "cert.pem", KeyFile: "key.pem", SNI: "linked.consul", }}, }, }, }, ApiConsulConnectToStructs(&api.ConsulConnect{ Gateway: &api.ConsulGateway{ Terminating: &api.ConsulTerminatingConfigEntry{ Services: []*api.ConsulLinkedService{{ Name: "linked-service", CAFile: "ca.pem", CertFile: "cert.pem", KeyFile: "key.pem", SNI: "linked.consul", }}, }, }, })) }) t.Run("gateway mesh", func(t *testing.T) { require.Equal(t, &structs.ConsulConnect{ Gateway: &structs.ConsulGateway{ Mesh: &structs.ConsulMeshConfigEntry{ // nothing }, }, }, ApiConsulConnectToStructs(&api.ConsulConnect{ Gateway: &api.ConsulGateway{ Mesh: &api.ConsulMeshConfigEntry{ // nothing }, }, })) }) t.Run("native", func(t *testing.T) { require.Equal(t, &structs.ConsulConnect{ Native: true, }, ApiConsulConnectToStructs(&api.ConsulConnect{ Native: true, })) }) }