open-nomad/command/agent/job_endpoint_test.go
James Rasell 751c8217d1
core: allow setting and propagation of eval priority on job de/registration (#11532)
This change modifies the Nomad job register and deregister RPCs to
accept an updated option set which includes eval priority. This
param is optional and override the use of the job priority to set
the eval priority.

In order to ensure all evaluations as a result of the request use
the same eval priority, the priority is shared to the
allocReconciler and deploymentWatcher. This creates a new
distinction between eval priority and job priority.

The Nomad agent HTTP API has been modified to allow setting the
eval priority on job update and delete. To keep consistency with
the current v1 API, job update accepts this as a payload param;
job delete accepts this as a query param.

Any user supplied value is validated within the agent HTTP handler
removing the need to pass invalid requests to the server.

The register and deregister opts functions now all for setting
the eval priority on requests.

The change includes a small change to the DeregisterOpts function
which handles nil opts. This brings the function inline with the
RegisterOpts.
2021-11-23 09:23:31 +01:00

3644 lines
92 KiB
Go

package agent
import (
"net/http"
"net/http/httptest"
"reflect"
"strconv"
"testing"
"time"
"github.com/golang/snappy"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
api "github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/helper"
"github.com/hashicorp/nomad/nomad/mock"
"github.com/hashicorp/nomad/nomad/structs"
)
func TestHTTP_JobsList(t *testing.T) {
t.Parallel()
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.HeaderMap.Get("X-Nomad-Index") == "" {
t.Fatalf("missing index")
}
if respW.HeaderMap.Get("X-Nomad-KnownLeader") != "true" {
t.Fatalf("missing known leader")
}
if respW.HeaderMap.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) {
ids := []string{
"aaaaaaaa-e8f7-fd38-c855-ab94ceb89706",
"aabbbbbb-e8f7-fd38-c855-ab94ceb89706",
"aabbcccc-e8f7-fd38-c855-ab94ceb89706",
}
t.Parallel()
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.HeaderMap.Get("X-Nomad-Index") == "" {
t.Fatalf("missing index")
}
if respW.HeaderMap.Get("X-Nomad-KnownLeader") != "true" {
t.Fatalf("missing known leader")
}
if respW.HeaderMap.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) {
t.Parallel()
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.HeaderMap.Get("X-Nomad-Index"), "missing index")
require.Equal(t, "true", respW.HeaderMap.Get("X-Nomad-KnownLeader"), "missing known leader")
require.NotEmpty(t, respW.HeaderMap.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) {
t.Parallel()
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.HeaderMap.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) {
t.Parallel()
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.HeaderMap.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) {
t.Parallel()
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) {
t.Parallel()
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.HeaderMap.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) {
t.Parallel()
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_JobQuery(t *testing.T) {
t.Parallel()
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.HeaderMap.Get("X-Nomad-Index") == "" {
t.Fatalf("missing index")
}
if respW.HeaderMap.Get("X-Nomad-KnownLeader") != "true" {
t.Fatalf("missing known leader")
}
if respW.HeaderMap.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) {
t.Parallel()
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.HeaderMap.Get("X-Nomad-Index") == "" {
t.Fatalf("missing index")
}
if respW.HeaderMap.Get("X-Nomad-KnownLeader") != "true" {
t.Fatalf("missing known leader")
}
if respW.HeaderMap.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) {
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
// Create the job
job := MockJob()
job.Type = helper.StringToPtr("system")
job.TaskGroups[0].Scaling = &api.ScalingPolicy{Enabled: helper.BoolToPtr(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 stanzas"), err)
})
}
func TestHTTP_JobUpdate(t *testing.T) {
t.Parallel()
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.HeaderMap.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) {
t.Parallel()
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) {
t.Parallel()
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.HeaderMap.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) {
t.Parallel()
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.HeaderMap.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.HeaderMap.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) {
t.Parallel()
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) {
t.Parallel()
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: helper.Int64ToPtr(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) {
t.Parallel()
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) {
t.Parallel()
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.HeaderMap.Get("X-Nomad-Index") == "" {
t.Fatalf("missing index")
}
})
}
func TestHTTP_JobEvaluate_ForceReschedule(t *testing.T) {
t.Parallel()
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.HeaderMap.Get("X-Nomad-Index") == "" {
t.Fatalf("missing index")
}
})
}
func TestHTTP_JobEvaluations(t *testing.T) {
t.Parallel()
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.HeaderMap.Get("X-Nomad-Index") == "" {
t.Fatalf("missing index")
}
if respW.HeaderMap.Get("X-Nomad-KnownLeader") != "true" {
t.Fatalf("missing known leader")
}
if respW.HeaderMap.Get("X-Nomad-LastContact") == "" {
t.Fatalf("missing last contact")
}
})
}
func TestHTTP_JobAllocations(t *testing.T) {
t.Parallel()
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.HeaderMap.Get("X-Nomad-Index") == "" {
t.Fatalf("missing index")
}
if respW.HeaderMap.Get("X-Nomad-KnownLeader") != "true" {
t.Fatalf("missing known leader")
}
if respW.HeaderMap.Get("X-Nomad-LastContact") == "" {
t.Fatalf("missing last contact")
}
})
}
func TestHTTP_JobDeployments(t *testing.T) {
assert := assert.New(t)
t.Parallel()
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.HeaderMap.Get("X-Nomad-Index"), "missing index")
assert.Equal("true", respW.HeaderMap.Get("X-Nomad-KnownLeader"), "missing known leader")
assert.NotZero(respW.HeaderMap.Get("X-Nomad-LastContact"), "missing last contact")
})
}
func TestHTTP_JobDeployment(t *testing.T) {
assert := assert.New(t)
t.Parallel()
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.HeaderMap.Get("X-Nomad-Index"), "missing index")
assert.Equal("true", respW.HeaderMap.Get("X-Nomad-KnownLeader"), "missing known leader")
assert.NotZero(respW.HeaderMap.Get("X-Nomad-LastContact"), "missing last contact")
})
}
func TestHTTP_JobVersions(t *testing.T) {
t.Parallel()
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.HeaderMap.Get("X-Nomad-Index") == "" {
t.Fatalf("missing index")
}
if respW.HeaderMap.Get("X-Nomad-KnownLeader") != "true" {
t.Fatalf("missing known leader")
}
if respW.HeaderMap.Get("X-Nomad-LastContact") == "" {
t.Fatalf("missing last contact")
}
})
}
func TestHTTP_PeriodicForce(t *testing.T) {
t.Parallel()
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.HeaderMap.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) {
t.Parallel()
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) {
t.Parallel()
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) {
t.Parallel()
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) {
t.Parallel()
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", &regReq, &regResp); 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", &regReq, &regResp); 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.HeaderMap.Get("X-Nomad-Index") == "" {
t.Fatalf("missing index")
}
})
}
func TestHTTP_JobStable(t *testing.T) {
t.Parallel()
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", &regReq, &regResp); err != nil {
t.Fatalf("err: %v", err)
}
if err := s.Agent.RPC("Job.Register", &regReq, &regResp); 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.HeaderMap.Get("X-Nomad-Index") == "" {
t.Fatalf("missing index")
}
})
}
func TestJobs_ParsingWriteRequest(t *testing.T) {
t.Parallel()
// 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: helper.StringToPtr(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)
require.Equal(t, tc.expectedJobRegion, sJob.Region)
require.Equal(t, tc.expectedNamespace, sJob.Namespace)
require.Equal(t, tc.expectedNamespace, sWriteReq.Namespace)
require.Equal(t, tc.expectedRequestRegion, sWriteReq.Region)
require.Equal(t, tc.expectedToken, sWriteReq.AuthToken)
})
}
}
func TestJobs_RegionForJob(t *testing.T) {
t.Parallel()
// 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: helper.StringToPtr(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) {
t.Parallel()
// 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 TestJobs_ApiJobToStructsJob(t *testing.T) {
apiJob := &api.Job{
Stop: helper.BoolToPtr(true),
Region: helper.StringToPtr("global"),
Namespace: helper.StringToPtr("foo"),
ID: helper.StringToPtr("foo"),
ParentID: helper.StringToPtr("lol"),
Name: helper.StringToPtr("name"),
Type: helper.StringToPtr("service"),
Priority: helper.IntToPtr(50),
AllAtOnce: helper.BoolToPtr(true),
Datacenters: []string{"dc1", "dc2"},
Constraints: []*api.Constraint{
{
LTarget: "a",
RTarget: "b",
Operand: "c",
},
},
Affinities: []*api.Affinity{
{
LTarget: "a",
RTarget: "b",
Operand: "c",
Weight: helper.Int8ToPtr(50),
},
},
Update: &api.UpdateStrategy{
Stagger: helper.TimeToPtr(1 * time.Second),
MaxParallel: helper.IntToPtr(5),
HealthCheck: helper.StringToPtr(structs.UpdateStrategyHealthCheck_Manual),
MinHealthyTime: helper.TimeToPtr(1 * time.Minute),
HealthyDeadline: helper.TimeToPtr(3 * time.Minute),
ProgressDeadline: helper.TimeToPtr(3 * time.Minute),
AutoRevert: helper.BoolToPtr(false),
Canary: helper.IntToPtr(1),
},
Spreads: []*api.Spread{
{
Attribute: "${meta.rack}",
Weight: helper.Int8ToPtr(100),
SpreadTarget: []*api.SpreadTarget{
{
Value: "r1",
Percent: 50,
},
},
},
},
Periodic: &api.PeriodicConfig{
Enabled: helper.BoolToPtr(true),
Spec: helper.StringToPtr("spec"),
SpecType: helper.StringToPtr("cron"),
ProhibitOverlap: helper.BoolToPtr(true),
TimeZone: helper.StringToPtr("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: helper.IntToPtr(2),
OnFailure: helper.StringToPtr("fail_all"),
},
Regions: []*api.MultiregionRegion{
{
Name: "west",
Count: helper.IntToPtr(1),
Datacenters: []string{"dc1", "dc2"},
Meta: map[string]string{"region_code": "W"},
},
},
},
TaskGroups: []*api.TaskGroup{
{
Name: helper.StringToPtr("group1"),
Count: helper.IntToPtr(5),
Constraints: []*api.Constraint{
{
LTarget: "x",
RTarget: "y",
Operand: "z",
},
},
Affinities: []*api.Affinity{
{
LTarget: "x",
RTarget: "y",
Operand: "z",
Weight: helper.Int8ToPtr(100),
},
},
RestartPolicy: &api.RestartPolicy{
Interval: helper.TimeToPtr(1 * time.Second),
Attempts: helper.IntToPtr(5),
Delay: helper.TimeToPtr(10 * time.Second),
Mode: helper.StringToPtr("delay"),
},
ReschedulePolicy: &api.ReschedulePolicy{
Interval: helper.TimeToPtr(12 * time.Hour),
Attempts: helper.IntToPtr(5),
DelayFunction: helper.StringToPtr("constant"),
Delay: helper.TimeToPtr(30 * time.Second),
Unlimited: helper.BoolToPtr(true),
MaxDelay: helper.TimeToPtr(20 * time.Minute),
},
Migrate: &api.MigrateStrategy{
MaxParallel: helper.IntToPtr(12),
HealthCheck: helper.StringToPtr("task_events"),
MinHealthyTime: helper.TimeToPtr(12 * time.Hour),
HealthyDeadline: helper.TimeToPtr(12 * time.Hour),
},
Spreads: []*api.Spread{
{
Attribute: "${node.datacenter}",
Weight: helper.Int8ToPtr(100),
SpreadTarget: []*api.SpreadTarget{
{
Value: "dc1",
Percent: 100,
},
},
},
},
EphemeralDisk: &api.EphemeralDisk{
SizeMB: helper.IntToPtr(100),
Sticky: helper.BoolToPtr(true),
Migrate: helper.BoolToPtr(true),
},
Update: &api.UpdateStrategy{
HealthCheck: helper.StringToPtr(structs.UpdateStrategyHealthCheck_Checks),
MinHealthyTime: helper.TimeToPtr(2 * time.Minute),
HealthyDeadline: helper.TimeToPtr(5 * time.Minute),
ProgressDeadline: helper.TimeToPtr(5 * time.Minute),
AutoRevert: helper.BoolToPtr(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",
Meta: map[string]string{
"servicemeta": "foobar",
},
CheckRestart: &api.CheckRestart{
Limit: 4,
Grace: helper.TimeToPtr(11 * time.Second),
},
Checks: []api.ServiceCheck{
{
Id: "hello",
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,
},
},
},
},
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: helper.Int8ToPtr(50),
},
},
VolumeMounts: []*api.VolumeMount{
{
Volume: helper.StringToPtr("vol"),
Destination: helper.StringToPtr("dest"),
ReadOnly: helper.BoolToPtr(false),
PropagationMode: helper.StringToPtr("a"),
},
},
RestartPolicy: &api.RestartPolicy{
Interval: helper.TimeToPtr(2 * time.Second),
Attempts: helper.IntToPtr(10),
Delay: helper.TimeToPtr(20 * time.Second),
Mode: helper.StringToPtr("delay"),
},
Services: []*api.Service{
{
Id: "id",
Name: "serviceA",
Tags: []string{"1", "2"},
CanaryTags: []string{"3", "4"},
EnableTagOverride: true,
PortLabel: "foo",
Meta: map[string]string{
"servicemeta": "foobar",
},
CheckRestart: &api.CheckRestart{
Limit: 4,
Grace: helper.TimeToPtr(11 * time.Second),
},
Checks: []api.ServiceCheck{
{
Id: "hello",
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,
},
},
{
Id: "check2id",
Name: "check2",
Type: "tcp",
PortLabel: "foo",
Interval: 4 * time.Second,
Timeout: 2 * time.Second,
},
},
},
},
Resources: &api.Resources{
CPU: helper.IntToPtr(100),
MemoryMB: helper.IntToPtr(10),
Networks: []*api.NetworkResource{
{
IP: "10.10.11.1",
MBits: helper.IntToPtr(10),
Hostname: "foobar",
ReservedPorts: []api.Port{
{
Label: "http",
Value: 80,
},
},
DynamicPorts: []api.Port{
{
Label: "ssh",
Value: 2000,
},
},
},
},
Devices: []*api.RequestedDevice{
{
Name: "nvidia/gpu",
Count: helper.Uint64ToPtr(4),
Constraints: []*api.Constraint{
{
LTarget: "x",
RTarget: "y",
Operand: "z",
},
},
Affinities: []*api.Affinity{
{
LTarget: "a",
RTarget: "b",
Operand: "c",
Weight: helper.Int8ToPtr(50),
},
},
},
{
Name: "gpu",
Count: nil,
},
},
},
Meta: map[string]string{
"lol": "code",
},
KillTimeout: helper.TimeToPtr(10 * time.Second),
KillSignal: "SIGQUIT",
LogConfig: &api.LogConfig{
MaxFiles: helper.IntToPtr(10),
MaxFileSizeMB: helper.IntToPtr(100),
},
Artifacts: []*api.TaskArtifact{
{
GetterSource: helper.StringToPtr("source"),
GetterOptions: map[string]string{
"a": "b",
},
GetterMode: helper.StringToPtr("dir"),
RelativeDest: helper.StringToPtr("dest"),
},
},
Vault: &api.Vault{
Namespace: helper.StringToPtr("ns1"),
Policies: []string{"a", "b", "c"},
Env: helper.BoolToPtr(true),
ChangeMode: helper.StringToPtr("c"),
ChangeSignal: helper.StringToPtr("sighup"),
},
Templates: []*api.Template{
{
SourcePath: helper.StringToPtr("source"),
DestPath: helper.StringToPtr("dest"),
EmbeddedTmpl: helper.StringToPtr("embedded"),
ChangeMode: helper.StringToPtr("change"),
ChangeSignal: helper.StringToPtr("signal"),
Splay: helper.TimeToPtr(1 * time.Minute),
Perms: helper.StringToPtr("666"),
LeftDelim: helper.StringToPtr("abc"),
RightDelim: helper.StringToPtr("def"),
Envvars: helper.BoolToPtr(true),
},
},
DispatchPayload: &api.DispatchPayloadConfig{
File: "fileA",
},
},
},
},
},
ConsulToken: helper.StringToPtr("abc123"),
VaultToken: helper.StringToPtr("def456"),
VaultNamespace: helper.StringToPtr("ghi789"),
Status: helper.StringToPtr("status"),
StatusDescription: helper.StringToPtr("status_desc"),
Version: helper.Uint64ToPtr(10),
CreateIndex: helper.Uint64ToPtr(1),
ModifyIndex: helper.Uint64ToPtr(3),
JobModifyIndex: helper.Uint64ToPtr(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",
Tags: []string{"a", "b"},
CanaryTags: []string{"d", "e"},
EnableTagOverride: true,
PortLabel: "1234",
AddressMode: "auto",
Meta: map[string]string{
"servicemeta": "foobar",
},
OnUpdate: "require_healthy",
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: "require_healthy",
SuccessBeforePassing: 2,
FailuresBeforeCritical: 3,
},
},
Connect: &structs.ConsulConnect{
Native: false,
SidecarService: &structs.ConsulSidecarService{
Tags: []string{"f", "g"},
Port: "9000",
DisableDefaultTCPCheck: true,
},
},
},
},
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",
Tags: []string{"1", "2"},
CanaryTags: []string{"3", "4"},
EnableTagOverride: true,
PortLabel: "foo",
AddressMode: "auto",
Meta: map[string]string{
"servicemeta": "foobar",
},
OnUpdate: "require_healthy",
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: "require_healthy",
},
{
Name: "check2",
Type: "tcp",
PortLabel: "foo",
Interval: 4 * time.Second,
Timeout: 2 * time.Second,
CheckRestart: &structs.CheckRestart{
Limit: 4,
Grace: 11 * time.Second,
},
OnUpdate: "require_healthy",
},
},
},
},
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",
Splay: 1 * time.Minute,
Perms: "666",
LeftDelim: "abc",
RightDelim: "def",
Envvars: true,
},
},
DispatchPayload: &structs.DispatchPayloadConfig{
File: "fileA",
},
},
},
},
},
ConsulToken: "abc123",
VaultToken: "def456",
}
structsJob := ApiJobToStructJob(apiJob)
require.Equal(t, expected, structsJob)
systemAPIJob := &api.Job{
Stop: helper.BoolToPtr(true),
Region: helper.StringToPtr("global"),
Namespace: helper.StringToPtr("foo"),
ID: helper.StringToPtr("foo"),
ParentID: helper.StringToPtr("lol"),
Name: helper.StringToPtr("name"),
Type: helper.StringToPtr("system"),
Priority: helper.IntToPtr(50),
AllAtOnce: helper.BoolToPtr(true),
Datacenters: []string{"dc1", "dc2"},
Constraints: []*api.Constraint{
{
LTarget: "a",
RTarget: "b",
Operand: "c",
},
},
TaskGroups: []*api.TaskGroup{
{
Name: helper.StringToPtr("group1"),
Count: helper.IntToPtr(5),
Constraints: []*api.Constraint{
{
LTarget: "x",
RTarget: "y",
Operand: "z",
},
},
RestartPolicy: &api.RestartPolicy{
Interval: helper.TimeToPtr(1 * time.Second),
Attempts: helper.IntToPtr(5),
Delay: helper.TimeToPtr(10 * time.Second),
Mode: helper.StringToPtr("delay"),
},
EphemeralDisk: &api.EphemeralDisk{
SizeMB: helper.IntToPtr(100),
Sticky: helper.BoolToPtr(true),
Migrate: helper.BoolToPtr(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: helper.IntToPtr(100),
MemoryMB: helper.IntToPtr(10),
Networks: []*api.NetworkResource{
{
IP: "10.10.11.1",
MBits: helper.IntToPtr(10),
ReservedPorts: []api.Port{
{
Label: "http",
Value: 80,
},
},
DynamicPorts: []api.Port{
{
Label: "ssh",
Value: 2000,
},
},
},
},
},
Meta: map[string]string{
"lol": "code",
},
KillTimeout: helper.TimeToPtr(10 * time.Second),
KillSignal: "SIGQUIT",
LogConfig: &api.LogConfig{
MaxFiles: helper.IntToPtr(10),
MaxFileSizeMB: helper.IntToPtr(100),
},
Artifacts: []*api.TaskArtifact{
{
GetterSource: helper.StringToPtr("source"),
GetterOptions: map[string]string{"a": "b"},
GetterHeaders: map[string]string{"User-Agent": "nomad"},
GetterMode: helper.StringToPtr("dir"),
RelativeDest: helper.StringToPtr("dest"),
},
},
DispatchPayload: &api.DispatchPayloadConfig{
File: "fileA",
},
},
},
},
},
Status: helper.StringToPtr("status"),
StatusDescription: helper.StringToPtr("status_desc"),
Version: helper.Uint64ToPtr(10),
CreateIndex: helper.Uint64ToPtr(1),
ModifyIndex: helper.Uint64ToPtr(3),
JobModifyIndex: helper.Uint64ToPtr(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) {
apiJob := &api.Job{
Update: &api.UpdateStrategy{
Stagger: helper.TimeToPtr(1 * time.Second),
MaxParallel: helper.IntToPtr(5),
HealthCheck: helper.StringToPtr(structs.UpdateStrategyHealthCheck_Manual),
MinHealthyTime: helper.TimeToPtr(1 * time.Minute),
HealthyDeadline: helper.TimeToPtr(3 * time.Minute),
ProgressDeadline: helper.TimeToPtr(3 * time.Minute),
AutoRevert: helper.BoolToPtr(false),
AutoPromote: nil,
Canary: helper.IntToPtr(1),
},
TaskGroups: []*api.TaskGroup{
{
Update: &api.UpdateStrategy{
Canary: helper.IntToPtr(2),
AutoRevert: helper.BoolToPtr(true),
},
}, {
Update: &api.UpdateStrategy{
Canary: helper.IntToPtr(3),
AutoPromote: helper.BoolToPtr(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) {
t.Parallel()
// 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
// stanza fails to validate but does not panic (see #5477).
func TestHTTP_JobValidate_SystemMigrate(t *testing.T) {
t.Parallel()
httpTest(t, nil, func(s *TestAgent) {
// Create the job
job := &api.Job{
Region: helper.StringToPtr("global"),
Datacenters: []string{"dc1"},
ID: helper.StringToPtr("systemmigrate"),
Name: helper.StringToPtr("systemmigrate"),
TaskGroups: []*api.TaskGroup{
{Name: helper.StringToPtr("web")},
},
// System job...
Type: helper.StringToPtr("system"),
// ...with an empty migrate stanza
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)
require.NoError(t, err)
respW := httptest.NewRecorder()
// Make the request
obj, err := s.Server.ValidateJobRequest(respW, req)
require.NoError(t, err)
// Check the response
resp := obj.(structs.JobValidateResponse)
require.Contains(t, resp.Error, `Job type "system" does not allow migrate block`)
})
}
func TestConversion_dereferenceInt(t *testing.T) {
t.Parallel()
require.Equal(t, 0, dereferenceInt(nil))
require.Equal(t, 42, dereferenceInt(helper.IntToPtr(42)))
}
func TestConversion_apiLogConfigToStructs(t *testing.T) {
t.Parallel()
require.Nil(t, apiLogConfigToStructs(nil))
require.Equal(t, &structs.LogConfig{
MaxFiles: 2,
MaxFileSizeMB: 8,
}, apiLogConfigToStructs(&api.LogConfig{
MaxFiles: helper.IntToPtr(2),
MaxFileSizeMB: helper.IntToPtr(8),
}))
}
func TestConversion_apiResourcesToStructs(t *testing.T) {
t.Parallel()
cases := []struct {
name string
input *api.Resources
expected *structs.Resources
}{
{
"nil",
nil,
nil,
},
{
"plain",
&api.Resources{
CPU: helper.IntToPtr(100),
MemoryMB: helper.IntToPtr(200),
},
&structs.Resources{
CPU: 100,
MemoryMB: 200,
},
},
{
"with memory max",
&api.Resources{
CPU: helper.IntToPtr(100),
MemoryMB: helper.IntToPtr(200),
MemoryMaxMB: helper.IntToPtr(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) {
t.Parallel()
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: helper.IntToPtr(1),
MemoryMB: helper.IntToPtr(128),
},
Meta: meta,
KillTimeout: &timeout,
LogConfig: &api.LogConfig{
MaxFiles: helper.IntToPtr(2),
MaxFileSizeMB: helper.IntToPtr(8),
},
ShutdownDelay: &delay,
KillSignal: "SIGTERM",
}))
}
func TestConversion_apiConsulExposePathsToStructs(t *testing.T) {
t.Parallel()
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) {
t.Parallel()
require.Nil(t, apiConsulExposeConfigToStructs(nil))
require.Equal(t, &structs.ConsulExposeConfig{
Paths: []structs.ConsulExposePath{{Path: "/health"}},
}, apiConsulExposeConfigToStructs(&api.ConsulExposeConfig{
Path: []*api.ConsulExposePath{{Path: "/health"}},
}))
}
func TestConversion_apiUpstreamsToStructs(t *testing.T) {
t.Parallel()
require.Nil(t, apiUpstreamsToStructs(nil))
require.Nil(t, apiUpstreamsToStructs(make([]*api.ConsulUpstream, 0)))
require.Equal(t, []structs.ConsulUpstream{{
DestinationName: "upstream",
LocalBindPort: 8000,
Datacenter: "dc2",
LocalBindAddress: "127.0.0.2",
MeshGateway: &structs.ConsulMeshGateway{Mode: "local"},
}}, apiUpstreamsToStructs([]*api.ConsulUpstream{{
DestinationName: "upstream",
LocalBindPort: 8000,
Datacenter: "dc2",
LocalBindAddress: "127.0.0.2",
MeshGateway: &api.ConsulMeshGateway{Mode: "local"},
}}))
}
func TestConversion_apiConsulMeshGatewayToStructs(t *testing.T) {
t.Parallel()
require.Nil(t, apiMeshGatewayToStructs(nil))
require.Equal(t, &structs.ConsulMeshGateway{Mode: "remote"},
apiMeshGatewayToStructs(&api.ConsulMeshGateway{Mode: "remote"}))
}
func TestConversion_apiConnectSidecarServiceProxyToStructs(t *testing.T) {
t.Parallel()
require.Nil(t, apiConnectSidecarServiceProxyToStructs(nil))
config := make(map[string]interface{})
require.Equal(t, &structs.ConsulProxy{
LocalServiceAddress: "192.168.30.1",
LocalServicePort: 9000,
Config: nil,
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",
}},
ExposeConfig: &api.ConsulExposeConfig{
Path: []*api.ConsulExposePath{{
Path: "/health",
}},
},
}))
}
func TestConversion_apiConnectSidecarServiceToStructs(t *testing.T) {
t.Parallel()
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) {
t.Parallel()
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: helper.TimeToPtr(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: helper.TimeToPtr(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},
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},
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,
}))
})
}