// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package states import ( "fmt" "sort" "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/lang/format" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/providers" ) // ResourceInstanceObject is the local representation of a specific remote // object associated with a resource instance. In practice not all remote // objects are actually remote in the sense of being accessed over the network, // but this is the most common case. // // It is not valid to mutate a ResourceInstanceObject once it has been created. // Instead, create a new object and replace the existing one. type ResourceInstanceObject struct { // Value is the object-typed value representing the remote object within // Terraform. Value cty.Value // Identity is the object-typed value representing the identity of the remote // object within Terraform. Identity cty.Value // Private is an opaque value set by the provider when this object was // last created or updated. Terraform Core does not use this value in // any way and it is not exposed anywhere in the user interface, so // a provider can use it for retaining any necessary private state. Private []byte // Status represents the "readiness" of the object as of the last time // it was updated. Status ObjectStatus // Dependencies is a set of absolute address to other resources this // instance dependeded on when it was applied. This is used to construct // the dependency relationships for an object whose configuration is no // longer available, such as if it has been removed from configuration // altogether, or is now deposed. Dependencies []addrs.ConfigResource // CreateBeforeDestroy reflects the status of the lifecycle // create_before_destroy option when this instance was last updated. // Because create_before_destroy also effects the overall ordering of the // destroy operations, we need to record the status to ensure a resource // removed from the config will still be destroyed in the same manner. CreateBeforeDestroy bool } // NewResourceInstanceObjectFromIR converts the receiving // ImportedResource into a ResourceInstanceObject that has status ObjectReady. // // The returned object does not know its own resource type, so the caller must // retain the ResourceType value from the source object if this information is // needed. // // The returned object also has no dependency addresses, but the caller may // freely modify the direct fields of the returned object without affecting // the receiver. func NewResourceInstanceObjectFromIR(ir providers.ImportedResource) *ResourceInstanceObject { return &ResourceInstanceObject{ Status: ObjectReady, Value: ir.State, Private: ir.Private, Identity: ir.Identity, } } // ObjectStatus represents the status of a RemoteObject. type ObjectStatus rune //go:generate go tool golang.org/x/tools/cmd/stringer -type ObjectStatus const ( // ObjectReady is an object status for an object that is ready to use. ObjectReady ObjectStatus = 'R' // ObjectTainted is an object status representing an object that is in // an unrecoverable bad state due to a partial failure during a create, // update, or delete operation. Since it cannot be moved into the // ObjectRead state, a tainted object must be replaced. ObjectTainted ObjectStatus = 'T' // ObjectPlanned is a special object status used only for the transient // placeholder objects we place into state during the refresh and plan // walks to stand in for objects that will be created during apply. // // Any object of this status must have a corresponding change recorded // in the current plan, whose value must then be used in preference to // the value stored in state when evaluating expressions. A planned // object stored in state will be incomplete if any of its attributes are // not yet known, and the plan must be consulted in order to "see" those // unknown values, because the state is not able to represent them. ObjectPlanned ObjectStatus = 'P' ) // Encode marshals values within the receiver to produce a // ResourceInstanceObjectSrc ready to be written to a state file. // // The schema must contain the resource type body, and the given value must // conform its implied type. The schema must also contain the version number // of the schema, which will be recorded in the source object so it can be // used to detect when schema migration is required on read. // The schema may also contain an resource identity schema and version number, // which will be used to encode the resource identity. // // 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 (o *ResourceInstanceObject) Encode(schema providers.Schema) (*ResourceInstanceObjectSrc, error) { // If it contains marks, remove these marks before traversing the // structure with UnknownAsNull, and save the PathValueMarks // so we can save them in state. val, sensitivePaths, err := unmarkValueForStorage(o.Value) if err != nil { return nil, err } // Our state serialization can't represent unknown values, so we convert // them to nulls here. This is lossy, but nobody should be writing unknown // values here and expecting to get them out again later. // // We get unknown values here while we're building out a "planned state" // during the plan phase, but the value stored in the plan takes precedence // for expression evaluation. The apply step should never produce unknown // values, but if it does it's the responsibility of the caller to detect // and raise an error about that. val = cty.UnknownAsNull(val) src, err := ctyjson.Marshal(val, schema.Body.ImpliedType()) if err != nil { return nil, err } var idJSON []byte // If the Identity is known and not null we can marshal it. if !o.Identity.IsNull() && o.Identity.IsWhollyKnown() && schema.Identity != nil { idJSON, err = ctyjson.Marshal(o.Identity, schema.Identity.ImpliedType()) if err != nil { return nil, err } } // Dependencies are collected and merged in an unordered format (using map // keys as a set), then later changed to a slice (in random ordering) to be // stored in state as an array. To avoid pointless thrashing of state in // refresh-only runs, we can either override comparison of dependency lists // (more desirable, but tricky for Reasons) or just sort when encoding. // Encoding of instances can happen concurrently, so we must copy the // dependencies to avoid mutating what may be a shared array of values. dependencies := make([]addrs.ConfigResource, len(o.Dependencies)) copy(dependencies, o.Dependencies) sort.Slice(dependencies, func(i, j int) bool { return dependencies[i].String() < dependencies[j].String() }) return &ResourceInstanceObjectSrc{ SchemaVersion: uint64(schema.Version), AttrsJSON: src, AttrSensitivePaths: sensitivePaths, Private: o.Private, Status: o.Status, Dependencies: dependencies, CreateBeforeDestroy: o.CreateBeforeDestroy, IdentityJSON: idJSON, IdentitySchemaVersion: uint64(schema.IdentityVersion), // The cached value must have all its marks since it bypasses decoding. decodeValueCache: o.Value, decodeIdentityCache: o.Identity, }, nil } // AsTainted returns a deep copy of the receiver with the status updated to // ObjectTainted. func (o *ResourceInstanceObject) AsTainted() *ResourceInstanceObject { if o == nil { // A nil object can't be tainted, but we'll allow this anyway to // avoid a crash, since we presumably intend to eventually record // the object has having been deleted anyway. return nil } ret := o.DeepCopy() ret.Status = ObjectTainted return ret } // unmarkValueForStorage takes a value that possibly contains marked values // and returns an equal value without markings along with the separated mark // metadata that should be stored alongside the value in another field. // // This function only accepts the marks that are valid to store, and so will // return an error if other marks are present. Marks that this package doesn't // know how to store must be dealt with somehow by a caller -- presumably by // replacing each marked value with some sort of storage placeholder -- before // writing a value into the state. func unmarkValueForStorage(v cty.Value) (unmarkedV cty.Value, sensitivePaths []cty.Path, err error) { val, pvms := v.UnmarkDeepWithPaths() sensitivePaths, withOtherMarks := marks.PathsWithMark(pvms, marks.Sensitive) _, withOtherMarks = marks.PathsWithMark(withOtherMarks, marks.Deprecation) if len(withOtherMarks) != 0 { return cty.NilVal, nil, fmt.Errorf( "%s: cannot serialize value marked as %#v for inclusion in a state snapshot (this is a bug in Terraform)", format.CtyPath(withOtherMarks[0].Path), withOtherMarks[0].Marks, ) } // sort the sensitive paths for consistency in comparison and serialization sort.Slice(sensitivePaths, func(i, j int) bool { // use our human-readable format of paths for comparison return format.CtyPath(sensitivePaths[i]) < format.CtyPath(sensitivePaths[j]) }) return val, sensitivePaths, nil }