mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
cli: add new acl role subcommands for CRUD role actions. (#14087)
This commit is contained in:
102
command/acl_role.go
Normal file
102
command/acl_role.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
// Ensure ACLRoleCommand satisfies the cli.Command interface.
|
||||
var _ cli.Command = &ACLRoleCommand{}
|
||||
|
||||
// ACLRoleCommand implements cli.Command.
|
||||
type ACLRoleCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
// Help satisfies the cli.Command Help function.
|
||||
func (a *ACLRoleCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad acl role <subcommand> [options] [args]
|
||||
|
||||
This command groups subcommands for interacting with ACL roles. Nomad's ACL
|
||||
system can be used to control access to data and APIs. ACL roles are
|
||||
associated with one or more ACL policies which grant specific capabilities.
|
||||
For a full guide see: https://www.nomadproject.io/guides/acl.html
|
||||
|
||||
Create an ACL role:
|
||||
|
||||
$ nomad acl role create -name="name" -policy-name="policy-name"
|
||||
|
||||
List all ACL roles:
|
||||
|
||||
$ nomad acl role list
|
||||
|
||||
Lookup a specific ACL role:
|
||||
|
||||
$ nomad acl role info <acl_role_id>
|
||||
|
||||
Update an ACL role:
|
||||
|
||||
$ nomad acl role update -name="updated-name" <acl_role_id>
|
||||
|
||||
Delete an ACL role:
|
||||
|
||||
$ nomad acl role delete <acl_role_id>
|
||||
|
||||
Please see the individual subcommand help for detailed usage information.
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
// Synopsis satisfies the cli.Command Synopsis function.
|
||||
func (a *ACLRoleCommand) Synopsis() string { return "Interact with ACL roles" }
|
||||
|
||||
// Name returns the name of this command.
|
||||
func (a *ACLRoleCommand) Name() string { return "acl role" }
|
||||
|
||||
// Run satisfies the cli.Command Run function.
|
||||
func (a *ACLRoleCommand) Run(_ []string) int { return cli.RunResultHelp }
|
||||
|
||||
// formatACLRole formats and converts the ACL role API object into a string KV
|
||||
// representation suitable for console output.
|
||||
func formatACLRole(aclRole *api.ACLRole) string {
|
||||
return formatKV([]string{
|
||||
fmt.Sprintf("ID|%s", aclRole.ID),
|
||||
fmt.Sprintf("Name|%s", aclRole.Name),
|
||||
fmt.Sprintf("Description|%s", aclRole.Description),
|
||||
fmt.Sprintf("Policies|%s", strings.Join(aclRolePolicyLinkToStringList(aclRole.Policies), ",")),
|
||||
fmt.Sprintf("Create Index|%d", aclRole.CreateIndex),
|
||||
fmt.Sprintf("Modify Index|%d", aclRole.ModifyIndex),
|
||||
})
|
||||
}
|
||||
|
||||
// aclRolePolicyLinkToStringList converts an array of ACL role policy links to
|
||||
// an array of string policy names. The returned array will be sorted.
|
||||
func aclRolePolicyLinkToStringList(policyLinks []*api.ACLRolePolicyLink) []string {
|
||||
policies := make([]string, len(policyLinks))
|
||||
for i, policy := range policyLinks {
|
||||
policies[i] = policy.Name
|
||||
}
|
||||
sort.Strings(policies)
|
||||
return policies
|
||||
}
|
||||
|
||||
// aclRolePolicyNamesToPolicyLinks takes a list of policy names as a string
|
||||
// array and converts this to an array of ACL role policy links. Any duplicate
|
||||
// names are removed.
|
||||
func aclRolePolicyNamesToPolicyLinks(policyNames []string) []*api.ACLRolePolicyLink {
|
||||
var policyLinks []*api.ACLRolePolicyLink
|
||||
keys := make(map[string]struct{})
|
||||
|
||||
for _, policyName := range policyNames {
|
||||
if _, ok := keys[policyName]; !ok {
|
||||
policyLinks = append(policyLinks, &api.ACLRolePolicyLink{Name: policyName})
|
||||
keys[policyName] = struct{}{}
|
||||
}
|
||||
}
|
||||
return policyLinks
|
||||
}
|
||||
148
command/acl_role_create.go
Normal file
148
command/acl_role_create.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
// Ensure ACLRoleCreateCommand satisfies the cli.Command interface.
|
||||
var _ cli.Command = &ACLRoleCreateCommand{}
|
||||
|
||||
// ACLRoleCreateCommand implements cli.Command.
|
||||
type ACLRoleCreateCommand struct {
|
||||
Meta
|
||||
|
||||
name string
|
||||
description string
|
||||
policyNames []string
|
||||
json bool
|
||||
tmpl string
|
||||
}
|
||||
|
||||
// Help satisfies the cli.Command Help function.
|
||||
func (a *ACLRoleCreateCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad acl token create [options]
|
||||
|
||||
Create is used to create new ACL roles. Use requires a management token.
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace) + `
|
||||
|
||||
ACL Create Options:
|
||||
|
||||
-name
|
||||
Sets the human readable name for the ACL role. The name must be between
|
||||
1-128 characters and is a required parameter.
|
||||
|
||||
-description
|
||||
A free form text description of the role that must not exceed 256
|
||||
characters.
|
||||
|
||||
-policy-name
|
||||
Specifies a policy to associate with the role identified by their name. This
|
||||
flag can be specified multiple times and must be specified at least once.
|
||||
|
||||
-json
|
||||
Output the ACL role in a JSON format.
|
||||
|
||||
-t
|
||||
Format and display the ACL role using a Go template.
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (a *ACLRoleCreateCommand) AutocompleteFlags() complete.Flags {
|
||||
return mergeAutocompleteFlags(a.Meta.AutocompleteFlags(FlagSetClient),
|
||||
complete.Flags{
|
||||
"-name": complete.PredictAnything,
|
||||
"-description": complete.PredictAnything,
|
||||
"-policy-name": complete.PredictAnything,
|
||||
"-json": complete.PredictNothing,
|
||||
"-t": complete.PredictAnything,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *ACLRoleCreateCommand) AutocompleteArgs() complete.Predictor { return complete.PredictNothing }
|
||||
|
||||
// Synopsis satisfies the cli.Command Synopsis function.
|
||||
func (a *ACLRoleCreateCommand) Synopsis() string { return "Create a new ACL role" }
|
||||
|
||||
// Name returns the name of this command.
|
||||
func (a *ACLRoleCreateCommand) Name() string { return "acl role create" }
|
||||
|
||||
// Run satisfies the cli.Command Run function.
|
||||
func (a *ACLRoleCreateCommand) Run(args []string) int {
|
||||
|
||||
flags := a.Meta.FlagSet(a.Name(), FlagSetClient)
|
||||
flags.Usage = func() { a.Ui.Output(a.Help()) }
|
||||
flags.StringVar(&a.name, "name", "", "")
|
||||
flags.StringVar(&a.description, "description", "", "")
|
||||
flags.Var((funcVar)(func(s string) error {
|
||||
a.policyNames = append(a.policyNames, s)
|
||||
return nil
|
||||
}), "policy-name", "")
|
||||
flags.BoolVar(&a.json, "json", false, "")
|
||||
flags.StringVar(&a.tmpl, "t", "", "")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check that we got no arguments.
|
||||
if len(flags.Args()) != 0 {
|
||||
a.Ui.Error("This command takes no arguments")
|
||||
a.Ui.Error(commandErrorText(a))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Perform some basic validation on the submitted role information to avoid
|
||||
// sending API and RPC requests which will fail basic validation.
|
||||
if a.name == "" {
|
||||
a.Ui.Error("ACL role name must be specified using the -name flag")
|
||||
return 1
|
||||
}
|
||||
if len(a.policyNames) < 1 {
|
||||
a.Ui.Error("At least one policy name must be specified using the -policy-name flag")
|
||||
return 1
|
||||
}
|
||||
|
||||
// Set up the ACL with the passed parameters.
|
||||
aclRole := api.ACLRole{
|
||||
Name: a.name,
|
||||
Description: a.description,
|
||||
Policies: aclRolePolicyNamesToPolicyLinks(a.policyNames),
|
||||
}
|
||||
|
||||
// Get the HTTP client.
|
||||
client, err := a.Meta.Client()
|
||||
if err != nil {
|
||||
a.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Create the ACL role via the API.
|
||||
role, _, err := client.ACLRoles().Create(&aclRole, nil)
|
||||
if err != nil {
|
||||
a.Ui.Error(fmt.Sprintf("Error creating ACL role: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
if a.json || len(a.tmpl) > 0 {
|
||||
out, err := Format(a.json, a.tmpl, role)
|
||||
if err != nil {
|
||||
a.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
a.Ui.Output(out)
|
||||
return 0
|
||||
}
|
||||
|
||||
a.Ui.Output(formatACLRole(role))
|
||||
return 0
|
||||
}
|
||||
80
command/acl_role_create_test.go
Normal file
80
command/acl_role_create_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/command/agent"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/nomad/testutil"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestACLRoleCreateCommand_Run(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Build a test server with ACLs enabled.
|
||||
srv, _, url := testServer(t, false, func(c *agent.Config) {
|
||||
c.ACL.Enabled = true
|
||||
})
|
||||
defer srv.Shutdown()
|
||||
|
||||
// Wait for the server to start fully and ensure we have a bootstrap token.
|
||||
testutil.WaitForLeader(t, srv.Agent.RPC)
|
||||
rootACLToken := srv.RootToken
|
||||
require.NotNil(t, rootACLToken)
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &ACLRoleCreateCommand{
|
||||
Meta: Meta{
|
||||
Ui: ui,
|
||||
flagAddress: url,
|
||||
},
|
||||
}
|
||||
|
||||
// Test the basic validation on the command.
|
||||
require.Equal(t, 1, cmd.Run([]string{"-address=" + url, "this-command-does-not-take-args"}))
|
||||
require.Contains(t, ui.ErrorWriter.String(), "This command takes no arguments")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
require.Equal(t, 1, cmd.Run([]string{"-address=" + url}))
|
||||
require.Contains(t, ui.ErrorWriter.String(), "ACL role name must be specified using the -name flag")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
require.Equal(t, 1, cmd.Run([]string{"-address=" + url, `-name="foobar"`}))
|
||||
require.Contains(t, ui.ErrorWriter.String(), "At least one policy name must be specified using the -policy-name flag")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Create an ACL policy that can be referenced within the ACL role.
|
||||
aclPolicy := structs.ACLPolicy{
|
||||
Name: "acl-role-cli-test-policy",
|
||||
Rules: `namespace "default" {
|
||||
policy = "read"
|
||||
}
|
||||
`,
|
||||
}
|
||||
err := srv.Agent.Server().State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{&aclPolicy})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an ACL role.
|
||||
args := []string{
|
||||
"-address=" + url, "-token=" + rootACLToken.SecretID, "-name=acl-role-cli-test",
|
||||
"-policy-name=acl-role-cli-test-policy", "-description=acl-role-all-the-things",
|
||||
}
|
||||
require.Equal(t, 0, cmd.Run(args))
|
||||
s := ui.OutputWriter.String()
|
||||
require.Contains(t, s, "Name = acl-role-cli-test")
|
||||
require.Contains(t, s, "Description = acl-role-all-the-things")
|
||||
require.Contains(t, s, "Policies = acl-role-cli-test-policy")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
}
|
||||
83
command/acl_role_delete.go
Normal file
83
command/acl_role_delete.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
// Ensure ACLRoleDeleteCommand satisfies the cli.Command interface.
|
||||
var _ cli.Command = &ACLRoleDeleteCommand{}
|
||||
|
||||
// ACLRoleDeleteCommand implements cli.Command.
|
||||
type ACLRoleDeleteCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
// Help satisfies the cli.Command Help function.
|
||||
func (a *ACLRoleDeleteCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad acl role delete <acl_role_id>
|
||||
|
||||
Delete is used to delete an existing ACL role. Use requires a management
|
||||
token.
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace)
|
||||
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (a *ACLRoleDeleteCommand) AutocompleteFlags() complete.Flags {
|
||||
return mergeAutocompleteFlags(a.Meta.AutocompleteFlags(FlagSetClient),
|
||||
complete.Flags{})
|
||||
}
|
||||
|
||||
func (a *ACLRoleDeleteCommand) AutocompleteArgs() complete.Predictor { return complete.PredictNothing }
|
||||
|
||||
// Synopsis satisfies the cli.Command Synopsis function.
|
||||
func (a *ACLRoleDeleteCommand) Synopsis() string { return "Delete an existing ACL role" }
|
||||
|
||||
// Name returns the name of this command.
|
||||
func (a *ACLRoleDeleteCommand) Name() string { return "acl token delete" }
|
||||
|
||||
// Run satisfies the cli.Command Run function.
|
||||
func (a *ACLRoleDeleteCommand) Run(args []string) int {
|
||||
|
||||
flags := a.Meta.FlagSet(a.Name(), FlagSetClient)
|
||||
flags.Usage = func() { a.Ui.Output(a.Help()) }
|
||||
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check that the last argument is the role ID to delete.
|
||||
if len(flags.Args()) != 1 {
|
||||
a.Ui.Error("This command takes one argument: <acl_role_id>")
|
||||
a.Ui.Error(commandErrorText(a))
|
||||
return 1
|
||||
}
|
||||
|
||||
aclRoleID := flags.Args()[0]
|
||||
|
||||
// Get the HTTP client.
|
||||
client, err := a.Meta.Client()
|
||||
if err != nil {
|
||||
a.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Delete the specified ACL role.
|
||||
_, err = client.ACLRoles().Delete(aclRoleID, nil)
|
||||
if err != nil {
|
||||
a.Ui.Error(fmt.Sprintf("Error deleting ACL role: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Give some feedback to indicate the deletion was successful.
|
||||
a.Ui.Output(fmt.Sprintf("ACL role %s successfully deleted", aclRoleID))
|
||||
return 0
|
||||
}
|
||||
77
command/acl_role_delete_test.go
Normal file
77
command/acl_role_delete_test.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/command/agent"
|
||||
"github.com/hashicorp/nomad/helper/uuid"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/nomad/testutil"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestACLRoleDeleteCommand_Run(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Build a test server with ACLs enabled.
|
||||
srv, _, url := testServer(t, false, func(c *agent.Config) {
|
||||
c.ACL.Enabled = true
|
||||
})
|
||||
defer srv.Shutdown()
|
||||
|
||||
// Wait for the server to start fully and ensure we have a bootstrap token.
|
||||
testutil.WaitForLeader(t, srv.Agent.RPC)
|
||||
rootACLToken := srv.RootToken
|
||||
require.NotNil(t, rootACLToken)
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &ACLRoleDeleteCommand{
|
||||
Meta: Meta{
|
||||
Ui: ui,
|
||||
flagAddress: url,
|
||||
},
|
||||
}
|
||||
|
||||
// Try and delete more than one ACL role.
|
||||
code := cmd.Run([]string{"-address=" + url, "acl-role-1", "acl-role-2"})
|
||||
require.Equal(t, 1, code)
|
||||
require.Contains(t, ui.ErrorWriter.String(), "This command takes one argument")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Try deleting a role that does not exist.
|
||||
require.Equal(t, 1, cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID, "acl-role-1"}))
|
||||
require.Contains(t, ui.ErrorWriter.String(), "ACL role not found")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Create an ACL policy that can be referenced within the ACL role.
|
||||
aclPolicy := structs.ACLPolicy{
|
||||
Name: "acl-role-cli-test",
|
||||
Rules: `namespace "default" {
|
||||
policy = "read"
|
||||
}
|
||||
`,
|
||||
}
|
||||
err := srv.Agent.Server().State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{&aclPolicy})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an ACL role referencing the previously created policy.
|
||||
aclRole := structs.ACLRole{
|
||||
ID: uuid.Generate(),
|
||||
Name: "acl-role-cli-test",
|
||||
Policies: []*structs.ACLRolePolicyLink{{Name: aclPolicy.Name}},
|
||||
}
|
||||
err = srv.Agent.Server().State().UpsertACLRoles(
|
||||
structs.MsgTypeTestSetup, 20, []*structs.ACLRole{&aclRole})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Delete the existing ACL role.
|
||||
require.Equal(t, 0, cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID, aclRole.ID}))
|
||||
require.Contains(t, ui.OutputWriter.String(), "successfully deleted")
|
||||
}
|
||||
121
command/acl_role_info.go
Normal file
121
command/acl_role_info.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
// Ensure ACLRoleInfoCommand satisfies the cli.Command interface.
|
||||
var _ cli.Command = &ACLRoleInfoCommand{}
|
||||
|
||||
// ACLRoleInfoCommand implements cli.Command.
|
||||
type ACLRoleInfoCommand struct {
|
||||
Meta
|
||||
|
||||
byName bool
|
||||
json bool
|
||||
tmpl string
|
||||
}
|
||||
|
||||
// Help satisfies the cli.Command Help function.
|
||||
func (a *ACLRoleInfoCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad acl role info [options] <acl_role_id>
|
||||
|
||||
Info is used to fetch information on an existing ACL roles. Requires a
|
||||
management token.
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace) + `
|
||||
|
||||
ACL Info Options:
|
||||
|
||||
-by-name
|
||||
Look up the ACL role using its name as the identifier. The command defaults
|
||||
to expecting the ACL ID as the argument.
|
||||
|
||||
-json
|
||||
Output the ACL role in a JSON format.
|
||||
|
||||
-t
|
||||
Format and display the ACL role using a Go template.
|
||||
`
|
||||
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (a *ACLRoleInfoCommand) AutocompleteFlags() complete.Flags {
|
||||
return mergeAutocompleteFlags(a.Meta.AutocompleteFlags(FlagSetClient),
|
||||
complete.Flags{
|
||||
"-by-name": complete.PredictNothing,
|
||||
"-json": complete.PredictNothing,
|
||||
"-t": complete.PredictAnything,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *ACLRoleInfoCommand) AutocompleteArgs() complete.Predictor { return complete.PredictNothing }
|
||||
|
||||
// Synopsis satisfies the cli.Command Synopsis function.
|
||||
func (a *ACLRoleInfoCommand) Synopsis() string { return "Fetch information on an existing ACL role" }
|
||||
|
||||
// Name returns the name of this command.
|
||||
func (a *ACLRoleInfoCommand) Name() string { return "acl role info" }
|
||||
|
||||
// Run satisfies the cli.Command Run function.
|
||||
func (a *ACLRoleInfoCommand) Run(args []string) int {
|
||||
|
||||
flags := a.Meta.FlagSet(a.Name(), FlagSetClient)
|
||||
flags.Usage = func() { a.Ui.Output(a.Help()) }
|
||||
flags.BoolVar(&a.byName, "by-name", false, "")
|
||||
flags.BoolVar(&a.json, "json", false, "")
|
||||
flags.StringVar(&a.tmpl, "t", "", "")
|
||||
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check that we have exactly one argument.
|
||||
if len(flags.Args()) != 1 {
|
||||
a.Ui.Error("This command takes one argument: <acl_role_id>")
|
||||
a.Ui.Error(commandErrorText(a))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Get the HTTP client.
|
||||
client, err := a.Meta.Client()
|
||||
if err != nil {
|
||||
a.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
var (
|
||||
aclRole *api.ACLRole
|
||||
apiErr error
|
||||
)
|
||||
|
||||
aclRoleID := flags.Args()[0]
|
||||
|
||||
// Use the correct API call depending on whether the lookup is by the name
|
||||
// or the ID.
|
||||
switch a.byName {
|
||||
case true:
|
||||
aclRole, _, apiErr = client.ACLRoles().GetByName(aclRoleID, nil)
|
||||
default:
|
||||
aclRole, _, apiErr = client.ACLRoles().Get(aclRoleID, nil)
|
||||
}
|
||||
|
||||
// Handle any error from the API.
|
||||
if apiErr != nil {
|
||||
a.Ui.Error(fmt.Sprintf("Error reading ACL role: %s", apiErr))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Format the output.
|
||||
a.Ui.Output(formatACLRole(aclRole))
|
||||
return 0
|
||||
}
|
||||
95
command/acl_role_info_test.go
Normal file
95
command/acl_role_info_test.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/command/agent"
|
||||
"github.com/hashicorp/nomad/helper/uuid"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/nomad/testutil"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestACLRoleInfoCommand_Run(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Build a test server with ACLs enabled.
|
||||
srv, _, url := testServer(t, false, func(c *agent.Config) {
|
||||
c.ACL.Enabled = true
|
||||
})
|
||||
defer srv.Shutdown()
|
||||
|
||||
// Wait for the server to start fully and ensure we have a bootstrap token.
|
||||
testutil.WaitForLeader(t, srv.Agent.RPC)
|
||||
rootACLToken := srv.RootToken
|
||||
require.NotNil(t, rootACLToken)
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &ACLRoleInfoCommand{
|
||||
Meta: Meta{
|
||||
Ui: ui,
|
||||
flagAddress: url,
|
||||
},
|
||||
}
|
||||
|
||||
// Perform a lookup without specifying an ID.
|
||||
require.Equal(t, 1, cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID}))
|
||||
require.Contains(t, ui.ErrorWriter.String(), "This command takes one argument: <acl_role_id>")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Perform a lookup specifying a random ID.
|
||||
require.Equal(t, 1, cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID, uuid.Generate()}))
|
||||
require.Contains(t, ui.ErrorWriter.String(), "ACL role not found")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Create an ACL policy that can be referenced within the ACL role.
|
||||
aclPolicy := structs.ACLPolicy{
|
||||
Name: "acl-role-policy-cli-test",
|
||||
Rules: `namespace "default" {
|
||||
policy = "read"
|
||||
}
|
||||
`,
|
||||
}
|
||||
err := srv.Agent.Server().State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{&aclPolicy})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an ACL role referencing the previously created policy.
|
||||
aclRole := structs.ACLRole{
|
||||
ID: uuid.Generate(),
|
||||
Name: "acl-role-cli-test",
|
||||
Policies: []*structs.ACLRolePolicyLink{{Name: aclPolicy.Name}},
|
||||
}
|
||||
err = srv.Agent.Server().State().UpsertACLRoles(
|
||||
structs.MsgTypeTestSetup, 20, []*structs.ACLRole{&aclRole})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Look up the ACL role using its ID.
|
||||
require.Equal(t, 0, cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID, aclRole.ID}))
|
||||
s := ui.OutputWriter.String()
|
||||
require.Contains(t, s, fmt.Sprintf("ID = %s", aclRole.ID))
|
||||
require.Contains(t, s, fmt.Sprintf("Name = %s", aclRole.Name))
|
||||
require.Contains(t, s, "Description = <none>")
|
||||
require.Contains(t, s, fmt.Sprintf("Policies = %s", aclPolicy.Name))
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Look up the ACL role using its Name.
|
||||
require.Equal(t, 0, cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID, "-by-name", aclRole.Name}))
|
||||
s = ui.OutputWriter.String()
|
||||
require.Contains(t, s, fmt.Sprintf("ID = %s", aclRole.ID))
|
||||
require.Contains(t, s, fmt.Sprintf("Name = %s", aclRole.Name))
|
||||
require.Contains(t, s, "Description = <none>")
|
||||
require.Contains(t, s, fmt.Sprintf("Policies = %s", aclPolicy.Name))
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
}
|
||||
123
command/acl_role_list.go
Normal file
123
command/acl_role_list.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
// Ensure ACLRoleListCommand satisfies the cli.Command interface.
|
||||
var _ cli.Command = &ACLRoleListCommand{}
|
||||
|
||||
// ACLRoleListCommand implements cli.Command.
|
||||
type ACLRoleListCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
// Help satisfies the cli.Command Help function.
|
||||
func (a *ACLRoleListCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad acl role list [options]
|
||||
|
||||
List is used to list existing ACL roles.
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace) + `
|
||||
|
||||
ACL List Options:
|
||||
|
||||
-json
|
||||
Output the ACL roles in a JSON format.
|
||||
|
||||
-t
|
||||
Format and display the ACL roles using a Go template.
|
||||
`
|
||||
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (a *ACLRoleListCommand) AutocompleteFlags() complete.Flags {
|
||||
return mergeAutocompleteFlags(a.Meta.AutocompleteFlags(FlagSetClient),
|
||||
complete.Flags{
|
||||
"-json": complete.PredictNothing,
|
||||
"-t": complete.PredictAnything,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *ACLRoleListCommand) AutocompleteArgs() complete.Predictor { return complete.PredictNothing }
|
||||
|
||||
// Synopsis satisfies the cli.Command Synopsis function.
|
||||
func (a *ACLRoleListCommand) Synopsis() string { return "List ACL roles" }
|
||||
|
||||
// Name returns the name of this command.
|
||||
func (a *ACLRoleListCommand) Name() string { return "acl role list" }
|
||||
|
||||
// Run satisfies the cli.Command Run function.
|
||||
func (a *ACLRoleListCommand) Run(args []string) int {
|
||||
var json bool
|
||||
var tmpl string
|
||||
|
||||
flags := a.Meta.FlagSet(a.Name(), FlagSetClient)
|
||||
flags.Usage = func() { a.Ui.Output(a.Help()) }
|
||||
flags.BoolVar(&json, "json", false, "")
|
||||
flags.StringVar(&tmpl, "t", "", "")
|
||||
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check that we got no arguments
|
||||
if len(flags.Args()) != 0 {
|
||||
a.Ui.Error("This command takes no arguments")
|
||||
a.Ui.Error(commandErrorText(a))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Get the HTTP client
|
||||
client, err := a.Meta.Client()
|
||||
if err != nil {
|
||||
a.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Fetch info on the policy
|
||||
roles, _, err := client.ACLRoles().List(nil)
|
||||
if err != nil {
|
||||
a.Ui.Error(fmt.Sprintf("Error listing ACL roles: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
if json || len(tmpl) > 0 {
|
||||
out, err := Format(json, tmpl, roles)
|
||||
if err != nil {
|
||||
a.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
a.Ui.Output(out)
|
||||
return 0
|
||||
}
|
||||
|
||||
a.Ui.Output(formatACLRoles(roles))
|
||||
return 0
|
||||
}
|
||||
|
||||
func formatACLRoles(roles []*api.ACLRole) string {
|
||||
if len(roles) == 0 {
|
||||
return "No ACL roles found"
|
||||
}
|
||||
|
||||
output := make([]string, 0, len(roles)+1)
|
||||
output = append(output, "ID|Name|Description|Policies")
|
||||
for _, role := range roles {
|
||||
output = append(output, fmt.Sprintf(
|
||||
"%s|%s|%s|%s",
|
||||
role.ID, role.Name, role.Description, strings.Join(aclRolePolicyLinkToStringList(role.Policies), ",")))
|
||||
}
|
||||
|
||||
return formatList(output)
|
||||
}
|
||||
77
command/acl_role_list_test.go
Normal file
77
command/acl_role_list_test.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/command/agent"
|
||||
"github.com/hashicorp/nomad/helper/uuid"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/nomad/testutil"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestACLRoleListCommand_Run(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Build a test server with ACLs enabled.
|
||||
srv, _, url := testServer(t, false, func(c *agent.Config) {
|
||||
c.ACL.Enabled = true
|
||||
})
|
||||
defer srv.Shutdown()
|
||||
|
||||
// Wait for the server to start fully and ensure we have a bootstrap token.
|
||||
testutil.WaitForLeader(t, srv.Agent.RPC)
|
||||
rootACLToken := srv.RootToken
|
||||
require.NotNil(t, rootACLToken)
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &ACLRoleListCommand{
|
||||
Meta: Meta{
|
||||
Ui: ui,
|
||||
flagAddress: url,
|
||||
},
|
||||
}
|
||||
|
||||
// Perform a list straight away without any roles held in state.
|
||||
require.Equal(t, 0, cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID}))
|
||||
require.Contains(t, ui.OutputWriter.String(), "No ACL roles found")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Create an ACL policy that can be referenced within the ACL role.
|
||||
aclPolicy := structs.ACLPolicy{
|
||||
Name: "acl-role-policy-cli-test",
|
||||
Rules: `namespace "default" {
|
||||
policy = "read"
|
||||
}
|
||||
`,
|
||||
}
|
||||
err := srv.Agent.Server().State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{&aclPolicy})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an ACL role referencing the previously created policy.
|
||||
aclRole := structs.ACLRole{
|
||||
ID: uuid.Generate(),
|
||||
Name: "acl-role-cli-test",
|
||||
Policies: []*structs.ACLRolePolicyLink{{Name: aclPolicy.Name}},
|
||||
}
|
||||
err = srv.Agent.Server().State().UpsertACLRoles(
|
||||
structs.MsgTypeTestSetup, 20, []*structs.ACLRole{&aclRole})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Perform a listing to get the created role.
|
||||
require.Equal(t, 0, cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID}))
|
||||
s := ui.OutputWriter.String()
|
||||
require.Contains(t, s, "ID")
|
||||
require.Contains(t, s, "Name")
|
||||
require.Contains(t, s, "Policies")
|
||||
require.Contains(t, s, "acl-role-cli-test")
|
||||
require.Contains(t, s, "acl-role-policy-cli-test")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
}
|
||||
61
command/acl_role_test.go
Normal file
61
command/acl_role_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_formatACLRole(t *testing.T) {
|
||||
inputACLRole := api.ACLRole{
|
||||
ID: "this-is-usually-a-uuid",
|
||||
Name: "this-is-my-friendly-name",
|
||||
Description: "this-is-my-friendly-name",
|
||||
Policies: []*api.ACLRolePolicyLink{
|
||||
{Name: "policy-link-1"},
|
||||
{Name: "policy-link-2"},
|
||||
{Name: "policy-link-3"},
|
||||
{Name: "policy-link-4"},
|
||||
},
|
||||
CreateIndex: 13,
|
||||
ModifyIndex: 1313,
|
||||
}
|
||||
expectedOutput := "ID = this-is-usually-a-uuid\nName = this-is-my-friendly-name\nDescription = this-is-my-friendly-name\nPolicies = policy-link-1,policy-link-2,policy-link-3,policy-link-4\nCreate Index = 13\nModify Index = 1313"
|
||||
actualOutput := formatACLRole(&inputACLRole)
|
||||
require.Equal(t, expectedOutput, actualOutput)
|
||||
}
|
||||
|
||||
func Test_aclRolePolicyLinkToStringList(t *testing.T) {
|
||||
inputPolicyLinks := []*api.ACLRolePolicyLink{
|
||||
{Name: "z-policy-link-1"},
|
||||
{Name: "a-policy-link-2"},
|
||||
{Name: "policy-link-3"},
|
||||
{Name: "b-policy-link-4"},
|
||||
}
|
||||
expectedOutput := []string{
|
||||
"a-policy-link-2",
|
||||
"b-policy-link-4",
|
||||
"policy-link-3",
|
||||
"z-policy-link-1",
|
||||
}
|
||||
actualOutput := aclRolePolicyLinkToStringList(inputPolicyLinks)
|
||||
require.Equal(t, expectedOutput, actualOutput)
|
||||
}
|
||||
|
||||
func Test_aclRolePolicyNamesToPolicyLinks(t *testing.T) {
|
||||
inputPolicyNames := []string{
|
||||
"policy-link-1", "policy-link-2", "policy-link-3", "policy-link-4",
|
||||
"policy-link-1", "policy-link-2", "policy-link-3", "policy-link-4",
|
||||
"policy-link-1", "policy-link-2", "policy-link-3", "policy-link-4",
|
||||
"policy-link-1", "policy-link-2", "policy-link-3", "policy-link-4",
|
||||
}
|
||||
expectedOutput := []*api.ACLRolePolicyLink{
|
||||
{Name: "policy-link-1"},
|
||||
{Name: "policy-link-2"},
|
||||
{Name: "policy-link-3"},
|
||||
{Name: "policy-link-4"},
|
||||
}
|
||||
actualOutput := aclRolePolicyNamesToPolicyLinks(inputPolicyNames)
|
||||
require.ElementsMatch(t, expectedOutput, actualOutput)
|
||||
}
|
||||
217
command/acl_role_update.go
Normal file
217
command/acl_role_update.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
// Ensure ACLRoleUpdateCommand satisfies the cli.Command interface.
|
||||
var _ cli.Command = &ACLRoleUpdateCommand{}
|
||||
|
||||
// ACLRoleUpdateCommand implements cli.Command.
|
||||
type ACLRoleUpdateCommand struct {
|
||||
Meta
|
||||
|
||||
name string
|
||||
description string
|
||||
policyNames []string
|
||||
noMerge bool
|
||||
json bool
|
||||
tmpl string
|
||||
}
|
||||
|
||||
// Help satisfies the cli.Command Help function.
|
||||
func (a *ACLRoleUpdateCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad acl role update [options] <acl_role_id>
|
||||
|
||||
Update is used to update an existing ACL token. Requires a management token.
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage(usageOptsDefault|usageOptsNoNamespace) + `
|
||||
|
||||
Update Options:
|
||||
|
||||
-name
|
||||
Sets the human readable name for the ACL role. The name must be between
|
||||
1-128 characters.
|
||||
|
||||
-description
|
||||
A free form text description of the role that must not exceed 256
|
||||
characters.
|
||||
|
||||
-policy-name
|
||||
Specifies a policy to associate with the role identified by their name. This
|
||||
flag can be specified multiple times.
|
||||
|
||||
-no-merge
|
||||
Do not merge the current role information with what is provided to the
|
||||
command. Instead overwrite all fields with the exception of the role ID
|
||||
which is immutable.
|
||||
|
||||
-json
|
||||
Output the ACL role in a JSON format.
|
||||
|
||||
-t
|
||||
Format and display the ACL role using a Go template.
|
||||
`
|
||||
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (a *ACLRoleUpdateCommand) AutocompleteFlags() complete.Flags {
|
||||
return mergeAutocompleteFlags(a.Meta.AutocompleteFlags(FlagSetClient),
|
||||
complete.Flags{
|
||||
"-name": complete.PredictAnything,
|
||||
"-description": complete.PredictAnything,
|
||||
"-no-merge": complete.PredictNothing,
|
||||
"-policy-name": complete.PredictAnything,
|
||||
"-json": complete.PredictNothing,
|
||||
"-t": complete.PredictAnything,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *ACLRoleUpdateCommand) AutocompleteArgs() complete.Predictor { return complete.PredictNothing }
|
||||
|
||||
// Synopsis satisfies the cli.Command Synopsis function.
|
||||
func (a *ACLRoleUpdateCommand) Synopsis() string { return "Update an existing ACL role" }
|
||||
|
||||
// Name returns the name of this command.
|
||||
func (*ACLRoleUpdateCommand) Name() string { return "acl role update" }
|
||||
|
||||
// Run satisfies the cli.Command Run function.
|
||||
func (a *ACLRoleUpdateCommand) Run(args []string) int {
|
||||
|
||||
flags := a.Meta.FlagSet(a.Name(), FlagSetClient)
|
||||
flags.Usage = func() { a.Ui.Output(a.Help()) }
|
||||
flags.StringVar(&a.name, "name", "", "")
|
||||
flags.StringVar(&a.description, "description", "", "")
|
||||
flags.Var((funcVar)(func(s string) error {
|
||||
a.policyNames = append(a.policyNames, s)
|
||||
return nil
|
||||
}), "policy-name", "")
|
||||
flags.BoolVar(&a.noMerge, "no-merge", false, "")
|
||||
flags.BoolVar(&a.json, "json", false, "")
|
||||
flags.StringVar(&a.tmpl, "t", "", "")
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check that we got exactly one argument which is expected to be the ACL
|
||||
// role ID.
|
||||
if len(flags.Args()) != 1 {
|
||||
a.Ui.Error("This command takes one argument: <acl_role_id>")
|
||||
a.Ui.Error(commandErrorText(a))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Get the HTTP client.
|
||||
client, err := a.Meta.Client()
|
||||
if err != nil {
|
||||
a.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
aclRoleID := flags.Args()[0]
|
||||
|
||||
// Read the current role in both cases, so we can fail better if not found.
|
||||
currentRole, _, err := client.ACLRoles().Get(aclRoleID, nil)
|
||||
if err != nil {
|
||||
a.Ui.Error(fmt.Sprintf("Error when retrieving ACL role: %v", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
var updatedRole api.ACLRole
|
||||
|
||||
// Depending on whether we are merging or not, we need to take a different
|
||||
// approach.
|
||||
switch a.noMerge {
|
||||
case true:
|
||||
|
||||
// Perform some basic validation on the submitted role information to
|
||||
// avoid sending API and RPC requests which will fail basic validation.
|
||||
if a.name == "" {
|
||||
a.Ui.Error("ACL role name must be specified using the -name flag")
|
||||
return 1
|
||||
}
|
||||
if len(a.policyNames) < 1 {
|
||||
a.Ui.Error("At least one policy name must be specified using the -policy-name flag")
|
||||
return 1
|
||||
}
|
||||
|
||||
updatedRole = api.ACLRole{
|
||||
ID: aclRoleID,
|
||||
Name: a.name,
|
||||
Description: a.description,
|
||||
Policies: aclRolePolicyNamesToPolicyLinks(a.policyNames),
|
||||
}
|
||||
default:
|
||||
// Check that the operator specified at least one flag to update the ACL
|
||||
// role with.
|
||||
if len(a.policyNames) == 0 && a.name == "" && a.description == "" {
|
||||
a.Ui.Error("Please provide at least one flag to update the ACL role")
|
||||
a.Ui.Error(commandErrorText(a))
|
||||
return 1
|
||||
}
|
||||
|
||||
updatedRole = *currentRole
|
||||
|
||||
// If the operator specified a name or description, overwrite the
|
||||
// existing value as these are simple strings.
|
||||
if a.name != "" {
|
||||
updatedRole.Name = a.name
|
||||
}
|
||||
if a.description != "" {
|
||||
updatedRole.Description = a.description
|
||||
}
|
||||
|
||||
// In order to merge the policy updates, we need to identify if the
|
||||
// specified policy names already exist within the ACL role linking.
|
||||
for _, policyName := range a.policyNames {
|
||||
|
||||
// Track whether we found the policy name already in the ACL role
|
||||
// linking.
|
||||
var found bool
|
||||
|
||||
for _, existingLinkedPolicy := range currentRole.Policies {
|
||||
if policyName == existingLinkedPolicy.Name {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If the policy name was not found, append this new link to the
|
||||
// updated role.
|
||||
if !found {
|
||||
updatedRole.Policies = append(updatedRole.Policies, &api.ACLRolePolicyLink{Name: policyName})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the ACL role with the new information via the API.
|
||||
updatedACLRoleRead, _, err := client.ACLRoles().Update(&updatedRole, nil)
|
||||
if err != nil {
|
||||
a.Ui.Error(fmt.Sprintf("Error updating ACL role: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
if a.json || len(a.tmpl) > 0 {
|
||||
out, err := Format(a.json, a.tmpl, updatedACLRoleRead)
|
||||
if err != nil {
|
||||
a.Ui.Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
a.Ui.Output(out)
|
||||
return 0
|
||||
}
|
||||
|
||||
// Format the output
|
||||
a.Ui.Output(formatACLRole(updatedACLRoleRead))
|
||||
return 0
|
||||
}
|
||||
124
command/acl_role_update_test.go
Normal file
124
command/acl_role_update_test.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/command/agent"
|
||||
"github.com/hashicorp/nomad/helper/uuid"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/hashicorp/nomad/testutil"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestACLRoleUpdateCommand_Run(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
// Build a test server with ACLs enabled.
|
||||
srv, _, url := testServer(t, false, func(c *agent.Config) {
|
||||
c.ACL.Enabled = true
|
||||
})
|
||||
defer srv.Shutdown()
|
||||
|
||||
// Wait for the server to start fully and ensure we have a bootstrap token.
|
||||
testutil.WaitForLeader(t, srv.Agent.RPC)
|
||||
rootACLToken := srv.RootToken
|
||||
require.NotNil(t, rootACLToken)
|
||||
|
||||
ui := cli.NewMockUi()
|
||||
cmd := &ACLRoleUpdateCommand{
|
||||
Meta: Meta{
|
||||
Ui: ui,
|
||||
flagAddress: url,
|
||||
},
|
||||
}
|
||||
|
||||
// Try calling the command without setting an ACL Role ID arg.
|
||||
require.Equal(t, 1, cmd.Run([]string{"-address=" + url}))
|
||||
require.Contains(t, ui.ErrorWriter.String(), "This command takes one argument")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Try calling the command with an ACL role ID that does not exist.
|
||||
code := cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID, "catch-me-if-you-can"})
|
||||
require.Equal(t, 1, code)
|
||||
require.Contains(t, ui.ErrorWriter.String(), "ACL role not found")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Create an ACL policy that can be referenced within the ACL role.
|
||||
aclPolicy := structs.ACLPolicy{
|
||||
Name: "acl-role-cli-test-policy",
|
||||
Rules: `namespace "default" {
|
||||
policy = "read"
|
||||
}
|
||||
`,
|
||||
}
|
||||
err := srv.Agent.Server().State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{&aclPolicy})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create an ACL role that can be used for updating.
|
||||
aclRole := structs.ACLRole{
|
||||
ID: uuid.Generate(),
|
||||
Name: "acl-role-cli-test",
|
||||
Description: "my-lovely-role",
|
||||
Policies: []*structs.ACLRolePolicyLink{{Name: aclPolicy.Name}},
|
||||
}
|
||||
|
||||
err = srv.Agent.Server().State().UpsertACLRoles(
|
||||
structs.MsgTypeTestSetup, 20, []*structs.ACLRole{&aclRole})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Try a merge update without setting any parameters to update.
|
||||
code = cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID, aclRole.ID})
|
||||
require.Equal(t, 1, code)
|
||||
require.Contains(t, ui.ErrorWriter.String(), "Please provide at least one flag to update the ACL role")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Update the description using the merge method.
|
||||
code = cmd.Run([]string{
|
||||
"-address=" + url, "-token=" + rootACLToken.SecretID, "-description=badger-badger-badger", aclRole.ID})
|
||||
require.Equal(t, 0, code)
|
||||
s := ui.OutputWriter.String()
|
||||
require.Contains(t, s, fmt.Sprintf("ID = %s", aclRole.ID))
|
||||
require.Contains(t, s, "Name = acl-role-cli-test")
|
||||
require.Contains(t, s, "Description = badger-badger-badger")
|
||||
require.Contains(t, s, "Policies = acl-role-cli-test-policy")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Try updating the role using no-merge without setting the required flags.
|
||||
code = cmd.Run([]string{"-address=" + url, "-token=" + rootACLToken.SecretID, "-no-merge", aclRole.ID})
|
||||
require.Equal(t, 1, code)
|
||||
require.Contains(t, ui.ErrorWriter.String(), "ACL role name must be specified using the -name flag")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
code = cmd.Run([]string{
|
||||
"-address=" + url, "-token=" + rootACLToken.SecretID, "-no-merge", "-name=update-role-name", aclRole.ID})
|
||||
require.Equal(t, 1, code)
|
||||
require.Contains(t, ui.ErrorWriter.String(), "At least one policy name must be specified using the -policy-name flag")
|
||||
|
||||
// Update the role using no-merge with all required flags set.
|
||||
code = cmd.Run([]string{
|
||||
"-address=" + url, "-token=" + rootACLToken.SecretID, "-no-merge", "-name=update-role-name",
|
||||
"-description=updated-description", "-policy-name=acl-role-cli-test-policy", aclRole.ID})
|
||||
require.Equal(t, 0, code)
|
||||
s = ui.OutputWriter.String()
|
||||
require.Contains(t, s, fmt.Sprintf("ID = %s", aclRole.ID))
|
||||
require.Contains(t, s, "Name = update-role-name")
|
||||
require.Contains(t, s, "Description = updated-description")
|
||||
require.Contains(t, s, "Policies = acl-role-cli-test-policy")
|
||||
|
||||
ui.OutputWriter.Reset()
|
||||
ui.ErrorWriter.Reset()
|
||||
}
|
||||
@@ -107,6 +107,36 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory {
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"acl role": func() (cli.Command, error) {
|
||||
return &ACLRoleCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"acl role create": func() (cli.Command, error) {
|
||||
return &ACLRoleCreateCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"acl role delete": func() (cli.Command, error) {
|
||||
return &ACLRoleDeleteCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"acl role info": func() (cli.Command, error) {
|
||||
return &ACLRoleInfoCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"acl role list": func() (cli.Command, error) {
|
||||
return &ACLRoleListCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"acl role update": func() (cli.Command, error) {
|
||||
return &ACLRoleUpdateCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
"acl token": func() (cli.Command, error) {
|
||||
return &ACLTokenCommand{
|
||||
Meta: meta,
|
||||
|
||||
Reference in New Issue
Block a user