Files
opentf/internal/states/instance_object_full_test.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

240 lines
7.3 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 (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/lang/marks"
"github.com/zclconf/go-cty-debug/ctydebug"
"github.com/zclconf/go-cty/cty"
)
// The tests in here are currently pretty minimal as a compromise while we're
// still trying to learn if this new representation is even a viable approach.
// The focus is mainly on whether we're correcting translating from and to the
// traditional old state representation, since we'll still be using the old
// models as our primary representation for now so we can continue to use
// unmodified state manager implementations, etc.
//
// TODO: If these new codepaths survive beyond the early exploratory work
// for the new language runtime then we should consider what fuller testing
// might be helpful here, and implement it.
func TestSyncStateResourceInstanceObjectFull(t *testing.T) {
instAddrRel := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "foo",
Name: "baz",
}.Instance(addrs.NoKey)
instAddrAbs := instAddrRel.Absolute(addrs.RootModuleInstance)
providerInstAddr := addrs.AbsProviderInstanceCorrect{
Config: addrs.AbsProviderConfigCorrect{
Module: addrs.RootModuleInstance.Child("mod", addrs.NoKey),
Config: addrs.ProviderConfigCorrect{
Provider: addrs.NewBuiltInProvider("foo"),
},
},
}
depAddr := addrs.ConfigResource{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "foo",
Name: "dependency",
},
}
deposedKey := DeposedKey("...")
legacyProviderConfigAddr := addrs.AbsProviderConfig{
Module: providerInstAddr.Config.Module.Module(),
Provider: providerInstAddr.Config.Config.Provider,
}
objTy := cty.Object(map[string]cty.Type{
"a": cty.Number,
"b": cty.Number,
})
s := NewState()
s.RootModule().SetResourceInstanceCurrent(instAddrRel, &ResourceInstanceObjectSrc{
Status: ObjectReady,
AttrsJSON: []byte(`{"a":1,"b":2}`),
AttrSensitivePaths: []cty.PathValueMarks{
{Path: cty.GetAttrPath("b"), Marks: cty.NewValueMarks(marks.Sensitive)},
},
Private: []byte(`private`),
CreateBeforeDestroy: true,
SchemaVersion: 5,
Dependencies: []addrs.ConfigResource{
depAddr,
},
}, legacyProviderConfigAddr, addrs.NoKey)
s.RootModule().SetResourceInstanceDeposed(instAddrRel, deposedKey, &ResourceInstanceObjectSrc{
Status: ObjectReady,
AttrsJSON: []byte(`{"a":0,"b":1}`),
}, legacyProviderConfigAddr, addrs.NoKey)
ss := s.SyncWrapper()
t.Run("current object", func(t *testing.T) {
gotObjSrc := ss.ResourceInstanceObjectFull(instAddrAbs.CurrentObject())
gotObj, err := DecodeResourceInstanceObjectFull(gotObjSrc, objTy)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
wantObj := &ResourceInstanceObjectFull{
Status: ObjectReady,
Value: cty.ObjectVal(map[string]cty.Value{
"a": cty.NumberIntVal(1),
"b": cty.NumberIntVal(2).Mark(marks.Sensitive),
}),
Private: []byte(`private`),
CreateBeforeDestroy: true,
SchemaVersion: 5,
ProviderInstanceAddr: providerInstAddr,
ResourceType: "foo",
Dependencies: []addrs.ConfigResource{
depAddr,
},
}
if diff := cmp.Diff(wantObj, gotObj, ctydebug.CmpOptions); diff != "" {
t.Error("wrong result\n" + diff)
}
})
t.Run("deposed object", func(t *testing.T) {
gotObjSrc := ss.ResourceInstanceObjectFull(instAddrAbs.Object(deposedKey))
gotObj, err := DecodeResourceInstanceObjectFull(gotObjSrc, objTy)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
wantObj := &ResourceInstanceObjectFull{
Status: ObjectReady,
Value: cty.ObjectVal(map[string]cty.Value{
"a": cty.NumberIntVal(0),
"b": cty.NumberIntVal(1),
}),
ProviderInstanceAddr: providerInstAddr,
ResourceType: "foo",
}
if diff := cmp.Diff(wantObj, gotObj, ctydebug.CmpOptions); diff != "" {
t.Error("wrong result\n" + diff)
}
})
}
func TestSyncStateSetResourceInstanceObjectFull(t *testing.T) {
instAddrRel := addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "foo",
Name: "baz",
}.Instance(addrs.NoKey)
instAddrAbs := instAddrRel.Absolute(addrs.RootModuleInstance)
providerInstAddr := addrs.AbsProviderInstanceCorrect{
Config: addrs.AbsProviderConfigCorrect{
Module: addrs.RootModuleInstance.Child("mod", addrs.NoKey),
Config: addrs.ProviderConfigCorrect{
Provider: addrs.NewBuiltInProvider("foo"),
},
},
}
depAddr := addrs.ConfigResource{
Resource: addrs.Resource{
Mode: addrs.ManagedResourceMode,
Type: "foo",
Name: "dependency",
},
}
deposedKey := DeposedKey("...")
legacyProviderConfigAddr := addrs.AbsProviderConfig{
Module: providerInstAddr.Config.Module.Module(),
Provider: providerInstAddr.Config.Config.Provider,
}
objTy := cty.Object(map[string]cty.Type{
"a": cty.Number,
"b": cty.Number,
})
currentObj := &ResourceInstanceObjectFull{
Status: ObjectReady,
Value: cty.ObjectVal(map[string]cty.Value{
"a": cty.NumberIntVal(1),
"b": cty.NumberIntVal(2).Mark(marks.Sensitive),
}),
Private: []byte(`private`),
CreateBeforeDestroy: true,
SchemaVersion: 5,
ProviderInstanceAddr: providerInstAddr,
ResourceType: "foo",
Dependencies: []addrs.ConfigResource{
depAddr,
},
}
deposedObj := &ResourceInstanceObjectFull{
Status: ObjectReady,
Value: cty.ObjectVal(map[string]cty.Value{
"a": cty.NumberIntVal(0),
"b": cty.NumberIntVal(1),
}),
ProviderInstanceAddr: providerInstAddr,
ResourceType: "foo",
}
currentObjSrc, err := EncodeResourceInstanceObjectFull(currentObj, objTy)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
deposedObjSrc, err := EncodeResourceInstanceObjectFull(deposedObj, objTy)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
gotState := BuildState(func(ss *SyncState) {
ss.SetResourceInstanceObjectFull(instAddrAbs.Object(deposedKey), deposedObjSrc)
ss.SetResourceInstanceObjectFull(instAddrAbs.CurrentObject(), currentObjSrc)
})
wantState := &State{
Modules: map[string]*Module{
"": {
Addr: addrs.RootModuleInstance,
Resources: map[string]*Resource{
"foo.baz": {
Addr: instAddrAbs.ContainingResource(),
ProviderConfig: legacyProviderConfigAddr,
Instances: map[addrs.InstanceKey]*ResourceInstance{
addrs.NoKey: {
Current: &ResourceInstanceObjectSrc{
Status: ObjectReady,
AttrsJSON: []byte(`{"a":1,"b":2}`),
AttrSensitivePaths: []cty.PathValueMarks{
{Path: cty.GetAttrPath("b"), Marks: cty.NewValueMarks(marks.Sensitive)},
},
Private: []byte(`private`),
CreateBeforeDestroy: true,
SchemaVersion: 5,
Dependencies: []addrs.ConfigResource{
depAddr,
},
},
Deposed: map[DeposedKey]*ResourceInstanceObjectSrc{
deposedKey: {
Status: ObjectReady,
AttrsJSON: []byte(`{"a":0,"b":1}`),
},
},
},
},
},
},
LocalValues: make(map[string]cty.Value),
OutputValues: make(map[string]*OutputValue),
},
},
}
if diff := cmp.Diff(wantState, gotState); diff != "" {
t.Error("wrong result\n" + diff)
}
}