Files
opentf/internal/states/instance_object_full.go
Martin Atkins 468d66678d states: Separate SyncState method for removing "full" objects
Our new language runtime uses a set of new methods on SyncState to work
with its preferred "full" representation of resource instance objects, but
those are implemented in terms of methods that already existed for the old
runtime's benefit and so we need to deal with some quirks of those existing
methods.

One such quirk is that the operations to write or remove objects also want
to update some resource-level and instance-level metadata as a side-effect,
and we need to carry through that metadata even when we're intending to
completely remove a resource instance object.

To preserve our goal of leaving the existing codepaths untouched for now,
this pushes a little complexity back up into the main caller in the apply
engine, forcing it to call a different method when it knows it has deleted
an object. That new method then only takes the metadata we need and not
an actual resource instance object, so it gels better with the underlying
ModuleState methods it's implemented in terms of.

Hopefully in the long run we'll rethink the state models to not rely on
these hidden side-effects, but that's beyond the scope of our current phase
of work on the new language runtime.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2026-03-11 07:28:09 -07:00

446 lines
19 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 states
import (
"iter"
"slices"
"github.com/zclconf/go-cty/cty"
ctyjson "github.com/zclconf/go-cty/cty/json"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/lang/marks"
)
// The definitions in this file are currently for use only by the new runtime
// implementation in its "walking skeleton" phase. All code outside of that
// work-in-progress should continue to use [ResourceInstanceObject] and
// [ResourceInstanceObjectSrc], and their associated definitions in
// instance_object.go and instance_object_src.go .
//
// If at some later point we remove or rename the [ResourceInstanceObject] and
// [ResourceInstanceObjectSrc] types then the "Full"-suffixed symbol names here
// should probably also be renamed to drop that suffix, but we're waiting to
// do that until we're more sure that this is how we're going to represent
// resource instance objects in the new runtime.
// ResourceInstanceObjectFull is a variant of [ResourceInstanceObject] that
// incorporates all of the context from parent containers so that a pointer
// to an object of this type is sufficient to represent everything about
// a resource instance object without cross-referencing with a containing
// [State], [Module], [ResourceInstance], etc.
//
// Use [EncodeResourceInstanceObjectFull] with the appropriate schema from the
// appropriate provider to transform this into [ResourceInstanceObjectFullSrc].
type ResourceInstanceObjectFull = resourceInstanceObjectRepr[cty.Value]
// ResourceInstanceObjectFullSrc is to [ResourceInstanceObjectSrc] what
// [ResourceInstanceObjectFull] is to [ResourceInstanceObject]: a variant
// representation with the main value not yet decoded, so that the caller
// can delay unmarshaling until it's able to obtain the necessary schema
// from the provider.
//
// Use [DecodeResourceInstanceObjectFull] with the appropriate schema from the
// appropriate provider to transform this into [ResourceInstanceObjectFull].
type ResourceInstanceObjectFullSrc = resourceInstanceObjectRepr[ValueJSONWithMetadata]
func DecodeResourceInstanceObjectFull(src *ResourceInstanceObjectFullSrc, ty cty.Type) (*ResourceInstanceObjectFull, error) {
v, err := src.Value.Decode(ty)
if err != nil {
return nil, err
}
return mapResourceInstanceObjectReprValue(src, v), nil
}
func EncodeResourceInstanceObjectFull(obj *ResourceInstanceObjectFull, ty cty.Type) (*ResourceInstanceObjectFullSrc, error) {
vSrc, err := EncodeValueJSONWithMetadata(obj.Value, ty)
if err != nil {
return nil, err
}
return mapResourceInstanceObjectReprValue(obj, vSrc), nil
}
// ResourceInstanceObjectFull returns a snapshot of the instance object of
// the given instance address and deposed key, or nil if no such object is
// tracked.
//
// Set deposedKey to [NotDeposed] to retrieve the "current" object associated
// with the given resource instance address, if any.
//
// This is currently for use with the experimental new language runtime only.
// Callers from the old runtime should use [SyncState.ResourceInstanceObject]
// instead. The "full" form is extended so that the returned object is a
// self-sufficient description of everything we store that's relevant to the
// requested resource instance object, without the recipient needing to refer
// to any other part of the state data structure.
//
// The return value is a pointer to a copy of the object, which the caller
// may then freely access and mutate.
func (s *SyncState) ResourceInstanceObjectFull(addr addrs.AbsResourceInstanceObject) *ResourceInstanceObjectFullSrc {
s.lock.RLock()
defer s.lock.RUnlock()
rsrc := s.state.Resource(addr.InstanceAddr.ContainingResource())
if rsrc == nil {
return nil
}
inst := rsrc.Instances[addr.InstanceAddr.Resource.Key]
if inst == nil {
return nil
}
var srcObj *ResourceInstanceObjectSrc
if addr.IsCurrent() {
srcObj = inst.Current
} else {
srcObj = inst.Deposed[addr.DeposedKey]
}
if srcObj == nil {
return nil
}
// We need to shim from the disjointed representation to the unified
// form of provider instance address that the new runtime prefers.
providerInstAddr := addrs.AbsProviderInstanceCorrect{
Config: rsrc.ProviderConfig.Correct(),
Key: inst.ProviderKey,
}
// For now our "deep copy" will take the form of explicitly translating the
// [ResourceInstanceObjectSrc] representation into the equivalent
// [ResourceInstanceObjectFullSrc] representation. We'll need to do
// something different here in future if we remove the old-shaped
// representation and use the "full" representation as the primary one.
return &ResourceInstanceObjectFullSrc{
Value: ValueJSONWithMetadata{
// We're assuming here that we'll never encounter a legacy state
// snapshot that uses AttrsFlat, because that form hasn't been
// used since Terraform v0.11 and we don't support upgrading
// to OpenTofu directly from such an old version of Terraform.
ValueJSON: slices.Clone(srcObj.AttrsJSON),
SensitivePaths: slices.Collect(func(yield func(cty.Path) bool) {
for _, pvm := range srcObj.AttrSensitivePaths {
if !yield(pvm.Path) {
return
}
}
}),
},
Private: slices.Clone(srcObj.Private),
Status: srcObj.Status,
ProviderInstanceAddr: providerInstAddr,
ResourceType: addr.InstanceAddr.Resource.Resource.Type,
SchemaVersion: srcObj.SchemaVersion,
Dependencies: slices.Clone(srcObj.Dependencies),
CreateBeforeDestroy: srcObj.CreateBeforeDestroy,
}
}
// SetResourceInstanceObjectFull stores a new state for the specified resource
// instance object, overwriting an existing object with the same identity
// if present.
//
// The object must not be nil. To represent removing the record of an object
// from the state completely, use [SyncState.RemoveResourceInstanceObjectFull]
// instead.
//
// This is currently for use with the experimental new language runtime only.
// Callers from the old runtime should use [SyncState.SetResourceInstance]
// or similar instead. The "full" form is extended so that the given object is a
// self-sufficient description of everything we store that's relevant to the
// requested resource instance object.
//
// Note that our current state model cannot support different objects for
// a single resource having different provider configuration addresses, or
// different resource instances of the same resource having different provider
// instance keys, and so currently this will clobber the provider configuration
// address for other objects in the same scope if they differ. In practice our
// language doesn't permit them to differ at the time of writing anyway and
// so that's not a big deal, but we will probably want to update the state
// model at some point to remove this constraint that isn't actually necessary
// for the new language runtime.
func (s *SyncState) SetResourceInstanceObjectFull(addr addrs.AbsResourceInstanceObject, obj *ResourceInstanceObjectFullSrc) {
s.lock.Lock()
defer s.lock.Unlock()
if obj == nil {
// If you see a panic here, the caller probably ought to have called
// [SyncState.RemoveResourceInstanceObjectFull] instead.
panic("called SetResourceInstanceObjectFull with nil object")
}
// Currently this is a wrapper around various other methods as we
// shim the new-style representation to fit the traditional representation.
ms := s.state.EnsureModule(addr.InstanceAddr.Module)
providerConfigAddr, providerInstanceKey := lossyProviderConfigAddrFromProviderInstanceAddr(obj.ProviderInstanceAddr)
smallerObj := &ResourceInstanceObjectSrc{
AttrsJSON: obj.Value.ValueJSON,
SchemaVersion: obj.SchemaVersion,
Status: obj.Status,
Private: obj.Private,
Dependencies: obj.Dependencies,
CreateBeforeDestroy: obj.CreateBeforeDestroy,
}
if len(obj.Value.SensitivePaths) != 0 {
smallerObj.AttrSensitivePaths = make([]cty.PathValueMarks, len(obj.Value.SensitivePaths))
marks := cty.NewValueMarks(marks.Sensitive)
for i, path := range obj.Value.SensitivePaths {
smallerObj.AttrSensitivePaths[i] = cty.PathValueMarks{
Path: path,
Marks: marks,
}
}
}
if addr.IsCurrent() {
ms.SetResourceInstanceCurrent(addr.InstanceAddr.Resource, smallerObj, providerConfigAddr, providerInstanceKey)
} else {
ms.SetResourceInstanceDeposed(addr.InstanceAddr.Resource, addr.DeposedKey, smallerObj, providerConfigAddr, providerInstanceKey)
}
s.maybePruneModule(addr.InstanceAddr.Module)
}
// RemoveResourceInstanceObjectFull removes any representation of the given
// resource instance object from the state.
//
// The providerInstAddr argument should be the address of the provider instance
// that was used to delete the corresponding remote object. This strange API
// quirk exists because changes to resource instance objects currently also
// implicitly update resource-level and instance-level metadata.
func (s *SyncState) RemoveResourceInstanceObjectFull(addr addrs.AbsResourceInstanceObject, providerInstAddr addrs.AbsProviderInstanceCorrect) {
// FIXME: Rework this so that removing an object does _not_ also implicitly
// update the resource-level and instance-level provider tracking, which
// is only currently necessary because we're trying to use preexisting
// [ModuleState] methods without modifying them.
ms := s.state.Module(addr.InstanceAddr.Module)
if ms == nil {
// If the containing module instance doesn't exist in the state at all
// then we have nothing to remove.
return
}
providerConfigAddr, providerInstanceKey := lossyProviderConfigAddrFromProviderInstanceAddr(providerInstAddr)
// These [ModuleState] methods represent "remove" by the caller passing a
// nil object, rather than by having a separate "remove" method.
if addr.IsCurrent() {
ms.SetResourceInstanceCurrent(addr.InstanceAddr.Resource, nil, providerConfigAddr, providerInstanceKey)
} else {
ms.SetResourceInstanceDeposed(addr.InstanceAddr.Resource, addr.DeposedKey, nil, providerConfigAddr, providerInstanceKey)
}
s.maybePruneModule(addr.InstanceAddr.Module)
}
// ResourceInstanceObjectFullRepr is the generic type that both
// [ResourceInstanceObjectFull] and [ResourceInstanceObjectFullSrc] are based
// on, since they vary only by the type of the Value field.
type resourceInstanceObjectRepr[V interface {
cty.Value | ValueJSONWithMetadata
}] struct {
// Value is the object-typed value representing the remote object within
// OpenTofu.
Value V
// Private is an opaque value set by the provider when this object was
// last created or updated. OpenTofu 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
// ProviderInstanceAddr is the fully-qualified address for the provider
// instance that produced the data in the Value and Private fields, and
// so which should be used to refresh and destroy this object if there's
// no provider instance selection still present in the configuration.
ProviderInstanceAddr addrs.AbsProviderInstanceCorrect
// ResourceType is the resource type name as would be understood by the
// provider given in ProviderInstanceAddr. This is captured explicitly
// here, rather than just implied by context, so that when we're
// dealing with an implicit conversion from one resource type to another
// we can distinguish whether we've already performed that conversion
// yet or not, and directly compare before and after objects to determine
// whether such a conversion has occurred.
ResourceType string
// 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
// Dependencies is a set of absolute address to other resources this
// instance depended 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.
//
// FIXME: In the long run under the new runtime this should probably be
// []addrs.AbsResourceInstance instead because the new runtime can track
// dependencies more precisely, but this is using ConfigResource for now
// just because that needs less shimming from the current underlying
// representation, and so we can wait until we better understand what the
// caller needs before we spend time implementing that.
//
// Use [State.InstancesMatchingConfigResource] to find all of the
// instances that match an address in this slice, which should include
// _at least_ the same instances that ought to have been recorded here,
// along with some spurious extras that we match due to the lossiness
// of this representation.
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
}
func mapResourceInstanceObjectReprValue[V1, V2 interface {
cty.Value | ValueJSONWithMetadata
}](input *resourceInstanceObjectRepr[V1], newValue V2) *resourceInstanceObjectRepr[V2] {
return &resourceInstanceObjectRepr[V2]{
Value: newValue,
Private: input.Private,
Status: input.Status,
ProviderInstanceAddr: input.ProviderInstanceAddr,
ResourceType: input.ResourceType,
SchemaVersion: input.SchemaVersion,
Dependencies: input.Dependencies,
CreateBeforeDestroy: input.CreateBeforeDestroy,
}
}
type ValueJSONWithMetadata struct {
// ValueJSON is a JSON representation of the cty value.
ValueJSON []byte
// SensitivePaths is an array of paths to mark as sensitive when decoding.
SensitivePaths []cty.Path
}
func (vj ValueJSONWithMetadata) Decode(ty cty.Type) (cty.Value, error) {
unmarkedV, err := ctyjson.Unmarshal(vj.ValueJSON, ty)
if err != nil {
return cty.NilVal, err
}
if len(vj.SensitivePaths) == 0 {
// If we don't have any marks to apply then we can skip a recursive
// walk dealing with those.
return unmarkedV, nil
}
// The following is O(Npaths*Nmarks), but Npaths is usually small for
// typical resource types, and Nmarks should never be greater than Npaths
// in a canonical representation (since paths that don't exist cannot
// be marked), so it isn't worth the cost of trying to do something
// cleverer.
return cty.Transform(unmarkedV, func(path cty.Path, pathV cty.Value) (cty.Value, error) {
for _, sensitivePath := range vj.SensitivePaths {
if sensitivePath.Equals(path) {
return pathV.Mark(marks.Sensitive), nil
}
}
return pathV, nil
})
}
func EncodeValueJSONWithMetadata(v cty.Value, ty cty.Type) (ValueJSONWithMetadata, error) {
var ret ValueJSONWithMetadata
unmarkedV, pvms := v.UnmarkDeepWithPaths()
src, err := ctyjson.Marshal(unmarkedV, ty)
if err != nil {
return ret, err
}
ret.ValueJSON = src
if len(pvms) != 0 {
ret.SensitivePaths = make([]cty.Path, 0, len(pvms))
for _, pvm := range pvms {
for mark := range pvm.Marks {
if mark != marks.Sensitive {
// The caller is expected to strip out any other marks and
// handle them in some reasonable way before calling this
// function, to make sure that we don't silently lose
// unexpected marks during marshaling.
return ret, pvm.Path.NewErrorf("cannot encode value with mark %#v", mark)
}
ret.SensitivePaths = append(ret.SensitivePaths, pvm.Path)
}
}
}
return ret, nil
}
// InstancesMatchingConfigResource is an adapter to help deal with the fact
// that our state model current represents dependencies between whole resources
// using their unexpanded addresses, but in the new experimental language
// runtime we want to work in relationships between individual resource
// instances instead.
//
// Given an [addrs.ConfigResource] address, the result is every
// [addrs.AbsResourceInstance] that matches the given address and has a current
// object in the state.
//
// TODO: Consider changing the state model so that we track relationships
// between resource instances as the _main_ representation, rather than throwing
// that information away and then trying to recover it lossily later.
func (s *State) InstancesMatchingConfigResource(addr addrs.ConfigResource) iter.Seq[addrs.AbsResourceInstance] {
// (This function is lurking in here just because it's exclusively for
// the new experimental langauge runtime and this is the file where we're
// gathering all of its state model extensions. It's not actually
// particularly related to "full" state object representations except that
// it's helping to compensate for a current concession we're making in
// _not_ representing dependencies properly in the "full" representations.)
return func(yield func(addrs.AbsResourceInstance) bool) {
for _, ms := range s.Modules {
if !ms.Addr.IsForModule(addr.Module) {
continue
}
for _, rs := range ms.Resources {
if !rs.Addr.Resource.Equal(addr.Resource) {
continue
}
for key, is := range rs.Instances {
if is.Current == nil {
continue // instances that only have deposed objects are not eligible
}
if !yield(rs.Addr.Instance(key)) {
return
}
}
}
}
}
}
// lossyProviderConfigAddrFromProviderInstanceAddr is a lossy adapter for
// converting from the "correct" representation of provider instance addresses
// in [addrs.AbsProviderInstanceCorrect] to the incomplete, legacy
// representation that our state models are still currently using.
//
// In particular, it loses any instance keys of modules in the module address,
// because the old runtime did not permit provider configurations inside
// multi-instanced modules.
//
// FIXME: Update our underlying model to support this more generally, once we're
// confident enough about the new runtime to risk changes that could impact code
// from the old runtime. At that point we can hopefully remove this helper
// function altogether.
func lossyProviderConfigAddrFromProviderInstanceAddr(addr addrs.AbsProviderInstanceCorrect) (addrs.AbsProviderConfig, addrs.InstanceKey) {
configAddr := addrs.AbsProviderConfig{
Module: addr.Config.Module.Module(),
Provider: addr.Config.Config.Provider,
Alias: addr.Config.Config.Alias,
}
instanceKey := addr.Key
return configAddr, instanceKey
}