diff --git a/command/acl_token_list.go b/command/acl_token_list.go new file mode 100644 index 000000000000..5d9aeec599af --- /dev/null +++ b/command/acl_token_list.go @@ -0,0 +1,117 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/hashicorp/nomad/api" + "github.com/posener/complete" +) + +type ACLTokenListCommand struct { + Meta +} + +func (c *ACLTokenListCommand) Help() string { + helpText := ` +Usage: nomad acl token list + + List is used to list existing ACL tokens. + +General Options: + + ` + generalOptionsUsage() + ` + +List Options: + + -json + Output the ACL tokens in a JSON format. + + -t + Format and display the ACL tokens using a Go template. +` + + return strings.TrimSpace(helpText) +} + +func (c *ACLTokenListCommand) AutocompleteFlags() complete.Flags { + return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), + complete.Flags{ + "-json": complete.PredictNothing, + "-t": complete.PredictAnything, + }) +} + +func (c *ACLTokenListCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} + +func (c *ACLTokenListCommand) Synopsis() string { + return "List ACL tokens" +} + +func (c *ACLTokenListCommand) Name() string { return "acl token list" } + +func (c *ACLTokenListCommand) Run(args []string) int { + var json bool + var tmpl string + + flags := c.Meta.FlagSet(c.Name(), FlagSetClient) + flags.Usage = func() { c.Ui.Output(c.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 + args = flags.Args() + if l := len(args); l != 0 { + c.Ui.Error("This command takes no arguments") + c.Ui.Error(commandErrorText(c)) + return 1 + } + + // Get the HTTP client + client, err := c.Meta.Client() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) + return 1 + } + + // Fetch info on the policy + tokens, _, err := client.ACLTokens().List(nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error listing ACL tokens: %s", err)) + return 1 + } + + if json || len(tmpl) > 0 { + out, err := Format(json, tmpl, tokens) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + c.Ui.Output(out) + return 0 + } + + c.Ui.Output(formatTokens(tokens)) + return 0 +} + +func formatTokens(tokens []*api.ACLTokenListStub) string { + if len(tokens) == 0 { + return "No tokens found" + } + + output := make([]string, 0, len(tokens)+1) + output = append(output, fmt.Sprintf("Name|Type|Global|Accessor ID")) + for _, p := range tokens { + output = append(output, fmt.Sprintf("%s|%s|%t|%s", p.Name, p.Type, p.Global, p.AccessorID)) + } + + return formatList(output) +} diff --git a/command/acl_token_list_test.go b/command/acl_token_list_test.go new file mode 100644 index 000000000000..55f2fd9c5518 --- /dev/null +++ b/command/acl_token_list_test.go @@ -0,0 +1,63 @@ +package command + +import ( + "strings" + "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 TestACLTokenListCommand(t *testing.T) { + assert := assert.New(t) + t.Parallel() + config := func(c *agent.Config) { + c.ACL.Enabled = true + } + + srv, _, url := testServer(t, true, config) + state := srv.Agent.Server().State() + defer srv.Shutdown() + + // Bootstrap an initial ACL token + token := srv.RootToken + assert.NotNil(token, "failed to bootstrap ACL token") + + // Create a valid token + mockToken := mock.ACLToken() + mockToken.Policies = []string{acl.PolicyWrite} + mockToken.SetHash() + assert.Nil(state.UpsertACLTokens(1000, []*structs.ACLToken{mockToken})) + + ui := new(cli.MockUi) + cmd := &ACLTokenListCommand{Meta: Meta{Ui: ui, flagAddress: url}} + + // Attempt to list tokens without a valid management token + invalidToken := mock.ACLToken() + code := cmd.Run([]string{"-address=" + url, "-token=" + invalidToken.SecretID}) + assert.Equal(1, code) + + // Apply a token with a valid management token + code = cmd.Run([]string{"-address=" + url, "-token=" + token.SecretID}) + assert.Equal(0, code) + + // Check the output + out := ui.OutputWriter.String() + if !strings.Contains(out, mockToken.Name) { + t.Fatalf("bad: %v", out) + } + + // List json + if code := cmd.Run([]string{"-address=" + url, "-token=" + token.SecretID, "-json"}); code != 0 { + t.Fatalf("expected exit 0, got: %d; %v", code, ui.ErrorWriter.String()) + } + out = ui.OutputWriter.String() + if !strings.Contains(out, "CreateIndex") { + t.Fatalf("expected json output, got: %s", out) + } + ui.OutputWriter.Reset() +} diff --git a/command/commands.go b/command/commands.go index 29be6200bdf9..0f146dc01f92 100644 --- a/command/commands.go +++ b/command/commands.go @@ -130,6 +130,11 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory { Meta: meta, }, nil }, + "acl token list": func() (cli.Command, error) { + return &ACLTokenListCommand{ + Meta: meta, + }, nil + }, "acl token self": func() (cli.Command, error) { return &ACLTokenSelfCommand{ Meta: meta, diff --git a/website/source/docs/commands/acl/policy-list.html.md.erb b/website/source/docs/commands/acl/policy-list.html.md.erb index 90d0c2fd34ab..b2247094edd9 100644 --- a/website/source/docs/commands/acl/policy-list.html.md.erb +++ b/website/source/docs/commands/acl/policy-list.html.md.erb @@ -6,7 +6,7 @@ description: > The policy list command is used to list available ACL policies. --- -# Command: acl policy info +# Command: acl policy list The `acl policy list` command is used to list available ACL policies. @@ -22,9 +22,9 @@ nomad acl policy list # ## List Options -* `-json` : Output the namespaces in their JSON format. +* `-json` : Output the policies in their JSON format. -* `-t` : Format and display the namespaces using a Go template. +* `-t` : Format and display the policies using a Go template. ## Examples @@ -32,7 +32,7 @@ List all ACL policies: ``` $ nomad acl policy list -Name Description -default-ns Write access to the default namespace -node-read Node read access +Name Description +policy-1 The first policy +policy-2 The second policy ``` diff --git a/website/source/docs/commands/acl/token-list.html.md.erb b/website/source/docs/commands/acl/token-list.html.md.erb new file mode 100644 index 000000000000..45a9ce7bd823 --- /dev/null +++ b/website/source/docs/commands/acl/token-list.html.md.erb @@ -0,0 +1,38 @@ +--- +layout: "docs" +page_title: "Commands: acl token list" +sidebar_current: "docs-commands-acl-token-list" +description: > + The token list command is used to list existing ACL tokens. +--- + +# Command: acl token list + +The `acl token list` command is used to list existing ACL tokens. + +## Usage + +``` +nomad acl token list +``` + +## General Options + +<%= partial "docs/commands/_general_options" %> +# +## List Options + +* `-json` : Output the tokens in their JSON format. + +* `-t` : Format and display the tokens using a Go template. + +## Examples + +List all ACL tokens: + +``` +$ nomad acl token list +Name Type Global Accessor ID +Bootstrap Token management true 32b61154-47f1-3694-1430-a5544bafcd3e + client false fcf2bf84-a257-8f39-9d16-a954ed25b5be +``` diff --git a/website/source/docs/commands/acl/token-self.html.md.erb b/website/source/docs/commands/acl/token-self.html.md.erb index 1fd33f06c024..4ca40247bf81 100644 --- a/website/source/docs/commands/acl/token-self.html.md.erb +++ b/website/source/docs/commands/acl/token-self.html.md.erb @@ -28,7 +28,7 @@ Fetch information about an existing ACL token: ``` $ export NOMAD_TOKEN=d532c40a-30f1-695c-19e5-c35b882b0efd -$ nomad acl tokenjself +$ nomad acl token self Accessor ID = d532c40a-30f1-695c-19e5-c35b882b0efd Secret ID = 85310d07-9afa-ef53-0933-0c043cd673c7 Name = my token diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index be0e8f518c41..b7070e77dd83 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -100,6 +100,9 @@ > token info + > + token list + > token self