From 8668c91b4cc6d94f253a098196bb7b062beb2519 Mon Sep 17 00:00:00 2001 From: majiayu000 <1835304752@qq.com> Date: Tue, 30 Dec 2025 14:01:48 +0800 Subject: [PATCH] Standardize output format options across all CLI commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement unified format flag handling across policy and secret commands to address issue #252. This change provides consistent format options with standardized aliases across all CLI commands. Changes: - Created shared format package with standardized format handling - Supports human/h/plain/p (default), json/j, and yaml/y formats - Added comprehensive tests for format parsing and validation - Updated policy commands to use shared format utilities - policy list and policy get now support yaml format - Maintained backward compatibility with existing formats - Added tests for yaml format and format aliases - Updated secret commands with standardized formats - secret get: Changed from plain/p to human/h/plain/p for consistency - secret list: Added format support (human, json, yaml) - secret metadata get: Added format support (human, json, yaml) - Updated tests to reflect new default format (human) All commands now support the same set of format options with consistent behavior, improving user experience and enabling reliable automation. Fixes #252 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: majiayu000 <1835304752@qq.com> --- app/spike/internal/cmd/format/format.go | 95 +++++++ app/spike/internal/cmd/format/format_test.go | 231 ++++++++++++++++++ app/spike/internal/cmd/policy/flag.go | 13 +- app/spike/internal/cmd/policy/format.go | 137 ++++++----- app/spike/internal/cmd/policy/format_test.go | 108 +++++++- app/spike/internal/cmd/secret/get.go | 80 +++--- app/spike/internal/cmd/secret/get_test.go | 4 +- app/spike/internal/cmd/secret/list.go | 73 ++++-- app/spike/internal/cmd/secret/metadata_get.go | 36 ++- 9 files changed, 639 insertions(+), 138 deletions(-) create mode 100644 app/spike/internal/cmd/format/format.go create mode 100644 app/spike/internal/cmd/format/format_test.go diff --git a/app/spike/internal/cmd/format/format.go b/app/spike/internal/cmd/format/format.go new file mode 100644 index 00000000..26bc6707 --- /dev/null +++ b/app/spike/internal/cmd/format/format.go @@ -0,0 +1,95 @@ +// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ +// \\\\\ Copyright 2024-present SPIKE contributors. +// \\\\\\\ SPDX-License-Identifier: Apache-2.0 + +package format + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// OutputFormat represents the supported output formats. +type OutputFormat int + +const ( + // Human represents human-readable output format. + Human OutputFormat = iota + // JSON represents JSON output format. + JSON + // YAML represents YAML output format. + YAML +) + +// String returns the canonical string representation of the format. +func (f OutputFormat) String() string { + switch f { + case Human: + return "human" + case JSON: + return "json" + case YAML: + return "yaml" + default: + return "unknown" + } +} + +// AddFormatFlag adds a standardized format flag to the given command. +// The format flag supports the following options: +// - human, h, plain, p: Human-readable, friendly output (default) +// - json, j: Valid JSON output (for scripting/parsing) +// - yaml, y: Valid YAML output (for scripting/parsing) +// +// Parameters: +// - cmd: The Cobra command to add the flag to +func AddFormatFlag(cmd *cobra.Command) { + cmd.Flags().StringP("format", "f", "human", + "Output format: human/h/plain/p, json/j, or yaml/y") +} + +// GetFormat retrieves and validates the format flag from the command. +// It supports multiple aliases for each format: +// - human, h, plain, p -> Human format +// - json, j -> JSON format +// - yaml, y -> YAML format +// +// Parameters: +// - cmd: The Cobra command containing the format flag +// +// Returns: +// - OutputFormat: The parsed output format +// - error: An error if the format is invalid +func GetFormat(cmd *cobra.Command) (OutputFormat, error) { + formatStr, _ := cmd.Flags().GetString("format") + return ParseFormat(formatStr) +} + +// ParseFormat parses a format string into an OutputFormat. +// It supports multiple aliases for each format: +// - human, h, plain, p, "" (empty) -> Human format +// - json, j -> JSON format +// - yaml, y -> YAML format +// +// Parameters: +// - formatStr: The format string to parse +// +// Returns: +// - OutputFormat: The parsed output format +// - error: An error if the format is invalid +func ParseFormat(formatStr string) (OutputFormat, error) { + switch formatStr { + case "human", "h", "plain", "p", "": + return Human, nil + case "json", "j": + return JSON, nil + case "yaml", "y": + return YAML, nil + default: + return Human, fmt.Errorf( + "invalid format '%s'. Valid formats are: "+ + "human/h/plain/p, json/j, yaml/y", + formatStr) + } +} diff --git a/app/spike/internal/cmd/format/format_test.go b/app/spike/internal/cmd/format/format_test.go new file mode 100644 index 00000000..55f072c9 --- /dev/null +++ b/app/spike/internal/cmd/format/format_test.go @@ -0,0 +1,231 @@ +// \\ SPIKE: Secure your secrets with SPIFFE. — https://spike.ist/ +// \\\\\ Copyright 2024-present SPIKE contributors. +// \\\\\\\ SPDX-License-Identifier: Apache-2.0 + +package format + +import ( + "testing" + + "github.com/spf13/cobra" +) + +func TestOutputFormat_String(t *testing.T) { + tests := []struct { + name string + format OutputFormat + want string + }{ + { + name: "Human format", + format: Human, + want: "human", + }, + { + name: "JSON format", + format: JSON, + want: "json", + }, + { + name: "YAML format", + format: YAML, + want: "yaml", + }, + { + name: "Unknown format", + format: OutputFormat(999), + want: "unknown", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.format.String(); got != tt.want { + t.Errorf("OutputFormat.String() = %v, want %v", + got, tt.want) + } + }) + } +} + +func TestParseFormat(t *testing.T) { + tests := []struct { + name string + formatStr string + want OutputFormat + wantErr bool + }{ + // Human format aliases + { + name: "Format 'human'", + formatStr: "human", + want: Human, + wantErr: false, + }, + { + name: "Format 'h'", + formatStr: "h", + want: Human, + wantErr: false, + }, + { + name: "Format 'plain'", + formatStr: "plain", + want: Human, + wantErr: false, + }, + { + name: "Format 'p'", + formatStr: "p", + want: Human, + wantErr: false, + }, + // JSON format aliases + { + name: "Format 'json'", + formatStr: "json", + want: JSON, + wantErr: false, + }, + { + name: "Format 'j'", + formatStr: "j", + want: JSON, + wantErr: false, + }, + // YAML format aliases + { + name: "Format 'yaml'", + formatStr: "yaml", + want: YAML, + wantErr: false, + }, + { + name: "Format 'y'", + formatStr: "y", + want: YAML, + wantErr: false, + }, + { + name: "Empty format defaults to human", + formatStr: "", + want: Human, + wantErr: false, + }, + // Invalid formats + { + name: "Invalid format", + formatStr: "invalid", + want: Human, + wantErr: true, + }, + { + name: "Case sensitive - JSON uppercase", + formatStr: "JSON", + want: Human, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseFormat(tt.formatStr) + if (err != nil) != tt.wantErr { + t.Errorf("ParseFormat() error = %v, wantErr %v", + err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ParseFormat() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAddFormatFlag(t *testing.T) { + cmd := &cobra.Command{ + Use: "test", + Short: "Test command", + } + + AddFormatFlag(cmd) + + // Test that the flag was added + flag := cmd.Flags().Lookup("format") + if flag == nil { + t.Fatal("AddFormatFlag() did not add format flag") + } + + // Test default value + if flag.DefValue != "human" { + t.Errorf("AddFormatFlag() default = %v, want %v", + flag.DefValue, "human") + } + + // Test shorthand + shortFlag := cmd.Flags().ShorthandLookup("f") + if shortFlag == nil { + t.Fatal("AddFormatFlag() did not add shorthand flag") + } +} + +func TestGetFormat(t *testing.T) { + tests := []struct { + name string + flagValue string + want OutputFormat + wantErr bool + }{ + { + name: "Get human format", + flagValue: "human", + want: Human, + wantErr: false, + }, + { + name: "Get json format", + flagValue: "json", + want: JSON, + wantErr: false, + }, + { + name: "Get yaml format", + flagValue: "yaml", + want: YAML, + wantErr: false, + }, + { + name: "Get format with alias 'p'", + flagValue: "p", + want: Human, + wantErr: false, + }, + { + name: "Get invalid format", + flagValue: "invalid", + want: Human, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := &cobra.Command{ + Use: "test", + Short: "Test command", + } + AddFormatFlag(cmd) + _ = cmd.Flags().Set("format", tt.flagValue) + + got, err := GetFormat(cmd) + if (err != nil) != tt.wantErr { + t.Errorf("GetFormat() error = %v, wantErr %v", + err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("GetFormat() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/app/spike/internal/cmd/policy/flag.go b/app/spike/internal/cmd/policy/flag.go index 18f40c35..fe8d19af 100644 --- a/app/spike/internal/cmd/policy/flag.go +++ b/app/spike/internal/cmd/policy/flag.go @@ -4,16 +4,19 @@ package policy -import "github.com/spf13/cobra" +import ( + "github.com/spf13/cobra" -// addFormatFlag adds a format flag to the given command to allow specifying -// the output format (human or JSON). + "github.com/spiffe/spike/app/spike/internal/cmd/format" +) + +// addFormatFlag adds a standardized format flag to the given command. +// Supports human/h/plain/p, json/j, and yaml/y formats. // // Parameters: // - cmd: The Cobra command to add the flag to func addFormatFlag(cmd *cobra.Command) { - cmd.Flags().String("format", "human", - "Output format: 'human' or 'json'") + format.AddFormatFlag(cmd) } // addNameFlag adds a name flag to the given command to allow specifying diff --git a/app/spike/internal/cmd/policy/format.go b/app/spike/internal/cmd/policy/format.go index e9a48abb..4b036163 100644 --- a/app/spike/internal/cmd/policy/format.go +++ b/app/spike/internal/cmd/policy/format.go @@ -12,12 +12,15 @@ import ( "github.com/spf13/cobra" "github.com/spiffe/spike-sdk-go/api/entity/data" + "gopkg.in/yaml.v3" + + "github.com/spiffe/spike/app/spike/internal/cmd/format" ) // formatPoliciesOutput formats the output of policy list items based on the -// format flag. It supports "human" (default) and "json" formats. For human -// format, it creates a readable tabular representation. For JSON format, it -// marshals the policies to indented JSON. +// format flag. It supports human/plain, json, and yaml formats. For human +// format, it creates a readable tabular representation. For JSON/YAML formats, +// it marshals the policies to the appropriate structured format. // // If the format flag is invalid, it returns an error message. // If the "policies" list is empty, it returns an appropriate message based on @@ -32,50 +35,55 @@ import ( func formatPoliciesOutput( cmd *cobra.Command, policies *[]data.PolicyListItem, ) string { - format, _ := cmd.Flags().GetString("format") - - // Validate format - if format != "" && format != "human" && format != "json" { - return fmt.Sprintf("Error: Invalid format '%s'."+ - " Valid formats are: human, json", format) + outputFormat, formatErr := format.GetFormat(cmd) + if formatErr != nil { + return fmt.Sprintf("Error: %v", formatErr) } // Check if "policies" is nil or empty isEmptyList := policies == nil || len(*policies) == 0 - if format == "json" { + switch outputFormat { + case format.JSON: if isEmptyList { - // Return an empty array instead of null for an empty list return "[]" } + output, marshalErr := json.MarshalIndent(policies, "", " ") + if marshalErr != nil { + return fmt.Sprintf("Error formatting output: %v", marshalErr) + } + return string(output) - output, err := json.MarshalIndent(policies, "", " ") - if err != nil { - return fmt.Sprintf("Error formatting output: %v", err) + case format.YAML: + if isEmptyList { + return "[]" + } + output, marshalErr := yaml.Marshal(policies) + if marshalErr != nil { + return fmt.Sprintf("Error formatting output: %v", marshalErr) } return string(output) - } - // Default human-readable format - if isEmptyList { - return "No policies found." - } + default: // format.Human + if isEmptyList { + return "No policies found." + } - var result strings.Builder - result.WriteString("POLICIES\n========\n\n") + var result strings.Builder + result.WriteString("POLICIES\n========\n\n") - for _, policy := range *policies { - result.WriteString(fmt.Sprintf("ID: %s\n", policy.ID)) - result.WriteString(fmt.Sprintf("Name: %s\n", policy.Name)) - result.WriteString("--------\n\n") - } + for _, policy := range *policies { + result.WriteString(fmt.Sprintf("ID: %s\n", policy.ID)) + result.WriteString(fmt.Sprintf("Name: %s\n", policy.Name)) + result.WriteString("--------\n\n") + } - return result.String() + return result.String() + } } // formatPolicy formats a single policy based on the format flag. -// It converts the policy to a slice and reuses the formatPoliciesOutput -// function for consistent formatting. +// It supports human/plain, json, and yaml formats. // // Parameters: // - cmd: The Cobra command containing the format flag @@ -84,51 +92,56 @@ func formatPoliciesOutput( // Returns: // - string: The formatted policy or error message func formatPolicy(cmd *cobra.Command, policy *data.Policy) string { - format, _ := cmd.Flags().GetString("format") - - // Validate format - if format != "" && format != "human" && format != "json" { - return fmt.Sprintf("Error: Invalid format '%s'. "+ - "Valid formats are: human, json", format) + outputFormat, formatErr := format.GetFormat(cmd) + if formatErr != nil { + return fmt.Sprintf("Error: %v", formatErr) } if policy == nil { return "No policy found." } - if format == "json" { - output, err := json.MarshalIndent(policy, "", " ") - if err != nil { - return fmt.Sprintf("Error formatting output: %v", err) + switch outputFormat { + case format.JSON: + output, marshalErr := json.MarshalIndent(policy, "", " ") + if marshalErr != nil { + return fmt.Sprintf("Error formatting output: %v", marshalErr) } return string(output) - } - // Human-readable format for a single policy: - var result strings.Builder - result.WriteString("POLICY DETAILS\n=============\n\n") + case format.YAML: + output, marshalErr := yaml.Marshal(policy) + if marshalErr != nil { + return fmt.Sprintf("Error formatting output: %v", marshalErr) + } + return string(output) - result.WriteString(fmt.Sprintf("ID: %s\n", policy.ID)) - result.WriteString(fmt.Sprintf("Name: %s\n", policy.Name)) - result.WriteString(fmt.Sprintf("SPIFFE ID Pattern: %s\n", - policy.SPIFFEIDPattern)) - result.WriteString(fmt.Sprintf("Path Pattern: %s\n", - policy.PathPattern)) + default: // format.Human + var result strings.Builder + result.WriteString("POLICY DETAILS\n=============\n\n") - perms := make([]string, 0, len(policy.Permissions)) - for _, p := range policy.Permissions { - perms = append(perms, string(p)) - } + result.WriteString(fmt.Sprintf("ID: %s\n", policy.ID)) + result.WriteString(fmt.Sprintf("Name: %s\n", policy.Name)) + result.WriteString(fmt.Sprintf("SPIFFE ID Pattern: %s\n", + policy.SPIFFEIDPattern)) + result.WriteString(fmt.Sprintf("Path Pattern: %s\n", + policy.PathPattern)) + + perms := make([]string, 0, len(policy.Permissions)) + for _, p := range policy.Permissions { + perms = append(perms, string(p)) + } - result.WriteString(fmt.Sprintf("Permissions: %s\n", - strings.Join(perms, ", "))) - result.WriteString(fmt.Sprintf("Created At: %s\n", - policy.CreatedAt.Format(time.RFC3339))) + result.WriteString(fmt.Sprintf("Permissions: %s\n", + strings.Join(perms, ", "))) + result.WriteString(fmt.Sprintf("Created At: %s\n", + policy.CreatedAt.Format(time.RFC3339))) - if !policy.UpdatedAt.IsZero() { - result.WriteString(fmt.Sprintf("Updated At: %s\n", - policy.UpdatedAt.Format(time.RFC3339))) - } + if !policy.UpdatedAt.IsZero() { + result.WriteString(fmt.Sprintf("Updated At: %s\n", + policy.UpdatedAt.Format(time.RFC3339))) + } - return result.String() + return result.String() + } } diff --git a/app/spike/internal/cmd/policy/format_test.go b/app/spike/internal/cmd/policy/format_test.go index f34d6d46..7a7771cb 100644 --- a/app/spike/internal/cmd/policy/format_test.go +++ b/app/spike/internal/cmd/policy/format_test.go @@ -79,7 +79,7 @@ func TestFormatPoliciesOutput_InvalidFormat(t *testing.T) { result := formatPoliciesOutput(cmd, policies) - if !strings.Contains(result, "Error: Invalid format") { + if !strings.Contains(result, "Error: invalid format") { t.Errorf("formatPoliciesOutput() should return error for invalid format") } if !strings.Contains(result, "xml") { @@ -166,12 +166,12 @@ func TestFormatPolicy_NilPolicy(t *testing.T) { } func TestFormatPolicy_InvalidFormat(t *testing.T) { - cmd := createTestCommandWithFormat("yaml") + cmd := createTestCommandWithFormat("xml") policy := &data.Policy{Name: "test"} result := formatPolicy(cmd, policy) - if !strings.Contains(result, "Error: Invalid format") { + if !strings.Contains(result, "Error: invalid format") { t.Error("formatPolicy() should return error for invalid format") } } @@ -279,3 +279,105 @@ func TestFormatPoliciesOutput_MultiplePolicies(t *testing.T) { t.Errorf("Expected at least 3 separators, got %d", separatorCount) } } + +func TestFormatPoliciesOutput_YAMLFormat(t *testing.T) { + policies := &[]data.PolicyListItem{ + { + ID: "123e4567-e89b-12d3-a456-426614174000", + Name: "test-policy", + }, + } + + tests := []struct { + name string + format string + }{ + {"yaml full name", "yaml"}, + {"yaml alias y", "y"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := createTestCommandWithFormat(tt.format) + result := formatPoliciesOutput(cmd, policies) + + // Check that output contains YAML-like content + if !strings.Contains(result, "id:") || + !strings.Contains(result, "name:") { + t.Errorf("YAML format should contain YAML fields") + } + }) + } +} + +func TestFormatPoliciesOutput_FormatAliases(t *testing.T) { + policies := &[]data.PolicyListItem{ + { + ID: "test-id", + Name: "test-name", + }, + } + + tests := []struct { + name string + format string + shouldContain string + shouldNotError bool + }{ + {"human alias h", "h", "POLICIES", true}, + {"human alias plain", "plain", "POLICIES", true}, + {"human alias p", "p", "POLICIES", true}, + {"json alias j", "j", `"id"`, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := createTestCommandWithFormat(tt.format) + result := formatPoliciesOutput(cmd, policies) + + if strings.Contains(result, "Error:") && tt.shouldNotError { + t.Errorf("Format alias %q should not produce error: %s", + tt.format, result) + } + + if !strings.Contains(result, tt.shouldContain) { + t.Errorf("Format %q output should contain %q, got: %s", + tt.format, tt.shouldContain, result) + } + }) + } +} + +func TestFormatPolicy_YAMLFormat(t *testing.T) { + createdAt := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC) + + policy := &data.Policy{ + ID: "123e4567-e89b-12d3-a456-426614174000", + Name: "test-policy", + SPIFFEIDPattern: "^spiffe://example\\.org/.*$", + PathPattern: "^secrets/.*$", + Permissions: []data.PolicyPermission{"read"}, + CreatedAt: createdAt, + } + + tests := []struct { + name string + format string + }{ + {"yaml full name", "yaml"}, + {"yaml alias y", "y"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := createTestCommandWithFormat(tt.format) + result := formatPolicy(cmd, policy) + + // Check that output contains YAML-like content + if !strings.Contains(result, "id:") || + !strings.Contains(result, "name:") { + t.Errorf("YAML format should contain YAML fields") + } + }) + } +} diff --git a/app/spike/internal/cmd/secret/get.go b/app/spike/internal/cmd/secret/get.go index 44294279..83a9f7e5 100644 --- a/app/spike/internal/cmd/secret/get.go +++ b/app/spike/internal/cmd/secret/get.go @@ -6,13 +6,13 @@ package secret import ( "encoding/json" - "slices" "github.com/spf13/cobra" "github.com/spiffe/go-spiffe/v2/workloadapi" spike "github.com/spiffe/spike-sdk-go/api" "gopkg.in/yaml.v3" + "github.com/spiffe/spike/app/spike/internal/cmd/format" "github.com/spiffe/spike/app/spike/internal/stdout" "github.com/spiffe/spike/app/spike/internal/trust" ) @@ -34,8 +34,8 @@ import ( // Flags: // - --version, -v (int): Specific version of the secret to retrieve // (default 0) where 0 represents the current version -// - --format, -f (string): Output format. Valid options: plain, p, yaml, y, -// json, j (default "plain") +// - --format, -f (string): Output format. Valid options: human/h/plain/p, +// json/j, yaml/y (default "human") // // Returns: // - *cobra.Command: Configured get command @@ -68,11 +68,10 @@ func newSecretGetCommand( path := args[0] version, _ := cmd.Flags().GetInt("version") - format, _ := cmd.Flags().GetString("format") - if !slices.Contains([]string{"plain", - "yaml", "json", "y", "p", "j"}, format) { - cmd.PrintErrf("Error: Invalid format: %s\n", format) + outputFormat, formatErr := format.GetFormat(cmd) + if formatErr != nil { + cmd.PrintErrf("Error: %v\n", formatErr) return } @@ -97,14 +96,19 @@ func newSecretGetCommand( } d := secret.Data + key := "" + if len(args) >= 2 { + key = args[1] + } - if format == "plain" || format == "p" { + // For human format, use plain key:value output + if outputFormat == format.Human { found := false for k, v := range d { - if len(args) < 2 || args[1] == "" { + if key == "" { cmd.Printf("%s: %s\n", k, v) found = true - } else if args[1] == k { + } else if key == k { cmd.Printf("%s\n", v) found = true break @@ -116,59 +120,41 @@ func newSecretGetCommand( return } - if len(args) < 2 || args[1] == "" { - if format == "yaml" || format == "y" { - b, marshalErr := yaml.Marshal(d) - if marshalErr != nil { - cmd.PrintErrf("Error: %v\n", marshalErr) - return - } - - cmd.Printf("%s\n", string(b)) + // For structured formats (JSON/YAML) + var dataToFormat interface{} + if key == "" { + dataToFormat = d + } else { + val, exists := d[key] + if !exists { + cmd.PrintErrln("Error: Key not found.") return } + dataToFormat = val + } - b, marshalErr := json.MarshalIndent(d, "", " ") + switch outputFormat { + case format.YAML: + b, marshalErr := yaml.Marshal(dataToFormat) if marshalErr != nil { cmd.PrintErrf("Error: %v\n", marshalErr) return } - cmd.Printf("%s\n", string(b)) - return - } - for k, v := range d { - if args[1] == k { - if format == "yaml" || format == "y" { - b, marshalErr := yaml.Marshal(v) - if marshalErr != nil { - cmd.PrintErrf("Error: %v\n", marshalErr) - return - } - - cmd.Printf("%s\n", string(b)) - return - } - - b, marshalErr := json.Marshal(v) - if marshalErr != nil { - cmd.PrintErrf("Error: %v\n", marshalErr) - return - } - - cmd.Printf("%s\n", string(b)) + case format.JSON: + b, marshalErr := json.MarshalIndent(dataToFormat, "", " ") + if marshalErr != nil { + cmd.PrintErrf("Error: %v\n", marshalErr) return } + cmd.Printf("%s\n", string(b)) } - - cmd.PrintErrln("Error: Key not found.") }, } getCmd.Flags().IntP("version", "v", 0, "Specific version to retrieve") - getCmd.Flags().StringP("format", "f", "plain", - "Format to use. Valid options: plain, p, yaml, y, json, j") + format.AddFormatFlag(getCmd) return getCmd } diff --git a/app/spike/internal/cmd/secret/get_test.go b/app/spike/internal/cmd/secret/get_test.go index f6bb1a24..56582777 100644 --- a/app/spike/internal/cmd/secret/get_test.go +++ b/app/spike/internal/cmd/secret/get_test.go @@ -107,8 +107,8 @@ func TestSecretGetCommandFlagDefaults(t *testing.T) { t.Fatal("Expected 'format' flag to be present") return } - if formatFlag.DefValue != "plain" { - t.Errorf("Expected format default to be 'plain', got '%s'", + if formatFlag.DefValue != "human" { + t.Errorf("Expected format default to be 'human', got '%s'", formatFlag.DefValue) } if formatFlag.Shorthand != "f" { diff --git a/app/spike/internal/cmd/secret/list.go b/app/spike/internal/cmd/secret/list.go index be86458e..1a746b55 100644 --- a/app/spike/internal/cmd/secret/list.go +++ b/app/spike/internal/cmd/secret/list.go @@ -5,10 +5,14 @@ package secret import ( + "encoding/json" + "github.com/spf13/cobra" "github.com/spiffe/go-spiffe/v2/workloadapi" spike "github.com/spiffe/spike-sdk-go/api" + "gopkg.in/yaml.v3" + "github.com/spiffe/spike/app/spike/internal/cmd/format" "github.com/spiffe/spike/app/spike/internal/stdout" "github.com/spiffe/spike/app/spike/internal/trust" ) @@ -28,15 +32,18 @@ import ( // // The command will: // 1. Make a network request to retrieve all available secret paths -// 2. Display the results in a formatted list -// 3. Show "No secrets found" if the system is empty +// 2. Display the results based on the --format flag +// 3. Show "No secrets found" or "[]" if the system is empty +// +// Flags: +// - --format, -f (string): Output format. Valid options: human/h/plain/p, +// json/j, yaml/y (default "human") // -// Output format: +// Example output (human format): // -// Secrets: -// - secret/path1 -// - secret/path2 -// - secret/path3 +// - secret/path1 +// - secret/path2 +// - secret/path3 // // Note: Requires an initialized SPIKE system and valid authentication func newSecretListCommand( @@ -53,27 +60,59 @@ func newSecretListCommand( return } + outputFormat, formatErr := format.GetFormat(cmd) + if formatErr != nil { + cmd.PrintErrf("Error: %v\n", formatErr) + return + } + api := spike.NewWithSource(source) keys, err := api.ListSecretKeys() if stdout.HandleAPIError(cmd, err) { return } - if keys == nil { - cmd.Println("No secrets found.") - return - } - if len(*keys) == 0 { - cmd.Println("No secrets found.") - return - } + isEmptyList := keys == nil || len(*keys) == 0 - for _, key := range *keys { - cmd.Printf("- %s\n", key) + switch outputFormat { + case format.JSON: + if isEmptyList { + cmd.Println("[]") + return + } + output, marshalErr := json.MarshalIndent(keys, "", " ") + if marshalErr != nil { + cmd.PrintErrf("Error formatting output: %v\n", marshalErr) + return + } + cmd.Println(string(output)) + + case format.YAML: + if isEmptyList { + cmd.Println("[]") + return + } + output, marshalErr := yaml.Marshal(keys) + if marshalErr != nil { + cmd.PrintErrf("Error formatting output: %v\n", marshalErr) + return + } + cmd.Print(string(output)) + + default: // format.Human + if isEmptyList { + cmd.Println("No secrets found.") + return + } + for _, key := range *keys { + cmd.Printf("- %s\n", key) + } } }, } + format.AddFormatFlag(listCmd) + return listCmd } diff --git a/app/spike/internal/cmd/secret/metadata_get.go b/app/spike/internal/cmd/secret/metadata_get.go index f802ecfb..d03505c2 100644 --- a/app/spike/internal/cmd/secret/metadata_get.go +++ b/app/spike/internal/cmd/secret/metadata_get.go @@ -5,10 +5,14 @@ package secret import ( + "encoding/json" + "github.com/spf13/cobra" "github.com/spiffe/go-spiffe/v2/workloadapi" spike "github.com/spiffe/spike-sdk-go/api" + "gopkg.in/yaml.v3" + "github.com/spiffe/spike/app/spike/internal/cmd/format" "github.com/spiffe/spike/app/spike/internal/stdout" "github.com/spiffe/spike/app/spike/internal/trust" ) @@ -29,6 +33,8 @@ import ( // Flags: // - --version, -v (int): Specific version of the secret to retrieve // (default 0) where 0 represents the current version +// - --format, -f (string): Output format. Valid options: human/h/plain/p, +// json/j, yaml/y (default "human") // // Returns: // - *cobra.Command: Configured get command @@ -36,7 +42,7 @@ import ( // The command will: // 1. Verify SPIKE initialization status via admin token // 2. Retrieve the secret metadata from the specified path and version -// 3. Display all metadata fields and secret versions +// 3. Display metadata based on the --format flag // // Error cases: // - SPIKE not initialized: Prompts user to run 'spike init' @@ -62,6 +68,12 @@ func newSecretMetadataGetCommand( return } + outputFormat, formatErr := format.GetFormat(cmd) + if formatErr != nil { + cmd.PrintErrf("Error: %v\n", formatErr) + return + } + api := spike.NewWithSource(source) path := args[0] @@ -77,11 +89,31 @@ func newSecretMetadataGetCommand( return } - printSecretResponse(cmd, secret) + switch outputFormat { + case format.JSON: + output, marshalErr := json.MarshalIndent(secret, "", " ") + if marshalErr != nil { + cmd.PrintErrf("Error formatting output: %v\n", marshalErr) + return + } + cmd.Println(string(output)) + + case format.YAML: + output, marshalErr := yaml.Marshal(secret) + if marshalErr != nil { + cmd.PrintErrf("Error formatting output: %v\n", marshalErr) + return + } + cmd.Print(string(output)) + + default: // format.Human + printSecretResponse(cmd, secret) + } }, } getCmd.Flags().IntP("version", "v", 0, "Specific version to retrieve") + format.AddFormatFlag(getCmd) cmd.AddCommand(getCmd)