// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package states import ( "bytes" "fmt" "reflect" "github.com/zclconf/go-cty/cty" ctyjson "github.com/zclconf/go-cty/cty/json" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/hcl2shim" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/providers" ) // ResourceInstanceObjectSrc is a not-fully-decoded version of // ResourceInstanceObject. Decoding of it can be completed by first handling // any schema migration steps to get to the latest schema version and then // calling method Decode with the implied type of the latest schema. type ResourceInstanceObjectSrc struct { // SchemaVersion is the resource-type-specific schema version number that // was current when either AttrsJSON or AttrsFlat was encoded. Migration // steps are required if this is less than the current version number // reported by the corresponding provider. SchemaVersion uint64 // AttrsJSON is a JSON-encoded representation of the object attributes, // encoding the value (of the object type implied by the associated resource // type schema) that represents this remote object in Terraform Language // expressions, and is compared with configuration when producing a diff. // // This is retained in JSON format here because it may require preprocessing // before decoding if, for example, the stored attributes are for an older // schema version which the provider must upgrade before use. If the // version is current, it is valid to simply decode this using the // type implied by the current schema, without the need for the provider // to perform an upgrade first. // // When writing a ResourceInstanceObject into the state, AttrsJSON should // always be conformant to the current schema version and the current // schema version should be recorded in the SchemaVersion field. AttrsJSON []byte IdentitySchemaVersion uint64 IdentityJSON []byte // AttrsFlat is a legacy form of attributes used in older state file // formats, and in the new state format for objects that haven't yet been // upgraded. This attribute is mutually exclusive with Attrs: for any // ResourceInstanceObject, only one of these attributes may be populated // and the other must be nil. // // An instance object with this field populated should be upgraded to use // Attrs at the earliest opportunity, since this legacy flatmap-based // format will be phased out over time. AttrsFlat should not be used when // writing new or updated objects to state; instead, callers must follow // the recommendations in the AttrsJSON documentation above. AttrsFlat map[string]string // AttrSensitivePaths is an array of paths to mark as sensitive coming out of // state, or to save as sensitive paths when saving state AttrSensitivePaths []cty.Path // These fields all correspond to the fields of the same name on // ResourceInstanceObject. Private []byte Status ObjectStatus Dependencies []addrs.ConfigResource CreateBeforeDestroy bool // decodeValueCache stored the decoded value for repeated decodings. decodeValueCache cty.Value // decodeIdentityCache stored the decoded identity for repeated decodings. decodeIdentityCache cty.Value } // Decode unmarshals the raw representation of the object attributes. Pass the // schema of the corresponding resource type for correct operation. // // Before calling Decode, the caller must check that the SchemaVersion field // exactly equals the version number of the schema whose implied type is being // passed, or else the result is undefined. // // If the object has an identity, the schema must also contain a resource // identity schema for the identity to be decoded. // // The returned object may share internal references with the receiver and // so the caller must not mutate the receiver any further once once this // method is called. func (os *ResourceInstanceObjectSrc) Decode(schema providers.Schema) (*ResourceInstanceObject, error) { var val cty.Value var err error attrsTy := schema.Body.ImpliedType() switch { case os.decodeValueCache != cty.NilVal: val = os.decodeValueCache case os.AttrsFlat != nil: // Legacy mode. We'll do our best to unpick this from the flatmap. val, err = hcl2shim.HCL2ValueFromFlatmap(os.AttrsFlat, attrsTy) if err != nil { return nil, err } default: val, err = ctyjson.Unmarshal(os.AttrsJSON, attrsTy) val = marks.MarkPaths(val, marks.Sensitive, os.AttrSensitivePaths) if err != nil { return nil, err } } var identity cty.Value if os.decodeIdentityCache != cty.NilVal { identity = os.decodeIdentityCache } else if os.IdentityJSON != nil && schema.Identity != nil { identity, err = ctyjson.Unmarshal(os.IdentityJSON, schema.Identity.ImpliedType()) if err != nil { return nil, fmt.Errorf("failed to decode identity: %s. This is most likely a bug in the Provider, providers must not change the identity schema without updating the identity schema version", err.Error()) } } return &ResourceInstanceObject{ Value: val, Identity: identity, Status: os.Status, Dependencies: os.Dependencies, Private: os.Private, CreateBeforeDestroy: os.CreateBeforeDestroy, }, nil } // CompleteUpgrade creates a new ResourceInstanceObjectSrc by copying the // metadata from the receiver and writing in the given new schema version // and attribute value that are presumed to have resulted from upgrading // from an older schema version. func (os *ResourceInstanceObjectSrc) CompleteUpgrade(newAttrs cty.Value, newType cty.Type, newSchemaVersion uint64) (*ResourceInstanceObjectSrc, error) { new := os.DeepCopy() new.AttrsFlat = nil // We always use JSON after an upgrade, even if the source used flatmap // This is the same principle as ResourceInstanceObject.Encode, but // avoiding a decode/re-encode cycle because we don't have type info // available for the "old" attributes. newAttrs = cty.UnknownAsNull(newAttrs) src, err := ctyjson.Marshal(newAttrs, newType) if err != nil { return nil, err } new.AttrsJSON = src new.SchemaVersion = newSchemaVersion return new, nil } func (os *ResourceInstanceObjectSrc) CompleteIdentityUpgrade(newAttrs cty.Value, schema providers.Schema) (*ResourceInstanceObjectSrc, error) { new := os.DeepCopy() src, err := ctyjson.Marshal(newAttrs, schema.Identity.ImpliedType()) if err != nil { return nil, err } new.IdentityJSON = src new.IdentitySchemaVersion = uint64(schema.IdentityVersion) return new, nil } // Equal compares two ResourceInstanceObjectSrc objects for equality, skipping // any internal fields which are not stored to the final serialized state. func (os *ResourceInstanceObjectSrc) Equal(other *ResourceInstanceObjectSrc) bool { if os == other { return true } if os == nil || other == nil { return false } if os.SchemaVersion != other.SchemaVersion { return false } if os.IdentitySchemaVersion != other.IdentitySchemaVersion { return false } if os.Status != other.Status { return false } if os.CreateBeforeDestroy != other.CreateBeforeDestroy { return false } if !bytes.Equal(os.AttrsJSON, other.AttrsJSON) { return false } if !bytes.Equal(os.IdentityJSON, other.IdentityJSON) { return false } if !bytes.Equal(os.Private, other.Private) { return false } // Compare legacy AttrsFlat maps. We shouldn't see this ever being used, but // deal with in just in case until we remove it entirely. These are all // simple maps of strings, so DeepEqual is perfectly fine here. if !reflect.DeepEqual(os.AttrsFlat, other.AttrsFlat) { return false } // We skip fields that have no functional impact on resource state. return true }