// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package structured import ( "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 } var out interface{} if err := json.Unmarshal(raw, &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 }