mirror of
https://github.com/hashicorp/terraform.git
synced 2026-03-23 10:54:11 -04:00
Changes between empty strings and `null` were hidden in the CLI output, because the SDK could not reliably detect the difference and may return either value depending on the situation. This legacy behavior can be confusing for authors of new provider which can correctly handle `null`, and it would be preferable to be able to render those changes in the CLI. While we don't have enough information to detect when the legacy behavior is required, we can detect a number of cases where it's certain that we are not dealing with a legacy schema and should output the full diff.
301 lines
11 KiB
Go
301 lines
11 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package structured
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"reflect"
|
|
|
|
"github.com/hashicorp/terraform/internal/command/jsonformat/structured/attribute_path"
|
|
"github.com/hashicorp/terraform/internal/command/jsonplan"
|
|
"github.com/hashicorp/terraform/internal/command/jsonstate"
|
|
viewsjson "github.com/hashicorp/terraform/internal/command/views/json"
|
|
"github.com/hashicorp/terraform/internal/plans"
|
|
)
|
|
|
|
// Change contains the unmarshalled generic interface{} types that are output by
|
|
// the JSON functions in the various json packages (such as jsonplan and
|
|
// jsonprovider).
|
|
//
|
|
// A Change can be converted into a computed.Diff, ready for rendering, with the
|
|
// ComputeDiffForAttribute, ComputeDiffForOutput, and ComputeDiffForBlock
|
|
// functions.
|
|
//
|
|
// The Before and After fields are actually go-cty values, but we cannot convert
|
|
// them directly because of the Terraform Cloud redacted endpoint. The redacted
|
|
// endpoint turns sensitive values into strings regardless of their types.
|
|
// Because of this, we cannot just do a direct conversion using the ctyjson
|
|
// package. We would have to iterate through the schema first, find the
|
|
// sensitive values and their mapped types, update the types inside the schema
|
|
// to strings, and then go back and do the overall conversion. This isn't
|
|
// including any of the more complicated parts around what happens if something
|
|
// was sensitive before and isn't sensitive after or vice versa. This would mean
|
|
// the type would need to change between the before and after value. It is in
|
|
// fact just easier to iterate through the values as generic JSON interfaces.
|
|
type Change struct {
|
|
|
|
// BeforeExplicit matches AfterExplicit except references the Before value.
|
|
BeforeExplicit bool
|
|
|
|
// AfterExplicit refers to whether the After value is explicit or
|
|
// implicit. It is explicit if it has been specified by the user, and
|
|
// implicit if it has been set as a consequence of other changes.
|
|
//
|
|
// For example, explicitly setting a value to null in a list should result
|
|
// in After being null and AfterExplicit being true. In comparison,
|
|
// removing an element from a list should also result in After being null
|
|
// and AfterExplicit being false. Without the explicit information our
|
|
// functions would not be able to tell the difference between these two
|
|
// cases.
|
|
AfterExplicit bool
|
|
|
|
// Before contains the value before the proposed change.
|
|
//
|
|
// The type of the value should be informed by the schema and cast
|
|
// appropriately when needed.
|
|
Before interface{}
|
|
|
|
// After contains the value after the proposed change.
|
|
//
|
|
// The type of the value should be informed by the schema and cast
|
|
// appropriately when needed.
|
|
After interface{}
|
|
|
|
// Unknown describes whether the After value is known or unknown at the time
|
|
// of the plan. In practice, this means the after value should be rendered
|
|
// simply as `(known after apply)`.
|
|
//
|
|
// The concrete value could be a boolean describing whether the entirety of
|
|
// the After value is unknown, or it could be a list or a map depending on
|
|
// the schema describing whether specific elements or attributes within the
|
|
// value are unknown.
|
|
Unknown interface{}
|
|
|
|
// BeforeSensitive matches Unknown, but references whether the Before value
|
|
// is sensitive.
|
|
BeforeSensitive interface{}
|
|
|
|
// AfterSensitive matches Unknown, but references whether the After value is
|
|
// sensitive.
|
|
AfterSensitive interface{}
|
|
|
|
// ReplacePaths contains a set of paths that point to attributes/elements
|
|
// that are causing the overall resource to be replaced rather than simply
|
|
// updated.
|
|
ReplacePaths attribute_path.Matcher
|
|
|
|
// RelevantAttributes contains a set of paths that point attributes/elements
|
|
// that we should display. Any element/attribute not matched by this Matcher
|
|
// should be skipped.
|
|
RelevantAttributes attribute_path.Matcher
|
|
|
|
// NonLegacySchema must only be used when rendering the change to the CLI,
|
|
// and is otherwise ignored. This flag is set when we can be sure that the
|
|
// change originated from a resource which is not using the legacy SDK, so
|
|
// we don't need to hide changes between empty and null strings.
|
|
// NonLegacySchema is only switched to true by the renderer, because that is
|
|
// where we have most of the schema information to detect the condition.
|
|
NonLegacySchema bool
|
|
}
|
|
|
|
// FromJsonChange unmarshals the raw []byte values in the jsonplan.Change
|
|
// structs into generic interface{} types that can be reasoned about.
|
|
func FromJsonChange(change jsonplan.Change, relevantAttributes attribute_path.Matcher) Change {
|
|
ret := Change{
|
|
Before: unmarshalGeneric(change.Before),
|
|
After: unmarshalGeneric(change.After),
|
|
Unknown: unmarshalGeneric(change.AfterUnknown),
|
|
BeforeSensitive: unmarshalGeneric(change.BeforeSensitive),
|
|
AfterSensitive: unmarshalGeneric(change.AfterSensitive),
|
|
ReplacePaths: attribute_path.Parse(change.ReplacePaths, false),
|
|
RelevantAttributes: relevantAttributes,
|
|
}
|
|
|
|
// A forget-only action (i.e. ["forget"], not ["create", "forget"])
|
|
// should be represented as a no-op, so it does not look like we are
|
|
// proposing to delete the resource.
|
|
if len(change.Actions) == 1 && change.Actions[0] == "forget" {
|
|
ret = ret.AsNoOp()
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
// FromJsonResource unmarshals the raw values in the jsonstate.Resource structs
|
|
// into generic interface{} types that can be reasoned about.
|
|
func FromJsonResource(resource jsonstate.Resource) Change {
|
|
return Change{
|
|
// We model resource formatting as NoOps.
|
|
Before: unwrapAttributeValues(resource.AttributeValues),
|
|
After: unwrapAttributeValues(resource.AttributeValues),
|
|
|
|
// We have some sensitive values, but we don't have any unknown values.
|
|
Unknown: false,
|
|
BeforeSensitive: unmarshalGeneric(resource.SensitiveValues),
|
|
AfterSensitive: unmarshalGeneric(resource.SensitiveValues),
|
|
|
|
// We don't display replacement data for resources, and all attributes
|
|
// are relevant.
|
|
ReplacePaths: attribute_path.Empty(false),
|
|
RelevantAttributes: attribute_path.AlwaysMatcher(),
|
|
}
|
|
}
|
|
|
|
// FromJsonOutput unmarshals the raw values in the jsonstate.Output structs into
|
|
// generic interface{} types that can be reasoned about.
|
|
func FromJsonOutput(output jsonstate.Output) Change {
|
|
return Change{
|
|
// We model resource formatting as NoOps.
|
|
Before: unmarshalGeneric(output.Value),
|
|
After: unmarshalGeneric(output.Value),
|
|
|
|
// We have some sensitive values, but we don't have any unknown values.
|
|
Unknown: false,
|
|
BeforeSensitive: output.Sensitive,
|
|
AfterSensitive: output.Sensitive,
|
|
|
|
// We don't display replacement data for resources, and all attributes
|
|
// are relevant.
|
|
ReplacePaths: attribute_path.Empty(false),
|
|
RelevantAttributes: attribute_path.AlwaysMatcher(),
|
|
}
|
|
}
|
|
|
|
// FromJsonViewsOutput unmarshals the raw values in the viewsjson.Output structs into
|
|
// generic interface{} types that can be reasoned about.
|
|
func FromJsonViewsOutput(output viewsjson.Output) Change {
|
|
return Change{
|
|
// We model resource formatting as NoOps.
|
|
Before: unmarshalGeneric(output.Value),
|
|
After: unmarshalGeneric(output.Value),
|
|
|
|
// We have some sensitive values, but we don't have any unknown values.
|
|
Unknown: false,
|
|
BeforeSensitive: output.Sensitive,
|
|
AfterSensitive: output.Sensitive,
|
|
|
|
// We don't display replacement data for resources, and all attributes
|
|
// are relevant.
|
|
ReplacePaths: attribute_path.Empty(false),
|
|
RelevantAttributes: attribute_path.AlwaysMatcher(),
|
|
}
|
|
}
|
|
|
|
// CalculateAction does a very simple analysis to make the best guess at the
|
|
// action this change describes. For complex types such as objects, maps, lists,
|
|
// or sets it is likely more efficient to work out the action directly instead
|
|
// of relying on this function.
|
|
func (change Change) CalculateAction() plans.Action {
|
|
if (change.Before == nil && !change.BeforeExplicit) && (change.After != nil || change.AfterExplicit) {
|
|
return plans.Create
|
|
}
|
|
if (change.After == nil && !change.AfterExplicit) && (change.Before != nil || change.BeforeExplicit) {
|
|
return plans.Delete
|
|
}
|
|
|
|
if reflect.DeepEqual(change.Before, change.After) && change.AfterExplicit == change.BeforeExplicit && change.IsAfterSensitive() == change.IsBeforeSensitive() {
|
|
return plans.NoOp
|
|
}
|
|
|
|
return plans.Update
|
|
}
|
|
|
|
// GetDefaultActionForIteration is used to guess what the change could be for
|
|
// complex attributes (collections and objects) and blocks.
|
|
//
|
|
// You can't really tell the difference between a NoOp and an Update just by
|
|
// looking at the attribute itself as you need to inspect the children.
|
|
//
|
|
// This function returns a Delete or a Create action if the before or after
|
|
// values were null, and returns a NoOp for all other cases. It should be used
|
|
// in conjunction with compareActions to calculate the actual action based on
|
|
// the actions of the children.
|
|
func (change Change) GetDefaultActionForIteration() plans.Action {
|
|
if change.Before == nil && change.After == nil {
|
|
return plans.NoOp
|
|
}
|
|
|
|
if change.Before == nil {
|
|
return plans.Create
|
|
}
|
|
if change.After == nil {
|
|
return plans.Delete
|
|
}
|
|
return plans.NoOp
|
|
}
|
|
|
|
// AsNoOp returns the current change as if it is a NoOp operation.
|
|
//
|
|
// Basically it replaces all the after values with the before values.
|
|
func (change Change) AsNoOp() Change {
|
|
return Change{
|
|
BeforeExplicit: change.BeforeExplicit,
|
|
AfterExplicit: change.BeforeExplicit,
|
|
Before: change.Before,
|
|
After: change.Before,
|
|
Unknown: false,
|
|
BeforeSensitive: change.BeforeSensitive,
|
|
AfterSensitive: change.BeforeSensitive,
|
|
ReplacePaths: change.ReplacePaths,
|
|
RelevantAttributes: change.RelevantAttributes,
|
|
}
|
|
}
|
|
|
|
// AsDelete returns the current change as if it is a Delete operation.
|
|
//
|
|
// Basically it replaces all the after values with nil or false.
|
|
func (change Change) AsDelete() Change {
|
|
return Change{
|
|
BeforeExplicit: change.BeforeExplicit,
|
|
AfterExplicit: false,
|
|
Before: change.Before,
|
|
After: nil,
|
|
Unknown: nil,
|
|
BeforeSensitive: change.BeforeSensitive,
|
|
AfterSensitive: nil,
|
|
ReplacePaths: change.ReplacePaths,
|
|
RelevantAttributes: change.RelevantAttributes,
|
|
}
|
|
}
|
|
|
|
// AsCreate returns the current change as if it is a Create operation.
|
|
//
|
|
// Basically it replaces all the before values with nil or false.
|
|
func (change Change) AsCreate() Change {
|
|
return Change{
|
|
BeforeExplicit: false,
|
|
AfterExplicit: change.AfterExplicit,
|
|
Before: nil,
|
|
After: change.After,
|
|
Unknown: change.Unknown,
|
|
BeforeSensitive: nil,
|
|
AfterSensitive: change.AfterSensitive,
|
|
ReplacePaths: change.ReplacePaths,
|
|
RelevantAttributes: change.RelevantAttributes,
|
|
}
|
|
}
|
|
|
|
func unmarshalGeneric(raw json.RawMessage) interface{} {
|
|
if raw == nil {
|
|
return nil
|
|
}
|
|
|
|
decoder := json.NewDecoder(bytes.NewReader(raw))
|
|
decoder.UseNumber() // We support very big (> 2^64) numbers.
|
|
|
|
var out interface{}
|
|
if err := decoder.Decode(&out); err != nil {
|
|
panic("unrecognized json type: " + err.Error())
|
|
}
|
|
return out
|
|
}
|
|
|
|
func unwrapAttributeValues(values jsonstate.AttributeValues) map[string]interface{} {
|
|
out := make(map[string]interface{})
|
|
for key, value := range values {
|
|
out[key] = unmarshalGeneric(value)
|
|
}
|
|
return out
|
|
}
|