diff --git a/command/acl_token_update.go b/command/acl_token_update.go new file mode 100644 index 000000000..b922927ba --- /dev/null +++ b/command/acl_token_update.go @@ -0,0 +1,129 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/posener/complete" +) + +type ACLTokenUpdateCommand struct { + Meta +} + +func (c *ACLTokenUpdateCommand) Help() string { + helpText := ` +Usage: nomad acl token update + +Update is used to update an existing ACL token. Requires a management token. + +General Options: + + ` + generalOptionsUsage() + ` + +Update Options: + + -name="" + Sets the human readable name for the ACL token. + + -type="client" + Sets the type of token. Must be one of "client" (default), or "management". + + -global=false + Toggles the global mode of the token. Global tokens are replicated to all regions. + + -policy="" + Specifies a policy to associate with the token. Can be specified multiple times, + but only with client type tokens. +` + + return strings.TrimSpace(helpText) +} + +func (c *ACLTokenUpdateCommand) AutocompleteFlags() complete.Flags { + return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), + complete.Flags{ + "name": complete.PredictAnything, + "type": complete.PredictAnything, + "global": complete.PredictNothing, + "policy": complete.PredictAnything, + }) +} + +func (c *ACLTokenUpdateCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} + +func (c *ACLTokenUpdateCommand) Synopsis() string { + return "Update an existing ACL token" +} + +func (c *ACLTokenUpdateCommand) Run(args []string) int { + var name, tokenType string + var global bool + var policies []string + flags := c.Meta.FlagSet("acl token update", FlagSetClient) + flags.Usage = func() { c.Ui.Output(c.Help()) } + flags.StringVar(&name, "name", "", "") + flags.StringVar(&tokenType, "type", "client", "") + flags.BoolVar(&global, "global", false, "") + flags.Var((funcVar)(func(s string) error { + policies = append(policies, s) + return nil + }), "policy", "") + if err := flags.Parse(args); err != nil { + return 1 + } + + // Check that we got exactly one argument + args = flags.Args() + if l := len(args); l != 1 { + c.Ui.Error(c.Help()) + return 1 + } + + tokenAccessorID := args[0] + + // Get the HTTP client + client, err := c.Meta.Client() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) + return 1 + } + + // Get the specified token + token, _, err := client.ACLTokens().Info(tokenAccessorID, nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error fetching token: %s", err)) + return 1 + } + + // Create the updated token + if name != "" { + token.Name = name + } + + if tokenType != "" { + token.Type = tokenType + } + + // This will default to false if the user does not specify it + if global != token.Global { + token.Global = global + } + + if len(policies) != 0 { + token.Policies = policies + } + + // Update the token + updatedToken, _, err := client.ACLTokens().Update(token, nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error updating token: %s", err)) + return 1 + } + + // Format the output + c.Ui.Output(formatKVACLToken(updatedToken)) + return 0 +} diff --git a/command/acl_token_update_test.go b/command/acl_token_update_test.go new file mode 100644 index 000000000..a07345bda --- /dev/null +++ b/command/acl_token_update_test.go @@ -0,0 +1,55 @@ +package command + +import ( + "os" + "testing" + + "github.com/hashicorp/nomad/acl" + "github.com/hashicorp/nomad/command/agent" + "github.com/hashicorp/nomad/nomad/mock" + "github.com/hashicorp/nomad/nomad/structs" + "github.com/mitchellh/cli" + "github.com/stretchr/testify/assert" +) + +func TestACLTokenUpdateCommand(t *testing.T) { + assert := assert.New(t) + t.Parallel() + config := func(c *agent.Config) { + c.ACL.Enabled = true + } + + srv, _, url := testServer(t, true, config) + defer srv.Shutdown() + + // Bootstrap an initial ACL token + token := srv.Token + assert.NotNil(token, "failed to bootstrap ACL token") + + ui := new(cli.MockUi) + cmd := &ACLTokenUpdateCommand{Meta: Meta{Ui: ui, flagAddress: url}} + state := srv.Agent.Server().State() + + // Create a valid token + mockToken := mock.ACLToken() + mockToken.Policies = []string{acl.PolicyWrite} + mockToken.SetHash() + assert.Nil(state.UpsertACLTokens(1000, []*structs.ACLToken{mockToken})) + + // Request to update a new token without providing a valid management token + invalidToken := mock.ACLToken() + os.Setenv("NOMAD_TOKEN", invalidToken.SecretID) + code := cmd.Run([]string{"-address=" + url, "-name=bar", mockToken.AccessorID}) + //code := cmd.Run([]string{"-address=" + url, "-policy=foo", "-type=client"}) + assert.Equal(1, code) + + // Request to update a new token with a valid management token + os.Setenv("NOMAD_TOKEN", token.SecretID) + code = cmd.Run([]string{"-address=" + url, "-name=bar", mockToken.AccessorID}) + assert.Equal(0, code) + + // Check the output + out := ui.OutputWriter.String() + assert.Contains(out, mockToken.AccessorID) + assert.Contains(out, "bar") +} diff --git a/commands.go b/commands.go index 1620d4456..24e7e74f7 100644 --- a/commands.go +++ b/commands.go @@ -66,6 +66,11 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory { Meta: meta, }, nil }, + "acl token update": func() (cli.Command, error) { + return &command.ACLTokenUpdateCommand{ + Meta: meta, + }, nil + }, "acl token delete": func() (cli.Command, error) { return &command.ACLTokenDeleteCommand{ Meta: meta, diff --git a/website/source/docs/commands/acl.html.md.erb b/website/source/docs/commands/acl.html.md.erb index 7536ae152..30c33c047 100644 --- a/website/source/docs/commands/acl.html.md.erb +++ b/website/source/docs/commands/acl.html.md.erb @@ -24,6 +24,7 @@ subcommands are available: * [`acl policy delete`][policydelete] - Delete an existing ACL policies * [`acl policy info`][policyinfo] - Fetch information on an existing ACL policy * [`acl token create`][tokencreate] - Create new ACL token +* [`acl token update`][tokenupdate] - Update existing ACL token * [`acl token delete`][tokendelete] - Delete an existing ACL token * [`acl token info`][tokeninfo] - Get info on an existing ACL token @@ -32,6 +33,7 @@ subcommands are available: [policydelete]: /docs/commands/acl/policy-delete.html [policyinfo]: /docs/commands/acl/policy-info.html [tokencreate]: /docs/commands/acl/token-create.html +[tokenupdate]: /docs/commands/acl/token-update.html [tokendelete]: /docs/commands/acl/token-delete.html [tokeninfo]: /docs/commands/acl/token-info.html diff --git a/website/source/docs/commands/acl/token-update.html.md.erb b/website/source/docs/commands/acl/token-update.html.md.erb new file mode 100644 index 000000000..f786d1665 --- /dev/null +++ b/website/source/docs/commands/acl/token-update.html.md.erb @@ -0,0 +1,51 @@ +--- +layout: "docs" +page_title: "Commands: acl token create" +sidebar_current: "docs-commands-acl-token-create" +description: > + The token create command is used to update existing ACL tokens. +--- + +# Command: acl token update + +The `acl token update` command is used to update existing ACL tokens. + +## Usage + +``` +nomad acl token update [options] +``` + +The `acl token update` command requires an existing token's accessor ID. + +## General Options + +<%= partial "docs/commands/_general_options" %> + +## Create Options + +* `-name`: Sets the human readable name for the ACL token. + +* `-type`: Sets the type of token. Must be one of "client" (default), or "management". + +* `-global`: Toggles the global mode of the token. Global tokens are replicated to all regions. Defaults false. + +* `-policy`: Specifies a policy to associate with the token. Can be specified multiple times, + but only with client type tokens. + +## Examples + +Update an existing ACL token: + +``` +$ nomad acl token update -name="my updated token" -policy=foo -policy=bar d532c40a-30f1-695c-19e5-c35b882b0efd +Accessor ID = d532c40a-30f1-695c-19e5-c35b882b0efd +Secret ID = 85310d07-9afa-ef53-0933-0c043cd673c7 +Name = my updated token +Type = client +Global = false +Policies = [foo bar] +Create Time = 2017-09-15 05:04:41.814954949 +0000 UTC +Create Index = 8 +Modify Index = 8 +```