diff --git a/api/secret.go b/api/secret.go index 77e3ee9a9..37e60892e 100644 --- a/api/secret.go +++ b/api/secret.go @@ -2,8 +2,11 @@ package api import ( "bytes" + "encoding/json" "fmt" "io" + "reflect" + "strings" "time" "github.com/hashicorp/errwrap" @@ -302,7 +305,15 @@ func ParseSecret(r io.Reader) (*Secret, error) { // First read the data into a buffer. Not super efficient but we want to // know if we actually have a body or not. var buf bytes.Buffer - _, err := buf.ReadFrom(r) + + // io.Reader is treated like a stream and cannot be read + // multiple times. Duplicating this stream using TeeReader + // to use this data in case there is no top-level data from + // api response + var teebuf bytes.Buffer + tee := io.TeeReader(r, &teebuf) + + _, err := buf.ReadFrom(tee) if err != nil { return nil, err } @@ -316,5 +327,38 @@ func ParseSecret(r io.Reader) (*Secret, error) { return nil, err } + // If the secret is null, add raw data to secret data if present + if reflect.DeepEqual(secret, Secret{}) { + data := make(map[string]interface{}) + if err := jsonutil.DecodeJSONFromReader(&teebuf, &data); err != nil { + return nil, err + } + errRaw, errPresent := data["errors"] + + // if only errors are present in the resp.Body return nil + // to return value not found as it does not have any raw data + if len(data) == 1 && errPresent { + return nil, nil + } + + // if errors are present along with raw data return the error + if errPresent { + var errStrArray []string + errBytes, err := json.Marshal(errRaw) + if err != nil { + return nil, err + } + if err := json.Unmarshal(errBytes, &errStrArray); err != nil { + return nil, err + } + return nil, fmt.Errorf(strings.Join(errStrArray, " ")) + } + + // if any raw data is present in resp.Body, add it to secret + if len(data) > 0 { + secret.Data = data + } + } + return &secret, nil } diff --git a/changelog/17913.txt b/changelog/17913.txt new file mode 100644 index 000000000..f26a779c2 --- /dev/null +++ b/changelog/17913.txt @@ -0,0 +1,3 @@ +```release-note:bug +cli: Fix vault read handling to return raw data as secret.Data when there is no top-level data object from api response. +``` \ No newline at end of file diff --git a/command/read_test.go b/command/read_test.go index 13f41da7e..4a8ec877a 100644 --- a/command/read_test.go +++ b/command/read_test.go @@ -128,6 +128,33 @@ func TestReadCommand_Run(t *testing.T) { } }) + t.Run("no_data_object_from_api_response", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + ui, cmd := testReadCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "sys/health", + }) + if exp := 0; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + expected := []string{ + "cluster_id", "cluster_name", "initialized", "performance_standby", "replication_dr_mode", "replication_performance_mode", "sealed", + "server_time_utc", "standby", "version", + } + for _, expectedField := range expected { + if !strings.Contains(combined, expectedField) { + t.Errorf("expected %q to contain %q", combined, expected) + } + } + }) + t.Run("no_tabs", func(t *testing.T) { t.Parallel()