api: prevent excessice CPU load on job parse
Add new namespace ACL requirement for the /v1/jobs/parse endpoint and return early if HCLv2 parsing fails. The endpoint now requires the new `parse-job` ACL capability or `submit-job`.
This commit is contained in:
parent
437bb4b86d
commit
15f9d54dea
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:security
|
||||||
|
Add ACL requirement and HCL validation to the job parse API endpoint to prevent excessive CPU usage. [CVE-2022-24685](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-24685)
|
||||||
|
```
|
|
@ -26,6 +26,7 @@ const (
|
||||||
|
|
||||||
NamespaceCapabilityDeny = "deny"
|
NamespaceCapabilityDeny = "deny"
|
||||||
NamespaceCapabilityListJobs = "list-jobs"
|
NamespaceCapabilityListJobs = "list-jobs"
|
||||||
|
NamespaceCapabilityParseJob = "parse-job"
|
||||||
NamespaceCapabilityReadJob = "read-job"
|
NamespaceCapabilityReadJob = "read-job"
|
||||||
NamespaceCapabilitySubmitJob = "submit-job"
|
NamespaceCapabilitySubmitJob = "submit-job"
|
||||||
NamespaceCapabilityDispatchJob = "dispatch-job"
|
NamespaceCapabilityDispatchJob = "dispatch-job"
|
||||||
|
@ -146,7 +147,7 @@ func (p *PluginPolicy) isValid() bool {
|
||||||
// isNamespaceCapabilityValid ensures the given capability is valid for a namespace policy
|
// isNamespaceCapabilityValid ensures the given capability is valid for a namespace policy
|
||||||
func isNamespaceCapabilityValid(cap string) bool {
|
func isNamespaceCapabilityValid(cap string) bool {
|
||||||
switch cap {
|
switch cap {
|
||||||
case NamespaceCapabilityDeny, NamespaceCapabilityListJobs, NamespaceCapabilityReadJob,
|
case NamespaceCapabilityDeny, NamespaceCapabilityParseJob, NamespaceCapabilityListJobs, NamespaceCapabilityReadJob,
|
||||||
NamespaceCapabilitySubmitJob, NamespaceCapabilityDispatchJob, NamespaceCapabilityReadLogs,
|
NamespaceCapabilitySubmitJob, NamespaceCapabilityDispatchJob, NamespaceCapabilityReadLogs,
|
||||||
NamespaceCapabilityReadFS, NamespaceCapabilityAllocLifecycle,
|
NamespaceCapabilityReadFS, NamespaceCapabilityAllocLifecycle,
|
||||||
NamespaceCapabilityAllocExec, NamespaceCapabilityAllocNodeExec,
|
NamespaceCapabilityAllocExec, NamespaceCapabilityAllocNodeExec,
|
||||||
|
@ -166,6 +167,7 @@ func isNamespaceCapabilityValid(cap string) bool {
|
||||||
func expandNamespacePolicy(policy string) []string {
|
func expandNamespacePolicy(policy string) []string {
|
||||||
read := []string{
|
read := []string{
|
||||||
NamespaceCapabilityListJobs,
|
NamespaceCapabilityListJobs,
|
||||||
|
NamespaceCapabilityParseJob,
|
||||||
NamespaceCapabilityReadJob,
|
NamespaceCapabilityReadJob,
|
||||||
NamespaceCapabilityCSIListVolume,
|
NamespaceCapabilityCSIListVolume,
|
||||||
NamespaceCapabilityCSIReadVolume,
|
NamespaceCapabilityCSIReadVolume,
|
||||||
|
|
|
@ -29,6 +29,7 @@ func TestParse(t *testing.T) {
|
||||||
Policy: PolicyRead,
|
Policy: PolicyRead,
|
||||||
Capabilities: []string{
|
Capabilities: []string{
|
||||||
NamespaceCapabilityListJobs,
|
NamespaceCapabilityListJobs,
|
||||||
|
NamespaceCapabilityParseJob,
|
||||||
NamespaceCapabilityReadJob,
|
NamespaceCapabilityReadJob,
|
||||||
NamespaceCapabilityCSIListVolume,
|
NamespaceCapabilityCSIListVolume,
|
||||||
NamespaceCapabilityCSIReadVolume,
|
NamespaceCapabilityCSIReadVolume,
|
||||||
|
@ -78,6 +79,7 @@ func TestParse(t *testing.T) {
|
||||||
Policy: PolicyRead,
|
Policy: PolicyRead,
|
||||||
Capabilities: []string{
|
Capabilities: []string{
|
||||||
NamespaceCapabilityListJobs,
|
NamespaceCapabilityListJobs,
|
||||||
|
NamespaceCapabilityParseJob,
|
||||||
NamespaceCapabilityReadJob,
|
NamespaceCapabilityReadJob,
|
||||||
NamespaceCapabilityCSIListVolume,
|
NamespaceCapabilityCSIListVolume,
|
||||||
NamespaceCapabilityCSIReadVolume,
|
NamespaceCapabilityCSIReadVolume,
|
||||||
|
@ -91,6 +93,7 @@ func TestParse(t *testing.T) {
|
||||||
Policy: PolicyWrite,
|
Policy: PolicyWrite,
|
||||||
Capabilities: []string{
|
Capabilities: []string{
|
||||||
NamespaceCapabilityListJobs,
|
NamespaceCapabilityListJobs,
|
||||||
|
NamespaceCapabilityParseJob,
|
||||||
NamespaceCapabilityReadJob,
|
NamespaceCapabilityReadJob,
|
||||||
NamespaceCapabilityCSIListVolume,
|
NamespaceCapabilityCSIListVolume,
|
||||||
NamespaceCapabilityCSIReadVolume,
|
NamespaceCapabilityCSIReadVolume,
|
||||||
|
|
|
@ -16,7 +16,6 @@ import (
|
||||||
"github.com/docker/docker/pkg/ioutils"
|
"github.com/docker/docker/pkg/ioutils"
|
||||||
log "github.com/hashicorp/go-hclog"
|
log "github.com/hashicorp/go-hclog"
|
||||||
"github.com/hashicorp/go-msgpack/codec"
|
"github.com/hashicorp/go-msgpack/codec"
|
||||||
"github.com/hashicorp/nomad/acl"
|
|
||||||
"github.com/hashicorp/nomad/api"
|
"github.com/hashicorp/nomad/api"
|
||||||
cstructs "github.com/hashicorp/nomad/client/structs"
|
cstructs "github.com/hashicorp/nomad/client/structs"
|
||||||
"github.com/hashicorp/nomad/command/agent/host"
|
"github.com/hashicorp/nomad/command/agent/host"
|
||||||
|
@ -62,24 +61,7 @@ func (s *HTTPServer) AgentSelfRequest(resp http.ResponseWriter, req *http.Reques
|
||||||
return nil, CodedError(405, ErrInvalidMethod)
|
return nil, CodedError(405, ErrInvalidMethod)
|
||||||
}
|
}
|
||||||
|
|
||||||
var secret string
|
aclObj, err := s.ResolveToken(req)
|
||||||
s.parseToken(req, &secret)
|
|
||||||
|
|
||||||
var aclObj *acl.ACL
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// Get the member as a server
|
|
||||||
var member serf.Member
|
|
||||||
if srv := s.agent.Server(); srv != nil {
|
|
||||||
member = srv.LocalMember()
|
|
||||||
aclObj, err = srv.ResolveToken(secret)
|
|
||||||
} else {
|
|
||||||
// Not a Server, so use the Client for token resolution. Note
|
|
||||||
// this gets forwarded to a server with AllowStale = true if
|
|
||||||
// the local ACL cache TTL has expired (30s by default)
|
|
||||||
aclObj, err = s.agent.Client().ResolveToken(secret)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -89,6 +71,12 @@ func (s *HTTPServer) AgentSelfRequest(resp http.ResponseWriter, req *http.Reques
|
||||||
return nil, structs.ErrPermissionDenied
|
return nil, structs.ErrPermissionDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the member as a server
|
||||||
|
var member serf.Member
|
||||||
|
if srv := s.agent.Server(); srv != nil {
|
||||||
|
member = srv.LocalMember()
|
||||||
|
}
|
||||||
|
|
||||||
self := agentSelf{
|
self := agentSelf{
|
||||||
Member: nomadMember(member),
|
Member: nomadMember(member),
|
||||||
Stats: s.agent.Stats(),
|
Stats: s.agent.Stats(),
|
||||||
|
@ -671,27 +659,19 @@ func (s *HTTPServer) AgentHostRequest(resp http.ResponseWriter, req *http.Reques
|
||||||
return nil, CodedError(405, ErrInvalidMethod)
|
return nil, CodedError(405, ErrInvalidMethod)
|
||||||
}
|
}
|
||||||
|
|
||||||
var secret string
|
aclObj, err := s.ResolveToken(req)
|
||||||
s.parseToken(req, &secret)
|
|
||||||
|
|
||||||
// Check agent read permissions
|
|
||||||
var aclObj *acl.ACL
|
|
||||||
var enableDebug bool
|
|
||||||
var err error
|
|
||||||
if srv := s.agent.Server(); srv != nil {
|
|
||||||
aclObj, err = srv.ResolveToken(secret)
|
|
||||||
enableDebug = srv.GetConfig().EnableDebug
|
|
||||||
} else {
|
|
||||||
// Not a Server, so use the Client for token resolution. Note
|
|
||||||
// this gets forwarded to a server with AllowStale = true if
|
|
||||||
// the local ACL cache TTL has expired (30s by default)
|
|
||||||
aclObj, err = s.agent.Client().ResolveToken(secret)
|
|
||||||
enableDebug = s.agent.Client().GetConfig().EnableDebug
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check agent read permissions
|
||||||
|
var enableDebug bool
|
||||||
|
if srv := s.agent.Server(); srv != nil {
|
||||||
|
enableDebug = srv.GetConfig().EnableDebug
|
||||||
|
} else {
|
||||||
|
enableDebug = s.agent.Client().GetConfig().EnableDebug
|
||||||
|
}
|
||||||
|
|
||||||
if (aclObj != nil && !aclObj.AllowAgentRead()) ||
|
if (aclObj != nil && !aclObj.AllowAgentRead()) ||
|
||||||
(aclObj == nil && !enableDebug) {
|
(aclObj == nil && !enableDebug) {
|
||||||
return nil, structs.ErrPermissionDenied
|
return nil, structs.ErrPermissionDenied
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
multierror "github.com/hashicorp/go-multierror"
|
multierror "github.com/hashicorp/go-multierror"
|
||||||
"github.com/rs/cors"
|
"github.com/rs/cors"
|
||||||
|
|
||||||
|
"github.com/hashicorp/nomad/acl"
|
||||||
"github.com/hashicorp/nomad/helper/noxssrw"
|
"github.com/hashicorp/nomad/helper/noxssrw"
|
||||||
"github.com/hashicorp/nomad/helper/tlsutil"
|
"github.com/hashicorp/nomad/helper/tlsutil"
|
||||||
"github.com/hashicorp/nomad/nomad/structs"
|
"github.com/hashicorp/nomad/nomad/structs"
|
||||||
|
@ -270,6 +271,31 @@ func (s *HTTPServer) Shutdown() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResolveToken extracts the ACL token secret ID from the request and
|
||||||
|
// translates it into an ACL object. Returns nil if ACLs are disabled.
|
||||||
|
func (s *HTTPServer) ResolveToken(req *http.Request) (*acl.ACL, error) {
|
||||||
|
var secret string
|
||||||
|
s.parseToken(req, &secret)
|
||||||
|
|
||||||
|
var aclObj *acl.ACL
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if srv := s.agent.Server(); srv != nil {
|
||||||
|
aclObj, err = srv.ResolveToken(secret)
|
||||||
|
} else {
|
||||||
|
// Not a Server, so use the Client for token resolution. Note
|
||||||
|
// this gets forwarded to a server with AllowStale = true if
|
||||||
|
// the local ACL cache TTL has expired (30s by default)
|
||||||
|
aclObj, err = s.agent.Client().ResolveToken(secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to resolve ACL token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return aclObj, nil
|
||||||
|
}
|
||||||
|
|
||||||
// registerHandlers is used to attach our handlers to the mux
|
// registerHandlers is used to attach our handlers to the mux
|
||||||
func (s HTTPServer) registerHandlers(enableDebug bool) {
|
func (s HTTPServer) registerHandlers(enableDebug bool) {
|
||||||
s.mux.HandleFunc("/v1/jobs", s.wrap(s.JobsRequest))
|
s.mux.HandleFunc("/v1/jobs", s.wrap(s.JobsRequest))
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/hashicorp/nomad/acl"
|
||||||
"github.com/hashicorp/nomad/api"
|
"github.com/hashicorp/nomad/api"
|
||||||
"github.com/hashicorp/nomad/helper"
|
"github.com/hashicorp/nomad/helper"
|
||||||
"github.com/hashicorp/nomad/helper/testlog"
|
"github.com/hashicorp/nomad/helper/testlog"
|
||||||
|
@ -1315,6 +1316,57 @@ func TestHTTPServer_Limits_OK(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHTTPServer_ResolveToken(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Setup two servers, one with ACL enabled and another with ACL disabled.
|
||||||
|
noACLServer := makeHTTPServer(t, func(c *Config) {
|
||||||
|
c.ACL = &ACLConfig{Enabled: false}
|
||||||
|
})
|
||||||
|
defer noACLServer.Shutdown()
|
||||||
|
|
||||||
|
ACLServer := makeHTTPServer(t, func(c *Config) {
|
||||||
|
c.ACL = &ACLConfig{Enabled: true}
|
||||||
|
})
|
||||||
|
defer ACLServer.Shutdown()
|
||||||
|
|
||||||
|
// Register sample token.
|
||||||
|
state := ACLServer.Agent.server.State()
|
||||||
|
token := mock.CreatePolicyAndToken(t, state, 1000, "node", mock.NodePolicy(acl.PolicyWrite))
|
||||||
|
|
||||||
|
// Tests cases.
|
||||||
|
t.Run("acl disabled", func(t *testing.T) {
|
||||||
|
req := &http.Request{Body: http.NoBody}
|
||||||
|
got, err := noACLServer.Server.ResolveToken(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Nil(t, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("token not found", func(t *testing.T) {
|
||||||
|
req := &http.Request{
|
||||||
|
Body: http.NoBody,
|
||||||
|
Header: make(map[string][]string),
|
||||||
|
}
|
||||||
|
setToken(req, mock.ACLToken())
|
||||||
|
got, err := ACLServer.Server.ResolveToken(req)
|
||||||
|
require.Nil(t, got)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "ACL token not found")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("set token", func(t *testing.T) {
|
||||||
|
req := &http.Request{
|
||||||
|
Body: http.NoBody,
|
||||||
|
Header: make(map[string][]string),
|
||||||
|
}
|
||||||
|
setToken(req, token)
|
||||||
|
got, err := ACLServer.Server.ResolveToken(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, got)
|
||||||
|
require.True(t, got.AllowNodeWrite())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func Test_IsAPIClientError(t *testing.T) {
|
func Test_IsAPIClientError(t *testing.T) {
|
||||||
trueCases := []int{400, 403, 404, 499}
|
trueCases := []int{400, 403, 404, 499}
|
||||||
for _, c := range trueCases {
|
for _, c := range trueCases {
|
||||||
|
@ -1410,6 +1462,12 @@ func setToken(req *http.Request, token *structs.ACLToken) {
|
||||||
req.Header.Set("X-Nomad-Token", token.SecretID)
|
req.Header.Set("X-Nomad-Token", token.SecretID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setNamespace(req *http.Request, ns string) {
|
||||||
|
q := req.URL.Query()
|
||||||
|
q.Add("namespace", ns)
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
func encodeReq(obj interface{}) io.ReadCloser {
|
func encodeReq(obj interface{}) io.ReadCloser {
|
||||||
buf := bytes.NewBuffer(nil)
|
buf := bytes.NewBuffer(nil)
|
||||||
enc := json.NewEncoder(buf)
|
enc := json.NewEncoder(buf)
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/golang/snappy"
|
"github.com/golang/snappy"
|
||||||
|
"github.com/hashicorp/nomad/acl"
|
||||||
api "github.com/hashicorp/nomad/api"
|
api "github.com/hashicorp/nomad/api"
|
||||||
"github.com/hashicorp/nomad/helper"
|
"github.com/hashicorp/nomad/helper"
|
||||||
"github.com/hashicorp/nomad/jobspec"
|
"github.com/hashicorp/nomad/jobspec"
|
||||||
|
@ -703,6 +704,25 @@ func (s *HTTPServer) JobsParseRequest(resp http.ResponseWriter, req *http.Reques
|
||||||
return nil, CodedError(405, ErrInvalidMethod)
|
return nil, CodedError(405, ErrInvalidMethod)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var namespace string
|
||||||
|
parseNamespace(req, &namespace)
|
||||||
|
|
||||||
|
aclObj, err := s.ResolveToken(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check job parse permissions
|
||||||
|
if aclObj != nil {
|
||||||
|
hasParseJob := aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityParseJob)
|
||||||
|
hasSubmitJob := aclObj.AllowNsOp(namespace, acl.NamespaceCapabilitySubmitJob)
|
||||||
|
|
||||||
|
allowed := hasParseJob || hasSubmitJob
|
||||||
|
if !allowed {
|
||||||
|
return nil, structs.ErrPermissionDenied
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
args := &api.JobsParseRequest{}
|
args := &api.JobsParseRequest{}
|
||||||
if err := decodeBody(req, &args); err != nil {
|
if err := decodeBody(req, &args); err != nil {
|
||||||
return nil, CodedError(400, err.Error())
|
return nil, CodedError(400, err.Error())
|
||||||
|
@ -712,7 +732,6 @@ func (s *HTTPServer) JobsParseRequest(resp http.ResponseWriter, req *http.Reques
|
||||||
}
|
}
|
||||||
|
|
||||||
var jobStruct *api.Job
|
var jobStruct *api.Job
|
||||||
var err error
|
|
||||||
if args.HCLv1 {
|
if args.HCLv1 {
|
||||||
jobStruct, err = jobspec.Parse(strings.NewReader(args.JobHCL))
|
jobStruct, err = jobspec.Parse(strings.NewReader(args.JobHCL))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/hashicorp/nomad/acl"
|
||||||
api "github.com/hashicorp/nomad/api"
|
api "github.com/hashicorp/nomad/api"
|
||||||
"github.com/hashicorp/nomad/helper"
|
"github.com/hashicorp/nomad/helper"
|
||||||
"github.com/hashicorp/nomad/nomad/mock"
|
"github.com/hashicorp/nomad/nomad/mock"
|
||||||
|
@ -407,6 +408,128 @@ func TestHTTP_JobsParse(t *testing.T) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHTTP_JobsParse_ACL(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
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) {
|
func TestHTTP_JobQuery(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
httpTest(t, nil, func(s *TestAgent) {
|
httpTest(t, nil, func(s *TestAgent) {
|
||||||
|
|
|
@ -96,6 +96,12 @@ func decode(c *jobConfig) error {
|
||||||
diags = append(diags, ds...)
|
diags = append(diags, ds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return early if the input job or variable files are not valid.
|
||||||
|
// Decoding and evaluating invalid files may result in unexpected results.
|
||||||
|
if diags.HasErrors() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
diags = append(diags, c.decodeBody(file.Body)...)
|
diags = append(diags, c.decodeBody(file.Body)...)
|
||||||
|
|
||||||
if diags.HasErrors() {
|
if diags.HasErrors() {
|
||||||
|
|
|
@ -374,6 +374,49 @@ job "example" {
|
||||||
require.Equal(t, "3", out.TaskGroups[2].Tasks[0].Meta["VERSION"])
|
require.Equal(t, "3", out.TaskGroups[2].Tasks[0].Meta["VERSION"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParse_InvalidHCL(t *testing.T) {
|
||||||
|
t.Run("invalid body", func(t *testing.T) {
|
||||||
|
hcl := `invalid{hcl`
|
||||||
|
|
||||||
|
_, err := ParseWithConfig(&ParseConfig{
|
||||||
|
Path: "input.hcl",
|
||||||
|
Body: []byte(hcl),
|
||||||
|
ArgVars: []string{},
|
||||||
|
AllowFS: true,
|
||||||
|
})
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid vars file", func(t *testing.T) {
|
||||||
|
tmp, err := ioutil.TempFile("", "nomad-jobspec2-")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.Remove(tmp.Name())
|
||||||
|
|
||||||
|
vars := `invalid{hcl`
|
||||||
|
_, err = tmp.Write([]byte(vars))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
hcl := `
|
||||||
|
variables {
|
||||||
|
region_var = "default"
|
||||||
|
}
|
||||||
|
job "example" {
|
||||||
|
datacenters = [for s in ["dc1", "dc2"] : upper(s)]
|
||||||
|
region = var.region_var
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
_, err = ParseWithConfig(&ParseConfig{
|
||||||
|
Path: "input.hcl",
|
||||||
|
Body: []byte(hcl),
|
||||||
|
VarFiles: []string{tmp.Name()},
|
||||||
|
ArgVars: []string{},
|
||||||
|
AllowFS: true,
|
||||||
|
})
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestParse_InvalidScalingSyntax(t *testing.T) {
|
func TestParse_InvalidScalingSyntax(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
Loading…
Reference in New Issue