diff --git a/otdfctl/cmd/profile.go b/otdfctl/cmd/profile.go index 5ee92a3293..883bfabcc4 100644 --- a/otdfctl/cmd/profile.go +++ b/otdfctl/cmd/profile.go @@ -3,7 +3,9 @@ package cmd import ( "errors" "fmt" + "os" "runtime" + "strconv" "strings" osprofiles "github.com/jrschumacher/go-osprofiles" @@ -26,6 +28,25 @@ const ( " If that still doesn't work, you can remove all profiles from the filesystem via the `delete-all` command." ) +type profileListOutput struct { + Store string `json:"store"` + Profiles []profileSummary `json:"profiles"` +} + +type profileSummary struct { + Name string `json:"name"` + IsDefault bool `json:"is_default"` +} + +type profileGetOutput struct { + Profile string `json:"profile"` + Endpoint string `json:"endpoint"` + IsDefault bool `json:"is_default"` + OutputFormat string `json:"output_format"` + AuthType string `json:"auth_type,omitempty"` + ClientID string `json:"client_id,omitempty"` +} + func newProfilerFromCLI(c *cli.Cli) *osprofiles.Profiler { driverType := getDriverTypeFromUser(c) profiler, err := profiles.NewProfiler(string(driverType)) @@ -104,15 +125,24 @@ var profileListCmd = &cobra.Command{ var sb strings.Builder fmt.Fprintf(&sb, "Listing profiles from %s\n", driverType) + out := profileListOutput{ + Store: string(driverType), + Profiles: []profileSummary{}, + } for _, p := range osprofiles.ListProfiles(profiler) { - if p == defaultProfile { + isDefault := p == defaultProfile + out.Profiles = append(out.Profiles, profileSummary{ + Name: p, + IsDefault: isDefault, + }) + if isDefault { fmt.Fprintf(&sb, "* %s\n", p) continue } fmt.Fprintf(&sb, " %s\n", p) } - c.ExitWithMessage(sb.String(), cli.ExitCodeSuccess) + c.ExitWith(sb.String(), out, cli.ExitCodeSuccess, os.Stdout) }, } @@ -130,27 +160,35 @@ var profileGetCmd = &cobra.Command{ cli.ExitWithError("Error loading profile store for profile "+profileName, err) } - isDefault := "false" - if profileStore.IsDefault() { - isDefault = "true" - } + isDefault := profileStore.IsDefault() var auth string + authType := "" + clientID := "" ac := profileStore.GetAuthCredentials() if ac.AuthType == profiles.AuthTypeClientCredentials { maskedSecret := "********" auth = "client-credentials (" + ac.ClientID + ", " + maskedSecret + ")" + authType = ac.AuthType + clientID = ac.ClientID } t := cli.NewTabular( []string{"Profile", profileStore.Name()}, []string{"Endpoint", profileStore.GetEndpoint()}, - []string{"Is default", isDefault}, + []string{"Is default", strconv.FormatBool(isDefault)}, []string{"Output format", profileStore.GetOutputFormat()}, []string{"Auth type", auth}, ) - c.ExitWithMessage(t.View(), cli.ExitCodeSuccess) + c.ExitWith(t.View(), profileGetOutput{ + Profile: profileStore.Name(), + Endpoint: profileStore.GetEndpoint(), + IsDefault: isDefault, + OutputFormat: profileStore.GetOutputFormat(), + AuthType: authType, + ClientID: clientID, + }, cli.ExitCodeSuccess, os.Stdout) }, } diff --git a/otdfctl/e2e/kas-keys.bats b/otdfctl/e2e/kas-keys.bats index 127849a619..3f7c64b052 100644 --- a/otdfctl/e2e/kas-keys.bats +++ b/otdfctl/e2e/kas-keys.bats @@ -572,7 +572,8 @@ format_kas_name_as_uri() { @test "kas-keys: update key (missing id)" { run_otdfctl_key update --json assert_failure - assert_output --partial "ERROR Flag '--id' is required" + assert_equal "$(echo "$output" | jq -r .status)" "ERROR" + assert_equal "$(echo "$output" | jq -r .message)" "Flag '--id' is required" } # LIST Tests diff --git a/otdfctl/e2e/profile.bats b/otdfctl/e2e/profile.bats index b23867fe3e..92880284b1 100755 --- a/otdfctl/e2e/profile.bats +++ b/otdfctl/e2e/profile.bats @@ -139,6 +139,46 @@ teardown() { refute_output --partial "$target_profile_keyring" } +@test "profile list supports json output" { + profile1="${PROFILE_TEST_PREFIX}-list-json-1" + profile2="${PROFILE_TEST_PREFIX}-list-json-2" + + run_otdfctl create "$profile1" http://localhost:8080 + assert_success + + run_otdfctl create "$profile2" http://localhost:8080 --set-default + assert_success + + run_otdfctl list --json + assert_success + assert_equal "$(echo "$output" | jq -r .store)" "filesystem" + echo "$output" | jq -e --arg profile1 "$profile1" --arg profile2 "$profile2" \ + 'any(.profiles[]; .name == $profile1 and .is_default == false) and any(.profiles[]; .name == $profile2 and .is_default == true)' +} + +@test "profile get supports json output" { + profile="${PROFILE_TEST_PREFIX}-get-json" + + run_otdfctl create "$profile" http://localhost:8080 --set-default --output-format json + assert_success + + run_otdfctl get "$profile" --json + assert_success + assert_equal "$(echo "$output" | jq -r .profile)" "$profile" + assert_equal "$(echo "$output" | jq -r .endpoint)" "http://localhost:8080" + assert_equal "$(echo "$output" | jq -r .is_default)" "true" + assert_equal "$(echo "$output" | jq -r .output_format)" "json" +} + +@test "profile errors support json output" { + profile="${PROFILE_TEST_PREFIX}-missing-json" + + run_otdfctl get "$profile" --json + assert_failure + assert_equal "$(echo "$output" | jq -r .status)" "ERROR" + echo "$output" | jq -e --arg profile "$profile" '.message | contains($profile)' +} + @test "profile set-default updates default profile" { base="${PROFILE_TEST_PREFIX}-set-default" profile1="${base}-1" diff --git a/otdfctl/pkg/cli/cli.go b/otdfctl/pkg/cli/cli.go index 13e64a59da..e6ffcdb3a9 100644 --- a/otdfctl/pkg/cli/cli.go +++ b/otdfctl/pkg/cli/cli.go @@ -38,10 +38,7 @@ func New(cmd *cobra.Command, args []string, options ...cliVariadicOption) *Cli { // Temp wrapper for FlagHelper until we can remove it cli.FlagHelper = cli.Flags - cli.printer = newPrinter(cli) - if opts.printerJSON { - cli.printer.setJSON(true) - } + cli.printer = newPrinter(opts.printerJSON || cli.Flags.GetOptionalBool("json")) return cli } diff --git a/otdfctl/pkg/cli/errors.go b/otdfctl/pkg/cli/errors.go index 61be89db94..4d79d9cc4e 100644 --- a/otdfctl/pkg/cli/errors.go +++ b/otdfctl/pkg/cli/errors.go @@ -3,6 +3,7 @@ package cli import ( "io" "os" + "strings" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -15,17 +16,17 @@ const ( func ExitWithError(errMsg string, err error) { // This is temporary until we can refactor the code to use the Cli struct - (&Cli{printer: &Printer{enabled: true}}).ExitWithError(errMsg, err) + (&Cli{printer: defaultPrinter()}).ExitWithError(errMsg, err) } func ExitWithNotFoundError(errMsg string, err error) { // This is temporary until we can refactor the code to use the Cli struct - (&Cli{printer: &Printer{enabled: true}}).ExitWithNotFoundError(errMsg, err) + (&Cli{printer: defaultPrinter()}).ExitWithNotFoundError(errMsg, err) } func ExitWithWarning(warnMsg string) { // This is temporary until we can refactor the code to use the Cli struct - (&Cli{printer: &Printer{enabled: true}}).ExitWithWarning(warnMsg) + (&Cli{printer: defaultPrinter()}).ExitWithWarning(warnMsg) } // ExitWithError prints an error message and exits with a non-zero status code. @@ -57,10 +58,16 @@ func (c *Cli) ExitWithSuccess(msg string) { } func (c *Cli) ExitWithMessage(msg string, code int) { - if c.printer.enabled { - c.println(os.Stdout, msg) - os.Exit(code) + w := os.Stdout + if code != ExitCodeSuccess { + w = os.Stderr + } + if c.printer.json { + c.printJSON(MessageJSON(statusForExitCode(code), strings.TrimSpace(msg)), w) + } else { + c.println(w, msg) } + os.Exit(code) } func (c *Cli) ExitWithJSON(v interface{}, code int) { @@ -80,3 +87,10 @@ func (c *Cli) ExitWith(styledMsg string, jsonMsg interface{}, code int, w io.Wri } os.Exit(code) } + +func statusForExitCode(code int) string { + if code == ExitCodeSuccess { + return "SUCCESS" + } + return "ERROR" +} diff --git a/otdfctl/pkg/cli/printer.go b/otdfctl/pkg/cli/printer.go index f630bcbf3e..a4b89c0c7b 100644 --- a/otdfctl/pkg/cli/printer.go +++ b/otdfctl/pkg/cli/printer.go @@ -5,17 +5,20 @@ import ( "errors" "fmt" "io" + "sync/atomic" ) var ErrPrinterExpectsCommand = errors.New("printer expects a command") +var defaultJSONOutput atomic.Bool + type Printer struct { enabled bool json bool debug bool } -func newPrinter(cli *Cli) *Printer { +func newPrinter(json bool) *Printer { p := &Printer{ enabled: true, json: false, @@ -23,8 +26,8 @@ func newPrinter(cli *Cli) *Printer { } // if json output is enabled, disable the printer - printJSON := cli.Flags.GetOptionalBool("json") - p.setJSON(printJSON) + defaultJSONOutput.Store(json) + p.setJSON(json) return p } @@ -57,3 +60,11 @@ func (c *Cli) SetJSONOutput(enabled bool) { } c.printer.setJSON(enabled) } + +func defaultPrinter() *Printer { + json := defaultJSONOutput.Load() + return &Printer{ + enabled: !json, + json: json, + } +}