From 3d9ccf98e88ea8200c3c02b6e53f2c7ee0b273ff Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Thu, 7 May 2026 12:43:02 -0700 Subject: [PATCH 1/7] fix(otdfctl): support json profile output Signed-off-by: jakedoublev --- otdfctl/cmd/profile.go | 48 ++++++++++++++++++++++++++++++++++---- otdfctl/e2e/profile.bats | 31 ++++++++++++++++++++++++ otdfctl/pkg/cli/errors.go | 20 ++++++++++++---- otdfctl/pkg/cli/printer.go | 12 ++++++++++ 4 files changed, 101 insertions(+), 10 deletions(-) diff --git a/otdfctl/cmd/profile.go b/otdfctl/cmd/profile.go index 5ee92a3293..11b397c2a7 100644 --- a/otdfctl/cmd/profile.go +++ b/otdfctl/cmd/profile.go @@ -3,6 +3,7 @@ package cmd import ( "errors" "fmt" + "os" "runtime" "strings" @@ -26,6 +27,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,7 +124,13 @@ var profileListCmd = &cobra.Command{ var sb strings.Builder fmt.Fprintf(&sb, "Listing profiles from %s\n", driverType) + out := profileListOutput{Store: string(driverType)} for _, p := range osprofiles.ListProfiles(profiler) { + isDefault := p == defaultProfile + out.Profiles = append(out.Profiles, profileSummary{ + Name: p, + IsDefault: isDefault, + }) if p == defaultProfile { fmt.Fprintf(&sb, "* %s\n", p) continue @@ -112,7 +138,7 @@ var profileListCmd = &cobra.Command{ fmt.Fprintf(&sb, " %s\n", p) } - c.ExitWithMessage(sb.String(), cli.ExitCodeSuccess) + c.ExitWith(sb.String(), out, cli.ExitCodeSuccess, os.Stdout) }, } @@ -130,27 +156,39 @@ var profileGetCmd = &cobra.Command{ cli.ExitWithError("Error loading profile store for profile "+profileName, err) } - isDefault := "false" + isDefault := profileStore.IsDefault() + isDefaultString := "false" if profileStore.IsDefault() { - isDefault = "true" + isDefaultString = "true" } 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", isDefaultString}, []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/profile.bats b/otdfctl/e2e/profile.bats index b23867fe3e..7f0ee69bce 100755 --- a/otdfctl/e2e/profile.bats +++ b/otdfctl/e2e/profile.bats @@ -139,6 +139,37 @@ 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 bash -c "set -o pipefail; ./otdfctl profile list --json | jq -e --arg profile1 '$profile1' --arg profile2 '$profile2' '.store == \"filesystem\" and any(.profiles[]; .name == \$profile1 and .is_default == false) and any(.profiles[]; .name == \$profile2 and .is_default == true)'" + assert_success +} + +@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 bash -c "set -o pipefail; ./otdfctl profile get '$profile' --json | jq -e --arg profile '$profile' '.profile == \$profile and .endpoint == \"http://localhost:8080\" and .is_default == true and .output_format == \"json\"'" + assert_success +} + +@test "profile errors support json output" { + profile="${PROFILE_TEST_PREFIX}-missing-json" + + run bash -c "output=\$(./otdfctl profile get '$profile' --json 2>&1 >/dev/null); status=\$?; test \$status -ne 0 && jq -e --arg profile '$profile' '.status == \"ERROR\" and (.message | contains(\$profile))' <<< \"\$output\"" + assert_success +} + @test "profile set-default updates default profile" { base="${PROFILE_TEST_PREFIX}-set-default" profile1="${base}-1" diff --git a/otdfctl/pkg/cli/errors.go b/otdfctl/pkg/cli/errors.go index 61be89db94..365ba6ce7d 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,12 @@ func (c *Cli) ExitWithSuccess(msg string) { } func (c *Cli) ExitWithMessage(msg string, code int) { - if c.printer.enabled { + if c.printer.json { + c.printJSON(MessageJSON(statusForExitCode(code), strings.TrimSpace(msg)), os.Stdout) + } else { c.println(os.Stdout, msg) - os.Exit(code) } + os.Exit(code) } func (c *Cli) ExitWithJSON(v interface{}, code int) { @@ -80,3 +83,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..214baccdd9 100644 --- a/otdfctl/pkg/cli/printer.go +++ b/otdfctl/pkg/cli/printer.go @@ -5,10 +5,13 @@ 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 @@ -32,6 +35,7 @@ func newPrinter(cli *Cli) *Printer { func (p *Printer) setJSON(json bool) { p.json = json p.enabled = !json + defaultJSONOutput.Store(json) } // PrintJSON prints the given value as json @@ -57,3 +61,11 @@ func (c *Cli) SetJSONOutput(enabled bool) { } c.printer.setJSON(enabled) } + +func defaultPrinter() *Printer { + json := defaultJSONOutput.Load() + return &Printer{ + enabled: !json, + json: json, + } +} From a85e4fb31885aa41ded6cd98bd40644281c9d3f1 Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Thu, 7 May 2026 13:17:09 -0700 Subject: [PATCH 2/7] fix(otdfctl): emit empty profile list as json array Signed-off-by: jakedoublev --- otdfctl/cmd/profile.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/otdfctl/cmd/profile.go b/otdfctl/cmd/profile.go index 11b397c2a7..97aed190b8 100644 --- a/otdfctl/cmd/profile.go +++ b/otdfctl/cmd/profile.go @@ -124,7 +124,10 @@ var profileListCmd = &cobra.Command{ var sb strings.Builder fmt.Fprintf(&sb, "Listing profiles from %s\n", driverType) - out := profileListOutput{Store: string(driverType)} + out := profileListOutput{ + Store: string(driverType), + Profiles: []profileSummary{}, + } for _, p := range osprofiles.ListProfiles(profiler) { isDefault := p == defaultProfile out.Profiles = append(out.Profiles, profileSummary{ From f1ce05d59887f10ae40f07ff2ebd41913239dac4 Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Thu, 7 May 2026 13:17:55 -0700 Subject: [PATCH 3/7] fix(otdfctl): polish profile json output Signed-off-by: jakedoublev --- otdfctl/cmd/profile.go | 4 ++-- otdfctl/pkg/cli/errors.go | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/otdfctl/cmd/profile.go b/otdfctl/cmd/profile.go index 97aed190b8..0b307e089c 100644 --- a/otdfctl/cmd/profile.go +++ b/otdfctl/cmd/profile.go @@ -134,7 +134,7 @@ var profileListCmd = &cobra.Command{ Name: p, IsDefault: isDefault, }) - if p == defaultProfile { + if isDefault { fmt.Fprintf(&sb, "* %s\n", p) continue } @@ -161,7 +161,7 @@ var profileGetCmd = &cobra.Command{ isDefault := profileStore.IsDefault() isDefaultString := "false" - if profileStore.IsDefault() { + if isDefault { isDefaultString = "true" } diff --git a/otdfctl/pkg/cli/errors.go b/otdfctl/pkg/cli/errors.go index 365ba6ce7d..4d79d9cc4e 100644 --- a/otdfctl/pkg/cli/errors.go +++ b/otdfctl/pkg/cli/errors.go @@ -58,10 +58,14 @@ func (c *Cli) ExitWithSuccess(msg string) { } func (c *Cli) ExitWithMessage(msg string, code int) { + w := os.Stdout + if code != ExitCodeSuccess { + w = os.Stderr + } if c.printer.json { - c.printJSON(MessageJSON(statusForExitCode(code), strings.TrimSpace(msg)), os.Stdout) + c.printJSON(MessageJSON(statusForExitCode(code), strings.TrimSpace(msg)), w) } else { - c.println(os.Stdout, msg) + c.println(w, msg) } os.Exit(code) } From 85b3666f8767e1f7a59f61d08b69189328c55dee Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Thu, 7 May 2026 14:10:07 -0700 Subject: [PATCH 4/7] test(otdfctl): expect json missing id error Signed-off-by: jakedoublev --- otdfctl/e2e/kas-keys.bats | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From 5a0bc4c73ec46003e59d3951b736a75088d9ab48 Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Thu, 7 May 2026 14:14:28 -0700 Subject: [PATCH 5/7] test(otdfctl): use profile helper for json tests Signed-off-by: jakedoublev --- otdfctl/e2e/profile.bats | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/otdfctl/e2e/profile.bats b/otdfctl/e2e/profile.bats index 7f0ee69bce..92880284b1 100755 --- a/otdfctl/e2e/profile.bats +++ b/otdfctl/e2e/profile.bats @@ -149,8 +149,11 @@ teardown() { run_otdfctl create "$profile2" http://localhost:8080 --set-default assert_success - run bash -c "set -o pipefail; ./otdfctl profile list --json | jq -e --arg profile1 '$profile1' --arg profile2 '$profile2' '.store == \"filesystem\" and any(.profiles[]; .name == \$profile1 and .is_default == false) and any(.profiles[]; .name == \$profile2 and .is_default == true)'" + 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" { @@ -159,15 +162,21 @@ teardown() { run_otdfctl create "$profile" http://localhost:8080 --set-default --output-format json assert_success - run bash -c "set -o pipefail; ./otdfctl profile get '$profile' --json | jq -e --arg profile '$profile' '.profile == \$profile and .endpoint == \"http://localhost:8080\" and .is_default == true and .output_format == \"json\"'" + 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 bash -c "output=\$(./otdfctl profile get '$profile' --json 2>&1 >/dev/null); status=\$?; test \$status -ne 0 && jq -e --arg profile '$profile' '.status == \"ERROR\" and (.message | contains(\$profile))' <<< \"\$output\"" - assert_success + 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" { From 88650147d9d5fd25ff563b0f927fc7ede89ad7e6 Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Thu, 7 May 2026 14:20:20 -0700 Subject: [PATCH 6/7] fix(otdfctl): keep json fallback stable Signed-off-by: jakedoublev --- otdfctl/pkg/cli/cli.go | 5 +---- otdfctl/pkg/cli/printer.go | 7 +++---- 2 files changed, 4 insertions(+), 8 deletions(-) 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/printer.go b/otdfctl/pkg/cli/printer.go index 214baccdd9..a4b89c0c7b 100644 --- a/otdfctl/pkg/cli/printer.go +++ b/otdfctl/pkg/cli/printer.go @@ -18,7 +18,7 @@ type Printer struct { debug bool } -func newPrinter(cli *Cli) *Printer { +func newPrinter(json bool) *Printer { p := &Printer{ enabled: true, json: false, @@ -26,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 } @@ -35,7 +35,6 @@ func newPrinter(cli *Cli) *Printer { func (p *Printer) setJSON(json bool) { p.json = json p.enabled = !json - defaultJSONOutput.Store(json) } // PrintJSON prints the given value as json From 8495cf29d19ddcc4e9fb2d2e5b3d2837e97f2fa7 Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Thu, 7 May 2026 14:27:57 -0700 Subject: [PATCH 7/7] fix(otdfctl): simplify default profile display Signed-off-by: jakedoublev --- otdfctl/cmd/profile.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/otdfctl/cmd/profile.go b/otdfctl/cmd/profile.go index 0b307e089c..883bfabcc4 100644 --- a/otdfctl/cmd/profile.go +++ b/otdfctl/cmd/profile.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "runtime" + "strconv" "strings" osprofiles "github.com/jrschumacher/go-osprofiles" @@ -160,10 +161,6 @@ var profileGetCmd = &cobra.Command{ } isDefault := profileStore.IsDefault() - isDefaultString := "false" - if isDefault { - isDefaultString = "true" - } var auth string authType := "" @@ -179,7 +176,7 @@ var profileGetCmd = &cobra.Command{ t := cli.NewTabular( []string{"Profile", profileStore.Name()}, []string{"Endpoint", profileStore.GetEndpoint()}, - []string{"Is default", isDefaultString}, + []string{"Is default", strconv.FormatBool(isDefault)}, []string{"Output format", profileStore.GetOutputFormat()}, []string{"Auth type", auth}, )