79c4478f5b
* Support error_on_missing_value for templates * Update docs for template stanza
3930 lines
100 KiB
Go
3930 lines
100 KiB
Go
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/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 stanzas"), 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)
|
|
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) {
|
|
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
|
|
// stanza 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 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) {
|
|
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{
|
|
Path: []*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",
|
|
}},
|
|
ExposeConfig: &api.ConsulExposeConfig{
|
|
Path: []*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,
|
|
}))
|
|
})
|
|
}
|