open-nomad/nomad/job_endpoint_oss_test.go

425 lines
15 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
//go:build !ent
// +build !ent
package nomad
import (
"testing"
"github.com/hashicorp/go-memdb"
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
"github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/command/agent/consul"
"github.com/hashicorp/nomad/helper/pointer"
"github.com/hashicorp/nomad/helper/uuid"
"github.com/hashicorp/nomad/nomad/mock"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/testutil"
"github.com/shoenig/test/must"
"github.com/stretchr/testify/require"
)
// TestJobEndpoint_Register_Connect_AllowUnauthenticatedFalse asserts that a job
// submission fails allow_unauthenticated is false, and either an invalid or no
// operator Consul token is provided.
func TestJobEndpoint_Register_Connect_AllowUnauthenticatedFalse_oss(t *testing.T) {
ci.Parallel(t)
s1, cleanupS1 := TestServer(t, func(c *Config) {
c.NumSchedulers = 0 // Prevent automatic dequeue
c.ConsulConfig.AllowUnauthenticated = pointer.Of(false)
})
defer cleanupS1()
codec := rpcClient(t, s1)
testutil.WaitForLeader(t, s1.RPC)
newJob := func(namespace string) *structs.Job {
// Create the register request
job := mock.Job()
job.TaskGroups[0].Networks[0].Mode = "bridge"
job.TaskGroups[0].Services = []*structs.Service{
{
Name: "service1", // matches consul.ExamplePolicyID1
PortLabel: "8080",
Connect: &structs.ConsulConnect{
SidecarService: &structs.ConsulSidecarService{},
},
},
}
// For this test we only care about authorizing the connect service
job.TaskGroups[0].Tasks[0].Services = nil
// If testing with a Consul namespace, set it on the group
if namespace != "" {
job.TaskGroups[0].Consul = &structs.Consul{
Namespace: namespace,
}
}
return job
}
newRequest := func(job *structs.Job) *structs.JobRegisterRequest {
return &structs.JobRegisterRequest{
Job: job,
WriteRequest: structs.WriteRequest{
Region: "global",
Namespace: job.Namespace,
},
}
}
noTokenOnJob := func(t *testing.T, job *structs.Job) {
fsmState := s1.State()
ws := memdb.NewWatchSet()
storedJob, err := fsmState.JobByID(ws, job.Namespace, job.ID)
require.NoError(t, err)
require.NotNil(t, storedJob)
require.Empty(t, storedJob.ConsulToken)
}
// Non-sense Consul ACL tokens that should be rejected
missingToken := ""
fakeToken := uuid.Generate()
// Consul ACL tokens in no Consul namespace
ossTokenNoPolicyNoNS := consul.ExampleOperatorTokenID3
ossTokenNoNS := consul.ExampleOperatorTokenID1
// Consul ACL tokens in "default" Consul namespace
entTokenNoPolicyDefaultNS := consul.ExampleOperatorTokenID20
entTokenDefaultNS := consul.ExampleOperatorTokenID21
// Consul ACL tokens in "banana" Consul namespace
entTokenNoPolicyBananaNS := consul.ExampleOperatorTokenID10
entTokenBananaNS := consul.ExampleOperatorTokenID11
t.Run("group consul namespace unset", func(t *testing.T) {
// When the group namespace is unset (which is always the case with
// Nomad OSS), Consul tokens with no namespace or are in the "default"
// namespace should be accepted (assuming a sufficient service policy).
namespace := ""
t.Run("no token provided", func(t *testing.T) {
request := newRequest(newJob(namespace))
request.Job.ConsulToken = missingToken
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, "job-submitter consul token denied: missing consul token")
})
t.Run("unknown token provided", func(t *testing.T) {
request := newRequest(newJob(namespace))
request.Job.ConsulToken = fakeToken
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, "job-submitter consul token denied: unable to read consul token: no such token")
})
t.Run("unauthorized oss token provided", func(t *testing.T) {
request := newRequest(newJob(namespace))
request.Job.ConsulToken = ossTokenNoPolicyNoNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, `job-submitter consul token denied: insufficient Consul ACL permissions to write service "service1"`)
})
t.Run("authorized oss token provided", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = ossTokenNoNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.NoError(t, err)
noTokenOnJob(t, job)
})
t.Run("unauthorized token in default namespace", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = entTokenNoPolicyDefaultNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, `job-submitter consul token denied: insufficient Consul ACL permissions to write service "service1"`)
})
t.Run("authorized token in default namespace", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = entTokenDefaultNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.NoError(t, err)
noTokenOnJob(t, job)
})
t.Run("unauthorized token in banana namespace", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = entTokenNoPolicyBananaNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, `job-submitter consul token denied: consul ACL token requires using namespace "banana"`)
})
t.Run("authorized token in banana namespace", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = entTokenBananaNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, `job-submitter consul token denied: consul ACL token requires using namespace "banana"`)
})
})
t.Run("group consul namespace banana", func(t *testing.T) {
// Nomad OSS does not respect setting the consul namespace field on the group,
// and for backwards compatibility accepts tokens in the "default" namespace
// for groups with no namespace set. The net result is setting the group namespace
// to something like "banana" and using a token in "default" namespace will
// be accepted in Nomad OSS (assuming sufficient service write policy).
//
// Using a Consul token in the non-"default" namespace will always fail in
// Nomad OSS, again because the group namespace is ignored.
namespace := "banana"
t.Run("no token provided", func(t *testing.T) {
request := newRequest(newJob(namespace))
request.Job.ConsulToken = missingToken
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, "job-submitter consul token denied: missing consul token")
})
t.Run("unknown token provided", func(t *testing.T) {
request := newRequest(newJob(namespace))
request.Job.ConsulToken = fakeToken
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, "job-submitter consul token denied: unable to read consul token: no such token")
})
t.Run("unauthorized oss token provided", func(t *testing.T) {
request := newRequest(newJob(namespace))
request.Job.ConsulToken = ossTokenNoPolicyNoNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, `job-submitter consul token denied: insufficient Consul ACL permissions to write service "service1"`)
})
t.Run("authorized oss token provided", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = ossTokenNoNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.NoError(t, err)
noTokenOnJob(t, job)
})
t.Run("unauthorized token in default namespace", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = entTokenNoPolicyDefaultNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, `job-submitter consul token denied: insufficient Consul ACL permissions to write service "service1"`)
})
t.Run("authorized token in default namespace", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = entTokenDefaultNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.NoError(t, err)
noTokenOnJob(t, job)
})
// Consul token in custom namespace will always fail in nomad oss
t.Run("unauthorized token in banana namespace", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = entTokenNoPolicyBananaNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, `job-submitter consul token denied: consul ACL token requires using namespace "banana"`)
})
t.Run("authorized token in banana namespace", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = entTokenBananaNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, `job-submitter consul token denied: consul ACL token requires using namespace "banana"`)
})
})
t.Run("group consul namespace default", func(t *testing.T) {
// Nomad OSS ignores the group consul namespace, and setting it as default
// should effectively be the same as leaving it unset.
namespace := "default"
t.Run("no token provided", func(t *testing.T) {
request := newRequest(newJob(namespace))
request.Job.ConsulToken = missingToken
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, "job-submitter consul token denied: missing consul token")
})
t.Run("unknown token provided", func(t *testing.T) {
request := newRequest(newJob(namespace))
request.Job.ConsulToken = fakeToken
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, "job-submitter consul token denied: unable to read consul token: no such token")
})
t.Run("unauthorized oss token provided", func(t *testing.T) {
request := newRequest(newJob(namespace))
request.Job.ConsulToken = ossTokenNoPolicyNoNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, `job-submitter consul token denied: insufficient Consul ACL permissions to write service "service1"`)
})
t.Run("authorized oss token provided", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = ossTokenNoNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.NoError(t, err)
noTokenOnJob(t, job)
})
t.Run("unauthorized token in default namespace", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = entTokenNoPolicyDefaultNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, `job-submitter consul token denied: insufficient Consul ACL permissions to write service "service1"`)
})
t.Run("authorized token in default namespace", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = entTokenDefaultNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.NoError(t, err)
noTokenOnJob(t, job)
})
t.Run("unauthorized token in banana namespace", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = entTokenNoPolicyBananaNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, `job-submitter consul token denied: consul ACL token requires using namespace "banana"`)
})
t.Run("authorized token in banana namespace", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = entTokenBananaNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, `job-submitter consul token denied: consul ACL token requires using namespace "banana"`)
})
})
}
func TestJobEndpoint_Register_NodePool(t *testing.T) {
ci.Parallel(t)
s, cleanupS := TestServer(t, func(c *Config) {
c.NumSchedulers = 0
})
defer cleanupS()
codec := rpcClient(t, s)
testutil.WaitForLeader(t, s.RPC)
// Create test namespace.
ns := mock.Namespace()
nsReq := &structs.NamespaceUpsertRequest{
Namespaces: []*structs.Namespace{ns},
WriteRequest: structs.WriteRequest{Region: "global"},
}
var nsResp structs.GenericResponse
err := msgpackrpc.CallWithCodec(codec, "Namespace.UpsertNamespaces", nsReq, &nsResp)
must.NoError(t, err)
// Create test node pool.
pool := mock.NodePool()
poolReq := &structs.NodePoolUpsertRequest{
NodePools: []*structs.NodePool{pool},
WriteRequest: structs.WriteRequest{Region: "global"},
}
var poolResp structs.GenericResponse
err = msgpackrpc.CallWithCodec(codec, "NodePool.UpsertNodePools", poolReq, &poolResp)
must.NoError(t, err)
testCases := []struct {
name string
namespace string
nodePool string
expectedPool string
expectedErr string
}{
{
name: "job in default namespace uses default node pool",
namespace: structs.DefaultNamespace,
nodePool: "",
expectedPool: structs.NodePoolDefault,
},
{
name: "job without node pool uses default node pool",
namespace: ns.Name,
nodePool: "",
expectedPool: structs.NodePoolDefault,
},
{
name: "job can set node pool",
namespace: ns.Name,
nodePool: pool.Name,
expectedPool: pool.Name,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
job := mock.Job()
job.Namespace = tc.namespace
job.NodePool = tc.nodePool
req := &structs.JobRegisterRequest{
Job: job,
WriteRequest: structs.WriteRequest{
Region: "global",
Namespace: job.Namespace,
},
}
var resp structs.JobRegisterResponse
err = msgpackrpc.CallWithCodec(codec, "Job.Register", req, &resp)
if tc.expectedErr != "" {
must.ErrorContains(t, err, tc.expectedErr)
} else {
must.NoError(t, err)
got, err := s.State().JobByID(nil, job.Namespace, job.ID)
must.NoError(t, err)
must.Eq(t, tc.expectedPool, got.NodePool)
}
})
}
}