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:
Luiz Aoqui 2023-03-09 15:00:04 -05:00 committed by GitHub
parent 3239539526
commit 1aceff7806
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 1631 additions and 47 deletions

3
.changelog/16380.txt Normal file
View File

@ -0,0 +1,3 @@
``release-note:improvement
cli: Remove requirement for `list-jobs` capability on several job subcommands that didn't strictly needed it
```

View File

@ -19,8 +19,9 @@ Usage: nomad job allocs [options] <job>
Display allocations for a particular job. Display allocations for a particular job.
When ACLs are enabled, this command requires a token with the 'read-job' and When ACLs are enabled, this command requires a token with the 'read-job'
'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.
General Options: General Options:

View File

@ -3,11 +3,14 @@ package command
import ( import (
"testing" "testing"
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/command/agent"
"github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/mock"
"github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/nomad/structs"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
"github.com/posener/complete" "github.com/posener/complete"
"github.com/shoenig/test/must"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -172,3 +175,116 @@ func TestJobAllocsCommand_AutocompleteArgs(t *testing.T) {
require.Equal(t, 1, len(res)) require.Equal(t, 1, len(res))
require.Equal(t, j.ID, res[0]) 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)
}
})
}
}

View File

@ -19,8 +19,9 @@ Usage: nomad job deployments [options] <job>
Deployments is used to display the deployments for a particular 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 When ACLs are enabled, this command requires a token with the 'read-job'
'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.
General Options: General Options:

View File

@ -4,11 +4,14 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/command/agent"
"github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/mock"
"github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/nomad/structs"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
"github.com/posener/complete" "github.com/posener/complete"
"github.com/shoenig/test/must"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -152,3 +155,112 @@ func TestJobDeploymentsCommand_AutocompleteArgs(t *testing.T) {
assert.Equal(1, len(res)) assert.Equal(1, len(res))
assert.Equal(j.ID, res[0]) 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)
}
})
}
}

View File

@ -33,7 +33,10 @@ Usage: nomad job dispatch [options] <parameterized job> [input source]
detach flag. detach flag.
When ACLs are enabled, this command requires a token with the 'dispatch-job' 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: General Options:

View File

@ -4,11 +4,14 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/command/agent"
"github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/mock"
"github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/nomad/structs"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
"github.com/posener/complete" "github.com/posener/complete"
"github.com/shoenig/test/must"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -85,3 +88,123 @@ func TestJobDispatchCommand_AutocompleteArgs(t *testing.T) {
require.Equal(t, 1, len(res)) require.Equal(t, 1, len(res))
require.Equal(t, j1.ID, res[0]) 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)
}
})
}
}

View File

@ -23,8 +23,9 @@ Usage: nomad job eval [options] <job_id>
operators to force the scheduler to create new allocations under certain operators to force the scheduler to create new allocations under certain
scenarios. scenarios.
When ACLs are enabled, this command requires a token with the 'submit-job' When ACLs are enabled, this command requires a token with the 'read-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.
General Options: General Options:

View File

@ -5,12 +5,15 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/command/agent"
"github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/mock"
"github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/testutil" "github.com/hashicorp/nomad/testutil"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
"github.com/posener/complete" "github.com/posener/complete"
"github.com/shoenig/test/must"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -125,3 +128,102 @@ func TestJobEvalCommand_AutocompleteArgs(t *testing.T) {
assert.Equal(1, len(res)) assert.Equal(1, len(res))
assert.Equal(j.ID, res[0]) 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)
}
})
}
}

View File

@ -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 the changes that occurred to the job as well as deciding job versions to revert
to. to.
When ACLs are enabled, this command requires a token with the 'read-job' and When ACLs are enabled, this command requires a token with the 'read-job'
'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.
General Options: General Options:

View File

@ -4,11 +4,14 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/command/agent"
"github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/mock"
"github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/nomad/structs"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
"github.com/posener/complete" "github.com/posener/complete"
"github.com/shoenig/test/must"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -63,3 +66,102 @@ func TestJobHistoryCommand_AutocompleteArgs(t *testing.T) {
assert.Equal(1, len(res)) assert.Equal(1, len(res))
assert.Equal(j.ID, res[0]) 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)
}
})
}
}

View File

@ -20,8 +20,9 @@ Alias: nomad inspect
Inspect is used to see the specification of a submitted job. 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 When ACLs are enabled, this command requires a token with the 'read-job'
'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.
General Options: General Options:

View File

@ -4,11 +4,14 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/command/agent"
"github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/mock"
"github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/nomad/structs"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
"github.com/posener/complete" "github.com/posener/complete"
"github.com/shoenig/test/must"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -83,3 +86,102 @@ func TestInspectCommand_AutocompleteArgs(t *testing.T) {
assert.Equal(1, len(res)) assert.Equal(1, len(res))
assert.Equal(j.ID, res[0]) 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)
}
})
}
}

View File

@ -22,7 +22,10 @@ Usage: nomad job periodic force <job id>
prohibit_overlap setting. prohibit_overlap setting.
When ACLs are enabled, this command requires a token with the 'submit-job' 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: General Options:

View File

@ -6,12 +6,14 @@ import (
"github.com/hashicorp/nomad/api" "github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/command/agent"
"github.com/hashicorp/nomad/helper/pointer" "github.com/hashicorp/nomad/helper/pointer"
"github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/mock"
"github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/testutil" "github.com/hashicorp/nomad/testutil"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
"github.com/posener/complete" "github.com/posener/complete"
"github.com/shoenig/test/must"
"github.com/stretchr/testify/require" "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, "Monitoring evaluation")
require.Contains(t, out, "finished with status \"complete\"") 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)
}
})
}
}

View File

@ -28,7 +28,9 @@ Usage: nomad job promote [options] <job id>
"nomad job revert" command. "nomad job revert" command.
When ACLs are enabled, this command requires a token with the 'submit-job', 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: General Options:

View File

@ -4,8 +4,11 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/command/agent"
"github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/nomad/structs"
"github.com/shoenig/test/must"
"github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/mock"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
@ -64,3 +67,119 @@ func TestJobPromoteCommand_AutocompleteArgs(t *testing.T) {
assert.Equal(1, len(res)) assert.Equal(1, len(res))
assert.Equal(j.ID, res[0]) 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)
}
})
}
}

View File

@ -22,7 +22,10 @@ Usage: nomad job revert [options] <job> <version>
versions to revert to can be found using "nomad job history" command. 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' 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: General Options:

View File

@ -4,11 +4,14 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/command/agent"
"github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/mock"
structs "github.com/hashicorp/nomad/nomad/structs" structs "github.com/hashicorp/nomad/nomad/structs"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
"github.com/posener/complete" "github.com/posener/complete"
"github.com/shoenig/test/must"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -63,3 +66,136 @@ func TestJobRevertCommand_AutocompleteArgs(t *testing.T) {
assert.Equal(1, len(res)) assert.Equal(1, len(res))
assert.Equal(j.ID, res[0]) 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)
}
})
}
}

View File

@ -32,8 +32,12 @@ Usage: nomad job scale [options] <job> [<group>] <count>
onto nodes. The monitor will end once job placement is done. It onto nodes. The monitor will end once job placement is done. It
is safe to exit the monitor early using ctrl+c. is safe to exit the monitor early using ctrl+c.
When ACLs are enabled, this command requires a token with the 'scale-job' When ACLs are enabled, this command requires a token with the
capability for the job's namespace. '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: General Options:

View File

@ -7,9 +7,13 @@ import (
"github.com/hashicorp/nomad/api" "github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/command/agent"
"github.com/hashicorp/nomad/helper/pointer" "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/hashicorp/nomad/testutil"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
"github.com/shoenig/test/must"
) )
func TestJobScaleCommand_SingleGroup(t *testing.T) { 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) 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)
}
})
}
}

View File

@ -27,8 +27,10 @@ Usage: nomad job scaling-events [options] <args>
List the scaling events for the specified job. List the scaling events for the specified job.
When ACLs are enabled, this command requires a token with the When ACLs are enabled, this command requires a token with either the
'read-job-scaling' capability for the job's namespace. '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: General Options:

View File

@ -5,10 +5,15 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/command/agent"
"github.com/hashicorp/nomad/helper/pointer" "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/hashicorp/nomad/testutil"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
"github.com/shoenig/test/must"
) )
func TestJobScalingEventsCommand_Run(t *testing.T) { 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) 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)
}
})
}
}

View File

@ -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 Display status information about a job. If no job ID is given, a list of all
known jobs will be displayed. known jobs will be displayed.
When ACLs are enabled, this command requires a token with the 'read-job' and When ACLs are enabled, this command requires a token with the 'read-job'
'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.
General Options: General Options:

View File

@ -15,6 +15,7 @@ import (
"github.com/hashicorp/nomad/testutil" "github.com/hashicorp/nomad/testutil"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
"github.com/posener/complete" "github.com/posener/complete"
"github.com/shoenig/test/must"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -392,6 +393,105 @@ func TestJobStatusCommand_RescheduleEvals(t *testing.T) {
require.Contains(out, e.ID[:8]) 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 { func waitForSuccess(ui cli.Ui, client *api.Client, length int, t *testing.T, evalId string) int {
mon := newMonitor(ui, client, length) mon := newMonitor(ui, client, length)
monErr := mon.monitor(evalId) monErr := mon.monitor(evalId)

View File

@ -25,8 +25,10 @@ Alias: nomad stop
allocations and completes shutting down. It is safe to exit the monitor allocations and completes shutting down. It is safe to exit the monitor
early using ctrl+c. early using ctrl+c.
When ACLs are enabled, this command requires a token with the 'submit-job', When ACLs are enabled, this command requires a token with the 'submit-job'
'read-job', and 'list-jobs' 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 job prefixes instead of exact
job IDs.
General Options: General Options:

View File

@ -6,6 +6,7 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/command/agent" "github.com/hashicorp/nomad/command/agent"
"github.com/hashicorp/nomad/helper/pointer" "github.com/hashicorp/nomad/helper/pointer"
@ -148,3 +149,117 @@ func TestStopCommand_AutocompleteArgs(t *testing.T) {
must.Len(t, 1, res) must.Len(t, 1, res)
must.Eq(t, j.ID, res[0]) 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)
}
})
}
}

View File

@ -260,13 +260,17 @@ func (m *Meta) JobByPrefix(client *api.Client, prefix string, filter JobByPrefix
return job, nil return job, nil
} }
// JobIDByPrefix returns the job that best matches the given prefix and its // JobIDByPrefix provides best effort match for the given job prefix.
// namespace. Returns an error if there are no matches or if there are more // Returns the prefix itself if job prefix search is not allowed and an error
// than one exact match across namespaces. // 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) { 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. // Search job by prefix. Return an error if there is not an exact match.
jobs, _, err := client.Jobs().PrefixList(prefix) jobs, _, err := client.Jobs().PrefixList(prefix)
if err != nil { if err != nil {
if strings.Contains(err.Error(), api.PermissionDeniedErrorContent) {
return prefix, "", nil
}
return "", "", fmt.Errorf("Error querying job prefix %q: %s", prefix, err) return "", "", fmt.Errorf("Error querying job prefix %q: %s", prefix, err)
} }

View File

@ -3,6 +3,7 @@ package command
import ( import (
"fmt" "fmt"
"os" "os"
"regexp"
"testing" "testing"
"time" "time"
@ -14,6 +15,8 @@ import (
"github.com/shoenig/test/must" "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) { func testServer(t *testing.T, runClient bool, cb func(*agent.Config)) (*agent.TestAgent, *api.Client, string) {
// Make a new test server // Make a new test server
a := agent.NewTestAgent(t, t.Name(), func(config *agent.Config) { a := agent.NewTestAgent(t, t.Name(), func(config *agent.Config) {

View File

@ -19,8 +19,9 @@ nomad job allocs [options] <job>
The `job allocs` command requires a single argument, the job ID or an ID The `job allocs` command requires a single argument, the job ID or an ID
prefix of a job to display the list of allocations for. prefix of a job to display the list of allocations for.
When ACLs are enabled, this command requires a token with the `read-job` and When ACLs are enabled, this command requires a token with the `read-job`
`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.
## General Options ## General Options

View File

@ -19,8 +19,9 @@ nomad job deployments [options] <job>
The `job deployments` command requires a single argument, the job ID or an ID The `job deployments` command requires a single argument, the job ID or an ID
prefix of a job to display the list of deployments for. prefix of a job to display the list of deployments for.
When ACLs are enabled, this command requires a token with the `read-job` and When ACLs are enabled, this command requires a token with the `read-job`
`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.
## General Options ## General Options

View File

@ -45,7 +45,10 @@ exhaustion, etc), then the exit code will be 2. Any other errors, including
client connection issues or internal errors, are indicated by exit code 1. client connection issues or internal errors, are indicated by exit code 1.
When ACLs are enabled, this command requires a token with the `dispatch-job` 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.
See the [multiregion] documentation for additional considerations when See the [multiregion] documentation for additional considerations when
dispatching parameterized jobs. dispatching parameterized jobs.

View File

@ -20,8 +20,9 @@ The `job eval` command requires a single argument, specifying the job ID to
evaluate. If there is an exact match based on the provided job ID, then the job evaluate. If there is an exact match based on the provided job ID, then the job
will be evaluated, forcing a scheduler run. will be evaluated, forcing a scheduler run.
When ACLs are enabled, this command requires a token with the `submit-job` When ACLs are enabled, this command requires a token with the `read-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.
## General Options ## General Options

View File

@ -21,8 +21,9 @@ nomad job history [options] <job>
The `job history` command requires a single argument, the job ID or an ID prefix The `job history` command requires a single argument, the job ID or an ID prefix
of a job to display the history for. of a job to display the history for.
When ACLs are enabled, this command requires a token with the `read-job` and When ACLs are enabled, this command requires a token with the `read-job`
`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.
## General Options ## General Options

View File

@ -22,8 +22,9 @@ will retrieve the JSON version of the job. This JSON is valid to be submitted to
the [Job HTTP API]. This command is useful to inspect what version of a job the [Job HTTP API]. This command is useful to inspect what version of a job
Nomad is running. Nomad is running.
When ACLs are enabled, this command requires a token with the `read-job` and When ACLs are enabled, this command requires a token with the `read-job`
`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.
## General Options ## General Options

View File

@ -27,7 +27,10 @@ placement information for the forced evaluation. The monitor will exit after
scheduling has finished or failed. scheduling has finished or failed.
When ACLs are enabled, this command requires a token with the `submit-job` 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 ## General Options

View File

@ -28,7 +28,9 @@ promotes all task groups. The group flag can be specified multiple times to
select particular groups to promote. select particular groups to promote.
When ACLs are enabled, this command requires a token with the `submit-job`, 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 ## General Options

View File

@ -35,7 +35,10 @@ The `job revert` command requires two inputs, the job ID and the version of that
job to revert to. job to revert to.
When ACLs are enabled, this command requires a token with the `submit-job` 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 ## General Options

View File

@ -28,8 +28,12 @@ Scale will issue a request to update the matched job and then invoke an interact
monitor that exits automatically once the scheduler has processed the request. monitor that exits automatically once the scheduler has processed the request.
It is safe to exit the monitor early using ctrl+c. It is safe to exit the monitor early using ctrl+c.
When ACLs are enabled, this command requires a token with the `scale-job` When ACLs are enabled, this command requires a token with the
capability for the job's namespace. `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 ## General Options

View File

@ -19,8 +19,10 @@ nomad job scaling-events [options] <job>
The `job scaling-events` command requires a single argument, a submitted job's The `job scaling-events` command requires a single argument, a submitted job's
ID, and will output the stored scaling events for the job if there are any. ID, and will output the stored scaling events for the job if there are any.
When ACLs are enabled, this command requires a token with the When ACLs are enabled, this command requires a token with either the
`read-job-scaling` capability for the job's namespace. `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 ## General Options

View File

@ -26,8 +26,9 @@ shows allocation modification time in addition to create time. When the
`-verbose` flag is not set, allocation creation and modify times are shown in a `-verbose` flag is not set, allocation creation and modify times are shown in a
shortened relative time format like `5m ago`. shortened relative time format like `5m ago`.
When ACLs are enabled, this command requires a token with the `read-job` and When ACLs are enabled, this command requires a token with the `read-job`
`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.
## General Options ## General Options

View File

@ -27,8 +27,10 @@ Stop will issue a request to deregister the matched jobs and then invoke an
interactive monitor that exits automatically once the scheduler has processed interactive monitor that exits automatically once the scheduler has processed
the requests. It is safe to exit the monitor early using ctrl+c. the requests. It is safe to exit the monitor early using ctrl+c.
When ACLs are enabled, this command requires a token with the `submit-job`, When ACLs are enabled, this command requires a token with the `submit-job`
`read-job`, and `list-jobs` 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 job prefixes instead of exact
job IDs.
## General Options ## General Options