mirror of
https://github.com/kemko/nomad.git
synced 2026-01-06 10:25:42 +03:00
cli: remove hard requirement on list-jobs (#16380)
Most job subcommands allow for job ID prefix match as a convenience functionality so users don't have to type the full job ID. But this introduces a hard ACL requirement that the token used to run these commands have the `list-jobs` permission, even if the token has enough permission to execute the basic command action and the user passed an exact job ID. This change softens this requirement by not failing the prefix match in case the request results in a permission denied error and instead using the information passed by the user directly.
This commit is contained in:
@@ -19,8 +19,9 @@ Usage: nomad job allocs [options] <job>
|
||||
|
||||
Display allocations for a particular job.
|
||||
|
||||
When ACLs are enabled, this command requires a token with the 'read-job' and
|
||||
'list-jobs' capabilities for the job's namespace.
|
||||
When ACLs are enabled, this command requires a token with the 'read-job'
|
||||
capability for the job's namespace. The 'list-jobs' capability is required to
|
||||
run the command with a job prefix instead of the exact job ID.
|
||||
|
||||
General Options:
|
||||
|
||||
|
||||
@@ -3,11 +3,14 @@ package command
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/command/agent"
|
||||
"github.com/hashicorp/nomad/nomad/mock"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
"github.com/shoenig/test/must"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -172,3 +175,116 @@ func TestJobAllocsCommand_AutocompleteArgs(t *testing.T) {
|
||||
require.Equal(t, 1, len(res))
|
||||
require.Equal(t, j.ID, res[0])
|
||||
}
|
||||
|
||||
func TestJobAllocsCommand_ACL(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Start server with ACL enabled.
|
||||
srv, _, url := testServer(t, true, func(c *agent.Config) {
|
||||
c.ACL.Enabled = true
|
||||
})
|
||||
defer srv.Shutdown()
|
||||
|
||||
// Create a job with an alloc.
|
||||
job := mock.Job()
|
||||
state := srv.Agent.Server().State()
|
||||
err := state.UpsertJob(structs.MsgTypeTestSetup, 100, job)
|
||||
must.NoError(t, err)
|
||||
|
||||
a := mock.Alloc()
|
||||
a.Job = job
|
||||
a.JobID = job.ID
|
||||
a.TaskGroup = job.TaskGroups[0].Name
|
||||
a.Metrics = &structs.AllocMetric{}
|
||||
a.DesiredStatus = structs.AllocDesiredStatusRun
|
||||
a.ClientStatus = structs.AllocClientStatusRunning
|
||||
err = state.UpsertAllocs(structs.MsgTypeTestSetup, 200, []*structs.Allocation{a})
|
||||
must.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
jobPrefix bool
|
||||
aclPolicy string
|
||||
expectedErr string
|
||||
expectedOut string
|
||||
}{
|
||||
{
|
||||
name: "no token",
|
||||
aclPolicy: "",
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "missing read-job",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["alloc-lifecycle"]
|
||||
}
|
||||
`,
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "read-job allowed",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "job prefix requires list-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job"]
|
||||
}
|
||||
`,
|
||||
expectedOut: "No allocations",
|
||||
},
|
||||
{
|
||||
name: "job prefix works with list-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job", "list-jobs"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &JobAllocsCommand{Meta: Meta{Ui: ui}}
|
||||
args := []string{
|
||||
"-address", url,
|
||||
}
|
||||
|
||||
if tc.aclPolicy != "" {
|
||||
// Create ACL token with test case policy and add it to the
|
||||
// command.
|
||||
policyName := nonAlphaNum.ReplaceAllString(tc.name, "-")
|
||||
token := mock.CreatePolicyAndToken(t, state, uint64(302+i), policyName, tc.aclPolicy)
|
||||
args = append(args, "-token", token.SecretID)
|
||||
}
|
||||
|
||||
// Add job ID or job ID prefix to the command.
|
||||
if tc.jobPrefix {
|
||||
args = append(args, job.ID[:3])
|
||||
} else {
|
||||
args = append(args, job.ID)
|
||||
}
|
||||
|
||||
// Run command.
|
||||
code := cmd.Run(args)
|
||||
if tc.expectedErr == "" {
|
||||
must.Zero(t, code)
|
||||
} else {
|
||||
must.One(t, code)
|
||||
must.StrContains(t, ui.ErrorWriter.String(), tc.expectedErr)
|
||||
}
|
||||
if tc.expectedOut != "" {
|
||||
must.StrContains(t, ui.OutputWriter.String(), tc.expectedOut)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,9 @@ Usage: nomad job deployments [options] <job>
|
||||
|
||||
Deployments is used to display the deployments for a particular job.
|
||||
|
||||
When ACLs are enabled, this command requires a token with the 'read-job' and
|
||||
'list-jobs' capabilities for the job's namespace.
|
||||
When ACLs are enabled, this command requires a token with the 'read-job'
|
||||
capability for the job's namespace. The 'list-jobs' capability is required to
|
||||
run the command with a job prefix instead of the exact job ID.
|
||||
|
||||
General Options:
|
||||
|
||||
|
||||
@@ -4,11 +4,14 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/command/agent"
|
||||
"github.com/hashicorp/nomad/nomad/mock"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
"github.com/shoenig/test/must"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -152,3 +155,112 @@ func TestJobDeploymentsCommand_AutocompleteArgs(t *testing.T) {
|
||||
assert.Equal(1, len(res))
|
||||
assert.Equal(j.ID, res[0])
|
||||
}
|
||||
|
||||
func TestJobDeploymentsCommand_ACL(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Start server with ACL enabled.
|
||||
srv, _, url := testServer(t, true, func(c *agent.Config) {
|
||||
c.ACL.Enabled = true
|
||||
})
|
||||
defer srv.Shutdown()
|
||||
|
||||
// Create a job with a deployment.
|
||||
job := mock.Job()
|
||||
state := srv.Agent.Server().State()
|
||||
err := state.UpsertJob(structs.MsgTypeTestSetup, 100, job)
|
||||
must.NoError(t, err)
|
||||
|
||||
d := mock.Deployment()
|
||||
d.JobID = job.ID
|
||||
d.JobCreateIndex = job.CreateIndex
|
||||
err = state.UpsertDeployment(101, d)
|
||||
must.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
jobPrefix bool
|
||||
aclPolicy string
|
||||
expectedErr string
|
||||
expectedOut string
|
||||
}{
|
||||
{
|
||||
name: "no token",
|
||||
aclPolicy: "",
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "missing read-job",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["alloc-lifecycle"]
|
||||
}
|
||||
`,
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "read-job allowed",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "job prefix requires list-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job"]
|
||||
}
|
||||
`,
|
||||
expectedOut: "No deployments",
|
||||
},
|
||||
{
|
||||
name: "job prefix works with list-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job", "list-jobs"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &JobDeploymentsCommand{Meta: Meta{Ui: ui}}
|
||||
args := []string{
|
||||
"-address", url,
|
||||
}
|
||||
|
||||
if tc.aclPolicy != "" {
|
||||
// Create ACL token with test case policy and add it to the
|
||||
// command.
|
||||
policyName := nonAlphaNum.ReplaceAllString(tc.name, "-")
|
||||
token := mock.CreatePolicyAndToken(t, state, uint64(302+i), policyName, tc.aclPolicy)
|
||||
args = append(args, "-token", token.SecretID)
|
||||
}
|
||||
|
||||
// Add job ID or job ID prefix to the command.
|
||||
if tc.jobPrefix {
|
||||
args = append(args, job.ID[:3])
|
||||
} else {
|
||||
args = append(args, job.ID)
|
||||
}
|
||||
|
||||
// Run command.
|
||||
code := cmd.Run(args)
|
||||
if tc.expectedErr == "" {
|
||||
must.Zero(t, code)
|
||||
} else {
|
||||
must.One(t, code)
|
||||
must.StrContains(t, ui.ErrorWriter.String(), tc.expectedErr)
|
||||
}
|
||||
if tc.expectedOut != "" {
|
||||
must.StrContains(t, ui.OutputWriter.String(), tc.expectedOut)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,10 @@ Usage: nomad job dispatch [options] <parameterized job> [input source]
|
||||
detach flag.
|
||||
|
||||
When ACLs are enabled, this command requires a token with the 'dispatch-job'
|
||||
capability for the job's namespace.
|
||||
capability for the job's namespace. The 'list-jobs' capability is required to
|
||||
run the command with a job prefix instead of the exact job ID. The 'read-job'
|
||||
capability is required to monitor the resulting evaluation when -detach is
|
||||
not used.
|
||||
|
||||
General Options:
|
||||
|
||||
|
||||
@@ -4,11 +4,14 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/command/agent"
|
||||
"github.com/hashicorp/nomad/nomad/mock"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
"github.com/shoenig/test/must"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -85,3 +88,123 @@ func TestJobDispatchCommand_AutocompleteArgs(t *testing.T) {
|
||||
require.Equal(t, 1, len(res))
|
||||
require.Equal(t, j1.ID, res[0])
|
||||
}
|
||||
|
||||
func TestJobDispatchCommand_ACL(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Start server with ACL enabled.
|
||||
srv, _, url := testServer(t, true, func(c *agent.Config) {
|
||||
c.ACL.Enabled = true
|
||||
})
|
||||
defer srv.Shutdown()
|
||||
|
||||
// Create a parameterized job.
|
||||
job := mock.MinJob()
|
||||
job.Type = "batch"
|
||||
job.ParameterizedJob = &structs.ParameterizedJobConfig{}
|
||||
state := srv.Agent.Server().State()
|
||||
err := state.UpsertJob(structs.MsgTypeTestSetup, 100, job)
|
||||
must.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
jobPrefix bool
|
||||
aclPolicy string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "no token",
|
||||
aclPolicy: "",
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "missing dispatch-job",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job"]
|
||||
}
|
||||
`,
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "dispatch-job allowed but can't monitor eval without read-job",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["dispatch-job"]
|
||||
}
|
||||
`,
|
||||
expectedErr: "No evaluation with id",
|
||||
},
|
||||
{
|
||||
name: "dispatch-job allowed and can monitor eval with read-job",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["dispatch-job", "read-job"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "job prefix requires list-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["dispatch-job"]
|
||||
}
|
||||
`,
|
||||
expectedErr: "job not found",
|
||||
},
|
||||
{
|
||||
name: "job prefix works with list-job but can't monitor eval without read-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["dispatch-job", "list-jobs"]
|
||||
}
|
||||
`,
|
||||
expectedErr: "No evaluation with id",
|
||||
},
|
||||
{
|
||||
name: "job prefix works with list-job and can monitor eval with read-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job", "dispatch-job", "list-jobs"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &JobDispatchCommand{Meta: Meta{Ui: ui}}
|
||||
args := []string{
|
||||
"-address", url,
|
||||
}
|
||||
|
||||
if tc.aclPolicy != "" {
|
||||
// Create ACL token with test case policy and add it to the
|
||||
// command.
|
||||
policyName := nonAlphaNum.ReplaceAllString(tc.name, "-")
|
||||
token := mock.CreatePolicyAndToken(t, state, uint64(302+i), policyName, tc.aclPolicy)
|
||||
args = append(args, "-token", token.SecretID)
|
||||
}
|
||||
|
||||
// Add job ID or job ID prefix to the command.
|
||||
if tc.jobPrefix {
|
||||
args = append(args, job.ID[:3])
|
||||
} else {
|
||||
args = append(args, job.ID)
|
||||
}
|
||||
|
||||
// Run command.
|
||||
code := cmd.Run(args)
|
||||
if tc.expectedErr == "" {
|
||||
must.Zero(t, code)
|
||||
} else {
|
||||
must.One(t, code)
|
||||
must.StrContains(t, ui.ErrorWriter.String(), tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,9 @@ Usage: nomad job eval [options] <job_id>
|
||||
operators to force the scheduler to create new allocations under certain
|
||||
scenarios.
|
||||
|
||||
When ACLs are enabled, this command requires a token with the 'submit-job'
|
||||
capability for the job's namespace.
|
||||
When ACLs are enabled, this command requires a token with the 'read-job'
|
||||
capability for the job's namespace. The 'list-jobs' capability is required to
|
||||
run the command with a job prefix instead of the exact job ID.
|
||||
|
||||
General Options:
|
||||
|
||||
|
||||
@@ -5,12 +5,15 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/command/agent"
|
||||
"github.com/hashicorp/nomad/nomad/mock"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/nomad/testutil"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
"github.com/shoenig/test/must"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -125,3 +128,102 @@ func TestJobEvalCommand_AutocompleteArgs(t *testing.T) {
|
||||
assert.Equal(1, len(res))
|
||||
assert.Equal(j.ID, res[0])
|
||||
}
|
||||
|
||||
func TestJobEvalCommand_ACL(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Start server with ACL enabled.
|
||||
srv, _, url := testServer(t, true, func(c *agent.Config) {
|
||||
c.ACL.Enabled = true
|
||||
})
|
||||
defer srv.Shutdown()
|
||||
|
||||
// Create a job.
|
||||
job := mock.MinJob()
|
||||
state := srv.Agent.Server().State()
|
||||
err := state.UpsertJob(structs.MsgTypeTestSetup, 100, job)
|
||||
must.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
jobPrefix bool
|
||||
aclPolicy string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "no token",
|
||||
aclPolicy: "",
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "missing read-job",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["list-jobs"]
|
||||
}
|
||||
`,
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "read-job allowed",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "job prefix requires list-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job"]
|
||||
}
|
||||
`,
|
||||
expectedErr: "job not found",
|
||||
},
|
||||
{
|
||||
name: "job prefix works with list-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job", "list-jobs"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &JobEvalCommand{Meta: Meta{Ui: ui}}
|
||||
args := []string{
|
||||
"-address", url,
|
||||
}
|
||||
|
||||
if tc.aclPolicy != "" {
|
||||
// Create ACL token with test case policy and add it to the
|
||||
// command.
|
||||
policyName := nonAlphaNum.ReplaceAllString(tc.name, "-")
|
||||
token := mock.CreatePolicyAndToken(t, state, uint64(302+i), policyName, tc.aclPolicy)
|
||||
args = append(args, "-token", token.SecretID)
|
||||
}
|
||||
|
||||
// Add job ID or job ID prefix to the command.
|
||||
if tc.jobPrefix {
|
||||
args = append(args, job.ID[:3])
|
||||
} else {
|
||||
args = append(args, job.ID)
|
||||
}
|
||||
|
||||
// Run command.
|
||||
code := cmd.Run(args)
|
||||
if tc.expectedErr == "" {
|
||||
must.Zero(t, code)
|
||||
} else {
|
||||
must.One(t, code)
|
||||
must.StrContains(t, ui.ErrorWriter.String(), tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,9 @@ Usage: nomad job history [options] <job>
|
||||
the changes that occurred to the job as well as deciding job versions to revert
|
||||
to.
|
||||
|
||||
When ACLs are enabled, this command requires a token with the 'read-job' and
|
||||
'list-jobs' capabilities for the job's namespace.
|
||||
When ACLs are enabled, this command requires a token with the 'read-job'
|
||||
capability for the job's namespace. The 'list-jobs' capability is required to
|
||||
run the command with a job prefix instead of the exact job ID.
|
||||
|
||||
General Options:
|
||||
|
||||
|
||||
@@ -4,11 +4,14 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/command/agent"
|
||||
"github.com/hashicorp/nomad/nomad/mock"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
"github.com/shoenig/test/must"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -63,3 +66,102 @@ func TestJobHistoryCommand_AutocompleteArgs(t *testing.T) {
|
||||
assert.Equal(1, len(res))
|
||||
assert.Equal(j.ID, res[0])
|
||||
}
|
||||
|
||||
func TestJobHistoryCommand_ACL(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Start server with ACL enabled.
|
||||
srv, _, url := testServer(t, true, func(c *agent.Config) {
|
||||
c.ACL.Enabled = true
|
||||
})
|
||||
defer srv.Shutdown()
|
||||
|
||||
// Create a job.
|
||||
job := mock.MinJob()
|
||||
state := srv.Agent.Server().State()
|
||||
err := state.UpsertJob(structs.MsgTypeTestSetup, 100, job)
|
||||
must.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
jobPrefix bool
|
||||
aclPolicy string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "no token",
|
||||
aclPolicy: "",
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "missing read-job",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["list-jobs"]
|
||||
}
|
||||
`,
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "read-job allowed",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "job prefix requires list-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job"]
|
||||
}
|
||||
`,
|
||||
expectedErr: "job versions not found",
|
||||
},
|
||||
{
|
||||
name: "job prefix works with list-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job", "list-jobs"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &JobHistoryCommand{Meta: Meta{Ui: ui}}
|
||||
args := []string{
|
||||
"-address", url,
|
||||
}
|
||||
|
||||
if tc.aclPolicy != "" {
|
||||
// Create ACL token with test case policy and add it to the
|
||||
// command.
|
||||
policyName := nonAlphaNum.ReplaceAllString(tc.name, "-")
|
||||
token := mock.CreatePolicyAndToken(t, state, uint64(302+i), policyName, tc.aclPolicy)
|
||||
args = append(args, "-token", token.SecretID)
|
||||
}
|
||||
|
||||
// Add job ID or job ID prefix to the command.
|
||||
if tc.jobPrefix {
|
||||
args = append(args, job.ID[:3])
|
||||
} else {
|
||||
args = append(args, job.ID)
|
||||
}
|
||||
|
||||
// Run command.
|
||||
code := cmd.Run(args)
|
||||
if tc.expectedErr == "" {
|
||||
must.Zero(t, code)
|
||||
} else {
|
||||
must.One(t, code)
|
||||
must.StrContains(t, ui.ErrorWriter.String(), tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,9 @@ Alias: nomad inspect
|
||||
|
||||
Inspect is used to see the specification of a submitted job.
|
||||
|
||||
When ACLs are enabled, this command requires a token with the 'read-job' and
|
||||
'list-jobs' capabilities for the job's namespace.
|
||||
When ACLs are enabled, this command requires a token with the 'read-job'
|
||||
capability for the job's namespace. The 'list-jobs' capability is required to
|
||||
run the command with a job prefix instead of the exact job ID.
|
||||
|
||||
General Options:
|
||||
|
||||
|
||||
@@ -4,11 +4,14 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/command/agent"
|
||||
"github.com/hashicorp/nomad/nomad/mock"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
"github.com/shoenig/test/must"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -83,3 +86,102 @@ func TestInspectCommand_AutocompleteArgs(t *testing.T) {
|
||||
assert.Equal(1, len(res))
|
||||
assert.Equal(j.ID, res[0])
|
||||
}
|
||||
|
||||
func TestJobInspectCommand_ACL(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Start server with ACL enabled.
|
||||
srv, _, url := testServer(t, true, func(c *agent.Config) {
|
||||
c.ACL.Enabled = true
|
||||
})
|
||||
defer srv.Shutdown()
|
||||
|
||||
// Create a job
|
||||
job := mock.MinJob()
|
||||
state := srv.Agent.Server().State()
|
||||
err := state.UpsertJob(structs.MsgTypeTestSetup, 100, job)
|
||||
must.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
jobPrefix bool
|
||||
aclPolicy string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "no token",
|
||||
aclPolicy: "",
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "missing read-job",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["list-jobs"]
|
||||
}
|
||||
`,
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "read-job allowed",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "job prefix requires list-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job"]
|
||||
}
|
||||
`,
|
||||
expectedErr: "job not found",
|
||||
},
|
||||
{
|
||||
name: "job prefix works with list-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job", "list-jobs"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &JobInspectCommand{Meta: Meta{Ui: ui}}
|
||||
args := []string{
|
||||
"-address", url,
|
||||
}
|
||||
|
||||
if tc.aclPolicy != "" {
|
||||
// Create ACL token with test case policy and add it to the
|
||||
// command.
|
||||
policyName := nonAlphaNum.ReplaceAllString(tc.name, "-")
|
||||
token := mock.CreatePolicyAndToken(t, state, uint64(302+i), policyName, tc.aclPolicy)
|
||||
args = append(args, "-token", token.SecretID)
|
||||
}
|
||||
|
||||
// Add job ID or job ID prefix to the command.
|
||||
if tc.jobPrefix {
|
||||
args = append(args, job.ID[:3])
|
||||
} else {
|
||||
args = append(args, job.ID)
|
||||
}
|
||||
|
||||
// Run command.
|
||||
code := cmd.Run(args)
|
||||
if tc.expectedErr == "" {
|
||||
must.Zero(t, code)
|
||||
} else {
|
||||
must.One(t, code)
|
||||
must.StrContains(t, ui.ErrorWriter.String(), tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,10 @@ Usage: nomad job periodic force <job id>
|
||||
prohibit_overlap setting.
|
||||
|
||||
When ACLs are enabled, this command requires a token with the 'submit-job'
|
||||
and 'list-jobs' capabilities for the job's namespace.
|
||||
capability for the job's namespace. The 'list-jobs' capability is required to
|
||||
run the command with a job prefix instead of the exact job ID. The 'read-job'
|
||||
capability is required to monitor the resulting evaluation when -detach is
|
||||
not used.
|
||||
|
||||
General Options:
|
||||
|
||||
|
||||
@@ -6,12 +6,14 @@ import (
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/command/agent"
|
||||
"github.com/hashicorp/nomad/helper/pointer"
|
||||
"github.com/hashicorp/nomad/nomad/mock"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/nomad/testutil"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
"github.com/shoenig/test/must"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -246,3 +248,132 @@ func TestJobPeriodicForceCommand_SuccessfulIfJobIDEqualsPrefix(t *testing.T) {
|
||||
require.Contains(t, out, "Monitoring evaluation")
|
||||
require.Contains(t, out, "finished with status \"complete\"")
|
||||
}
|
||||
|
||||
func TestJobPeriodicForceCommand_ACL(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Start server with ACL enabled.
|
||||
srv, client, url := testServer(t, true, func(c *agent.Config) {
|
||||
c.ACL.Enabled = true
|
||||
})
|
||||
defer srv.Shutdown()
|
||||
client.SetSecretID(srv.RootToken.SecretID)
|
||||
|
||||
// Create a periodic job.
|
||||
jobID := "test_job_periodic_force_acl"
|
||||
job := testJob(jobID)
|
||||
job.Periodic = &api.PeriodicConfig{
|
||||
SpecType: pointer.Of(api.PeriodicSpecCron),
|
||||
Spec: pointer.Of("*/15 * * * * *"),
|
||||
}
|
||||
|
||||
rootTokenOpts := &api.WriteOptions{
|
||||
AuthToken: srv.RootToken.SecretID,
|
||||
}
|
||||
_, _, err := client.Jobs().Register(job, rootTokenOpts)
|
||||
must.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
jobPrefix bool
|
||||
aclPolicy string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "no token",
|
||||
aclPolicy: "",
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "missing submit-job",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["list-jobs"]
|
||||
}
|
||||
`,
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "submit-job allowed but can't monitor eval without read-job",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["submit-job"]
|
||||
}
|
||||
`,
|
||||
expectedErr: "No evaluation with id",
|
||||
},
|
||||
{
|
||||
name: "submit-job allowed and can monitor eval with read-job",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["submit-job", "read-job"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "job prefix requires list-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["submit-job"]
|
||||
}
|
||||
`,
|
||||
expectedErr: "job not found",
|
||||
},
|
||||
{
|
||||
name: "job prefix works with list-job but can't monitor eval without read-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["submit-job", "list-jobs"]
|
||||
}
|
||||
`,
|
||||
expectedErr: "No evaluation with id",
|
||||
},
|
||||
{
|
||||
name: "job prefix works with list-job and can monitor eval with read-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job", "submit-job", "list-jobs"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &JobPeriodicForceCommand{Meta: Meta{Ui: ui}}
|
||||
args := []string{
|
||||
"-address", url,
|
||||
}
|
||||
|
||||
if tc.aclPolicy != "" {
|
||||
state := srv.Agent.Server().State()
|
||||
|
||||
// Create ACL token with test case policy and add it to the
|
||||
// command.
|
||||
policyName := nonAlphaNum.ReplaceAllString(tc.name, "-")
|
||||
token := mock.CreatePolicyAndToken(t, state, uint64(302+i), policyName, tc.aclPolicy)
|
||||
args = append(args, "-token", token.SecretID)
|
||||
}
|
||||
|
||||
// Add job ID or job ID prefix to the command.
|
||||
if tc.jobPrefix {
|
||||
args = append(args, jobID[:3])
|
||||
} else {
|
||||
args = append(args, jobID)
|
||||
}
|
||||
|
||||
// Run command.
|
||||
code := cmd.Run(args)
|
||||
if tc.expectedErr == "" {
|
||||
must.Zero(t, code)
|
||||
} else {
|
||||
must.One(t, code)
|
||||
must.StrContains(t, ui.ErrorWriter.String(), tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,9 @@ Usage: nomad job promote [options] <job id>
|
||||
"nomad job revert" command.
|
||||
|
||||
When ACLs are enabled, this command requires a token with the 'submit-job',
|
||||
'list-jobs', and 'read-job' capabilities for the job's namespace.
|
||||
and 'read-job' capabilities for the job's namespace. The 'list-jobs'
|
||||
capability is required to run the command with a job prefix instead of the
|
||||
exact job ID.
|
||||
|
||||
General Options:
|
||||
|
||||
|
||||
@@ -4,8 +4,11 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/command/agent"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/shoenig/test/must"
|
||||
|
||||
"github.com/hashicorp/nomad/nomad/mock"
|
||||
"github.com/mitchellh/cli"
|
||||
@@ -64,3 +67,119 @@ func TestJobPromoteCommand_AutocompleteArgs(t *testing.T) {
|
||||
assert.Equal(1, len(res))
|
||||
assert.Equal(j.ID, res[0])
|
||||
}
|
||||
|
||||
func TestJobPromoteCommand_ACL(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Start server with ACL enabled.
|
||||
srv, _, url := testServer(t, true, func(c *agent.Config) {
|
||||
c.ACL.Enabled = true
|
||||
})
|
||||
defer srv.Shutdown()
|
||||
|
||||
// Create a job.
|
||||
job := mock.MinJob()
|
||||
state := srv.Agent.Server().State()
|
||||
err := state.UpsertJob(structs.MsgTypeTestSetup, 100, job)
|
||||
must.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
jobPrefix bool
|
||||
aclPolicy string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "no token",
|
||||
aclPolicy: "",
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "missing submit-job",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job"]
|
||||
}
|
||||
`,
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "missing read-job",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["submit-job"]
|
||||
}
|
||||
`,
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "read-job and submit-job allowed",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job", "submit-job"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "job prefix requires list-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job", "submit-job",]
|
||||
}
|
||||
`,
|
||||
expectedErr: "no deployment to promote",
|
||||
},
|
||||
{
|
||||
name: "job prefix works with list-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job", "submit-job", "list-jobs"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &JobPromoteCommand{Meta: Meta{Ui: ui}}
|
||||
args := []string{
|
||||
"-address", url,
|
||||
"-detach",
|
||||
}
|
||||
|
||||
// Create deployment to promote.
|
||||
d := mock.Deployment()
|
||||
d.JobID = job.ID
|
||||
d.JobCreateIndex = job.CreateIndex
|
||||
err = state.UpsertDeployment(uint64(301+i), d)
|
||||
must.NoError(t, err)
|
||||
|
||||
if tc.aclPolicy != "" {
|
||||
// Create ACL token with test case policy and add it to the
|
||||
// command.
|
||||
policyName := nonAlphaNum.ReplaceAllString(tc.name, "-")
|
||||
token := mock.CreatePolicyAndToken(t, state, uint64(302+i), policyName, tc.aclPolicy)
|
||||
args = append(args, "-token", token.SecretID)
|
||||
}
|
||||
|
||||
// Add job ID or job ID prefix to the command.
|
||||
if tc.jobPrefix {
|
||||
args = append(args, job.ID[:3])
|
||||
} else {
|
||||
args = append(args, job.ID)
|
||||
}
|
||||
|
||||
// Run command.
|
||||
code := cmd.Run(args)
|
||||
if tc.expectedErr == "" {
|
||||
must.Zero(t, code)
|
||||
} else {
|
||||
must.One(t, code)
|
||||
must.StrContains(t, ui.ErrorWriter.String(), tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,10 @@ Usage: nomad job revert [options] <job> <version>
|
||||
versions to revert to can be found using "nomad job history" command.
|
||||
|
||||
When ACLs are enabled, this command requires a token with the 'submit-job'
|
||||
and 'list-jobs' capabilities for the job's namespace.
|
||||
capability for the job's namespace. The 'list-jobs' capability is required to
|
||||
run the command with a job prefix instead of the exact job ID. The 'read-job'
|
||||
capability is required to monitor the resulting evaluation when -detach is
|
||||
not used.
|
||||
|
||||
General Options:
|
||||
|
||||
|
||||
@@ -4,11 +4,14 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/command/agent"
|
||||
"github.com/hashicorp/nomad/nomad/mock"
|
||||
structs "github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
"github.com/shoenig/test/must"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -63,3 +66,136 @@ func TestJobRevertCommand_AutocompleteArgs(t *testing.T) {
|
||||
assert.Equal(1, len(res))
|
||||
assert.Equal(j.ID, res[0])
|
||||
}
|
||||
|
||||
func TestJobRevertCommand_ACL(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Start server with ACL enabled.
|
||||
srv, client, url := testServer(t, true, func(c *agent.Config) {
|
||||
c.ACL.Enabled = true
|
||||
})
|
||||
defer srv.Shutdown()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
jobPrefix bool
|
||||
aclPolicy string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "no token",
|
||||
aclPolicy: "",
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "missing submit-job",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job"]
|
||||
}
|
||||
`,
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "submit-job allowed but can't monitor eval without read-job",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["submit-job"]
|
||||
}
|
||||
`,
|
||||
expectedErr: "No evaluation with id",
|
||||
},
|
||||
{
|
||||
name: "submit-job allowed and can monitor eval with read-job",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job", "submit-job"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "job prefix requires list-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["submit-job"]
|
||||
}
|
||||
`,
|
||||
expectedErr: "not found",
|
||||
},
|
||||
{
|
||||
name: "job prefix works with list-job but can't monitor eval without read-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["submit-job", "list-jobs"]
|
||||
}
|
||||
`,
|
||||
expectedErr: "No evaluation with id",
|
||||
},
|
||||
{
|
||||
name: "job prefix works with list-job and can monitor eval with read-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job", "submit-job", "list-jobs"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &JobRevertCommand{Meta: Meta{Ui: ui}}
|
||||
args := []string{
|
||||
"-address", url,
|
||||
}
|
||||
|
||||
// Create a job.
|
||||
job := mock.MinJob()
|
||||
state := srv.Agent.Server().State()
|
||||
err := state.UpsertJob(structs.MsgTypeTestSetup, uint64(300+i), job)
|
||||
must.NoError(t, err)
|
||||
defer func() {
|
||||
client.Jobs().Deregister(job.ID, true, &api.WriteOptions{
|
||||
AuthToken: srv.RootToken.SecretID,
|
||||
})
|
||||
}()
|
||||
|
||||
// Modify job to create new version.
|
||||
newJob := job.Copy()
|
||||
newJob.Meta = map[string]string{
|
||||
"test": tc.name,
|
||||
}
|
||||
newJob.Version = uint64(i)
|
||||
err = state.UpsertJob(structs.MsgTypeTestSetup, uint64(301+i), newJob)
|
||||
must.NoError(t, err)
|
||||
|
||||
if tc.aclPolicy != "" {
|
||||
// Create ACL token with test case policy and add it to the
|
||||
// command.
|
||||
policyName := nonAlphaNum.ReplaceAllString(tc.name, "-")
|
||||
token := mock.CreatePolicyAndToken(t, state, uint64(302+i), policyName, tc.aclPolicy)
|
||||
args = append(args, "-token", token.SecretID)
|
||||
}
|
||||
|
||||
// Add job ID or job ID prefix to the command.
|
||||
if tc.jobPrefix {
|
||||
args = append(args, job.ID[:3])
|
||||
} else {
|
||||
args = append(args, job.ID)
|
||||
}
|
||||
|
||||
// Run command reverting job to version 0.
|
||||
args = append(args, "0")
|
||||
code := cmd.Run(args)
|
||||
if tc.expectedErr == "" {
|
||||
must.Zero(t, code)
|
||||
} else {
|
||||
must.One(t, code)
|
||||
must.StrContains(t, ui.ErrorWriter.String(), tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,12 @@ Usage: nomad job scale [options] <job> [<group>] <count>
|
||||
onto nodes. The monitor will end once job placement is done. It
|
||||
is safe to exit the monitor early using ctrl+c.
|
||||
|
||||
When ACLs are enabled, this command requires a token with the 'scale-job'
|
||||
capability for the job's namespace.
|
||||
When ACLs are enabled, this command requires a token with the
|
||||
'read-job-scaling' and either the 'scale-job' or 'submit-job' capabilities
|
||||
for the job's namespace. The 'list-jobs' capability is required to run the
|
||||
command with a job prefix instead of the exact job ID. The 'read-job'
|
||||
capability is required to monitor the resulting evaluation when -detach is
|
||||
not used.
|
||||
|
||||
General Options:
|
||||
|
||||
|
||||
@@ -7,9 +7,13 @@ import (
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/command/agent"
|
||||
"github.com/hashicorp/nomad/helper/pointer"
|
||||
"github.com/hashicorp/nomad/nomad/mock"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/nomad/testutil"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/shoenig/test/must"
|
||||
)
|
||||
|
||||
func TestJobScaleCommand_SingleGroup(t *testing.T) {
|
||||
@@ -122,3 +126,153 @@ func TestJobScaleCommand_MultiGroup(t *testing.T) {
|
||||
t.Fatalf("Expected Evaluation ID within output: %v", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJobScaleCommand_ACL(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Start server with ACL enabled.
|
||||
srv, client, url := testServer(t, true, func(c *agent.Config) {
|
||||
c.ACL.Enabled = true
|
||||
})
|
||||
defer srv.Shutdown()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
jobPrefix bool
|
||||
aclPolicy string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "no token",
|
||||
aclPolicy: "",
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "missing scale-job or job-submit",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job-scaling"]
|
||||
}
|
||||
`,
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "missing read-job-scaling",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["scale-job"]
|
||||
}
|
||||
`,
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "read-job-scaling and scale-job allowed but can't monitor eval without read-job",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job-scaling", "scale-job"]
|
||||
}
|
||||
`,
|
||||
expectedErr: "No evaluation with id",
|
||||
},
|
||||
{
|
||||
name: "read-job-scaling and submit-job allowed but can't monitor eval without read-job",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job-scaling", "submit-job"]
|
||||
}
|
||||
`,
|
||||
expectedErr: "No evaluation with id",
|
||||
},
|
||||
{
|
||||
name: "read-job-scaling and scale-job allowed and can monitor eval with read-job",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job", "read-job-scaling", "scale-job"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "read-job-scaling and submit-job allowed and can monitor eval with read-job",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job", "read-job-scaling", "submit-job"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "job prefix requires list-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job-scaling", "scale-job"]
|
||||
}
|
||||
`,
|
||||
expectedErr: "job not found",
|
||||
},
|
||||
{
|
||||
name: "job prefix works with list-job but can't monitor eval without read-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job-scaling", "scale-job", "list-jobs"]
|
||||
}
|
||||
`,
|
||||
expectedErr: "No evaluation with id",
|
||||
},
|
||||
{
|
||||
name: "job prefix works with list-job and can monitor eval with read-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job", "read-job-scaling", "scale-job", "list-jobs"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &JobScaleCommand{Meta: Meta{Ui: ui}}
|
||||
args := []string{
|
||||
"-address", url,
|
||||
}
|
||||
|
||||
// Create a job.
|
||||
job := mock.MinJob()
|
||||
state := srv.Agent.Server().State()
|
||||
err := state.UpsertJob(structs.MsgTypeTestSetup, uint64(300+i), job)
|
||||
must.NoError(t, err)
|
||||
defer func() {
|
||||
client.Jobs().Deregister(job.ID, true, &api.WriteOptions{
|
||||
AuthToken: srv.RootToken.SecretID,
|
||||
})
|
||||
}()
|
||||
|
||||
if tc.aclPolicy != "" {
|
||||
// Create ACL token with test case policy and add it to the
|
||||
// command.
|
||||
policyName := nonAlphaNum.ReplaceAllString(tc.name, "-")
|
||||
token := mock.CreatePolicyAndToken(t, state, uint64(302+i), policyName, tc.aclPolicy)
|
||||
args = append(args, "-token", token.SecretID)
|
||||
}
|
||||
|
||||
// Add job ID or job ID prefix to the command.
|
||||
if tc.jobPrefix {
|
||||
args = append(args, job.ID[:3])
|
||||
} else {
|
||||
args = append(args, job.ID)
|
||||
}
|
||||
|
||||
// Run command scaling job to 2.
|
||||
args = append(args, "2")
|
||||
code := cmd.Run(args)
|
||||
if tc.expectedErr == "" {
|
||||
must.Zero(t, code)
|
||||
} else {
|
||||
must.One(t, code)
|
||||
must.StrContains(t, ui.ErrorWriter.String(), tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,10 @@ Usage: nomad job scaling-events [options] <args>
|
||||
|
||||
List the scaling events for the specified job.
|
||||
|
||||
When ACLs are enabled, this command requires a token with the
|
||||
'read-job-scaling' capability for the job's namespace.
|
||||
When ACLs are enabled, this command requires a token with either the
|
||||
'read-job' or 'read-job-scaling' capability for the job's namespace. The
|
||||
'list-jobs' capability is required to run the command with a job prefix
|
||||
instead of the exact job ID.
|
||||
|
||||
General Options:
|
||||
|
||||
|
||||
@@ -5,10 +5,15 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/command/agent"
|
||||
"github.com/hashicorp/nomad/helper/pointer"
|
||||
"github.com/hashicorp/nomad/nomad/mock"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/nomad/testutil"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/shoenig/test/must"
|
||||
)
|
||||
|
||||
func TestJobScalingEventsCommand_Run(t *testing.T) {
|
||||
@@ -89,3 +94,110 @@ func TestJobScalingEventsCommand_Run(t *testing.T) {
|
||||
t.Fatalf("Expected to verbose table headers: %v", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJobScalingEventsCommand_ACL(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Start server with ACL enabled.
|
||||
srv, _, url := testServer(t, true, func(c *agent.Config) {
|
||||
c.ACL.Enabled = true
|
||||
})
|
||||
defer srv.Shutdown()
|
||||
|
||||
// Create a job.
|
||||
job := mock.MinJob()
|
||||
state := srv.Agent.Server().State()
|
||||
err := state.UpsertJob(structs.MsgTypeTestSetup, 100, job)
|
||||
must.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
jobPrefix bool
|
||||
aclPolicy string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "no token",
|
||||
aclPolicy: "",
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "missing read-job or read-job-scaling",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["submit-job"]
|
||||
}
|
||||
`,
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "read-job-scaling allowed",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job-scaling"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "read-job allowed",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "job prefix requires list-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job-scaling"]
|
||||
}
|
||||
`,
|
||||
expectedErr: "job not found",
|
||||
},
|
||||
{
|
||||
name: "job prefix works with list-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job-scaling","list-jobs"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &JobScalingEventsCommand{Meta: Meta{Ui: ui}}
|
||||
args := []string{
|
||||
"-address", url,
|
||||
}
|
||||
|
||||
if tc.aclPolicy != "" {
|
||||
// Create ACL token with test case policy and add it to the
|
||||
// command.
|
||||
policyName := nonAlphaNum.ReplaceAllString(tc.name, "-")
|
||||
token := mock.CreatePolicyAndToken(t, state, uint64(302+i), policyName, tc.aclPolicy)
|
||||
args = append(args, "-token", token.SecretID)
|
||||
}
|
||||
|
||||
// Add job ID or job ID prefix to the command.
|
||||
if tc.jobPrefix {
|
||||
args = append(args, job.ID[:3])
|
||||
} else {
|
||||
args = append(args, job.ID)
|
||||
}
|
||||
|
||||
// Run command.
|
||||
code := cmd.Run(args)
|
||||
if tc.expectedErr == "" {
|
||||
must.Zero(t, code)
|
||||
} else {
|
||||
must.One(t, code)
|
||||
must.StrContains(t, ui.ErrorWriter.String(), tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,9 @@ Usage: nomad status [options] <job>
|
||||
Display status information about a job. If no job ID is given, a list of all
|
||||
known jobs will be displayed.
|
||||
|
||||
When ACLs are enabled, this command requires a token with the 'read-job' and
|
||||
'list-jobs' capabilities for the job's namespace.
|
||||
When ACLs are enabled, this command requires a token with the 'read-job'
|
||||
capability for the job's namespace. The 'list-jobs' capability is required to
|
||||
run the command with a job prefix instead of the exact job ID.
|
||||
|
||||
General Options:
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/hashicorp/nomad/testutil"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
"github.com/shoenig/test/must"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -392,6 +393,105 @@ func TestJobStatusCommand_RescheduleEvals(t *testing.T) {
|
||||
require.Contains(out, e.ID[:8])
|
||||
}
|
||||
|
||||
func TestJobStatusCommand_ACL(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Start server with ACL enabled.
|
||||
srv, _, url := testServer(t, true, func(c *agent.Config) {
|
||||
c.ACL.Enabled = true
|
||||
})
|
||||
defer srv.Shutdown()
|
||||
|
||||
// Create a job.
|
||||
job := mock.MinJob()
|
||||
state := srv.Agent.Server().State()
|
||||
err := state.UpsertJob(structs.MsgTypeTestSetup, 100, job)
|
||||
must.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
jobPrefix bool
|
||||
aclPolicy string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "no token",
|
||||
aclPolicy: "",
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "missing read-job",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["submit-job"]
|
||||
}
|
||||
`,
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "read-job allowed",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "job prefix requires list-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job"]
|
||||
}
|
||||
`,
|
||||
expectedErr: "job not found",
|
||||
},
|
||||
{
|
||||
name: "job prefix works with list-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job", "list-jobs"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &JobStatusCommand{Meta: Meta{Ui: ui}}
|
||||
args := []string{
|
||||
"-address", url,
|
||||
}
|
||||
|
||||
if tc.aclPolicy != "" {
|
||||
// Create ACL token with test case policy and add it to the
|
||||
// command.
|
||||
policyName := nonAlphaNum.ReplaceAllString(tc.name, "-")
|
||||
token := mock.CreatePolicyAndToken(t, state, uint64(302+i), policyName, tc.aclPolicy)
|
||||
args = append(args, "-token", token.SecretID)
|
||||
}
|
||||
|
||||
// Add job ID or job ID prefix to the command.
|
||||
if tc.jobPrefix {
|
||||
args = append(args, job.ID[:3])
|
||||
} else {
|
||||
args = append(args, job.ID)
|
||||
}
|
||||
code := cmd.Run(args)
|
||||
|
||||
// Run command.
|
||||
if tc.expectedErr == "" {
|
||||
must.Zero(t, code)
|
||||
} else {
|
||||
must.One(t, code)
|
||||
must.StrContains(t, ui.ErrorWriter.String(), tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func waitForSuccess(ui cli.Ui, client *api.Client, length int, t *testing.T, evalId string) int {
|
||||
mon := newMonitor(ui, client, length)
|
||||
monErr := mon.monitor(evalId)
|
||||
|
||||
@@ -25,8 +25,10 @@ Alias: nomad stop
|
||||
allocations and completes shutting down. It is safe to exit the monitor
|
||||
early using ctrl+c.
|
||||
|
||||
When ACLs are enabled, this command requires a token with the 'submit-job',
|
||||
'read-job', and 'list-jobs' capabilities for the job's namespace.
|
||||
When ACLs are enabled, this command requires a token with the 'submit-job'
|
||||
and 'read-job' capabilities for the job's namespace. The 'list-jobs'
|
||||
capability is required to run the command with job prefixes instead of exact
|
||||
job IDs.
|
||||
|
||||
General Options:
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/command/agent"
|
||||
"github.com/hashicorp/nomad/helper/pointer"
|
||||
@@ -148,3 +149,117 @@ func TestStopCommand_AutocompleteArgs(t *testing.T) {
|
||||
must.Len(t, 1, res)
|
||||
must.Eq(t, j.ID, res[0])
|
||||
}
|
||||
|
||||
func TestJobStopCommand_ACL(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Start server with ACL enabled.
|
||||
srv, client, url := testServer(t, true, func(c *agent.Config) {
|
||||
c.ACL.Enabled = true
|
||||
})
|
||||
defer srv.Shutdown()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
jobPrefix bool
|
||||
aclPolicy string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "no token",
|
||||
aclPolicy: "",
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "missing submit-job",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job"]
|
||||
}
|
||||
`,
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "missing read-job",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["submit-job"]
|
||||
}
|
||||
`,
|
||||
expectedErr: api.PermissionDeniedErrorContent,
|
||||
},
|
||||
{
|
||||
name: "read-job and submit-job allowed",
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job", "submit-job"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "job prefix requires list-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["read-job", "submit-job"]
|
||||
}
|
||||
`,
|
||||
expectedErr: "job not found",
|
||||
},
|
||||
{
|
||||
name: "job prefix works with list-job",
|
||||
jobPrefix: true,
|
||||
aclPolicy: `
|
||||
namespace "default" {
|
||||
capabilities = ["list-jobs", "read-job", "submit-job"]
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &JobStopCommand{Meta: Meta{Ui: ui}}
|
||||
args := []string{
|
||||
"-address", url,
|
||||
"-yes",
|
||||
}
|
||||
|
||||
// Create a job.
|
||||
job := mock.MinJob()
|
||||
state := srv.Agent.Server().State()
|
||||
err := state.UpsertJob(structs.MsgTypeTestSetup, uint64(300+i), job)
|
||||
must.NoError(t, err)
|
||||
defer func() {
|
||||
client.Jobs().Deregister(job.ID, true, &api.WriteOptions{
|
||||
AuthToken: srv.RootToken.SecretID,
|
||||
})
|
||||
}()
|
||||
|
||||
if tc.aclPolicy != "" {
|
||||
// Create ACL token with test case policy and add it to the
|
||||
// command.
|
||||
policyName := nonAlphaNum.ReplaceAllString(tc.name, "-")
|
||||
token := mock.CreatePolicyAndToken(t, state, uint64(302+i), policyName, tc.aclPolicy)
|
||||
args = append(args, "-token", token.SecretID)
|
||||
}
|
||||
|
||||
// Add job ID or job ID prefix to the command.
|
||||
if tc.jobPrefix {
|
||||
args = append(args, job.ID[:3])
|
||||
} else {
|
||||
args = append(args, job.ID)
|
||||
}
|
||||
|
||||
// Run command.
|
||||
code := cmd.Run(args)
|
||||
if tc.expectedErr == "" {
|
||||
must.Zero(t, code)
|
||||
} else {
|
||||
must.One(t, code)
|
||||
must.StrContains(t, ui.ErrorWriter.String(), tc.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,13 +260,17 @@ func (m *Meta) JobByPrefix(client *api.Client, prefix string, filter JobByPrefix
|
||||
return job, nil
|
||||
}
|
||||
|
||||
// JobIDByPrefix returns the job that best matches the given prefix and its
|
||||
// namespace. Returns an error if there are no matches or if there are more
|
||||
// than one exact match across namespaces.
|
||||
// JobIDByPrefix provides best effort match for the given job prefix.
|
||||
// Returns the prefix itself if job prefix search is not allowed and an error
|
||||
// if there are no matches or if there are more than one exact match across
|
||||
// namespaces.
|
||||
func (m *Meta) JobIDByPrefix(client *api.Client, prefix string, filter JobByPrefixFilterFunc) (string, string, error) {
|
||||
// Search job by prefix. Return an error if there is not an exact match.
|
||||
jobs, _, err := client.Jobs().PrefixList(prefix)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), api.PermissionDeniedErrorContent) {
|
||||
return prefix, "", nil
|
||||
}
|
||||
return "", "", fmt.Errorf("Error querying job prefix %q: %s", prefix, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package command
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -14,6 +15,8 @@ import (
|
||||
"github.com/shoenig/test/must"
|
||||
)
|
||||
|
||||
var nonAlphaNum = regexp.MustCompile(`[^a-zA-Z0-9]+`)
|
||||
|
||||
func testServer(t *testing.T, runClient bool, cb func(*agent.Config)) (*agent.TestAgent, *api.Client, string) {
|
||||
// Make a new test server
|
||||
a := agent.NewTestAgent(t, t.Name(), func(config *agent.Config) {
|
||||
|
||||
Reference in New Issue
Block a user