Cloud: Split private readRedactedPlan func into two

Since `terraform show -json` needs to get a raw hunk of json bytes and sling it
right back out again, it's going to be more convenient if plain `show` can ALSO
take in raw json. In order for that to happen, I need a function that basically
acts like `client.Plans.ReadJSONOutput()`, without eagerly unmarshalling that
`jsonformat.Plan` struct.

As a slight bonus, this also lets us make the tfe client mocks slightly
stupider.
This commit is contained in:
Nick Fagerlund 2023-06-26 17:41:24 -07:00 committed by Sebastian Rivera
parent 0df3c143bb
commit 2a08a5b46e
4 changed files with 27 additions and 24 deletions

View File

@ -5,6 +5,7 @@ package cloud
import ( import (
"bufio" "bufio"
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
@ -550,11 +551,12 @@ func (b *Cloud) confirm(stopCtx context.Context, op *backend.Operation, opts *te
return <-result return <-result
} }
// This method will fetch the redacted plan output and marshal the response into // This method will fetch the redacted plan output as a byte slice, mirroring
// a struct the jsonformat.Renderer expects. // 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 // Note: Apologies for the lengthy definition, this is a result of not being
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) { // 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 := retryablehttp.NewClient()
client.RetryMax = 10 client.RetryMax = 10
client.RetryWaitMin = 100 * time.Millisecond 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("Authorization", "Bearer "+token)
req.Header.Set("Accept", "application/json") req.Header.Set("Accept", "application/json")
p := &jsonformat.Plan{}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
@ -586,10 +587,17 @@ var readRedactedPlan func(context.Context, url.URL, string, string) (*jsonformat
return nil, err 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 nil, err
} }
return p, nil return p, nil
} }

View File

@ -509,10 +509,14 @@ func (b *Cloud) renderPlanLogs(ctx context.Context, op *backend.Operation, run *
return err return err
} }
if renderSRO || shouldGenerateConfig { 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 { if err != nil {
return generalError("Failed to read JSON plan", err) 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 // Write any generated config before rendering the plan, so we can stop in case of errors

View File

@ -26,7 +26,6 @@ import (
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/backend" "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"
"github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/httpclient" "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/" 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) return mc.RedactedPlans.Read(ctx, baseURL.Hostname(), token, planID)
} }
@ -332,7 +331,7 @@ func testUnconfiguredBackend(t *testing.T) (*Cloud, func()) {
} }
baseURL.Path = "/api/v2/" 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) return mc.RedactedPlans.Read(ctx, baseURL.Hostname(), token, planID)
} }

View File

@ -7,7 +7,6 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/base64" "encoding/base64"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -22,7 +21,6 @@ import (
tfe "github.com/hashicorp/go-tfe" tfe "github.com/hashicorp/go-tfe"
"github.com/mitchellh/copystructure" "github.com/mitchellh/copystructure"
"github.com/hashicorp/terraform/internal/command/jsonformat"
tfversion "github.com/hashicorp/terraform/version" tfversion "github.com/hashicorp/terraform/version"
) )
@ -468,13 +466,13 @@ func (m *MockOrganizations) ReadRunQueue(ctx context.Context, name string, optio
type MockRedactedPlans struct { type MockRedactedPlans struct {
client *MockClient client *MockClient
redactedPlans map[string]*jsonformat.Plan redactedPlans map[string][]byte
} }
func newMockRedactedPlans(client *MockClient) *MockRedactedPlans { func newMockRedactedPlans(client *MockClient) *MockRedactedPlans {
return &MockRedactedPlans{ return &MockRedactedPlans{
client: client, 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 return err
} }
raw, err := ioutil.ReadAll(redactedPlanFile) raw, err := io.ReadAll(redactedPlanFile)
if err != nil { if err != nil {
return err return err
} }
redactedPlan := &jsonformat.Plan{} m.redactedPlans[planID] = raw
err = json.Unmarshal(raw, redactedPlan)
if err != nil {
return err
}
m.redactedPlans[planID] = redactedPlan
return nil 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 { if p, ok := m.redactedPlans[planID]; ok {
return p, nil return p, nil
} }