diff --git a/command/acl/acl_helpers.go b/command/acl/acl_helpers.go index e7cbb735fa..590a22b3bb 100644 --- a/command/acl/acl_helpers.go +++ b/command/acl/acl_helpers.go @@ -1,257 +1,13 @@ package acl import ( - "encoding/json" "fmt" "strings" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/api" - "github.com/mitchellh/cli" ) -func PrintToken(token *api.ACLToken, ui cli.Ui, showMeta bool) { - ui.Info(fmt.Sprintf("AccessorID: %s", token.AccessorID)) - ui.Info(fmt.Sprintf("SecretID: %s", token.SecretID)) - if token.Namespace != "" { - ui.Info(fmt.Sprintf("Namespace: %s", token.Namespace)) - } - ui.Info(fmt.Sprintf("Description: %s", token.Description)) - ui.Info(fmt.Sprintf("Local: %t", token.Local)) - ui.Info(fmt.Sprintf("Create Time: %v", token.CreateTime)) - if token.ExpirationTime != nil && !token.ExpirationTime.IsZero() { - ui.Info(fmt.Sprintf("Expiration Time: %v", *token.ExpirationTime)) - } - if showMeta { - ui.Info(fmt.Sprintf("Hash: %x", token.Hash)) - ui.Info(fmt.Sprintf("Create Index: %d", token.CreateIndex)) - ui.Info(fmt.Sprintf("Modify Index: %d", token.ModifyIndex)) - } - if len(token.Policies) > 0 { - ui.Info(fmt.Sprintf("Policies:")) - for _, policy := range token.Policies { - ui.Info(fmt.Sprintf(" %s - %s", policy.ID, policy.Name)) - } - } - if len(token.Roles) > 0 { - ui.Info(fmt.Sprintf("Roles:")) - for _, role := range token.Roles { - ui.Info(fmt.Sprintf(" %s - %s", role.ID, role.Name)) - } - } - if len(token.ServiceIdentities) > 0 { - ui.Info(fmt.Sprintf("Service Identities:")) - for _, svcid := range token.ServiceIdentities { - if len(svcid.Datacenters) > 0 { - ui.Info(fmt.Sprintf(" %s (Datacenters: %s)", svcid.ServiceName, strings.Join(svcid.Datacenters, ", "))) - } else { - ui.Info(fmt.Sprintf(" %s (Datacenters: all)", svcid.ServiceName)) - } - } - } - if token.Rules != "" { - ui.Info(fmt.Sprintf("Rules:")) - ui.Info(token.Rules) - } -} - -func PrintTokenListEntry(token *api.ACLTokenListEntry, ui cli.Ui, showMeta bool) { - ui.Info(fmt.Sprintf("AccessorID: %s", token.AccessorID)) - if token.Namespace != "" { - ui.Info(fmt.Sprintf("Namespace: %s", token.Namespace)) - } - ui.Info(fmt.Sprintf("Description: %s", token.Description)) - ui.Info(fmt.Sprintf("Local: %t", token.Local)) - ui.Info(fmt.Sprintf("Create Time: %v", token.CreateTime)) - if token.ExpirationTime != nil && !token.ExpirationTime.IsZero() { - ui.Info(fmt.Sprintf("Expiration Time: %v", *token.ExpirationTime)) - } - ui.Info(fmt.Sprintf("Legacy: %t", token.Legacy)) - if showMeta { - ui.Info(fmt.Sprintf("Hash: %x", token.Hash)) - ui.Info(fmt.Sprintf("Create Index: %d", token.CreateIndex)) - ui.Info(fmt.Sprintf("Modify Index: %d", token.ModifyIndex)) - } - if len(token.Policies) > 0 { - ui.Info(fmt.Sprintf("Policies:")) - for _, policy := range token.Policies { - ui.Info(fmt.Sprintf(" %s - %s", policy.ID, policy.Name)) - } - } - if len(token.Roles) > 0 { - ui.Info(fmt.Sprintf("Roles:")) - for _, role := range token.Roles { - ui.Info(fmt.Sprintf(" %s - %s", role.ID, role.Name)) - } - } - if len(token.ServiceIdentities) > 0 { - ui.Info(fmt.Sprintf("Service Identities:")) - for _, svcid := range token.ServiceIdentities { - if len(svcid.Datacenters) > 0 { - ui.Info(fmt.Sprintf(" %s (Datacenters: %s)", svcid.ServiceName, strings.Join(svcid.Datacenters, ", "))) - } else { - ui.Info(fmt.Sprintf(" %s (Datacenters: all)", svcid.ServiceName)) - } - } - } -} - -func PrintPolicy(policy *api.ACLPolicy, ui cli.Ui, showMeta bool) { - ui.Info(fmt.Sprintf("ID: %s", policy.ID)) - ui.Info(fmt.Sprintf("Name: %s", policy.Name)) - if policy.Namespace != "" { - ui.Info(fmt.Sprintf("Namespace: %s", policy.Namespace)) - } - ui.Info(fmt.Sprintf("Description: %s", policy.Description)) - ui.Info(fmt.Sprintf("Datacenters: %s", strings.Join(policy.Datacenters, ", "))) - if showMeta { - ui.Info(fmt.Sprintf("Hash: %x", policy.Hash)) - ui.Info(fmt.Sprintf("Create Index: %d", policy.CreateIndex)) - ui.Info(fmt.Sprintf("Modify Index: %d", policy.ModifyIndex)) - } - ui.Info(fmt.Sprintf("Rules:")) - ui.Info(policy.Rules) -} - -func PrintPolicyListEntry(policy *api.ACLPolicyListEntry, ui cli.Ui, showMeta bool) { - ui.Info(fmt.Sprintf("%s:", policy.Name)) - ui.Info(fmt.Sprintf(" ID: %s", policy.ID)) - if policy.Namespace != "" { - ui.Info(fmt.Sprintf(" Namespace: %s", policy.Namespace)) - } - ui.Info(fmt.Sprintf(" Description: %s", policy.Description)) - ui.Info(fmt.Sprintf(" Datacenters: %s", strings.Join(policy.Datacenters, ", "))) - if showMeta { - ui.Info(fmt.Sprintf(" Hash: %x", policy.Hash)) - ui.Info(fmt.Sprintf(" Create Index: %d", policy.CreateIndex)) - ui.Info(fmt.Sprintf(" Modify Index: %d", policy.ModifyIndex)) - } -} - -func PrintRole(role *api.ACLRole, ui cli.Ui, showMeta bool) { - ui.Info(fmt.Sprintf("ID: %s", role.ID)) - ui.Info(fmt.Sprintf("Name: %s", role.Name)) - if role.Namespace != "" { - ui.Info(fmt.Sprintf("Namespace: %s", role.Namespace)) - } - ui.Info(fmt.Sprintf("Description: %s", role.Description)) - if showMeta { - ui.Info(fmt.Sprintf("Hash: %x", role.Hash)) - ui.Info(fmt.Sprintf("Create Index: %d", role.CreateIndex)) - ui.Info(fmt.Sprintf("Modify Index: %d", role.ModifyIndex)) - } - if len(role.Policies) > 0 { - ui.Info(fmt.Sprintf("Policies:")) - for _, policy := range role.Policies { - ui.Info(fmt.Sprintf(" %s - %s", policy.ID, policy.Name)) - } - } - if len(role.ServiceIdentities) > 0 { - ui.Info(fmt.Sprintf("Service Identities:")) - for _, svcid := range role.ServiceIdentities { - if len(svcid.Datacenters) > 0 { - ui.Info(fmt.Sprintf(" %s (Datacenters: %s)", svcid.ServiceName, strings.Join(svcid.Datacenters, ", "))) - } else { - ui.Info(fmt.Sprintf(" %s (Datacenters: all)", svcid.ServiceName)) - } - } - } -} - -func PrintRoleListEntry(role *api.ACLRole, ui cli.Ui, showMeta bool) { - ui.Info(fmt.Sprintf("%s:", role.Name)) - ui.Info(fmt.Sprintf(" ID: %s", role.ID)) - if role.Namespace != "" { - ui.Info(fmt.Sprintf(" Namespace: %s", role.Namespace)) - } - ui.Info(fmt.Sprintf(" Description: %s", role.Description)) - if showMeta { - ui.Info(fmt.Sprintf(" Hash: %x", role.Hash)) - ui.Info(fmt.Sprintf(" Create Index: %d", role.CreateIndex)) - ui.Info(fmt.Sprintf(" Modify Index: %d", role.ModifyIndex)) - } - if len(role.Policies) > 0 { - ui.Info(fmt.Sprintf(" Policies:")) - for _, policy := range role.Policies { - ui.Info(fmt.Sprintf(" %s - %s", policy.ID, policy.Name)) - } - } - if len(role.ServiceIdentities) > 0 { - ui.Info(fmt.Sprintf(" Service Identities:")) - for _, svcid := range role.ServiceIdentities { - if len(svcid.Datacenters) > 0 { - ui.Info(fmt.Sprintf(" %s (Datacenters: %s)", svcid.ServiceName, strings.Join(svcid.Datacenters, ", "))) - } else { - ui.Info(fmt.Sprintf(" %s (Datacenters: all)", svcid.ServiceName)) - } - } - } -} - -func PrintAuthMethod(method *api.ACLAuthMethod, ui cli.Ui, showMeta bool) { - ui.Info(fmt.Sprintf("Name: %s", method.Name)) - ui.Info(fmt.Sprintf("Type: %s", method.Type)) - if method.Namespace != "" { - ui.Info(fmt.Sprintf("Namespace: %s", method.Namespace)) - } - ui.Info(fmt.Sprintf("Description: %s", method.Description)) - if showMeta { - ui.Info(fmt.Sprintf("Create Index: %d", method.CreateIndex)) - ui.Info(fmt.Sprintf("Modify Index: %d", method.ModifyIndex)) - } - ui.Info(fmt.Sprintf("Config:")) - output, err := json.MarshalIndent(method.Config, "", " ") - if err != nil { - ui.Error(fmt.Sprintf("Error formatting auth method configuration: %s", err)) - } - ui.Output(string(output)) -} - -func PrintAuthMethodListEntry(method *api.ACLAuthMethodListEntry, ui cli.Ui, showMeta bool) { - ui.Info(fmt.Sprintf("%s:", method.Name)) - ui.Info(fmt.Sprintf(" Type: %s", method.Type)) - if method.Namespace != "" { - ui.Info(fmt.Sprintf(" Namespace: %s", method.Namespace)) - } - ui.Info(fmt.Sprintf(" Description: %s", method.Description)) - if showMeta { - ui.Info(fmt.Sprintf(" Create Index: %d", method.CreateIndex)) - ui.Info(fmt.Sprintf(" Modify Index: %d", method.ModifyIndex)) - } -} - -func PrintBindingRule(rule *api.ACLBindingRule, ui cli.Ui, showMeta bool) { - ui.Info(fmt.Sprintf("ID: %s", rule.ID)) - if rule.Namespace != "" { - ui.Info(fmt.Sprintf("Namespace: %s", rule.Namespace)) - } - ui.Info(fmt.Sprintf("AuthMethod: %s", rule.AuthMethod)) - ui.Info(fmt.Sprintf("Description: %s", rule.Description)) - ui.Info(fmt.Sprintf("BindType: %s", rule.BindType)) - ui.Info(fmt.Sprintf("BindName: %s", rule.BindName)) - ui.Info(fmt.Sprintf("Selector: %s", rule.Selector)) - if showMeta { - ui.Info(fmt.Sprintf("Create Index: %d", rule.CreateIndex)) - ui.Info(fmt.Sprintf("Modify Index: %d", rule.ModifyIndex)) - } -} - -func PrintBindingRuleListEntry(rule *api.ACLBindingRule, ui cli.Ui, showMeta bool) { - ui.Info(fmt.Sprintf("%s:", rule.ID)) - if rule.Namespace != "" { - ui.Info(fmt.Sprintf(" Namespace: %s", rule.Namespace)) - } - ui.Info(fmt.Sprintf(" AuthMethod: %s", rule.AuthMethod)) - ui.Info(fmt.Sprintf(" Description: %s", rule.Description)) - ui.Info(fmt.Sprintf(" BindType: %s", rule.BindType)) - ui.Info(fmt.Sprintf(" BindName: %s", rule.BindName)) - ui.Info(fmt.Sprintf(" Selector: %s", rule.Selector)) - if showMeta { - ui.Info(fmt.Sprintf(" Create Index: %d", rule.CreateIndex)) - ui.Info(fmt.Sprintf(" Modify Index: %d", rule.ModifyIndex)) - } -} - func GetTokenIDFromPartial(client *api.Client, partialID string) (string, error) { if partialID == "anonymous" { return structs.ACLTokenAnonymousID, nil diff --git a/command/acl/authmethod/create/authmethod_create.go b/command/acl/authmethod/create/authmethod_create.go index 515d26e751..7d45fdd954 100644 --- a/command/acl/authmethod/create/authmethod_create.go +++ b/command/acl/authmethod/create/authmethod_create.go @@ -4,9 +4,10 @@ import ( "flag" "fmt" "io" + "strings" "github.com/hashicorp/consul/api" - "github.com/hashicorp/consul/command/acl" + "github.com/hashicorp/consul/command/acl/authmethod" "github.com/hashicorp/consul/command/flags" "github.com/hashicorp/consul/command/helpers" "github.com/mitchellh/cli" @@ -33,6 +34,7 @@ type cmd struct { k8sServiceAccountJWT string showMeta bool + format string testStdin io.Reader } @@ -90,6 +92,12 @@ func (c *cmd) init() { "validate other JWTs during login. "+ "This flag is required for type=kubernetes.", ) + c.flags.StringVar( + &c.format, + "format", + authmethod.PrettyFormat, + fmt.Sprintf("Output format {%s}", strings.Join(authmethod.GetSupportedFormats(), "|")), + ) c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) @@ -159,7 +167,21 @@ func (c *cmd) Run(args []string) int { return 1 } - acl.PrintAuthMethod(method, c.UI, c.showMeta) + formatter, err := authmethod.NewFormatter(c.format, c.showMeta) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + out, err := formatter.FormatAuthMethod(method) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + if out != "" { + c.UI.Info(out) + } + return 0 } diff --git a/command/acl/authmethod/create/authmethod_create_test.go b/command/acl/authmethod/create/authmethod_create_test.go index 5333e6cea8..fc0ad0c390 100644 --- a/command/acl/authmethod/create/authmethod_create_test.go +++ b/command/acl/authmethod/create/authmethod_create_test.go @@ -1,6 +1,7 @@ package authmethodcreate import ( + "encoding/json" "io/ioutil" "os" "path/filepath" @@ -13,6 +14,7 @@ import ( "github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/testrpc" "github.com/mitchellh/cli" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" // activate testing auth method @@ -107,6 +109,64 @@ func TestAuthMethodCreateCommand(t *testing.T) { }) } +func TestAuthMethodCreateCommand_JSON(t *testing.T) { + t.Parallel() + + testDir := testutil.TempDir(t, "acl") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t, t.Name(), ` + primary_datacenter = "dc1" + acl { + enabled = true + tokens { + master = "root" + } + }`) + + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + t.Run("type required", func(t *testing.T) { + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-format=json", + } + + ui := cli.NewMockUi() + cmd := New(ui) + + code := cmd.Run(args) + require.Equal(t, code, 1) + require.Contains(t, ui.ErrorWriter.String(), "Missing required '-type' flag") + }) + + t.Run("create testing", func(t *testing.T) { + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-type=testing", + "-name=test", + "-format=json", + } + + ui := cli.NewMockUi() + cmd := New(ui) + + code := cmd.Run(args) + out := ui.OutputWriter.String() + + require.Equal(t, code, 0) + require.Empty(t, ui.ErrorWriter.String()) + require.Contains(t, out, "test") + + var jsonOutput json.RawMessage + err := json.Unmarshal([]byte(out), &jsonOutput) + assert.NoError(t, err) + }) +} + func TestAuthMethodCreateCommand_k8s(t *testing.T) { t.Parallel() diff --git a/command/acl/authmethod/formatter.go b/command/acl/authmethod/formatter.go new file mode 100644 index 0000000000..ef6f1c2715 --- /dev/null +++ b/command/acl/authmethod/formatter.go @@ -0,0 +1,121 @@ +package authmethod + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/hashicorp/consul/api" +) + +const ( + PrettyFormat string = "pretty" + JSONFormat string = "json" +) + +// Formatter defines methods provided by authmethod command output formatter +type Formatter interface { + FormatAuthMethod(method *api.ACLAuthMethod) (string, error) + FormatAuthMethodList(methods []*api.ACLAuthMethodListEntry) (string, error) +} + +// GetSupportedFormats returns supported formats +func GetSupportedFormats() []string { + return []string{PrettyFormat, JSONFormat} +} + +// NewFormatter returns Formatter implementation +func NewFormatter(format string, showMeta bool) (formatter Formatter, err error) { + switch format { + case PrettyFormat: + formatter = newPrettyFormatter(showMeta) + case JSONFormat: + formatter = newJSONFormatter(showMeta) + default: + err = fmt.Errorf("Unknown format: %s", format) + } + + return formatter, err +} + +func newPrettyFormatter(showMeta bool) Formatter { + return &prettyFormatter{showMeta} +} + +type prettyFormatter struct { + showMeta bool +} + +func (f *prettyFormatter) FormatAuthMethod(method *api.ACLAuthMethod) (string, error) { + var buffer bytes.Buffer + + buffer.WriteString(fmt.Sprintf("Name: %s\n", method.Name)) + buffer.WriteString(fmt.Sprintf("Type: %s\n", method.Type)) + if method.Namespace != "" { + buffer.WriteString(fmt.Sprintf("Namespace: %s\n", method.Namespace)) + } + buffer.WriteString(fmt.Sprintf("Description: %s\n", method.Description)) + if f.showMeta { + buffer.WriteString(fmt.Sprintf("Create Index: %d\n", method.CreateIndex)) + buffer.WriteString(fmt.Sprintf("Modify Index: %d\n", method.ModifyIndex)) + } + buffer.WriteString(fmt.Sprintln("Config:")) + output, err := json.MarshalIndent(method.Config, "", " ") + if err != nil { + return "", fmt.Errorf("Error formatting auth method configuration: %s", err) + } + buffer.WriteString(string(output)) + + return buffer.String(), nil +} + +func (f *prettyFormatter) FormatAuthMethodList(methods []*api.ACLAuthMethodListEntry) (string, error) { + var buffer bytes.Buffer + + for _, method := range methods { + buffer.WriteString(f.formatAuthMethodListEntry(method)) + } + + return buffer.String(), nil +} + +func (f *prettyFormatter) formatAuthMethodListEntry(method *api.ACLAuthMethodListEntry) string { + var buffer bytes.Buffer + + buffer.WriteString(fmt.Sprintf("%s:\n", method.Name)) + buffer.WriteString(fmt.Sprintf(" Type: %s\n", method.Type)) + if method.Namespace != "" { + buffer.WriteString(fmt.Sprintf(" Namespace: %s\n", method.Namespace)) + } + buffer.WriteString(fmt.Sprintf(" Description: %s\n", method.Description)) + if f.showMeta { + buffer.WriteString(fmt.Sprintf(" Create Index: %d\n", method.CreateIndex)) + buffer.WriteString(fmt.Sprintf(" Modify Index: %d\n", method.ModifyIndex)) + } + + return buffer.String() +} + +func newJSONFormatter(showMeta bool) Formatter { + return &jsonFormatter{showMeta} +} + +type jsonFormatter struct { + showMeta bool +} + +func (f *jsonFormatter) FormatAuthMethod(method *api.ACLAuthMethod) (string, error) { + b, err := json.MarshalIndent(method, "", " ") + if err != nil { + return "", fmt.Errorf("Failed to marshal authmethod:, %v", err) + } + return string(b), nil +} + +func (f *jsonFormatter) FormatAuthMethodList(methods []*api.ACLAuthMethodListEntry) (string, error) { + b, err := json.MarshalIndent(methods, "", " ") + if err != nil { + return "", fmt.Errorf("Failed to marshal authmethods:, %v", err) + } + return string(b), nil +} diff --git a/command/acl/authmethod/list/authmethod_list.go b/command/acl/authmethod/list/authmethod_list.go index 614c831fd5..25b0478269 100644 --- a/command/acl/authmethod/list/authmethod_list.go +++ b/command/acl/authmethod/list/authmethod_list.go @@ -3,8 +3,9 @@ package authmethodlist import ( "flag" "fmt" + "strings" - "github.com/hashicorp/consul/command/acl" + "github.com/hashicorp/consul/command/acl/authmethod" "github.com/hashicorp/consul/command/flags" "github.com/mitchellh/cli" ) @@ -22,6 +23,7 @@ type cmd struct { help string showMeta bool + format string } func (c *cmd) init() { @@ -34,6 +36,12 @@ func (c *cmd) init() { "Indicates that auth method metadata such "+ "as the raft indices should be shown for each entry.", ) + c.flags.StringVar( + &c.format, + "format", + authmethod.PrettyFormat, + fmt.Sprintf("Output format {%s}", strings.Join(authmethod.GetSupportedFormats(), "|")), + ) c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) @@ -59,8 +67,19 @@ func (c *cmd) Run(args []string) int { return 1 } - for _, method := range methods { - acl.PrintAuthMethodListEntry(method, c.UI, c.showMeta) + formatter, err := authmethod.NewFormatter(c.format, c.showMeta) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + out, err := formatter.FormatAuthMethodList(methods) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + if out != "" { + c.UI.Info(out) } return 0 diff --git a/command/acl/authmethod/list/authmethod_list_test.go b/command/acl/authmethod/list/authmethod_list_test.go index b9b8b36a32..2b1975fb96 100644 --- a/command/acl/authmethod/list/authmethod_list_test.go +++ b/command/acl/authmethod/list/authmethod_list_test.go @@ -1,6 +1,7 @@ package authmethodlist import ( + "encoding/json" "os" "strings" "testing" @@ -11,6 +12,7 @@ import ( "github.com/hashicorp/consul/testrpc" "github.com/hashicorp/go-uuid" "github.com/mitchellh/cli" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" // activate testing auth method @@ -104,3 +106,73 @@ func TestAuthMethodListCommand(t *testing.T) { } }) } + +func TestAuthMethodListCommand_JSON(t *testing.T) { + t.Parallel() + + testDir := testutil.TempDir(t, "acl") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t, t.Name(), ` + primary_datacenter = "dc1" + acl { + enabled = true + tokens { + master = "root" + } + }`) + + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + client := a.Client() + + createAuthMethod := func(t *testing.T) string { + id, err := uuid.GenerateUUID() + require.NoError(t, err) + + methodName := "test-" + id + + _, _, err = client.ACL().AuthMethodCreate( + &api.ACLAuthMethod{ + Name: methodName, + Type: "testing", + Description: "test", + }, + &api.WriteOptions{Token: "root"}, + ) + require.NoError(t, err) + + return methodName + } + + var methodNames []string + for i := 0; i < 5; i++ { + methodName := createAuthMethod(t) + methodNames = append(methodNames, methodName) + } + + t.Run("found some", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-format=json", + } + + code := cmd.Run(args) + require.Equal(t, code, 0) + require.Empty(t, ui.ErrorWriter.String()) + output := ui.OutputWriter.String() + + for _, methodName := range methodNames { + require.Contains(t, output, methodName) + } + + var jsonOutput json.RawMessage + err := json.Unmarshal([]byte(output), &jsonOutput) + assert.NoError(t, err) + }) +} diff --git a/command/acl/authmethod/read/authmethod_read.go b/command/acl/authmethod/read/authmethod_read.go index a973a04a6b..19acdcd414 100644 --- a/command/acl/authmethod/read/authmethod_read.go +++ b/command/acl/authmethod/read/authmethod_read.go @@ -3,8 +3,9 @@ package authmethodread import ( "flag" "fmt" + "strings" - "github.com/hashicorp/consul/command/acl" + "github.com/hashicorp/consul/command/acl/authmethod" "github.com/hashicorp/consul/command/flags" "github.com/mitchellh/cli" ) @@ -24,6 +25,7 @@ type cmd struct { name string showMeta bool + format string } func (c *cmd) init() { @@ -44,6 +46,13 @@ func (c *cmd) init() { "The name of the auth method to read.", ) + c.flags.StringVar( + &c.format, + "format", + authmethod.PrettyFormat, + fmt.Sprintf("Output format {%s}", strings.Join(authmethod.GetSupportedFormats(), "|")), + ) + c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ServerFlags()) @@ -75,7 +84,22 @@ func (c *cmd) Run(args []string) int { c.UI.Error(fmt.Sprintf("Auth method not found with name %q", c.name)) return 1 } - acl.PrintAuthMethod(method, c.UI, c.showMeta) + + formatter, err := authmethod.NewFormatter(c.format, c.showMeta) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + out, err := formatter.FormatAuthMethod(method) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + if out != "" { + c.UI.Info(out) + } + return 0 } diff --git a/command/acl/authmethod/read/authmethod_read_test.go b/command/acl/authmethod/read/authmethod_read_test.go index 710b176cad..d91e125f58 100644 --- a/command/acl/authmethod/read/authmethod_read_test.go +++ b/command/acl/authmethod/read/authmethod_read_test.go @@ -1,6 +1,7 @@ package authmethodread import ( + "encoding/json" "os" "strings" "testing" @@ -11,6 +12,7 @@ import ( "github.com/hashicorp/consul/testrpc" "github.com/hashicorp/go-uuid" "github.com/mitchellh/cli" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" // activate testing auth method @@ -113,3 +115,68 @@ func TestAuthMethodReadCommand(t *testing.T) { require.Contains(t, output, name) }) } + +func TestAuthMethodReadCommand_JSON(t *testing.T) { + t.Parallel() + + testDir := testutil.TempDir(t, "acl") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t, t.Name(), ` + primary_datacenter = "dc1" + acl { + enabled = true + tokens { + master = "root" + } + }`) + + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + client := a.Client() + + createAuthMethod := func(t *testing.T) string { + id, err := uuid.GenerateUUID() + require.NoError(t, err) + + methodName := "test-" + id + + _, _, err = client.ACL().AuthMethodCreate( + &api.ACLAuthMethod{ + Name: methodName, + Type: "testing", + Description: "test", + }, + &api.WriteOptions{Token: "root"}, + ) + require.NoError(t, err) + + return methodName + } + + t.Run("read by name", func(t *testing.T) { + name := createAuthMethod(t) + + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-name=" + name, + "-format=json", + } + + code := cmd.Run(args) + require.Equal(t, code, 0) + require.Empty(t, ui.ErrorWriter.String()) + + output := ui.OutputWriter.String() + require.Contains(t, output, name) + + var jsonOutput json.RawMessage + err := json.Unmarshal([]byte(output), &jsonOutput) + assert.NoError(t, err) + }) +} diff --git a/command/acl/authmethod/update/authmethod_update.go b/command/acl/authmethod/update/authmethod_update.go index 73db051ca4..a28a2adc99 100644 --- a/command/acl/authmethod/update/authmethod_update.go +++ b/command/acl/authmethod/update/authmethod_update.go @@ -4,9 +4,10 @@ import ( "flag" "fmt" "io" + "strings" "github.com/hashicorp/consul/api" - "github.com/hashicorp/consul/command/acl" + "github.com/hashicorp/consul/command/acl/authmethod" "github.com/hashicorp/consul/command/flags" "github.com/hashicorp/consul/command/helpers" "github.com/mitchellh/cli" @@ -34,6 +35,7 @@ type cmd struct { noMerge bool showMeta bool + format string testStdin io.Reader } @@ -90,7 +92,12 @@ func (c *cmd) init() { c.flags.BoolVar(&c.noMerge, "no-merge", false, "Do not merge the current auth method "+ "information with what is provided to the command. Instead overwrite all fields "+ "with the exception of the name which is immutable.") - + c.flags.StringVar( + &c.format, + "format", + authmethod.PrettyFormat, + fmt.Sprintf("Output format {%s}", strings.Join(authmethod.GetSupportedFormats(), "|")), + ) c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ServerFlags()) @@ -190,8 +197,21 @@ func (c *cmd) Run(args []string) int { return 1 } - c.UI.Info(fmt.Sprintf("Auth method updated successfully")) - acl.PrintAuthMethod(method, c.UI, c.showMeta) + formatter, err := authmethod.NewFormatter(c.format, c.showMeta) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + out, err := formatter.FormatAuthMethod(method) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + if out != "" { + c.UI.Info(out) + } + return 0 } diff --git a/command/acl/authmethod/update/authmethod_update_test.go b/command/acl/authmethod/update/authmethod_update_test.go index d900344a1d..cb200eb54a 100644 --- a/command/acl/authmethod/update/authmethod_update_test.go +++ b/command/acl/authmethod/update/authmethod_update_test.go @@ -1,6 +1,7 @@ package authmethodupdate import ( + "encoding/json" "io/ioutil" "os" "path/filepath" @@ -15,6 +16,7 @@ import ( "github.com/hashicorp/consul/testrpc" "github.com/hashicorp/go-uuid" "github.com/mitchellh/cli" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" // activate testing auth method @@ -124,6 +126,94 @@ func TestAuthMethodUpdateCommand(t *testing.T) { }) } +func TestAuthMethodUpdateCommand_JSON(t *testing.T) { + t.Parallel() + + testDir := testutil.TempDir(t, "acl") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t, t.Name(), ` + primary_datacenter = "dc1" + acl { + enabled = true + tokens { + master = "root" + } + }`) + + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + client := a.Client() + + t.Run("update without name", func(t *testing.T) { + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-format=json", + } + + ui := cli.NewMockUi() + cmd := New(ui) + + code := cmd.Run(args) + require.Equal(t, code, 1) + require.Contains(t, ui.ErrorWriter.String(), "Cannot update an auth method without specifying the -name parameter") + }) + + createAuthMethod := func(t *testing.T) string { + id, err := uuid.GenerateUUID() + require.NoError(t, err) + + methodName := "test-" + id + + _, _, err = client.ACL().AuthMethodCreate( + &api.ACLAuthMethod{ + Name: methodName, + Type: "testing", + Description: "test", + }, + &api.WriteOptions{Token: "root"}, + ) + require.NoError(t, err) + + return methodName + } + + t.Run("update all fields", func(t *testing.T) { + name := createAuthMethod(t) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-name=" + name, + "-description", "updated description", + "-format=json", + } + + ui := cli.NewMockUi() + cmd := New(ui) + + code := cmd.Run(args) + require.Equal(t, code, 0) + require.Empty(t, ui.ErrorWriter.String()) + + method, _, err := client.ACL().AuthMethodRead( + name, + &api.QueryOptions{Token: "root"}, + ) + require.NoError(t, err) + require.NotNil(t, method) + require.Equal(t, "updated description", method.Description) + + output := ui.OutputWriter.String() + + var jsonOutput json.RawMessage + err = json.Unmarshal([]byte(output), &jsonOutput) + assert.NoError(t, err) + }) +} + func TestAuthMethodUpdateCommand_noMerge(t *testing.T) { t.Parallel() diff --git a/command/acl/bindingrule/create/bindingrule_create.go b/command/acl/bindingrule/create/bindingrule_create.go index a712433752..1059e0de47 100644 --- a/command/acl/bindingrule/create/bindingrule_create.go +++ b/command/acl/bindingrule/create/bindingrule_create.go @@ -3,9 +3,10 @@ package bindingrulecreate import ( "flag" "fmt" + "strings" "github.com/hashicorp/consul/api" - "github.com/hashicorp/consul/command/acl" + "github.com/hashicorp/consul/command/acl/bindingrule" "github.com/hashicorp/consul/command/flags" "github.com/mitchellh/cli" ) @@ -29,6 +30,7 @@ type cmd struct { bindName string showMeta bool + format string } func (c *cmd) init() { @@ -75,6 +77,12 @@ func (c *cmd) init() { "Name to bind on match. Can use ${var} interpolation. "+ "This flag is required.", ) + c.flags.StringVar( + &c.format, + "format", + bindingrule.PrettyFormat, + fmt.Sprintf("Output format {%s}", strings.Join(bindingrule.GetSupportedFormats(), "|")), + ) c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) @@ -122,7 +130,21 @@ func (c *cmd) Run(args []string) int { return 1 } - acl.PrintBindingRule(rule, c.UI, c.showMeta) + formatter, err := bindingrule.NewFormatter(c.format, c.showMeta) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + out, err := formatter.FormatBindingRule(rule) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + if out != "" { + c.UI.Info(out) + } + return 0 } diff --git a/command/acl/bindingrule/create/bindingrule_create_test.go b/command/acl/bindingrule/create/bindingrule_create_test.go index 70fae70f0c..bcc3caaa42 100644 --- a/command/acl/bindingrule/create/bindingrule_create_test.go +++ b/command/acl/bindingrule/create/bindingrule_create_test.go @@ -1,6 +1,7 @@ package bindingrulecreate import ( + "encoding/json" "os" "strings" "testing" @@ -10,6 +11,7 @@ import ( "github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/testrpc" "github.com/mitchellh/cli" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" // activate testing auth method @@ -173,3 +175,59 @@ func TestBindingRuleCreateCommand(t *testing.T) { require.Empty(t, ui.ErrorWriter.String()) }) } + +func TestBindingRuleCreateCommand_JSON(t *testing.T) { + t.Parallel() + + testDir := testutil.TempDir(t, "acl") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t, t.Name(), ` + primary_datacenter = "dc1" + acl { + enabled = true + tokens { + master = "root" + } + }`) + + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + client := a.Client() + + // create an auth method in advance + { + _, _, err := client.ACL().AuthMethodCreate( + &api.ACLAuthMethod{ + Name: "test", + Type: "testing", + }, + &api.WriteOptions{Token: "root"}, + ) + require.NoError(t, err) + } + + t.Run("create it with no selector", func(t *testing.T) { + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-method=test", + "-bind-type=service", + "-bind-name=demo", + "-format=json", + } + + ui := cli.NewMockUi() + cmd := New(ui) + + code := cmd.Run(args) + require.Equal(t, code, 0) + require.Empty(t, ui.ErrorWriter.String()) + + output := ui.OutputWriter.String() + var jsonOutput json.RawMessage + err := json.Unmarshal([]byte(output), &jsonOutput) + assert.NoError(t, err) + }) +} diff --git a/command/acl/bindingrule/formatter.go b/command/acl/bindingrule/formatter.go new file mode 100644 index 0000000000..92210dd3b3 --- /dev/null +++ b/command/acl/bindingrule/formatter.go @@ -0,0 +1,122 @@ +package bindingrule + +import ( + "bytes" + "encoding/json" + "fmt" + + "github.com/hashicorp/consul/api" +) + +const ( + PrettyFormat string = "pretty" + JSONFormat string = "json" +) + +// Formatter defines methods provided by bindingrule command output formatter +type Formatter interface { + FormatBindingRule(rule *api.ACLBindingRule) (string, error) + FormatBindingRuleList(rules []*api.ACLBindingRule) (string, error) +} + +// GetSupportedFormats returns supported formats +func GetSupportedFormats() []string { + return []string{PrettyFormat, JSONFormat} +} + +// NewFormatter returns Formatter implementation +func NewFormatter(format string, showMeta bool) (formatter Formatter, err error) { + switch format { + case PrettyFormat: + formatter = newPrettyFormatter(showMeta) + case JSONFormat: + formatter = newJSONFormatter(showMeta) + default: + err = fmt.Errorf("Unknown format: %s", format) + } + + return formatter, err +} + +func newPrettyFormatter(showMeta bool) Formatter { + return &prettyFormatter{showMeta} +} + +type prettyFormatter struct { + showMeta bool +} + +func (f *prettyFormatter) FormatBindingRule(rule *api.ACLBindingRule) (string, error) { + var buffer bytes.Buffer + + buffer.WriteString(fmt.Sprintf("ID: %s\n", rule.ID)) + if rule.Namespace != "" { + buffer.WriteString(fmt.Sprintf("Namespace: %s\n", rule.Namespace)) + } + buffer.WriteString(fmt.Sprintf("AuthMethod: %s\n", rule.AuthMethod)) + buffer.WriteString(fmt.Sprintf("Description: %s\n", rule.Description)) + buffer.WriteString(fmt.Sprintf("BindType: %s\n", rule.BindType)) + buffer.WriteString(fmt.Sprintf("BindName: %s\n", rule.BindName)) + buffer.WriteString(fmt.Sprintf("Selector: %s\n", rule.Selector)) + if f.showMeta { + buffer.WriteString(fmt.Sprintf("Create Index: %d\n", rule.CreateIndex)) + buffer.WriteString(fmt.Sprintf("Modify Index: %d\n", rule.ModifyIndex)) + } + + return buffer.String(), nil +} + +func (f *prettyFormatter) FormatBindingRuleList(rules []*api.ACLBindingRule) (string, error) { + var buffer bytes.Buffer + + for _, rule := range rules { + buffer.WriteString(f.formatBindingRuleListEntry(rule)) + } + + return buffer.String(), nil +} + +func (f *prettyFormatter) formatBindingRuleListEntry(rule *api.ACLBindingRule) string { + var buffer bytes.Buffer + + buffer.WriteString(fmt.Sprintf("%s:\n", rule.ID)) + if rule.Namespace != "" { + buffer.WriteString(fmt.Sprintf(" Namespace: %s\n", rule.Namespace)) + } + buffer.WriteString(fmt.Sprintf(" AuthMethod: %s\n", rule.AuthMethod)) + buffer.WriteString(fmt.Sprintf(" Description: %s\n", rule.Description)) + buffer.WriteString(fmt.Sprintf(" BindType: %s\n", rule.BindType)) + buffer.WriteString(fmt.Sprintf(" BindName: %s\n", rule.BindName)) + buffer.WriteString(fmt.Sprintf(" Selector: %s\n", rule.Selector)) + if f.showMeta { + buffer.WriteString(fmt.Sprintf(" Create Index: %d\n", rule.CreateIndex)) + buffer.WriteString(fmt.Sprintf(" Modify Index: %d\n", rule.ModifyIndex)) + } + + return buffer.String() +} + +func newJSONFormatter(showMeta bool) Formatter { + return &jsonFormatter{showMeta} +} + +type jsonFormatter struct { + showMeta bool +} + +func (f *jsonFormatter) FormatBindingRule(rule *api.ACLBindingRule) (string, error) { + b, err := json.MarshalIndent(rule, "", " ") + if err != nil { + return "", fmt.Errorf("Failed to marshal binding rule:, %v", err) + } + return string(b), nil +} + +func (f *jsonFormatter) FormatBindingRuleList(rules []*api.ACLBindingRule) (string, error) { + b, err := json.MarshalIndent(rules, "", " ") + if err != nil { + return "", fmt.Errorf("Failed to marshal binding rules:, %v", err) + + } + return string(b), nil +} diff --git a/command/acl/bindingrule/list/bindingrule_list.go b/command/acl/bindingrule/list/bindingrule_list.go index ddd8fbbec7..34785a43a1 100644 --- a/command/acl/bindingrule/list/bindingrule_list.go +++ b/command/acl/bindingrule/list/bindingrule_list.go @@ -3,8 +3,9 @@ package bindingrulelist import ( "flag" "fmt" + "strings" - "github.com/hashicorp/consul/command/acl" + "github.com/hashicorp/consul/command/acl/bindingrule" "github.com/hashicorp/consul/command/flags" "github.com/mitchellh/cli" ) @@ -24,6 +25,7 @@ type cmd struct { authMethodName string showMeta bool + format string } func (c *cmd) init() { @@ -44,6 +46,13 @@ func (c *cmd) init() { "Only show rules linked to the auth method with the given name.", ) + c.flags.StringVar( + &c.format, + "format", + bindingrule.PrettyFormat, + fmt.Sprintf("Output format {%s}", strings.Join(bindingrule.GetSupportedFormats(), "|")), + ) + c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ServerFlags()) @@ -68,8 +77,19 @@ func (c *cmd) Run(args []string) int { return 1 } - for _, rule := range rules { - acl.PrintBindingRuleListEntry(rule, c.UI, c.showMeta) + formatter, err := bindingrule.NewFormatter(c.format, c.showMeta) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + out, err := formatter.FormatBindingRuleList(rules) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + if out != "" { + c.UI.Info(out) } return 0 diff --git a/command/acl/bindingrule/list/bindingrule_list_test.go b/command/acl/bindingrule/list/bindingrule_list_test.go index e40dc55399..2108ff2fa3 100644 --- a/command/acl/bindingrule/list/bindingrule_list_test.go +++ b/command/acl/bindingrule/list/bindingrule_list_test.go @@ -1,6 +1,7 @@ package bindingrulelist import ( + "encoding/json" "fmt" "os" "strings" @@ -11,6 +12,7 @@ import ( "github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/testrpc" "github.com/mitchellh/cli" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" // activate testing auth method @@ -161,4 +163,29 @@ func TestBindingRuleListCommand(t *testing.T) { } } }) + + t.Run("normal json formatted", func(t *testing.T) { + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-format=json", + } + + ui := cli.NewMockUi() + cmd := New(ui) + + code := cmd.Run(args) + require.Equal(t, code, 0) + require.Empty(t, ui.ErrorWriter.String()) + output := ui.OutputWriter.String() + + for i, v := range ruleIDs { + require.Contains(t, output, fmt.Sprintf("test-rule-%d", i)) + require.Contains(t, output, v) + } + + var jsonOutput json.RawMessage + err := json.Unmarshal([]byte(output), &jsonOutput) + assert.NoError(t, err) + }) } diff --git a/command/acl/bindingrule/read/bindingrule_read.go b/command/acl/bindingrule/read/bindingrule_read.go index 20ca39aab3..5f3f7eeaea 100644 --- a/command/acl/bindingrule/read/bindingrule_read.go +++ b/command/acl/bindingrule/read/bindingrule_read.go @@ -3,8 +3,10 @@ package bindingruleread import ( "flag" "fmt" + "strings" "github.com/hashicorp/consul/command/acl" + "github.com/hashicorp/consul/command/acl/bindingrule" "github.com/hashicorp/consul/command/flags" "github.com/mitchellh/cli" ) @@ -24,6 +26,7 @@ type cmd struct { ruleID string showMeta bool + format string } func (c *cmd) init() { @@ -46,6 +49,12 @@ func (c *cmd) init() { "matches multiple binding rule IDs", ) + c.flags.StringVar( + &c.format, + "format", + bindingrule.PrettyFormat, + fmt.Sprintf("Output format {%s}", strings.Join(bindingrule.GetSupportedFormats(), "|")), + ) c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ServerFlags()) @@ -84,7 +93,21 @@ func (c *cmd) Run(args []string) int { return 1 } - acl.PrintBindingRule(rule, c.UI, c.showMeta) + formatter, err := bindingrule.NewFormatter(c.format, c.showMeta) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + out, err := formatter.FormatBindingRule(rule) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + if out != "" { + c.UI.Info(out) + } + return 0 } diff --git a/command/acl/bindingrule/read/bindingrule_read_test.go b/command/acl/bindingrule/read/bindingrule_read_test.go index 0e3cdffbe2..68c9cd09db 100644 --- a/command/acl/bindingrule/read/bindingrule_read_test.go +++ b/command/acl/bindingrule/read/bindingrule_read_test.go @@ -146,4 +146,26 @@ func TestBindingRuleReadCommand(t *testing.T) { require.Contains(t, output, fmt.Sprintf("test rule")) require.Contains(t, output, id) }) + + t.Run("read by id json formatted", func(t *testing.T) { + id := createRule(t) + + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-id=" + id, + "-format=json", + } + + code := cmd.Run(args) + require.Equal(t, code, 0) + require.Empty(t, ui.ErrorWriter.String()) + + output := ui.OutputWriter.String() + require.Contains(t, output, fmt.Sprintf("test rule")) + require.Contains(t, output, id) + }) } diff --git a/command/acl/bindingrule/update/bindingrule_update.go b/command/acl/bindingrule/update/bindingrule_update.go index 0fdb0950c4..3acaa7df32 100644 --- a/command/acl/bindingrule/update/bindingrule_update.go +++ b/command/acl/bindingrule/update/bindingrule_update.go @@ -3,9 +3,11 @@ package bindingruleupdate import ( "flag" "fmt" + "strings" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/command/acl" + "github.com/hashicorp/consul/command/acl/bindingrule" "github.com/hashicorp/consul/command/flags" "github.com/mitchellh/cli" ) @@ -31,6 +33,7 @@ type cmd struct { noMerge bool showMeta bool + format string } func (c *cmd) init() { @@ -88,6 +91,13 @@ func (c *cmd) init() { "with the exception of the binding rule ID which is immutable.", ) + c.flags.StringVar( + &c.format, + "format", + bindingrule.PrettyFormat, + fmt.Sprintf("Output format {%s}", strings.Join(bindingrule.GetSupportedFormats(), "|")), + ) + c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ServerFlags()) @@ -171,8 +181,21 @@ func (c *cmd) Run(args []string) int { return 1 } - c.UI.Info(fmt.Sprintf("Binding rule updated successfully")) - acl.PrintBindingRule(rule, c.UI, c.showMeta) + formatter, err := bindingrule.NewFormatter(c.format, c.showMeta) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + out, err := formatter.FormatBindingRule(rule) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + if out != "" { + c.UI.Info(out) + } + return 0 } diff --git a/command/acl/bindingrule/update/bindingrule_update_test.go b/command/acl/bindingrule/update/bindingrule_update_test.go index 6020cbc87a..85cca84c02 100644 --- a/command/acl/bindingrule/update/bindingrule_update_test.go +++ b/command/acl/bindingrule/update/bindingrule_update_test.go @@ -1,6 +1,7 @@ package bindingruleupdate import ( + "encoding/json" "os" "strings" "testing" @@ -11,6 +12,7 @@ import ( "github.com/hashicorp/consul/testrpc" uuid "github.com/hashicorp/go-uuid" "github.com/mitchellh/cli" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" // activate testing auth method @@ -424,6 +426,45 @@ func TestBindingRuleUpdateCommand(t *testing.T) { require.Equal(t, "role-updated", rule.BindName) require.Empty(t, rule.Selector) }) + + t.Run("update all fields json formatted", func(t *testing.T) { + id := createRule(t) + + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-id", id, + "-description=test rule edited", + "-bind-type", "role", + "-bind-name=role-updated", + "-selector=serviceaccount.namespace==alt and serviceaccount.name==demo", + "-format=json", + } + + code := cmd.Run(args) + require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String()) + require.Empty(t, ui.ErrorWriter.String()) + + rule, _, err := client.ACL().BindingRuleRead( + id, + &api.QueryOptions{Token: "root"}, + ) + require.NoError(t, err) + require.NotNil(t, rule) + + require.Equal(t, "test rule edited", rule.Description) + require.Equal(t, "role-updated", rule.BindName) + require.Equal(t, api.BindingRuleBindTypeRole, rule.BindType) + require.Equal(t, "serviceaccount.namespace==alt and serviceaccount.name==demo", rule.Selector) + + output := ui.OutputWriter.String() + var jsonOutput json.RawMessage + err = json.Unmarshal([]byte(output), &jsonOutput) + assert.NoError(t, err) + }) } func TestBindingRuleUpdateCommand_noMerge(t *testing.T) { diff --git a/command/acl/bootstrap/bootstrap.go b/command/acl/bootstrap/bootstrap.go index 634c637cef..37ec63a9da 100644 --- a/command/acl/bootstrap/bootstrap.go +++ b/command/acl/bootstrap/bootstrap.go @@ -3,8 +3,9 @@ package bootstrap import ( "flag" "fmt" + "strings" - "github.com/hashicorp/consul/command/acl" + "github.com/hashicorp/consul/command/acl/token" "github.com/hashicorp/consul/command/flags" "github.com/mitchellh/cli" ) @@ -16,14 +17,21 @@ func New(ui cli.Ui) *cmd { } type cmd struct { - UI cli.Ui - flags *flag.FlagSet - http *flags.HTTPFlags - help string + UI cli.Ui + flags *flag.FlagSet + http *flags.HTTPFlags + help string + format string } func (c *cmd) init() { c.flags = flag.NewFlagSet("", flag.ContinueOnError) + c.flags.StringVar( + &c.format, + "format", + token.PrettyFormat, + fmt.Sprintf("Output format {%s}", strings.Join(token.GetSupportedFormats(), "|")), + ) c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ServerFlags()) @@ -41,13 +49,26 @@ func (c *cmd) Run(args []string) int { return 1 } - token, _, err := client.ACL().Bootstrap() + t, _, err := client.ACL().Bootstrap() if err != nil { c.UI.Error(fmt.Sprintf("Failed ACL bootstrapping: %v", err)) return 1 } - acl.PrintToken(token, c.UI, false) + formatter, err := token.NewFormatter(c.format, false) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + out, err := formatter.FormatToken(t) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + if out != "" { + c.UI.Info(out) + } + return 0 } diff --git a/command/acl/bootstrap/bootstrap_test.go b/command/acl/bootstrap/bootstrap_test.go index fa8332c77c..f31c430010 100644 --- a/command/acl/bootstrap/bootstrap_test.go +++ b/command/acl/bootstrap/bootstrap_test.go @@ -1,6 +1,7 @@ package bootstrap import ( + "encoding/json" "os" "strings" "testing" @@ -11,6 +12,7 @@ import ( "github.com/hashicorp/consul/testrpc" "github.com/mitchellh/cli" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestBootstrapCommand_noTabs(t *testing.T) { @@ -21,7 +23,7 @@ func TestBootstrapCommand_noTabs(t *testing.T) { } } -func TestBootstrapCommand(t *testing.T) { +func TestBootstrapCommand_Pretty(t *testing.T) { t.Parallel() assert := assert.New(t) @@ -51,3 +53,39 @@ func TestBootstrapCommand(t *testing.T) { assert.Contains(output, "Bootstrap Token") assert.Contains(output, structs.ACLPolicyGlobalManagementID) } + +func TestBootstrapCommand_JSON(t *testing.T) { + t.Parallel() + assert := assert.New(t) + + testDir := testutil.TempDir(t, "acl") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t, t.Name(), ` + primary_datacenter = "dc1" + acl { + enabled = true + }`) + + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-format=json", + } + + code := cmd.Run(args) + assert.Equal(code, 0) + assert.Empty(ui.ErrorWriter.String()) + output := ui.OutputWriter.String() + assert.Contains(output, "Bootstrap Token") + assert.Contains(output, structs.ACLPolicyGlobalManagementID) + + var jsonOutput json.RawMessage + err := json.Unmarshal([]byte(output), &jsonOutput) + require.NoError(t, err, "token unmarshalling error") +} diff --git a/command/acl/policy/create/policy_create.go b/command/acl/policy/create/policy_create.go index ffe05a3a62..e09cad0623 100644 --- a/command/acl/policy/create/policy_create.go +++ b/command/acl/policy/create/policy_create.go @@ -4,10 +4,12 @@ import ( "flag" "fmt" "io" + "strings" "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/api" aclhelpers "github.com/hashicorp/consul/command/acl" + "github.com/hashicorp/consul/command/acl/policy" "github.com/hashicorp/consul/command/flags" "github.com/hashicorp/consul/command/helpers" "github.com/mitchellh/cli" @@ -33,6 +35,7 @@ type cmd struct { fromToken string tokenIsSecret bool showMeta bool + format string testStdin io.Reader } @@ -53,6 +56,12 @@ func (c *cmd) init() { "Similar to the -rules option the token to use can be loaded from stdin or from a file") c.flags.BoolVar(&c.tokenIsSecret, "token-secret", false, "Indicates the token provided with "+ "-from-token is a SecretID and not an AccessorID") + c.flags.StringVar( + &c.format, + "format", + policy.PrettyFormat, + fmt.Sprintf("Output format {%s}", strings.Join(policy.GetSupportedFormats(), "|")), + ) c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) @@ -114,13 +123,26 @@ func (c *cmd) Run(args []string) int { Rules: rules, } - policy, _, err := client.ACL().PolicyCreate(newPolicy, nil) + p, _, err := client.ACL().PolicyCreate(newPolicy, nil) if err != nil { c.UI.Error(fmt.Sprintf("Failed to create new policy: %v", err)) return 1 } - aclhelpers.PrintPolicy(policy, c.UI, c.showMeta) + formatter, err := policy.NewFormatter(c.format, c.showMeta) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + out, err := formatter.FormatPolicy(p) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + if out != "" { + c.UI.Info(out) + } + return 0 } diff --git a/command/acl/policy/create/policy_create_test.go b/command/acl/policy/create/policy_create_test.go index c646d3782c..3aaf93790e 100644 --- a/command/acl/policy/create/policy_create_test.go +++ b/command/acl/policy/create/policy_create_test.go @@ -1,6 +1,7 @@ package policycreate import ( + "encoding/json" "io/ioutil" "os" "strings" @@ -10,6 +11,7 @@ import ( "github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/testrpc" "github.com/mitchellh/cli" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -58,3 +60,46 @@ func TestPolicyCreateCommand(t *testing.T) { require.Equal(code, 0) require.Empty(ui.ErrorWriter.String()) } + +func TestPolicyCreateCommand_JSON(t *testing.T) { + t.Parallel() + require := require.New(t) + + testDir := testutil.TempDir(t, "acl") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t, t.Name(), ` + primary_datacenter = "dc1" + acl { + enabled = true + tokens { + master = "root" + } + }`) + + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + ui := cli.NewMockUi() + cmd := New(ui) + + rules := []byte("service \"\" { policy = \"write\" }") + err := ioutil.WriteFile(testDir+"/rules.hcl", rules, 0644) + require.NoError(err) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-name=foobar", + "-rules=@" + testDir + "/rules.hcl", + "-format=json", + } + + code := cmd.Run(args) + require.Equal(code, 0) + require.Empty(ui.ErrorWriter.String()) + + var jsonOutput json.RawMessage + err = json.Unmarshal([]byte(ui.OutputWriter.String()), &jsonOutput) + assert.NoError(t, err) +} diff --git a/command/acl/policy/formatter.go b/command/acl/policy/formatter.go new file mode 100644 index 0000000000..e385f2bccf --- /dev/null +++ b/command/acl/policy/formatter.go @@ -0,0 +1,122 @@ +package policy + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" + + "github.com/hashicorp/consul/api" +) + +const ( + PrettyFormat string = "pretty" + JSONFormat string = "json" +) + +// Formatter defines methods provided by policy command output formatter +type Formatter interface { + FormatPolicy(policy *api.ACLPolicy) (string, error) + FormatPolicyList(policies []*api.ACLPolicyListEntry) (string, error) +} + +// GetSupportedFormats returns supported formats +func GetSupportedFormats() []string { + return []string{PrettyFormat, JSONFormat} +} + +// NewFormatter returns Formatter implementation +func NewFormatter(format string, showMeta bool) (formatter Formatter, err error) { + switch format { + case PrettyFormat: + formatter = newPrettyFormatter(showMeta) + case JSONFormat: + formatter = newJSONFormatter(showMeta) + default: + err = fmt.Errorf("Unknown format: %s", format) + } + + return formatter, err +} + +func newPrettyFormatter(showMeta bool) Formatter { + return &prettyFormatter{showMeta} +} + +type prettyFormatter struct { + showMeta bool +} + +func (f *prettyFormatter) FormatPolicy(policy *api.ACLPolicy) (string, error) { + var buffer bytes.Buffer + + buffer.WriteString(fmt.Sprintf("ID: %s\n", policy.ID)) + buffer.WriteString(fmt.Sprintf("Name: %s\n", policy.Name)) + if policy.Namespace != "" { + buffer.WriteString(fmt.Sprintf("Namespace: %s\n", policy.Namespace)) + } + buffer.WriteString(fmt.Sprintf("Description: %s\n", policy.Description)) + buffer.WriteString(fmt.Sprintf("Datacenters: %s\n", strings.Join(policy.Datacenters, ", "))) + if f.showMeta { + buffer.WriteString(fmt.Sprintf("Hash: %x\n", policy.Hash)) + buffer.WriteString(fmt.Sprintf("Create Index: %d\n", policy.CreateIndex)) + buffer.WriteString(fmt.Sprintf("Modify Index: %d\n", policy.ModifyIndex)) + } + buffer.WriteString(fmt.Sprintln("Rules:")) + buffer.WriteString(policy.Rules) + + return buffer.String(), nil +} + +func (f *prettyFormatter) FormatPolicyList(policies []*api.ACLPolicyListEntry) (string, error) { + var buffer bytes.Buffer + + for _, policy := range policies { + buffer.WriteString(f.formatPolicyListEntry(policy)) + } + + return buffer.String(), nil +} + +func (f *prettyFormatter) formatPolicyListEntry(policy *api.ACLPolicyListEntry) string { + var buffer bytes.Buffer + + buffer.WriteString(fmt.Sprintf("%s:\n", policy.Name)) + buffer.WriteString(fmt.Sprintf(" ID: %s\n", policy.ID)) + if policy.Namespace != "" { + buffer.WriteString(fmt.Sprintf(" Namespace: %s\n", policy.Namespace)) + } + buffer.WriteString(fmt.Sprintf(" Description: %s\n", policy.Description)) + buffer.WriteString(fmt.Sprintf(" Datacenters: %s\n", strings.Join(policy.Datacenters, ", "))) + if f.showMeta { + buffer.WriteString(fmt.Sprintf(" Hash: %x\n", policy.Hash)) + buffer.WriteString(fmt.Sprintf(" Create Index: %d\n", policy.CreateIndex)) + buffer.WriteString(fmt.Sprintf(" Modify Index: %d\n", policy.ModifyIndex)) + } + + return buffer.String() +} + +func newJSONFormatter(showMeta bool) Formatter { + return &jsonFormatter{showMeta} +} + +type jsonFormatter struct { + showMeta bool +} + +func (f *jsonFormatter) FormatPolicy(policy *api.ACLPolicy) (string, error) { + b, err := json.MarshalIndent(policy, "", " ") + if err != nil { + return "", fmt.Errorf("Failed to marshal policy: %v", err) + } + return string(b), nil +} + +func (f *jsonFormatter) FormatPolicyList(policies []*api.ACLPolicyListEntry) (string, error) { + b, err := json.MarshalIndent(policies, "", " ") + if err != nil { + return "", fmt.Errorf("Failed to marshal policies: %v", err) + } + return string(b), nil +} diff --git a/command/acl/policy/list/policy_list.go b/command/acl/policy/list/policy_list.go index 60d1d16949..3d1e819345 100644 --- a/command/acl/policy/list/policy_list.go +++ b/command/acl/policy/list/policy_list.go @@ -3,8 +3,9 @@ package policylist import ( "flag" "fmt" + "strings" - "github.com/hashicorp/consul/command/acl" + "github.com/hashicorp/consul/command/acl/policy" "github.com/hashicorp/consul/command/flags" "github.com/mitchellh/cli" ) @@ -22,12 +23,19 @@ type cmd struct { help string showMeta bool + format string } func (c *cmd) init() { c.flags = flag.NewFlagSet("", flag.ContinueOnError) c.flags.BoolVar(&c.showMeta, "meta", false, "Indicates that policy metadata such "+ "as the content hash and raft indices should be shown for each entry") + c.flags.StringVar( + &c.format, + "format", + policy.PrettyFormat, + fmt.Sprintf("Output format {%s}", strings.Join(policy.GetSupportedFormats(), "|")), + ) c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) @@ -53,8 +61,18 @@ func (c *cmd) Run(args []string) int { return 1 } - for _, policy := range policies { - acl.PrintPolicyListEntry(policy, c.UI, c.showMeta) + formatter, err := policy.NewFormatter(c.format, c.showMeta) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + out, err := formatter.FormatPolicyList(policies) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + if out != "" { + c.UI.Info(out) } return 0 diff --git a/command/acl/policy/list/policy_list_test.go b/command/acl/policy/list/policy_list_test.go index 420deaf5d9..4de67df309 100644 --- a/command/acl/policy/list/policy_list_test.go +++ b/command/acl/policy/list/policy_list_test.go @@ -1,6 +1,7 @@ package policylist import ( + "encoding/json" "fmt" "os" "strings" @@ -75,3 +76,62 @@ func TestPolicyListCommand(t *testing.T) { assert.Contains(output, v) } } + +func TestPolicyListCommand_JSON(t *testing.T) { + t.Parallel() + assert := assert.New(t) + + testDir := testutil.TempDir(t, "acl") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t, t.Name(), ` + primary_datacenter = "dc1" + acl { + enabled = true + tokens { + master = "root" + } + }`) + + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + ui := cli.NewMockUi() + cmd := New(ui) + + var policyIDs []string + + // Create a couple polices to list + client := a.Client() + for i := 0; i < 5; i++ { + name := fmt.Sprintf("test-policy-%d", i) + + policy, _, err := client.ACL().PolicyCreate( + &api.ACLPolicy{Name: name}, + &api.WriteOptions{Token: "root"}, + ) + policyIDs = append(policyIDs, policy.ID) + + assert.NoError(err) + } + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-format=json", + } + + code := cmd.Run(args) + assert.Equal(code, 0) + assert.Empty(ui.ErrorWriter.String()) + output := ui.OutputWriter.String() + + for i, v := range policyIDs { + assert.Contains(output, fmt.Sprintf("test-policy-%d", i)) + assert.Contains(output, v) + } + + var jsonOutput json.RawMessage + err := json.Unmarshal([]byte(output), &jsonOutput) + assert.NoError(err) +} diff --git a/command/acl/policy/read/policy_read.go b/command/acl/policy/read/policy_read.go index a087de5450..703443bf79 100644 --- a/command/acl/policy/read/policy_read.go +++ b/command/acl/policy/read/policy_read.go @@ -3,8 +3,10 @@ package policyread import ( "flag" "fmt" + "strings" "github.com/hashicorp/consul/command/acl" + "github.com/hashicorp/consul/command/acl/policy" "github.com/hashicorp/consul/command/flags" "github.com/mitchellh/cli" ) @@ -24,6 +26,7 @@ type cmd struct { policyID string policyName string showMeta bool + format string } func (c *cmd) init() { @@ -34,6 +37,12 @@ func (c *cmd) init() { "It may be specified as a unique ID prefix but will error if the prefix "+ "matches multiple policy IDs") c.flags.StringVar(&c.policyName, "name", "", "The name of the policy to read.") + c.flags.StringVar( + &c.format, + "format", + policy.PrettyFormat, + fmt.Sprintf("Output format {%s}", strings.Join(policy.GetSupportedFormats(), "|")), + ) c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ServerFlags()) @@ -68,12 +77,26 @@ func (c *cmd) Run(args []string) int { return 1 } - policy, _, err := client.ACL().PolicyRead(policyID, nil) + p, _, err := client.ACL().PolicyRead(policyID, nil) if err != nil { c.UI.Error(fmt.Sprintf("Error reading policy %q: %v", policyID, err)) return 1 } - acl.PrintPolicy(policy, c.UI, c.showMeta) + + formatter, err := policy.NewFormatter(c.format, c.showMeta) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + out, err := formatter.FormatPolicy(p) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + if out != "" { + c.UI.Info(out) + } + return 0 } diff --git a/command/acl/policy/read/policy_read_test.go b/command/acl/policy/read/policy_read_test.go index db1341f56b..800f96ab8b 100644 --- a/command/acl/policy/read/policy_read_test.go +++ b/command/acl/policy/read/policy_read_test.go @@ -1,6 +1,7 @@ package policyread import ( + "encoding/json" "fmt" "os" "strings" @@ -67,3 +68,54 @@ func TestPolicyReadCommand(t *testing.T) { assert.Contains(output, fmt.Sprintf("test-policy")) assert.Contains(output, policy.ID) } + +func TestPolicyReadCommand_JSON(t *testing.T) { + t.Parallel() + assert := assert.New(t) + + testDir := testutil.TempDir(t, "acl") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t, t.Name(), ` + primary_datacenter = "dc1" + acl { + enabled = true + tokens { + master = "root" + } + }`) + + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + ui := cli.NewMockUi() + cmd := New(ui) + + // Create a policy + client := a.Client() + + policy, _, err := client.ACL().PolicyCreate( + &api.ACLPolicy{Name: "test-policy"}, + &api.WriteOptions{Token: "root"}, + ) + assert.NoError(err) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-id=" + policy.ID, + "-format=json", + } + + code := cmd.Run(args) + assert.Equal(code, 0) + assert.Empty(ui.ErrorWriter.String()) + + output := ui.OutputWriter.String() + assert.Contains(output, fmt.Sprintf("test-policy")) + assert.Contains(output, policy.ID) + + var jsonOutput json.RawMessage + err = json.Unmarshal([]byte(output), &jsonOutput) + assert.NoError(err) +} diff --git a/command/acl/policy/update/policy_update.go b/command/acl/policy/update/policy_update.go index da34a4515d..2e2623f319 100644 --- a/command/acl/policy/update/policy_update.go +++ b/command/acl/policy/update/policy_update.go @@ -4,9 +4,11 @@ import ( "flag" "fmt" "io" + "strings" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/command/acl" + "github.com/hashicorp/consul/command/acl/policy" "github.com/hashicorp/consul/command/flags" "github.com/hashicorp/consul/command/helpers" "github.com/mitchellh/cli" @@ -34,6 +36,7 @@ type cmd struct { rules string noMerge bool showMeta bool + format string testStdin io.Reader } @@ -54,6 +57,13 @@ func (c *cmd) init() { c.flags.BoolVar(&c.noMerge, "no-merge", false, "Do not merge the current policy "+ "information with what is provided to the command. Instead overwrite all fields "+ "with the exception of the policy ID which is immutable.") + c.flags.StringVar( + &c.format, + "format", + policy.PrettyFormat, + fmt.Sprintf("Output format {%s}", strings.Join(policy.GetSupportedFormats(), "|")), + ) + c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ServerFlags()) @@ -116,7 +126,7 @@ func (c *cmd) Run(args []string) int { Rules: rules, } } else { - policy, _, err := client.ACL().PolicyRead(policyID, nil) + p, _, err := client.ACL().PolicyRead(policyID, nil) if err != nil { c.UI.Error(fmt.Sprintf("Error reading policy %q: %v", policyID, err)) return 1 @@ -124,10 +134,10 @@ func (c *cmd) Run(args []string) int { updated = &api.ACLPolicy{ ID: policyID, - Name: policy.Name, - Description: policy.Description, - Datacenters: policy.Datacenters, - Rules: policy.Rules, + Name: p.Name, + Description: p.Description, + Datacenters: p.Datacenters, + Rules: p.Rules, } if c.nameSet { @@ -144,14 +154,26 @@ func (c *cmd) Run(args []string) int { } } - policy, _, err := client.ACL().PolicyUpdate(updated, nil) + p, _, err := client.ACL().PolicyUpdate(updated, nil) if err != nil { c.UI.Error(fmt.Sprintf("Error updating policy %q: %v", policyID, err)) return 1 } - c.UI.Info(fmt.Sprintf("Policy updated successfully")) - acl.PrintPolicy(policy, c.UI, c.showMeta) + formatter, err := policy.NewFormatter(c.format, c.showMeta) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + out, err := formatter.FormatPolicy(p) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + if out != "" { + c.UI.Info(out) + } + return 0 } diff --git a/command/acl/policy/update/policy_update_test.go b/command/acl/policy/update/policy_update_test.go index 670037e3f3..24b6dc2e74 100644 --- a/command/acl/policy/update/policy_update_test.go +++ b/command/acl/policy/update/policy_update_test.go @@ -1,6 +1,7 @@ package policyupdate import ( + "encoding/json" "io/ioutil" "os" "strings" @@ -69,3 +70,56 @@ func TestPolicyUpdateCommand(t *testing.T) { assert.Equal(code, 0) assert.Empty(ui.ErrorWriter.String()) } + +func TestPolicyUpdateCommand_JSON(t *testing.T) { + t.Parallel() + assert := assert.New(t) + + testDir := testutil.TempDir(t, "acl") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t, t.Name(), ` + primary_datacenter = "dc1" + acl { + enabled = true + tokens { + master = "root" + } + }`) + + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + ui := cli.NewMockUi() + cmd := New(ui) + + rules := []byte("service \"\" { policy = \"write\" }") + err := ioutil.WriteFile(testDir+"/rules.hcl", rules, 0644) + assert.NoError(err) + + // Create a policy + client := a.Client() + + policy, _, err := client.ACL().PolicyCreate( + &api.ACLPolicy{Name: "test-policy"}, + &api.WriteOptions{Token: "root"}, + ) + assert.NoError(err) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-id=" + policy.ID, + "-name=new-name", + "-rules=@" + testDir + "/rules.hcl", + "-format=json", + } + + code := cmd.Run(args) + assert.Equal(code, 0) + assert.Empty(ui.ErrorWriter.String()) + + var jsonOutput json.RawMessage + err = json.Unmarshal([]byte(ui.OutputWriter.String()), &jsonOutput) + assert.NoError(err) +} diff --git a/command/acl/role/create/role_create.go b/command/acl/role/create/role_create.go index 6125c876b3..aae712a46c 100644 --- a/command/acl/role/create/role_create.go +++ b/command/acl/role/create/role_create.go @@ -3,10 +3,11 @@ package rolecreate import ( "flag" "fmt" + "strings" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/command/acl" - aclhelpers "github.com/hashicorp/consul/command/acl" + "github.com/hashicorp/consul/command/acl/role" "github.com/hashicorp/consul/command/flags" "github.com/mitchellh/cli" ) @@ -30,6 +31,7 @@ type cmd struct { serviceIdents []string showMeta bool + format string } func (c *cmd) init() { @@ -45,6 +47,12 @@ func (c *cmd) init() { c.flags.Var((*flags.AppendSliceValue)(&c.serviceIdents), "service-identity", "Name of a "+ "service identity to use for this role. May be specified multiple times. Format is "+ "the SERVICENAME or SERVICENAME:DATACENTER1,DATACENTER2,...") + c.flags.StringVar( + &c.format, + "format", + role.PrettyFormat, + fmt.Sprintf("Output format {%s}", strings.Join(role.GetSupportedFormats(), "|")), + ) c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ServerFlags()) @@ -101,13 +109,26 @@ func (c *cmd) Run(args []string) int { } newRole.ServiceIdentities = parsedServiceIdents - role, _, err := client.ACL().RoleCreate(newRole, nil) + r, _, err := client.ACL().RoleCreate(newRole, nil) if err != nil { c.UI.Error(fmt.Sprintf("Failed to create new role: %v", err)) return 1 } - aclhelpers.PrintRole(role, c.UI, c.showMeta) + formatter, err := role.NewFormatter(c.format, c.showMeta) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + out, err := formatter.FormatRole(r) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + if out != "" { + c.UI.Info(out) + } + return 0 } diff --git a/command/acl/role/create/role_create_test.go b/command/acl/role/create/role_create_test.go index 60852e554f..2127f64faa 100644 --- a/command/acl/role/create/role_create_test.go +++ b/command/acl/role/create/role_create_test.go @@ -1,6 +1,7 @@ package rolecreate import ( + "encoding/json" "os" "strings" "testing" @@ -10,6 +11,7 @@ import ( "github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/testrpc" "github.com/mitchellh/cli" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -21,7 +23,7 @@ func TestRoleCreateCommand_noTabs(t *testing.T) { } } -func TestRoleCreateCommand(t *testing.T) { +func TestRoleCreateCommand_Pretty(t *testing.T) { t.Parallel() testDir := testutil.TempDir(t, "acl") @@ -111,3 +113,54 @@ func TestRoleCreateCommand(t *testing.T) { require.Empty(t, ui.ErrorWriter.String()) } } + +func TestRoleCreateCommand_JSON(t *testing.T) { + t.Parallel() + + testDir := testutil.TempDir(t, "acl") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t, t.Name(), ` + primary_datacenter = "dc1" + acl { + enabled = true + tokens { + master = "root" + } + }`) + + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + ui := cli.NewMockUi() + cmd := New(ui) + + // Create a policy + client := a.Client() + + policy, _, err := client.ACL().PolicyCreate( + &api.ACLPolicy{Name: "test-policy"}, + &api.WriteOptions{Token: "root"}, + ) + require.NoError(t, err) + + // create with policy by name + { + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-name=role-with-policy-by-name", + "-description=test-role", + "-policy-name=" + policy.Name, + "-format=json", + } + + code := cmd.Run(args) + require.Equal(t, code, 0) + require.Empty(t, ui.ErrorWriter.String()) + + var jsonOutput json.RawMessage + err = json.Unmarshal([]byte(ui.OutputWriter.String()), &jsonOutput) + assert.NoError(t, err) + } +} diff --git a/command/acl/role/formatter.go b/command/acl/role/formatter.go new file mode 100644 index 0000000000..b000e88add --- /dev/null +++ b/command/acl/role/formatter.go @@ -0,0 +1,150 @@ +package role + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" + + "github.com/hashicorp/consul/api" +) + +const ( + PrettyFormat string = "pretty" + JSONFormat string = "json" +) + +// Formatter defines methods provided by role command output formatter +type Formatter interface { + FormatRole(role *api.ACLRole) (string, error) + FormatRoleList(roles []*api.ACLRole) (string, error) +} + +// GetSupportedFormats returns supported formats +func GetSupportedFormats() []string { + return []string{PrettyFormat, JSONFormat} +} + +// NewFormatter returns Formatter implementation +func NewFormatter(format string, showMeta bool) (formatter Formatter, err error) { + switch format { + case PrettyFormat: + formatter = newPrettyFormatter(showMeta) + case JSONFormat: + formatter = newJSONFormatter(showMeta) + default: + err = fmt.Errorf("Unknown format: %s", format) + } + + return formatter, err +} + +func newPrettyFormatter(showMeta bool) Formatter { + return &prettyFormatter{showMeta} +} + +type prettyFormatter struct { + showMeta bool +} + +func (f *prettyFormatter) FormatRole(role *api.ACLRole) (string, error) { + var buffer bytes.Buffer + + buffer.WriteString(fmt.Sprintf("ID: %s\n", role.ID)) + buffer.WriteString(fmt.Sprintf("Name: %s\n", role.Name)) + if role.Namespace != "" { + buffer.WriteString(fmt.Sprintf("Namespace: %s\n", role.Namespace)) + } + buffer.WriteString(fmt.Sprintf("Description: %s\n", role.Description)) + if f.showMeta { + buffer.WriteString(fmt.Sprintf("Hash: %x\n", role.Hash)) + buffer.WriteString(fmt.Sprintf("Create Index: %d\n", role.CreateIndex)) + buffer.WriteString(fmt.Sprintf("Modify Index: %d\n", role.ModifyIndex)) + } + if len(role.Policies) > 0 { + buffer.WriteString(fmt.Sprintln("Policies:")) + for _, policy := range role.Policies { + buffer.WriteString(fmt.Sprintf(" %s - %s\n", policy.ID, policy.Name)) + } + } + if len(role.ServiceIdentities) > 0 { + buffer.WriteString(fmt.Sprintln("Service Identities:")) + for _, svcid := range role.ServiceIdentities { + if len(svcid.Datacenters) > 0 { + buffer.WriteString(fmt.Sprintf(" %s (Datacenters: %s)\n", svcid.ServiceName, strings.Join(svcid.Datacenters, ", "))) + } else { + buffer.WriteString(fmt.Sprintf(" %s (Datacenters: all)\n", svcid.ServiceName)) + } + } + } + + return buffer.String(), nil +} + +func (f *prettyFormatter) FormatRoleList(roles []*api.ACLRole) (string, error) { + var buffer bytes.Buffer + + for _, role := range roles { + buffer.WriteString(f.formatRoleListEntry(role)) + } + + return buffer.String(), nil +} + +func (f *prettyFormatter) formatRoleListEntry(role *api.ACLRole) string { + var buffer bytes.Buffer + + buffer.WriteString(fmt.Sprintf("%s:\n", role.Name)) + buffer.WriteString(fmt.Sprintf(" ID: %s\n", role.ID)) + if role.Namespace != "" { + buffer.WriteString(fmt.Sprintf(" Namespace: %s\n", role.Namespace)) + } + buffer.WriteString(fmt.Sprintf(" Description: %s\n", role.Description)) + if f.showMeta { + buffer.WriteString(fmt.Sprintf(" Hash: %x\n", role.Hash)) + buffer.WriteString(fmt.Sprintf(" Create Index: %d\n", role.CreateIndex)) + buffer.WriteString(fmt.Sprintf(" Modify Index: %d\n", role.ModifyIndex)) + } + if len(role.Policies) > 0 { + buffer.WriteString(fmt.Sprintln(" Policies:")) + for _, policy := range role.Policies { + buffer.WriteString(fmt.Sprintf(" %s - %s\n", policy.ID, policy.Name)) + } + } + if len(role.ServiceIdentities) > 0 { + buffer.WriteString(fmt.Sprintln(" Service Identities:")) + for _, svcid := range role.ServiceIdentities { + if len(svcid.Datacenters) > 0 { + buffer.WriteString(fmt.Sprintf(" %s (Datacenters: %s)\n", svcid.ServiceName, strings.Join(svcid.Datacenters, ", "))) + } else { + buffer.WriteString(fmt.Sprintf(" %s (Datacenters: all)\n", svcid.ServiceName)) + } + } + } + + return buffer.String() +} + +func newJSONFormatter(showMeta bool) Formatter { + return &jsonFormatter{showMeta} +} + +type jsonFormatter struct { + showMeta bool +} + +func (f *jsonFormatter) FormatRole(role *api.ACLRole) (string, error) { + b, err := json.MarshalIndent(role, "", " ") + if err != nil { + return "", fmt.Errorf("Failed to marshal role: %v", err) + } + return string(b), nil +} + +func (f *jsonFormatter) FormatRoleList(roles []*api.ACLRole) (string, error) { + b, err := json.MarshalIndent(roles, "", " ") + if err != nil { + return "", fmt.Errorf("Failed to marshal roles: %v", err) + } + return string(b), nil +} diff --git a/command/acl/role/list/role_list.go b/command/acl/role/list/role_list.go index 7b5d4a5eeb..9fc1bd1247 100644 --- a/command/acl/role/list/role_list.go +++ b/command/acl/role/list/role_list.go @@ -3,8 +3,9 @@ package rolelist import ( "flag" "fmt" + "strings" - "github.com/hashicorp/consul/command/acl" + "github.com/hashicorp/consul/command/acl/role" "github.com/hashicorp/consul/command/flags" "github.com/mitchellh/cli" ) @@ -22,12 +23,19 @@ type cmd struct { help string showMeta bool + format string } func (c *cmd) init() { c.flags = flag.NewFlagSet("", flag.ContinueOnError) c.flags.BoolVar(&c.showMeta, "meta", false, "Indicates that policy metadata such "+ "as the content hash and raft indices should be shown for each entry") + c.flags.StringVar( + &c.format, + "format", + role.PrettyFormat, + fmt.Sprintf("Output format {%s}", strings.Join(role.GetSupportedFormats(), "|")), + ) c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) @@ -53,8 +61,18 @@ func (c *cmd) Run(args []string) int { return 1 } - for _, role := range roles { - acl.PrintRoleListEntry(role, c.UI, c.showMeta) + formatter, err := role.NewFormatter(c.format, c.showMeta) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + out, err := formatter.FormatRoleList(roles) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + if out != "" { + c.UI.Info(out) } return 0 diff --git a/command/acl/role/list/role_list_test.go b/command/acl/role/list/role_list_test.go index cddefbb41e..25dfb14f64 100644 --- a/command/acl/role/list/role_list_test.go +++ b/command/acl/role/list/role_list_test.go @@ -1,6 +1,7 @@ package rolelist import ( + "encoding/json" "fmt" "os" "strings" @@ -11,6 +12,7 @@ import ( "github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/testrpc" "github.com/mitchellh/cli" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -78,3 +80,65 @@ func TestRoleListCommand(t *testing.T) { require.Contains(output, v) } } + +func TestRoleListCommand_JSON(t *testing.T) { + t.Parallel() + require := require.New(t) + + testDir := testutil.TempDir(t, "acl") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t, t.Name(), ` + primary_datacenter = "dc1" + acl { + enabled = true + tokens { + master = "root" + } + }`) + + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + ui := cli.NewMockUi() + cmd := New(ui) + + var roleIDs []string + + // Create a couple roles to list + client := a.Client() + svcids := []*api.ACLServiceIdentity{ + &api.ACLServiceIdentity{ServiceName: "fake"}, + } + for i := 0; i < 5; i++ { + name := fmt.Sprintf("test-role-%d", i) + + role, _, err := client.ACL().RoleCreate( + &api.ACLRole{Name: name, ServiceIdentities: svcids}, + &api.WriteOptions{Token: "root"}, + ) + roleIDs = append(roleIDs, role.ID) + + require.NoError(err) + } + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-format=json", + } + + code := cmd.Run(args) + require.Equal(code, 0) + require.Empty(ui.ErrorWriter.String()) + output := ui.OutputWriter.String() + + for i, v := range roleIDs { + require.Contains(output, fmt.Sprintf("test-role-%d", i)) + require.Contains(output, v) + } + + var jsonOutput json.RawMessage + err := json.Unmarshal([]byte(output), &jsonOutput) + assert.NoError(t, err) +} diff --git a/command/acl/role/read/role_read.go b/command/acl/role/read/role_read.go index dd26d67693..8b77717d7f 100644 --- a/command/acl/role/read/role_read.go +++ b/command/acl/role/read/role_read.go @@ -3,9 +3,11 @@ package roleread import ( "flag" "fmt" + "strings" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/command/acl" + "github.com/hashicorp/consul/command/acl/role" "github.com/hashicorp/consul/command/flags" "github.com/mitchellh/cli" ) @@ -25,6 +27,7 @@ type cmd struct { roleID string roleName string showMeta bool + format string } func (c *cmd) init() { @@ -35,6 +38,12 @@ func (c *cmd) init() { "It may be specified as a unique ID prefix but will error if the prefix "+ "matches multiple policy IDs") c.flags.StringVar(&c.roleName, "name", "", "The name of the role to read.") + c.flags.StringVar( + &c.format, + "format", + role.PrettyFormat, + fmt.Sprintf("Output format {%s}", strings.Join(role.GetSupportedFormats(), "|")), + ) c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ServerFlags()) @@ -58,7 +67,7 @@ func (c *cmd) Run(args []string) int { return 1 } - var role *api.ACLRole + var r *api.ACLRole if c.roleID != "" { roleID, err := acl.GetRoleIDFromPartial(client, c.roleID) @@ -66,27 +75,40 @@ func (c *cmd) Run(args []string) int { c.UI.Error(fmt.Sprintf("Error determining role ID: %v", err)) return 1 } - role, _, err = client.ACL().RoleRead(roleID, nil) + r, _, err = client.ACL().RoleRead(roleID, nil) if err != nil { c.UI.Error(fmt.Sprintf("Error reading role %q: %v", roleID, err)) return 1 - } else if role == nil { + } else if r == nil { c.UI.Error(fmt.Sprintf("Role not found with ID %q", roleID)) return 1 } } else { - role, _, err = client.ACL().RoleReadByName(c.roleName, nil) + r, _, err = client.ACL().RoleReadByName(c.roleName, nil) if err != nil { c.UI.Error(fmt.Sprintf("Error reading role %q: %v", c.roleName, err)) return 1 - } else if role == nil { + } else if r == nil { c.UI.Error(fmt.Sprintf("Role not found with name %q", c.roleName)) return 1 } } - acl.PrintRole(role, c.UI, c.showMeta) + formatter, err := role.NewFormatter(c.format, c.showMeta) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + out, err := formatter.FormatRole(r) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + if out != "" { + c.UI.Info(out) + } + return 0 } diff --git a/command/acl/role/read/role_read_test.go b/command/acl/role/read/role_read_test.go index ba2c2dd988..6ae07eac1d 100644 --- a/command/acl/role/read/role_read_test.go +++ b/command/acl/role/read/role_read_test.go @@ -1,6 +1,7 @@ package roleread import ( + "encoding/json" "fmt" "os" "strings" @@ -12,6 +13,7 @@ import ( "github.com/hashicorp/consul/testrpc" "github.com/hashicorp/go-uuid" "github.com/mitchellh/cli" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -189,3 +191,62 @@ func TestRoleReadCommand(t *testing.T) { require.Contains(t, output, role.ID) }) } + +func TestRoleReadCommand_JSON(t *testing.T) { + t.Parallel() + + testDir := testutil.TempDir(t, "acl") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t, t.Name(), ` + primary_datacenter = "dc1" + acl { + enabled = true + tokens { + master = "root" + } + }`) + + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + client := a.Client() + + t.Run("read by id", func(t *testing.T) { + // create a role + role, _, err := client.ACL().RoleCreate( + &api.ACLRole{ + Name: "test-role-by-id", + ServiceIdentities: []*api.ACLServiceIdentity{ + &api.ACLServiceIdentity{ + ServiceName: "fake", + }, + }, + }, + &api.WriteOptions{Token: "root"}, + ) + require.NoError(t, err) + + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-id=" + role.ID, + "-format=json", + } + + code := cmd.Run(args) + require.Equal(t, code, 0) + require.Empty(t, ui.ErrorWriter.String()) + + output := ui.OutputWriter.String() + require.Contains(t, output, fmt.Sprintf("test-role")) + require.Contains(t, output, role.ID) + + var jsonOutput json.RawMessage + err = json.Unmarshal([]byte(output), &jsonOutput) + assert.NoError(t, err) + }) +} diff --git a/command/acl/role/update/role_update.go b/command/acl/role/update/role_update.go index fbd739aca1..a26858e29c 100644 --- a/command/acl/role/update/role_update.go +++ b/command/acl/role/update/role_update.go @@ -3,9 +3,11 @@ package roleupdate import ( "flag" "fmt" + "strings" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/command/acl" + "github.com/hashicorp/consul/command/acl/role" "github.com/hashicorp/consul/command/flags" "github.com/mitchellh/cli" ) @@ -31,6 +33,7 @@ type cmd struct { noMerge bool showMeta bool + format string } func (c *cmd) init() { @@ -52,6 +55,12 @@ func (c *cmd) init() { c.flags.BoolVar(&c.noMerge, "no-merge", false, "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.") + c.flags.StringVar( + &c.format, + "format", + role.PrettyFormat, + fmt.Sprintf("Output format {%s}", strings.Join(role.GetSupportedFormats(), "|")), + ) c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) @@ -98,9 +107,9 @@ func (c *cmd) Run(args []string) int { return 1 } - var role *api.ACLRole + var r *api.ACLRole if c.noMerge { - role = &api.ACLRole{ + r = &api.ACLRole{ ID: c.roleID, Name: c.name, Description: c.description, @@ -110,7 +119,7 @@ func (c *cmd) Run(args []string) int { for _, policyName := range c.policyNames { // We could resolve names to IDs here but there isn't any reason // why its would be better than allowing the agent to do it. - role.Policies = append(role.Policies, &api.ACLRolePolicyLink{Name: policyName}) + r.Policies = append(r.Policies, &api.ACLRolePolicyLink{Name: policyName}) } for _, policyID := range c.policyIDs { @@ -119,21 +128,21 @@ func (c *cmd) Run(args []string) int { c.UI.Error(fmt.Sprintf("Error resolving policy ID %s: %v", policyID, err)) return 1 } - role.Policies = append(role.Policies, &api.ACLRolePolicyLink{ID: policyID}) + r.Policies = append(r.Policies, &api.ACLRolePolicyLink{ID: policyID}) } } else { - role = currentRole + r = currentRole if c.name != "" { - role.Name = c.name + r.Name = c.name } if c.description != "" { - role.Description = c.description + r.Description = c.description } for _, policyName := range c.policyNames { found := false - for _, link := range role.Policies { + for _, link := range r.Policies { if link.Name == policyName { found = true break @@ -144,7 +153,7 @@ func (c *cmd) Run(args []string) int { // We could resolve names to IDs here but there isn't any // reason why its would be better than allowing the agent to do // it. - role.Policies = append(role.Policies, &api.ACLRolePolicyLink{Name: policyName}) + r.Policies = append(r.Policies, &api.ACLRolePolicyLink{Name: policyName}) } } @@ -156,7 +165,7 @@ func (c *cmd) Run(args []string) int { } found := false - for _, link := range role.Policies { + for _, link := range r.Policies { if link.ID == policyID { found = true break @@ -164,13 +173,13 @@ func (c *cmd) Run(args []string) int { } if !found { - role.Policies = append(role.Policies, &api.ACLRolePolicyLink{ID: policyID}) + r.Policies = append(r.Policies, &api.ACLRolePolicyLink{ID: policyID}) } } for _, svcid := range parsedServiceIdents { found := -1 - for i, link := range role.ServiceIdentities { + for i, link := range r.ServiceIdentities { if link.ServiceName == svcid.ServiceName { found = i break @@ -178,21 +187,33 @@ func (c *cmd) Run(args []string) int { } if found != -1 { - role.ServiceIdentities[found] = svcid + r.ServiceIdentities[found] = svcid } else { - role.ServiceIdentities = append(role.ServiceIdentities, svcid) + r.ServiceIdentities = append(r.ServiceIdentities, svcid) } } } - role, _, err = client.ACL().RoleUpdate(role, nil) + r, _, err = client.ACL().RoleUpdate(r, nil) if err != nil { c.UI.Error(fmt.Sprintf("Error updating role %q: %v", roleID, err)) return 1 } - c.UI.Info(fmt.Sprintf("Role updated successfully")) - acl.PrintRole(role, c.UI, c.showMeta) + formatter, err := role.NewFormatter(c.format, c.showMeta) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + out, err := formatter.FormatRole(r) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + if out != "" { + c.UI.Info(out) + } + return 0 } diff --git a/command/acl/role/update/role_update_test.go b/command/acl/role/update/role_update_test.go index 56a70e669d..17b6905c2b 100644 --- a/command/acl/role/update/role_update_test.go +++ b/command/acl/role/update/role_update_test.go @@ -1,6 +1,7 @@ package roleupdate import ( + "encoding/json" "os" "strings" "testing" @@ -10,6 +11,7 @@ import ( "github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/testrpc" "github.com/mitchellh/cli" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" uuid "github.com/hashicorp/go-uuid" @@ -192,6 +194,88 @@ func TestRoleUpdateCommand(t *testing.T) { }) } +func TestRoleUpdateCommand_JSON(t *testing.T) { + t.Parallel() + + testDir := testutil.TempDir(t, "acl") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t, t.Name(), ` + primary_datacenter = "dc1" + acl { + enabled = true + tokens { + master = "root" + } + }`) + + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + client := a.Client() + + // Create policy + policy1, _, err := client.ACL().PolicyCreate( + &api.ACLPolicy{Name: "test-policy1"}, + &api.WriteOptions{Token: "root"}, + ) + require.NoError(t, err) + + role, _, err := client.ACL().RoleCreate( + &api.ACLRole{ + Name: "test-role", + ServiceIdentities: []*api.ACLServiceIdentity{ + &api.ACLServiceIdentity{ + ServiceName: "fake", + }, + }, + }, + &api.WriteOptions{Token: "root"}, + ) + require.NoError(t, err) + + t.Run("update a role that does not exist", func(t *testing.T) { + fakeID, err := uuid.GenerateUUID() + require.NoError(t, err) + + ui := cli.NewMockUi() + cmd := New(ui) + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-id=" + fakeID, + "-token=root", + "-policy-name=" + policy1.Name, + "-description=test role edited", + "-format=json", + } + + code := cmd.Run(args) + require.Equal(t, code, 1) + require.Contains(t, ui.ErrorWriter.String(), "Role not found with ID") + }) + + t.Run("update with policy by name", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-id=" + role.ID, + "-token=root", + "-policy-name=" + policy1.Name, + "-description=test role edited", + "-format=json", + } + + code := cmd.Run(args) + require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String()) + require.Empty(t, ui.ErrorWriter.String()) + + var jsonOutput json.RawMessage + err := json.Unmarshal([]byte(ui.OutputWriter.String()), &jsonOutput) + assert.NoError(t, err) + }) +} + func TestRoleUpdateCommand_noMerge(t *testing.T) { t.Parallel() diff --git a/command/acl/token/clone/token_clone.go b/command/acl/token/clone/token_clone.go index c4816d6381..f08644f772 100644 --- a/command/acl/token/clone/token_clone.go +++ b/command/acl/token/clone/token_clone.go @@ -3,8 +3,10 @@ package tokenclone import ( "flag" "fmt" + "strings" "github.com/hashicorp/consul/command/acl" + "github.com/hashicorp/consul/command/acl/token" "github.com/hashicorp/consul/command/flags" "github.com/mitchellh/cli" ) @@ -23,6 +25,7 @@ type cmd struct { tokenID string description string + format string } func (c *cmd) init() { @@ -32,6 +35,12 @@ func (c *cmd) init() { "matches multiple token Accessor IDs. The special value of 'anonymous' may "+ "be provided instead of the anonymous tokens accessor ID") c.flags.StringVar(&c.description, "description", "", "A description of the new cloned token") + c.flags.StringVar( + &c.format, + "format", + token.PrettyFormat, + fmt.Sprintf("Output format {%s}", strings.Join(token.GetSupportedFormats(), "|")), + ) c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ServerFlags()) @@ -61,14 +70,26 @@ func (c *cmd) Run(args []string) int { return 1 } - token, _, err := client.ACL().TokenClone(tokenID, c.description, nil) + t, _, err := client.ACL().TokenClone(tokenID, c.description, nil) if err != nil { c.UI.Error(fmt.Sprintf("Error cloning token: %v", err)) return 1 } - c.UI.Info("Token cloned successfully.") - acl.PrintToken(token, c.UI, false) + formatter, err := token.NewFormatter(c.format, false) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + out, err := formatter.FormatToken(t) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + if out != "" { + c.UI.Info(out) + } + return 0 } diff --git a/command/acl/token/clone/token_clone_test.go b/command/acl/token/clone/token_clone_test.go index f1f458ef9d..ebfe91d02f 100644 --- a/command/acl/token/clone/token_clone_test.go +++ b/command/acl/token/clone/token_clone_test.go @@ -1,6 +1,7 @@ package tokenclone import ( + "encoding/json" "os" "regexp" "strconv" @@ -12,13 +13,13 @@ import ( "github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/testrpc" "github.com/mitchellh/cli" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func parseCloneOutput(t *testing.T, output string) *api.ACLToken { // This will only work for non-legacy tokens - re := regexp.MustCompile("Token cloned successfully.\n" + - "AccessorID: ([a-zA-Z0-9\\-]{36})\n" + + re := regexp.MustCompile("AccessorID: ([a-zA-Z0-9\\-]{36})\n" + "SecretID: ([a-zA-Z0-9\\-]{36})\n" + "(?:Namespace: default\n)?" + "Description: ([^\n]*)\n" + @@ -58,7 +59,7 @@ func TestTokenCloneCommand_noTabs(t *testing.T) { } } -func TestTokenCloneCommand(t *testing.T) { +func TestTokenCloneCommand_Pretty(t *testing.T) { t.Parallel() req := require.New(t) @@ -164,3 +165,84 @@ func TestTokenCloneCommand(t *testing.T) { req.Equal(cloned.Policies, apiToken.Policies) }) } + +func TestTokenCloneCommand_JSON(t *testing.T) { + t.Parallel() + req := require.New(t) + + testDir := testutil.TempDir(t, "acl") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t, t.Name(), ` + primary_datacenter = "dc1" + acl { + enabled = true + tokens { + master = "root" + } + }`) + + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + // Create a policy + client := a.Client() + + _, _, err := client.ACL().PolicyCreate( + &api.ACLPolicy{Name: "test-policy"}, + &api.WriteOptions{Token: "root"}, + ) + req.NoError(err) + + // create a token + token, _, err := client.ACL().TokenCreate( + &api.ACLToken{Description: "test", Policies: []*api.ACLTokenPolicyLink{&api.ACLTokenPolicyLink{Name: "test-policy"}}}, + &api.WriteOptions{Token: "root"}, + ) + req.NoError(err) + + // clone with description + t.Run("Description", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-id=" + token.AccessorID, + "-token=root", + "-description=test cloned", + "-format=json", + } + + code := cmd.Run(args) + req.Empty(ui.ErrorWriter.String()) + req.Equal(code, 0) + + output := ui.OutputWriter.String() + var jsonOutput json.RawMessage + err = json.Unmarshal([]byte(output), &jsonOutput) + assert.NoError(t, err) + }) + + // clone without description + t.Run("Without Description", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-id=" + token.AccessorID, + "-token=root", + "-format=json", + } + + code := cmd.Run(args) + req.Empty(ui.ErrorWriter.String()) + req.Equal(code, 0) + + output := ui.OutputWriter.String() + var jsonOutput json.RawMessage + err = json.Unmarshal([]byte(output), &jsonOutput) + assert.NoError(t, err) + }) +} diff --git a/command/acl/token/create/token_create.go b/command/acl/token/create/token_create.go index 603b4e25dc..03e04e5331 100644 --- a/command/acl/token/create/token_create.go +++ b/command/acl/token/create/token_create.go @@ -3,10 +3,12 @@ package tokencreate import ( "flag" "fmt" + "strings" "time" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/command/acl" + "github.com/hashicorp/consul/command/acl/token" "github.com/hashicorp/consul/command/flags" "github.com/mitchellh/cli" ) @@ -34,6 +36,7 @@ type cmd struct { expirationTTL time.Duration local bool showMeta bool + format string } func (c *cmd) init() { @@ -59,6 +62,12 @@ func (c *cmd) init() { "the SERVICENAME or SERVICENAME:DATACENTER1,DATACENTER2,...") c.flags.DurationVar(&c.expirationTTL, "expires-ttl", 0, "Duration of time this "+ "token should be valid for") + c.flags.StringVar( + &c.format, + "format", + token.PrettyFormat, + fmt.Sprintf("Output format {%s}", strings.Join(token.GetSupportedFormats(), "|")), + ) c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ServerFlags()) @@ -131,13 +140,26 @@ func (c *cmd) Run(args []string) int { newToken.Roles = append(newToken.Roles, &api.ACLTokenRoleLink{ID: roleID}) } - token, _, err := client.ACL().TokenCreate(newToken, nil) + t, _, err := client.ACL().TokenCreate(newToken, nil) if err != nil { c.UI.Error(fmt.Sprintf("Failed to create new token: %v", err)) return 1 } - acl.PrintToken(token, c.UI, c.showMeta) + formatter, err := token.NewFormatter(c.format, c.showMeta) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + out, err := formatter.FormatToken(t) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + if out != "" { + c.UI.Info(out) + } + return 0 } diff --git a/command/acl/token/create/token_create_test.go b/command/acl/token/create/token_create_test.go index c702a78d9a..a14f8e169e 100644 --- a/command/acl/token/create/token_create_test.go +++ b/command/acl/token/create/token_create_test.go @@ -1,6 +1,7 @@ package tokencreate import ( + "encoding/json" "os" "strings" "testing" @@ -21,7 +22,7 @@ func TestTokenCreateCommand_noTabs(t *testing.T) { } } -func TestTokenCreateCommand(t *testing.T) { +func TestTokenCreateCommand_Pretty(t *testing.T) { t.Parallel() require := require.New(t) @@ -111,3 +112,54 @@ func TestTokenCreateCommand(t *testing.T) { require.Equal("3a69a8d8-c4d4-485d-9b19-b5b61648ea0c", token.SecretID) } } + +func TestTokenCreateCommand_JSON(t *testing.T) { + t.Parallel() + require := require.New(t) + + testDir := testutil.TempDir(t, "acl") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t, t.Name(), ` + primary_datacenter = "dc1" + acl { + enabled = true + tokens { + master = "root" + } + }`) + + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + ui := cli.NewMockUi() + cmd := New(ui) + + // Create a policy + client := a.Client() + + policy, _, err := client.ACL().PolicyCreate( + &api.ACLPolicy{Name: "test-policy"}, + &api.WriteOptions{Token: "root"}, + ) + require.NoError(err) + + // create with policy by name + { + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-policy-name=" + policy.Name, + "-description=test token", + "-format=json", + } + + code := cmd.Run(args) + require.Equal(code, 0) + require.Empty(ui.ErrorWriter.String()) + + var jsonOutput json.RawMessage + err = json.Unmarshal([]byte(ui.OutputWriter.String()), &jsonOutput) + require.NoError(err, "token unmarshalling error") + } +} diff --git a/command/acl/token/formatter.go b/command/acl/token/formatter.go new file mode 100644 index 0000000000..7180a513b9 --- /dev/null +++ b/command/acl/token/formatter.go @@ -0,0 +1,181 @@ +package token + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" + + "github.com/hashicorp/consul/api" +) + +const ( + PrettyFormat string = "pretty" + JSONFormat string = "json" +) + +// Formatter defines methods provided by token command output formatter +type Formatter interface { + FormatToken(token *api.ACLToken) (string, error) + FormatTokenList(tokens []*api.ACLTokenListEntry) (string, error) +} + +// GetSupportedFormats returns supported formats +func GetSupportedFormats() []string { + return []string{PrettyFormat, JSONFormat} +} + +// NewFormatter returns Formatter implementation +func NewFormatter(format string, showMeta bool) (formatter Formatter, err error) { + switch format { + case PrettyFormat: + formatter = newPrettyFormatter(showMeta) + case JSONFormat: + formatter = newJSONFormatter(showMeta) + default: + err = fmt.Errorf("Unknown format: %s", format) + } + + return formatter, err +} + +func newPrettyFormatter(showMeta bool) Formatter { + return &prettyFormatter{showMeta} +} + +type prettyFormatter struct { + showMeta bool +} + +func (f *prettyFormatter) FormatToken(token *api.ACLToken) (string, error) { + var buffer bytes.Buffer + + buffer.WriteString(fmt.Sprintf("AccessorID: %s\n", token.AccessorID)) + buffer.WriteString(fmt.Sprintf("SecretID: %s\n", token.SecretID)) + if token.Namespace != "" { + buffer.WriteString(fmt.Sprintf("Namespace: %s\n", token.Namespace)) + } + buffer.WriteString(fmt.Sprintf("Description: %s\n", token.Description)) + buffer.WriteString(fmt.Sprintf("Local: %t\n", token.Local)) + buffer.WriteString(fmt.Sprintf("Create Time: %v\n", token.CreateTime)) + if token.ExpirationTime != nil && !token.ExpirationTime.IsZero() { + buffer.WriteString(fmt.Sprintf("Expiration Time: %v\n", *token.ExpirationTime)) + } + if f.showMeta { + buffer.WriteString(fmt.Sprintf("Hash: %x\n", token.Hash)) + buffer.WriteString(fmt.Sprintf("Create Index: %d\n", token.CreateIndex)) + buffer.WriteString(fmt.Sprintf("Modify Index: %d\n", token.ModifyIndex)) + } + if len(token.Policies) > 0 { + buffer.WriteString(fmt.Sprintln("Policies:")) + for _, policy := range token.Policies { + buffer.WriteString(fmt.Sprintf(" %s - %s\n", policy.ID, policy.Name)) + } + } + if len(token.Roles) > 0 { + buffer.WriteString(fmt.Sprintln("Roles:")) + for _, role := range token.Roles { + buffer.WriteString(fmt.Sprintf(" %s - %s\n", role.ID, role.Name)) + } + } + if len(token.ServiceIdentities) > 0 { + buffer.WriteString(fmt.Sprintln("Service Identities:")) + for _, svcid := range token.ServiceIdentities { + if len(svcid.Datacenters) > 0 { + buffer.WriteString(fmt.Sprintf(" %s (Datacenters: %s)\n", svcid.ServiceName, strings.Join(svcid.Datacenters, ", "))) + } else { + buffer.WriteString(fmt.Sprintf(" %s (Datacenters: all)\n", svcid.ServiceName)) + } + } + } + if token.Rules != "" { + buffer.WriteString(fmt.Sprintln("Rules:")) + buffer.WriteString(fmt.Sprintln(token.Rules)) + } + + return buffer.String(), nil +} + +func (f *prettyFormatter) FormatTokenList(tokens []*api.ACLTokenListEntry) (string, error) { + var buffer bytes.Buffer + + first := true + for _, token := range tokens { + if first { + first = false + } else { + buffer.WriteString("\n") + } + buffer.WriteString(f.formatTokenListEntry(token)) + } + + return buffer.String(), nil +} + +func (f *prettyFormatter) formatTokenListEntry(token *api.ACLTokenListEntry) string { + var buffer bytes.Buffer + + buffer.WriteString(fmt.Sprintf("AccessorID: %s\n", token.AccessorID)) + if token.Namespace != "" { + buffer.WriteString(fmt.Sprintf("Namespace: %s\n", token.Namespace)) + } + buffer.WriteString(fmt.Sprintf("Description: %s\n", token.Description)) + buffer.WriteString(fmt.Sprintf("Local: %t\n", token.Local)) + buffer.WriteString(fmt.Sprintf("Create Time: %v\n", token.CreateTime)) + if token.ExpirationTime != nil && !token.ExpirationTime.IsZero() { + buffer.WriteString(fmt.Sprintf("Expiration Time: %v\n", *token.ExpirationTime)) + } + buffer.WriteString(fmt.Sprintf("Legacy: %t\n", token.Legacy)) + if f.showMeta { + buffer.WriteString(fmt.Sprintf("Hash: %x\n", token.Hash)) + buffer.WriteString(fmt.Sprintf("Create Index: %d\n", token.CreateIndex)) + buffer.WriteString(fmt.Sprintf("Modify Index: %d\n", token.ModifyIndex)) + } + if len(token.Policies) > 0 { + buffer.WriteString(fmt.Sprintln("Policies:")) + for _, policy := range token.Policies { + buffer.WriteString(fmt.Sprintf(" %s - %s\n", policy.ID, policy.Name)) + } + } + if len(token.Roles) > 0 { + buffer.WriteString(fmt.Sprintln("Roles:")) + for _, role := range token.Roles { + buffer.WriteString(fmt.Sprintf(" %s - %s\n", role.ID, role.Name)) + } + } + if len(token.ServiceIdentities) > 0 { + buffer.WriteString(fmt.Sprintln("Service Identities:")) + for _, svcid := range token.ServiceIdentities { + if len(svcid.Datacenters) > 0 { + buffer.WriteString(fmt.Sprintf(" %s (Datacenters: %s)\n", svcid.ServiceName, strings.Join(svcid.Datacenters, ", "))) + } else { + buffer.WriteString(fmt.Sprintf(" %s (Datacenters: all)\n", svcid.ServiceName)) + } + } + } + return buffer.String() +} + +func newJSONFormatter(showMeta bool) Formatter { + return &jsonFormatter{showMeta} +} + +type jsonFormatter struct { + showMeta bool +} + +func (f *jsonFormatter) FormatTokenList(tokens []*api.ACLTokenListEntry) (string, error) { + b, err := json.MarshalIndent(tokens, "", " ") + if err != nil { + return "", fmt.Errorf("Failed to marshal tokens: %v", err) + } + return string(b), nil +} + +func (f *jsonFormatter) FormatToken(token *api.ACLToken) (string, error) { + b, err := json.MarshalIndent(token, "", " ") + if err != nil { + return "", fmt.Errorf("Failed to marshal token: %v", err) + } + return string(b), nil +} diff --git a/command/acl/token/list/token_list.go b/command/acl/token/list/token_list.go index 2ca5eb8000..c89340db63 100644 --- a/command/acl/token/list/token_list.go +++ b/command/acl/token/list/token_list.go @@ -3,8 +3,9 @@ package tokenlist import ( "flag" "fmt" + "strings" - "github.com/hashicorp/consul/command/acl" + "github.com/hashicorp/consul/command/acl/token" "github.com/hashicorp/consul/command/flags" "github.com/mitchellh/cli" ) @@ -22,12 +23,19 @@ type cmd struct { help string showMeta bool + format string } func (c *cmd) init() { c.flags = flag.NewFlagSet("", flag.ContinueOnError) c.flags.BoolVar(&c.showMeta, "meta", false, "Indicates that token metadata such "+ "as the content hash and Raft indices should be shown for each entry") + c.flags.StringVar( + &c.format, + "format", + token.PrettyFormat, + fmt.Sprintf("Output format {%s}", strings.Join(token.GetSupportedFormats(), "|")), + ) c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ServerFlags()) @@ -52,14 +60,18 @@ func (c *cmd) Run(args []string) int { return 1 } - first := true - for _, token := range tokens { - if first { - first = false - } else { - c.UI.Info("") - } - acl.PrintTokenListEntry(token, c.UI, c.showMeta) + formatter, err := token.NewFormatter(c.format, c.showMeta) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + out, err := formatter.FormatTokenList(tokens) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + if out != "" { + c.UI.Info(out) } return 0 diff --git a/command/acl/token/list/token_list_test.go b/command/acl/token/list/token_list_test.go index 1fad284b8c..764c9bbcf7 100644 --- a/command/acl/token/list/token_list_test.go +++ b/command/acl/token/list/token_list_test.go @@ -1,6 +1,7 @@ package tokenlist import ( + "encoding/json" "fmt" "os" "strings" @@ -12,6 +13,7 @@ import ( "github.com/hashicorp/consul/testrpc" "github.com/mitchellh/cli" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestTokenListCommand_noTabs(t *testing.T) { @@ -22,7 +24,7 @@ func TestTokenListCommand_noTabs(t *testing.T) { } } -func TestTokenListCommand(t *testing.T) { +func TestTokenListCommand_Pretty(t *testing.T) { t.Parallel() assert := assert.New(t) @@ -75,3 +77,56 @@ func TestTokenListCommand(t *testing.T) { assert.Contains(output, v) } } + +func TestTokenListCommand_JSON(t *testing.T) { + t.Parallel() + assert := assert.New(t) + + testDir := testutil.TempDir(t, "acl") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t, t.Name(), ` + primary_datacenter = "dc1" + acl { + enabled = true + tokens { + master = "root" + } + }`) + + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + ui := cli.NewMockUi() + cmd := New(ui) + + var tokenIds []string + + // Create a couple tokens to list + client := a.Client() + for i := 0; i < 5; i++ { + description := fmt.Sprintf("test token %d", i) + + token, _, err := client.ACL().TokenCreate( + &api.ACLToken{Description: description}, + &api.WriteOptions{Token: "root"}, + ) + tokenIds = append(tokenIds, token.AccessorID) + + assert.NoError(err) + } + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-format=json", + } + + code := cmd.Run(args) + assert.Equal(code, 0) + assert.Empty(ui.ErrorWriter.String()) + + var jsonOutput json.RawMessage + err := json.Unmarshal([]byte(ui.OutputWriter.String()), &jsonOutput) + require.NoError(t, err, "token unmarshalling error") +} diff --git a/command/acl/token/read/token_read.go b/command/acl/token/read/token_read.go index ce284dc2b1..377d0a3b9e 100644 --- a/command/acl/token/read/token_read.go +++ b/command/acl/token/read/token_read.go @@ -3,9 +3,11 @@ package tokenread import ( "flag" "fmt" + "strings" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/command/acl" + "github.com/hashicorp/consul/command/acl/token" "github.com/hashicorp/consul/command/flags" "github.com/mitchellh/cli" ) @@ -25,6 +27,7 @@ type cmd struct { tokenID string self bool showMeta bool + format string } func (c *cmd) init() { @@ -36,6 +39,12 @@ func (c *cmd) init() { c.flags.StringVar(&c.tokenID, "id", "", "The Accessor ID of the token to read. "+ "It may be specified as a unique ID prefix but will error if the prefix "+ "matches multiple token Accessor IDs") + c.flags.StringVar( + &c.format, + "format", + token.PrettyFormat, + fmt.Sprintf("Output format {%s}", strings.Join(token.GetSupportedFormats(), "|")), + ) c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ServerFlags()) @@ -59,7 +68,7 @@ func (c *cmd) Run(args []string) int { return 1 } - var token *api.ACLToken + var t *api.ACLToken if !c.self { tokenID, err := acl.GetTokenIDFromPartial(client, c.tokenID) if err != nil { @@ -67,20 +76,33 @@ func (c *cmd) Run(args []string) int { return 1 } - token, _, err = client.ACL().TokenRead(tokenID, nil) + t, _, err = client.ACL().TokenRead(tokenID, nil) if err != nil { c.UI.Error(fmt.Sprintf("Error reading token %q: %v", tokenID, err)) return 1 } } else { - token, _, err = client.ACL().TokenReadSelf(nil) + t, _, err = client.ACL().TokenReadSelf(nil) if err != nil { c.UI.Error(fmt.Sprintf("Error reading token: %v", err)) return 1 } } - acl.PrintToken(token, c.UI, c.showMeta) + formatter, err := token.NewFormatter(c.format, c.showMeta) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + out, err := formatter.FormatToken(t) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + if out != "" { + c.UI.Info(out) + } + return 0 } diff --git a/command/acl/token/read/token_read_test.go b/command/acl/token/read/token_read_test.go index 244f61c9e1..20a897a610 100644 --- a/command/acl/token/read/token_read_test.go +++ b/command/acl/token/read/token_read_test.go @@ -1,6 +1,7 @@ package tokenread import ( + "encoding/json" "fmt" "os" "strings" @@ -12,6 +13,7 @@ import ( "github.com/hashicorp/consul/testrpc" "github.com/mitchellh/cli" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestTokenReadCommand_noTabs(t *testing.T) { @@ -22,7 +24,7 @@ func TestTokenReadCommand_noTabs(t *testing.T) { } } -func TestTokenReadCommand(t *testing.T) { +func TestTokenReadCommand_Pretty(t *testing.T) { t.Parallel() assert := assert.New(t) @@ -68,3 +70,50 @@ func TestTokenReadCommand(t *testing.T) { assert.Contains(output, token.AccessorID) assert.Contains(output, token.SecretID) } + +func TestTokenReadCommand_JSON(t *testing.T) { + t.Parallel() + assert := assert.New(t) + + testDir := testutil.TempDir(t, "acl") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t, t.Name(), ` + primary_datacenter = "dc1" + acl { + enabled = true + tokens { + master = "root" + } + }`) + + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + ui := cli.NewMockUi() + cmd := New(ui) + + // Create a token + client := a.Client() + + token, _, err := client.ACL().TokenCreate( + &api.ACLToken{Description: "test"}, + &api.WriteOptions{Token: "root"}, + ) + assert.NoError(err) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-id=" + token.AccessorID, + "-format=json", + } + + code := cmd.Run(args) + assert.Equal(code, 0) + assert.Empty(ui.ErrorWriter.String()) + + var jsonOutput json.RawMessage + err = json.Unmarshal([]byte(ui.OutputWriter.String()), &jsonOutput) + require.NoError(t, err, "token unmarshalling error") +} diff --git a/command/acl/token/update/token_update.go b/command/acl/token/update/token_update.go index 841c6c284f..5e2c2224cd 100644 --- a/command/acl/token/update/token_update.go +++ b/command/acl/token/update/token_update.go @@ -3,9 +3,11 @@ package tokenupdate import ( "flag" "fmt" + "strings" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/command/acl" + "github.com/hashicorp/consul/command/acl/token" "github.com/hashicorp/consul/command/flags" "github.com/mitchellh/cli" ) @@ -34,6 +36,7 @@ type cmd struct { mergeServiceIdents bool showMeta bool upgradeLegacy bool + format string } func (c *cmd) init() { @@ -66,6 +69,12 @@ func (c *cmd) init() { "token to behave exactly like a new token but keep the same Secret.\n"+ "WARNING: you must ensure that the new policy or policies specified grant "+ "equivalent or appropriate access for the existing clients using this token.") + c.flags.StringVar( + &c.format, + "format", + token.PrettyFormat, + fmt.Sprintf("Output format {%s}", strings.Join(token.GetSupportedFormats(), "|")), + ) c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) @@ -96,14 +105,14 @@ func (c *cmd) Run(args []string) int { return 1 } - token, _, err := client.ACL().TokenRead(tokenID, nil) + t, _, err := client.ACL().TokenRead(tokenID, nil) if err != nil { c.UI.Error(fmt.Sprintf("Error when retrieving current token: %v", err)) return 1 } if c.upgradeLegacy { - if token.Rules == "" { + if t.Rules == "" { // This is just for convenience it should actually be harmless to allow it // to go through anyway. c.UI.Error(fmt.Sprintf("Can't use -upgrade-legacy on a non-legacy token")) @@ -111,7 +120,7 @@ func (c *cmd) Run(args []string) int { } // Reset the rules to nothing forcing this to be updated as a non-legacy // token but with same secret. - token.Rules = "" + t.Rules = "" } if c.description != "" { @@ -121,7 +130,7 @@ func (c *cmd) Run(args []string) int { // manually giving the new description. If it's a real issue we can always // add another explicit `-remove-description` flag but it feels like an edge // case that's not going to be critical to anyone. - token.Description = c.description + t.Description = c.description } parsedServiceIdents, err := acl.ExtractServiceIdentities(c.serviceIdents) @@ -133,7 +142,7 @@ func (c *cmd) Run(args []string) int { if c.mergePolicies { for _, policyName := range c.policyNames { found := false - for _, link := range token.Policies { + for _, link := range t.Policies { if link.Name == policyName { found = true break @@ -143,7 +152,7 @@ func (c *cmd) Run(args []string) int { if !found { // We could resolve names to IDs here but there isn't any reason why its would be better // than allowing the agent to do it. - token.Policies = append(token.Policies, &api.ACLTokenPolicyLink{Name: policyName}) + t.Policies = append(t.Policies, &api.ACLTokenPolicyLink{Name: policyName}) } } @@ -155,7 +164,7 @@ func (c *cmd) Run(args []string) int { } found := false - for _, link := range token.Policies { + for _, link := range t.Policies { if link.ID == policyID { found = true break @@ -163,16 +172,16 @@ func (c *cmd) Run(args []string) int { } if !found { - token.Policies = append(token.Policies, &api.ACLTokenPolicyLink{ID: policyID}) + t.Policies = append(t.Policies, &api.ACLTokenPolicyLink{ID: policyID}) } } } else { - token.Policies = nil + t.Policies = nil for _, policyName := range c.policyNames { // We could resolve names to IDs here but there isn't any reason why its would be better // than allowing the agent to do it. - token.Policies = append(token.Policies, &api.ACLTokenPolicyLink{Name: policyName}) + t.Policies = append(t.Policies, &api.ACLTokenPolicyLink{Name: policyName}) } for _, policyID := range c.policyIDs { @@ -181,14 +190,14 @@ func (c *cmd) Run(args []string) int { c.UI.Error(fmt.Sprintf("Error resolving policy ID %s: %v", policyID, err)) return 1 } - token.Policies = append(token.Policies, &api.ACLTokenPolicyLink{ID: policyID}) + t.Policies = append(t.Policies, &api.ACLTokenPolicyLink{ID: policyID}) } } if c.mergeRoles { for _, roleName := range c.roleNames { found := false - for _, link := range token.Roles { + for _, link := range t.Roles { if link.Name == roleName { found = true break @@ -198,7 +207,7 @@ func (c *cmd) Run(args []string) int { if !found { // We could resolve names to IDs here but there isn't any reason why its would be better // than allowing the agent to do it. - token.Roles = append(token.Roles, &api.ACLTokenRoleLink{Name: roleName}) + t.Roles = append(t.Roles, &api.ACLTokenRoleLink{Name: roleName}) } } @@ -210,7 +219,7 @@ func (c *cmd) Run(args []string) int { } found := false - for _, link := range token.Roles { + for _, link := range t.Roles { if link.ID == roleID { found = true break @@ -218,16 +227,16 @@ func (c *cmd) Run(args []string) int { } if !found { - token.Roles = append(token.Roles, &api.ACLTokenRoleLink{Name: roleID}) + t.Roles = append(t.Roles, &api.ACLTokenRoleLink{Name: roleID}) } } } else { - token.Roles = nil + t.Roles = nil for _, roleName := range c.roleNames { // We could resolve names to IDs here but there isn't any reason why its would be better // than allowing the agent to do it. - token.Roles = append(token.Roles, &api.ACLTokenRoleLink{Name: roleName}) + t.Roles = append(t.Roles, &api.ACLTokenRoleLink{Name: roleName}) } for _, roleID := range c.roleIDs { @@ -236,14 +245,14 @@ func (c *cmd) Run(args []string) int { c.UI.Error(fmt.Sprintf("Error resolving role ID %s: %v", roleID, err)) return 1 } - token.Roles = append(token.Roles, &api.ACLTokenRoleLink{ID: roleID}) + t.Roles = append(t.Roles, &api.ACLTokenRoleLink{ID: roleID}) } } if c.mergeServiceIdents { for _, svcid := range parsedServiceIdents { found := -1 - for i, link := range token.ServiceIdentities { + for i, link := range t.ServiceIdentities { if link.ServiceName == svcid.ServiceName { found = i break @@ -251,23 +260,35 @@ func (c *cmd) Run(args []string) int { } if found != -1 { - token.ServiceIdentities[found] = svcid + t.ServiceIdentities[found] = svcid } else { - token.ServiceIdentities = append(token.ServiceIdentities, svcid) + t.ServiceIdentities = append(t.ServiceIdentities, svcid) } } } else { - token.ServiceIdentities = parsedServiceIdents + t.ServiceIdentities = parsedServiceIdents } - token, _, err = client.ACL().TokenUpdate(token, nil) + t, _, err = client.ACL().TokenUpdate(t, nil) if err != nil { c.UI.Error(fmt.Sprintf("Failed to update token %s: %v", tokenID, err)) return 1 } - c.UI.Info("Token updated successfully.") - acl.PrintToken(token, c.UI, c.showMeta) + formatter, err := token.NewFormatter(c.format, c.showMeta) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + + out, err := formatter.FormatToken(t) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + if out != "" { + c.UI.Info(out) + } return 0 } diff --git a/command/acl/token/update/token_update_test.go b/command/acl/token/update/token_update_test.go index c01a2e6d9a..33f5c23446 100644 --- a/command/acl/token/update/token_update_test.go +++ b/command/acl/token/update/token_update_test.go @@ -1,6 +1,7 @@ package tokenupdate import ( + "encoding/json" "os" "strings" "testing" @@ -186,3 +187,63 @@ func TestTokenUpdateCommand(t *testing.T) { assert.Equal(legacyToken.SecretID, gotToken.SecretID) } } + +func TestTokenUpdateCommand_JSON(t *testing.T) { + t.Parallel() + assert := assert.New(t) + // Alias because we need to access require package in Retry below + req := require.New(t) + + testDir := testutil.TempDir(t, "acl") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t, t.Name(), ` + primary_datacenter = "dc1" + acl { + enabled = true + tokens { + master = "root" + } + }`) + + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + ui := cli.NewMockUi() + + // Create a policy + client := a.Client() + + policy, _, err := client.ACL().PolicyCreate( + &api.ACLPolicy{Name: "test-policy"}, + &api.WriteOptions{Token: "root"}, + ) + req.NoError(err) + + // create a token + token, _, err := client.ACL().TokenCreate( + &api.ACLToken{Description: "test"}, + &api.WriteOptions{Token: "root"}, + ) + req.NoError(err) + + t.Run("update with policy by name", func(t *testing.T) { + cmd := New(ui) + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-id=" + token.AccessorID, + "-token=root", + "-policy-name=" + policy.Name, + "-description=test token", + "-format=json", + } + + code := cmd.Run(args) + assert.Equal(code, 0) + assert.Empty(ui.ErrorWriter.String()) + + var jsonOutput json.RawMessage + err := json.Unmarshal([]byte(ui.OutputWriter.String()), &jsonOutput) + require.NoError(t, err, "token unmarshalling error") + }) +} diff --git a/website/source/docs/commands/acl/auth-method/create.html.md.erb b/website/source/docs/commands/acl/auth-method/create.html.md.erb index b2ecbb656c..1ae30eb4d0 100644 --- a/website/source/docs/commands/acl/auth-method/create.html.md.erb +++ b/website/source/docs/commands/acl/auth-method/create.html.md.erb @@ -42,6 +42,8 @@ Usage: `consul acl auth-method create [options] [args]` used to access the TokenReview API to validate other JWTs during login. This flag is required for `-type=kubernetes`. +* `-format={pretty|json}` - Command output format. The default value is `pretty`. + #### Enterprise Options <%= partial "docs/commands/http_api_namespace_options" %> diff --git a/website/source/docs/commands/acl/auth-method/list.html.md.erb b/website/source/docs/commands/acl/auth-method/list.html.md.erb index 19eedccd7e..f7f2bbb248 100644 --- a/website/source/docs/commands/acl/auth-method/list.html.md.erb +++ b/website/source/docs/commands/acl/auth-method/list.html.md.erb @@ -23,6 +23,8 @@ Usage: `consul acl auth-method list` * `-meta` - Indicates that auth method metadata such as the raft indices should be shown for each entry. + +* `-format={pretty|json}` - Command output format. The default value is `pretty`. #### Enterprise Options diff --git a/website/source/docs/commands/acl/auth-method/read.html.md.erb b/website/source/docs/commands/acl/auth-method/read.html.md.erb index 7f23f283f4..33665ee2eb 100644 --- a/website/source/docs/commands/acl/auth-method/read.html.md.erb +++ b/website/source/docs/commands/acl/auth-method/read.html.md.erb @@ -26,6 +26,8 @@ Usage: `consul acl auth-method read [options] [args]` * `-name=` - The name of the auth method to read. +* `-format={pretty|json}` - Command output format. The default value is `pretty`. + #### Enterprise Options <%= partial "docs/commands/http_api_namespace_options" %> diff --git a/website/source/docs/commands/acl/auth-method/update.html.md.erb b/website/source/docs/commands/acl/auth-method/update.html.md.erb index 825b9a9e9f..74bfa0d5ff 100644 --- a/website/source/docs/commands/acl/auth-method/update.html.md.erb +++ b/website/source/docs/commands/acl/auth-method/update.html.md.erb @@ -46,6 +46,8 @@ Usage: `consul acl auth-method update [options] [args]` * `-no-merge` - Do not merge the current auth method information with what is provided to the command. Instead overwrite all fields with the exception of the auth method ID which is immutable. + +* `-format={pretty|json}` - Command output format. The default value is `pretty`. #### Enterprise Options diff --git a/website/source/docs/commands/acl/binding-rule/create.html.md.erb b/website/source/docs/commands/acl/binding-rule/create.html.md.erb index b791f1f765..006b966acb 100644 --- a/website/source/docs/commands/acl/binding-rule/create.html.md.erb +++ b/website/source/docs/commands/acl/binding-rule/create.html.md.erb @@ -37,6 +37,8 @@ Usage: `consul acl binding-rule create [options] [args]` * `-selector=` - Selector is an expression that matches against verified identity attributes returned from the auth method during login. +* `-format={pretty|json}` - Command output format. The default value is `pretty`. + #### Enterprise Options <%= partial "docs/commands/http_api_namespace_options" %> diff --git a/website/source/docs/commands/acl/binding-rule/list.html.md.erb b/website/source/docs/commands/acl/binding-rule/list.html.md.erb index f71ec55dd1..658d9a256b 100644 --- a/website/source/docs/commands/acl/binding-rule/list.html.md.erb +++ b/website/source/docs/commands/acl/binding-rule/list.html.md.erb @@ -24,6 +24,8 @@ Usage: `consul acl binding-rule list` * `-meta` - Indicates that binding rule metadata such as the raft indices should be shown for each entry. +* `-format={pretty|json}` - Command output format. The default value is `pretty`. + #### Enterprise Options <%= partial "docs/commands/http_api_namespace_options" %> diff --git a/website/source/docs/commands/acl/binding-rule/read.html.md.erb b/website/source/docs/commands/acl/binding-rule/read.html.md.erb index cbb57e90a9..1cf64eef77 100644 --- a/website/source/docs/commands/acl/binding-rule/read.html.md.erb +++ b/website/source/docs/commands/acl/binding-rule/read.html.md.erb @@ -27,6 +27,8 @@ Usage: `consul acl binding-rule read [options] [args]` * `-meta` - Indicates that binding rule metadata such as the raft indices should be shown for each entry. +* `-format={pretty|json}` - Command output format. The default value is `pretty`. + #### Enterprise Options <%= partial "docs/commands/http_api_namespace_options" %> diff --git a/website/source/docs/commands/acl/binding-rule/update.html.md.erb b/website/source/docs/commands/acl/binding-rule/update.html.md.erb index 7cae275bb8..f7042fa7e3 100644 --- a/website/source/docs/commands/acl/binding-rule/update.html.md.erb +++ b/website/source/docs/commands/acl/binding-rule/update.html.md.erb @@ -44,6 +44,8 @@ Usage: `consul acl binding-rule update [options] [args]` * `-selector=` - Selector is an expression that matches against verified identity attributes returned from the auth method during login. +* `-format={pretty|json}` - Command output format. The default value is `pretty`. + #### Enterprise Options <%= partial "docs/commands/http_api_namespace_options" %> diff --git a/website/source/docs/commands/acl/bootstrap.html.md.erb b/website/source/docs/commands/acl/bootstrap.html.md.erb index e8501d8880..d31a67229a 100644 --- a/website/source/docs/commands/acl/bootstrap.html.md.erb +++ b/website/source/docs/commands/acl/bootstrap.html.md.erb @@ -24,6 +24,9 @@ Usage: `consul acl bootstrap [options]` <%= partial "docs/commands/http_api_options_client" %> <%= partial "docs/commands/http_api_options_server" %> +#### Command Options +* `-format={pretty|json}` - Command output format. The default value is `pretty`. + The output looks like this: ```text diff --git a/website/source/docs/commands/acl/policy/create.html.md.erb b/website/source/docs/commands/acl/policy/create.html.md.erb index 89377c61d4..18a92acd21 100644 --- a/website/source/docs/commands/acl/policy/create.html.md.erb +++ b/website/source/docs/commands/acl/policy/create.html.md.erb @@ -55,6 +55,8 @@ Usage: `consul acl policy create [options] [args]` * `-valid-datacenter=` - Datacenter that the policy should be valid within. This flag may be specified multiple times. +* `-format={pretty|json}` - Command output format. The default value is `pretty`. + #### Enterprise Options <%= partial "docs/commands/http_api_namespace_options" %> 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 3b573e6daf..de67004bbf 100644 --- a/website/source/docs/commands/acl/policy/list.html.md.erb +++ b/website/source/docs/commands/acl/policy/list.html.md.erb @@ -24,6 +24,8 @@ Usage: `consul acl policy list` * `-meta` - Indicates that policy metadata such as the content hash and Raft indices should be shown for each entry. +* `-format={pretty|json}` - Command output format. The default value is `pretty`. + #### Enterprise Options <%= partial "docs/commands/http_api_namespace_options" %> diff --git a/website/source/docs/commands/acl/policy/read.html.md.erb b/website/source/docs/commands/acl/policy/read.html.md.erb index 732d7985b8..65118ce01d 100644 --- a/website/source/docs/commands/acl/policy/read.html.md.erb +++ b/website/source/docs/commands/acl/policy/read.html.md.erb @@ -29,6 +29,8 @@ Usage: `consul acl policy read [options] [args]` * `-name=` - The name of the policy to read. +* `-format={pretty|json}` - Command output format. The default value is `pretty`. + #### Enterprise Options <%= partial "docs/commands/http_api_namespace_options" %> diff --git a/website/source/docs/commands/acl/policy/update.html.md.erb b/website/source/docs/commands/acl/policy/update.html.md.erb index 23f0ed8b23..c002bcdf63 100644 --- a/website/source/docs/commands/acl/policy/update.html.md.erb +++ b/website/source/docs/commands/acl/policy/update.html.md.erb @@ -46,6 +46,8 @@ Usage: `consul acl policy update [options] [args]` * `-valid-datacenter=` - Datacenter that the policy should be valid within. This flag may be specified multiple times. +* `-format={pretty|json}` - Command output format. The default value is `pretty`. + #### Enterprise Options <%= partial "docs/commands/http_api_namespace_options" %> diff --git a/website/source/docs/commands/acl/role/create.html.md.erb b/website/source/docs/commands/acl/role/create.html.md.erb index bb96a50921..0f7eac554b 100644 --- a/website/source/docs/commands/acl/role/create.html.md.erb +++ b/website/source/docs/commands/acl/role/create.html.md.erb @@ -38,6 +38,8 @@ Usage: `consul acl role create [options] [args]` role. May be specified multiple times. Format is the `SERVICENAME` or `SERVICENAME:DATACENTER1,DATACENTER2,...` +* `-format={pretty|json}` - Command output format. The default value is `pretty`. + #### Enterprise Options <%= partial "docs/commands/http_api_namespace_options" %> diff --git a/website/source/docs/commands/acl/role/list.html.md.erb b/website/source/docs/commands/acl/role/list.html.md.erb index 84a9674366..36fca09ef4 100644 --- a/website/source/docs/commands/acl/role/list.html.md.erb +++ b/website/source/docs/commands/acl/role/list.html.md.erb @@ -24,6 +24,8 @@ Usage: `consul acl role list` * `-meta` - Indicates that role metadata such as the content hash and Raft indices should be shown for each entry. +* `-format={pretty|json}` - Command output format. The default value is `pretty`. + #### Enterprise Options <%= partial "docs/commands/http_api_namespace_options" %> diff --git a/website/source/docs/commands/acl/role/read.html.md.erb b/website/source/docs/commands/acl/role/read.html.md.erb index b913a67167..ad97fe4cef 100644 --- a/website/source/docs/commands/acl/role/read.html.md.erb +++ b/website/source/docs/commands/acl/role/read.html.md.erb @@ -29,6 +29,8 @@ Usage: `consul acl role read [options] [args]` * `-name=` - The name of the role to read. +* `-format={pretty|json}` - Command output format. The default value is `pretty`. + #### Enterprise Options <%= partial "docs/commands/http_api_namespace_options" %> diff --git a/website/source/docs/commands/acl/role/update.html.md.erb b/website/source/docs/commands/acl/role/update.html.md.erb index 20018e478e..2d13fe69f9 100644 --- a/website/source/docs/commands/acl/role/update.html.md.erb +++ b/website/source/docs/commands/acl/role/update.html.md.erb @@ -49,6 +49,8 @@ Usage: `consul acl role update [options] [args]` role. May be specified multiple times. Format is the `SERVICENAME` or `SERVICENAME:DATACENTER1,DATACENTER2,...` +* `-format={pretty|json}` - Command output format. The default value is `pretty`. + #### Enterprise Options <%= partial "docs/commands/http_api_namespace_options" %> diff --git a/website/source/docs/commands/acl/token/clone.html.md.erb b/website/source/docs/commands/acl/token/clone.html.md.erb index 3ec458c5ea..9872ad874c 100644 --- a/website/source/docs/commands/acl/token/clone.html.md.erb +++ b/website/source/docs/commands/acl/token/clone.html.md.erb @@ -28,6 +28,8 @@ Usage: `consul acl token clone [options]` Accessor IDs. The special value of 'anonymous' may be provided instead of the anonymous tokens accessor ID +* `-format={pretty|json}` - Command output format. The default value is `pretty`. + #### Enterprise Options <%= partial "docs/commands/http_api_namespace_options" %> diff --git a/website/source/docs/commands/acl/token/create.html.md.erb b/website/source/docs/commands/acl/token/create.html.md.erb index aadd4fb468..084bcaee51 100644 --- a/website/source/docs/commands/acl/token/create.html.md.erb +++ b/website/source/docs/commands/acl/token/create.html.md.erb @@ -52,6 +52,8 @@ Usage: `consul acl token create [options] [args]` **Note**: The SecretID is used to authorize operations against Consul and should be generated from an appropriate cryptographic source. +* `-format={pretty|json}` - Command output format. The default value is `pretty`. + #### Enterprise Options <%= partial "docs/commands/http_api_namespace_options" %> diff --git a/website/source/docs/commands/acl/token/list.html.md.erb b/website/source/docs/commands/acl/token/list.html.md.erb index 18d3855706..8d26a6c5b5 100644 --- a/website/source/docs/commands/acl/token/list.html.md.erb +++ b/website/source/docs/commands/acl/token/list.html.md.erb @@ -24,6 +24,8 @@ Usage: `consul acl token list` * `-meta` - Indicates that token metadata such as the content hash and Raft indices should be shown for each entry. +* `-format={pretty|json}` - Command output format. The default value is `pretty`. + #### Enterprise Options <%= partial "docs/commands/http_api_namespace_options" %> diff --git a/website/source/docs/commands/acl/token/read.html.md.erb b/website/source/docs/commands/acl/token/read.html.md.erb index 5cbee72ffe..d1bc816299 100644 --- a/website/source/docs/commands/acl/token/read.html.md.erb +++ b/website/source/docs/commands/acl/token/read.html.md.erb @@ -30,6 +30,8 @@ Usage: `consul acl token read [options] [args]` * `-self` - Indicates that the current HTTP token should be read by secret ID instead of expecting a -id option. +* `-format={pretty|json}` - Command output format. The default value is `pretty`. + #### Enterprise Options <%= partial "docs/commands/http_api_namespace_options" %> diff --git a/website/source/docs/commands/acl/token/update.html.md.erb b/website/source/docs/commands/acl/token/update.html.md.erb index 7c4a4ef86d..f563caf420 100644 --- a/website/source/docs/commands/acl/token/update.html.md.erb +++ b/website/source/docs/commands/acl/token/update.html.md.erb @@ -59,6 +59,8 @@ token migration](https://learn.hashicorp.com/consul/day-2-agent-authentication/migrate-acl-tokens) guide. +* `-format={pretty|json}` - Command output format. The default value is `pretty`. + #### Enterprise Options <%= partial "docs/commands/http_api_namespace_options" %>