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 (
"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
}

View File

@ -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

View File

@ -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)
}

View File

@ -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
}