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