diff --git a/internal/cloud/backend_common.go b/internal/cloud/backend_common.go index c4b4ee254f..ee7fd33dd5 100644 --- a/internal/cloud/backend_common.go +++ b/internal/cloud/backend_common.go @@ -5,6 +5,7 @@ package cloud import ( "bufio" + "bytes" "context" "encoding/json" "errors" @@ -550,11 +551,12 @@ func (b *Cloud) confirm(stopCtx context.Context, op *backend.Operation, opts *te return <-result } -// This method will fetch the redacted plan output and marshal the response into -// a struct the jsonformat.Renderer expects. +// This method will fetch the redacted plan output as a byte slice, mirroring +// the behavior of the similar client.Plans.ReadJSONOutput method. // -// Note: Apologies for the lengthy definition, this is a result of not being able to mock receiver methods -var readRedactedPlan func(context.Context, url.URL, string, string) (*jsonformat.Plan, error) = func(ctx context.Context, baseURL url.URL, token string, planID string) (*jsonformat.Plan, error) { +// Note: Apologies for the lengthy definition, this is a result of not being +// able to mock receiver methods +var readRedactedPlan func(context.Context, url.URL, string, string) ([]byte, error) = func(ctx context.Context, baseURL url.URL, token string, planID string) ([]byte, error) { client := retryablehttp.NewClient() client.RetryMax = 10 client.RetryWaitMin = 100 * time.Millisecond @@ -575,7 +577,6 @@ var readRedactedPlan func(context.Context, url.URL, string, string) (*jsonformat req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Accept", "application/json") - p := &jsonformat.Plan{} resp, err := client.Do(req) if err != nil { return nil, err @@ -586,10 +587,17 @@ var readRedactedPlan func(context.Context, url.URL, string, string) (*jsonformat return nil, err } - if err := json.NewDecoder(resp.Body).Decode(p); err != nil { + return io.ReadAll(resp.Body) +} + +// decodeRedactedPlan unmarshals a downloaded redacted plan into a struct the +// jsonformat.Renderer expects. +func decodeRedactedPlan(jsonBytes []byte) (*jsonformat.Plan, error) { + r := bytes.NewReader(jsonBytes) + p := &jsonformat.Plan{} + if err := json.NewDecoder(r).Decode(p); err != nil { return nil, err } - return p, nil } diff --git a/internal/cloud/backend_plan.go b/internal/cloud/backend_plan.go index 13cff76ee2..562307227d 100644 --- a/internal/cloud/backend_plan.go +++ b/internal/cloud/backend_plan.go @@ -509,10 +509,14 @@ func (b *Cloud) renderPlanLogs(ctx context.Context, op *backend.Operation, run * return err } if renderSRO || shouldGenerateConfig { - redactedPlan, err = readRedactedPlan(ctx, b.client.BaseURL(), b.token, run.Plan.ID) + jsonBytes, err := readRedactedPlan(ctx, b.client.BaseURL(), b.token, run.Plan.ID) if err != nil { return generalError("Failed to read JSON plan", err) } + redactedPlan, err = decodeRedactedPlan(jsonBytes) + if err != nil { + return generalError("Failed to decode JSON plan", err) + } } // Write any generated config before rendering the plan, so we can stop in case of errors diff --git a/internal/cloud/testing.go b/internal/cloud/testing.go index 992a961de3..0f6d89d42e 100644 --- a/internal/cloud/testing.go +++ b/internal/cloud/testing.go @@ -26,7 +26,6 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/backend" - "github.com/hashicorp/terraform/internal/command/jsonformat" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/httpclient" @@ -264,7 +263,7 @@ func testBackend(t *testing.T, obj cty.Value, handlers map[string]func(http.Resp } baseURL.Path = "/api/v2/" - readRedactedPlan = func(ctx context.Context, baseURL url.URL, token, planID string) (*jsonformat.Plan, error) { + readRedactedPlan = func(ctx context.Context, baseURL url.URL, token, planID string) ([]byte, error) { return mc.RedactedPlans.Read(ctx, baseURL.Hostname(), token, planID) } @@ -332,7 +331,7 @@ func testUnconfiguredBackend(t *testing.T) (*Cloud, func()) { } baseURL.Path = "/api/v2/" - readRedactedPlan = func(ctx context.Context, baseURL url.URL, token, planID string) (*jsonformat.Plan, error) { + readRedactedPlan = func(ctx context.Context, baseURL url.URL, token, planID string) ([]byte, error) { return mc.RedactedPlans.Read(ctx, baseURL.Hostname(), token, planID) } diff --git a/internal/cloud/tfe_client_mock.go b/internal/cloud/tfe_client_mock.go index 02e8280860..ecc7a96767 100644 --- a/internal/cloud/tfe_client_mock.go +++ b/internal/cloud/tfe_client_mock.go @@ -7,7 +7,6 @@ import ( "bytes" "context" "encoding/base64" - "encoding/json" "errors" "fmt" "io" @@ -22,7 +21,6 @@ import ( tfe "github.com/hashicorp/go-tfe" "github.com/mitchellh/copystructure" - "github.com/hashicorp/terraform/internal/command/jsonformat" tfversion "github.com/hashicorp/terraform/version" ) @@ -468,13 +466,13 @@ func (m *MockOrganizations) ReadRunQueue(ctx context.Context, name string, optio type MockRedactedPlans struct { client *MockClient - redactedPlans map[string]*jsonformat.Plan + redactedPlans map[string][]byte } func newMockRedactedPlans(client *MockClient) *MockRedactedPlans { return &MockRedactedPlans{ client: client, - redactedPlans: make(map[string]*jsonformat.Plan), + redactedPlans: make(map[string][]byte), } } @@ -495,23 +493,17 @@ func (m *MockRedactedPlans) create(cvID, workspaceID, planID string) error { return err } - raw, err := ioutil.ReadAll(redactedPlanFile) + raw, err := io.ReadAll(redactedPlanFile) if err != nil { return err } - redactedPlan := &jsonformat.Plan{} - err = json.Unmarshal(raw, redactedPlan) - if err != nil { - return err - } - - m.redactedPlans[planID] = redactedPlan + m.redactedPlans[planID] = raw return nil } -func (m *MockRedactedPlans) Read(ctx context.Context, hostname, token, planID string) (*jsonformat.Plan, error) { +func (m *MockRedactedPlans) Read(ctx context.Context, hostname, token, planID string) ([]byte, error) { if p, ok := m.redactedPlans[planID]; ok { return p, nil }