mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
286 lines
10 KiB
Go
286 lines
10 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package structured
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"reflect"
|
|
|
|
"github.com/opentofu/opentofu/internal/command/jsonformat/structured/attribute_path"
|
|
"github.com/opentofu/opentofu/internal/command/jsonplan"
|
|
"github.com/opentofu/opentofu/internal/command/jsonstate"
|
|
viewsjson "github.com/opentofu/opentofu/internal/command/views/json"
|
|
"github.com/opentofu/opentofu/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
|
|
}
|
|
|
|
// 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 {
|
|
return 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,
|
|
}
|
|
}
|
|
|
|
// 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.NewBuffer(raw))
|
|
decoder.UseNumber()
|
|
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
|
|
}
|