terraform: refactor EvalWriteStateDeposed (#27149)
* terraform: refactor EvalWriteStateDeposed EvalWriteStateDeposed is now NodeDestroyDeposedResourceInstanceObject.writeResourceInstanceState. Since that's the only caller I considered putting the logic directly inline, but things are clunky enough right now that I think this is good enough for this refactor.
This commit is contained in:
parent
ae025248cc
commit
bedc08f5eb
|
@ -1,126 +0,0 @@
|
|||
package terraform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/hashicorp/terraform/addrs"
|
||||
"github.com/hashicorp/terraform/states"
|
||||
"github.com/hashicorp/terraform/tfdiags"
|
||||
)
|
||||
|
||||
type phaseState int
|
||||
|
||||
const (
|
||||
workingState phaseState = iota
|
||||
refreshState
|
||||
)
|
||||
|
||||
// UpdateStateHook calls the PostStateUpdate hook with the current state.
|
||||
func UpdateStateHook(ctx EvalContext) error {
|
||||
// In principle we could grab the lock here just long enough to take a
|
||||
// deep copy and then pass that to our hooks below, but we'll instead
|
||||
// hold the hook for the duration to avoid the potential confusing
|
||||
// situation of us racing to call PostStateUpdate concurrently with
|
||||
// different state snapshots.
|
||||
stateSync := ctx.State()
|
||||
state := stateSync.Lock().DeepCopy()
|
||||
defer stateSync.Unlock()
|
||||
|
||||
// Call the hook
|
||||
err := ctx.Hook(func(h Hook) (HookAction, error) {
|
||||
return h.PostStateUpdate(state)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// EvalWriteStateDeposed is an EvalNode implementation that writes
|
||||
// an InstanceState out to the Deposed list of a resource in the state.
|
||||
type EvalWriteStateDeposed struct {
|
||||
// Addr is the address of the instance to read state for.
|
||||
Addr addrs.ResourceInstance
|
||||
|
||||
// Key indicates which deposed object to write to.
|
||||
Key states.DeposedKey
|
||||
|
||||
// State is the object state to save.
|
||||
State **states.ResourceInstanceObject
|
||||
|
||||
// ProviderSchema is the schema for the provider given in ProviderAddr.
|
||||
ProviderSchema **ProviderSchema
|
||||
|
||||
// ProviderAddr is the address of the provider configuration that
|
||||
// produced the given object.
|
||||
ProviderAddr addrs.AbsProviderConfig
|
||||
}
|
||||
|
||||
func (n *EvalWriteStateDeposed) Eval(ctx EvalContext) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
if n.State == nil {
|
||||
// Note that a pointer _to_ nil is valid here, indicating the total
|
||||
// absense of an object as we'd see during destroy.
|
||||
panic("EvalWriteStateDeposed used with no ResourceInstanceObject")
|
||||
}
|
||||
|
||||
absAddr := n.Addr.Absolute(ctx.Path())
|
||||
key := n.Key
|
||||
state := ctx.State()
|
||||
|
||||
if key == states.NotDeposed {
|
||||
// should never happen
|
||||
diags = diags.Append(fmt.Errorf("can't save deposed object for %s without a deposed key; this is a bug in Terraform that should be reported", absAddr))
|
||||
return diags
|
||||
}
|
||||
|
||||
obj := *n.State
|
||||
if obj == nil {
|
||||
// No need to encode anything: we'll just write it directly.
|
||||
state.SetResourceInstanceDeposed(absAddr, key, nil, n.ProviderAddr)
|
||||
log.Printf("[TRACE] EvalWriteStateDeposed: removing state object for %s deposed %s", absAddr, key)
|
||||
return diags
|
||||
}
|
||||
if n.ProviderSchema == nil || *n.ProviderSchema == nil {
|
||||
// Should never happen, unless our state object is nil
|
||||
panic("EvalWriteStateDeposed used with no ProviderSchema object")
|
||||
}
|
||||
|
||||
schema, currentVersion := (*n.ProviderSchema).SchemaForResourceAddr(n.Addr.ContainingResource())
|
||||
if schema == nil {
|
||||
// It shouldn't be possible to get this far in any real scenario
|
||||
// without a schema, but we might end up here in contrived tests that
|
||||
// fail to set up their world properly.
|
||||
diags = diags.Append(fmt.Errorf("failed to encode %s in state: no resource type schema available", absAddr))
|
||||
return diags
|
||||
}
|
||||
src, err := obj.Encode(schema.ImpliedType(), currentVersion)
|
||||
if err != nil {
|
||||
diags = diags.Append(fmt.Errorf("failed to encode %s in state: %s", absAddr, err))
|
||||
return diags
|
||||
}
|
||||
|
||||
log.Printf("[TRACE] EvalWriteStateDeposed: writing state object for %s deposed %s", absAddr, key)
|
||||
state.SetResourceInstanceDeposed(absAddr, key, src, n.ProviderAddr)
|
||||
return diags
|
||||
}
|
||||
|
||||
// EvalDeposeState is an EvalNode implementation that moves the current object
|
||||
// for the given instance to instead be a deposed object, leaving the instance
|
||||
// with no current object.
|
||||
// This is used at the beginning of a create-before-destroy replace action so
|
||||
// that the create can create while preserving the old state of the
|
||||
// to-be-destroyed object.
|
||||
type EvalDeposeState struct {
|
||||
Addr addrs.ResourceInstance
|
||||
|
||||
// ForceKey, if a value other than states.NotDeposed, will be used as the
|
||||
// key for the newly-created deposed object that results from this action.
|
||||
// If set to states.NotDeposed (the zero value), a new unique key will be
|
||||
// allocated.
|
||||
ForceKey states.DeposedKey
|
||||
|
||||
// OutputKey, if non-nil, will be written with the deposed object key that
|
||||
// was generated for the object. This can then be passed to
|
||||
// EvalUndeposeState.Key so it knows which deposed instance to forget.
|
||||
OutputKey *states.DeposedKey
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
package terraform
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/hashicorp/terraform/addrs"
|
||||
"github.com/hashicorp/terraform/configs/configschema"
|
||||
"github.com/hashicorp/terraform/states"
|
||||
)
|
||||
|
||||
func TestEvalWriteStateDeposed(t *testing.T) {
|
||||
state := states.NewState()
|
||||
ctx := new(MockEvalContext)
|
||||
ctx.StateState = state.SyncWrapper()
|
||||
ctx.PathPath = addrs.RootModuleInstance
|
||||
|
||||
mockProvider := mockProviderWithResourceTypeSchema("aws_instance", &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
providerSchema := mockProvider.GetSchemaReturn
|
||||
|
||||
obj := &states.ResourceInstanceObject{
|
||||
Value: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("i-abc123"),
|
||||
}),
|
||||
Status: states.ObjectReady,
|
||||
}
|
||||
node := &EvalWriteStateDeposed{
|
||||
Addr: addrs.Resource{
|
||||
Mode: addrs.ManagedResourceMode,
|
||||
Type: "aws_instance",
|
||||
Name: "foo",
|
||||
}.Instance(addrs.NoKey),
|
||||
Key: states.DeposedKey("deadbeef"),
|
||||
|
||||
State: &obj,
|
||||
|
||||
ProviderSchema: &providerSchema,
|
||||
ProviderAddr: addrs.RootModuleInstance.ProviderConfigDefault(addrs.NewDefaultProvider("aws")),
|
||||
}
|
||||
diags := node.Eval(ctx)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("Got err: %#v", diags.ErrWithWarnings())
|
||||
}
|
||||
|
||||
checkStateString(t, state, `
|
||||
aws_instance.foo: (1 deposed)
|
||||
ID = <not created>
|
||||
provider = provider["registry.terraform.io/hashicorp/aws"]
|
||||
Deposed ID 1 = i-abc123
|
||||
`)
|
||||
}
|
||||
|
||||
func TestUpdateStateHook(t *testing.T) {
|
||||
mockHook := new(MockHook)
|
||||
|
||||
state := states.NewState()
|
||||
state.Module(addrs.RootModuleInstance).SetLocalValue("foo", cty.StringVal("hello"))
|
||||
|
||||
ctx := new(MockEvalContext)
|
||||
ctx.HookHook = mockHook
|
||||
ctx.StateState = state.SyncWrapper()
|
||||
|
||||
if err := UpdateStateHook(ctx); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if !mockHook.PostStateUpdateCalled {
|
||||
t.Fatal("should call PostStateUpdate")
|
||||
}
|
||||
if mockHook.PostStateUpdateState.LocalValue(addrs.LocalValue{Name: "foo"}.Absolute(addrs.RootModuleInstance)) != cty.StringVal("hello") {
|
||||
t.Fatalf("wrong state passed to hook: %s", spew.Sdump(mockHook.PostStateUpdateState))
|
||||
}
|
||||
}
|
|
@ -245,6 +245,13 @@ func (n *NodeAbstractResourceInstance) PostApplyHook(ctx EvalContext, state *sta
|
|||
return diags
|
||||
}
|
||||
|
||||
type phaseState int
|
||||
|
||||
const (
|
||||
workingState phaseState = iota
|
||||
refreshState
|
||||
)
|
||||
|
||||
// writeResourceInstanceState saves the given object as the current object for
|
||||
// the selected resource instance.
|
||||
//
|
||||
|
|
|
@ -2,6 +2,7 @@ package terraform
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/hashicorp/terraform/addrs"
|
||||
"github.com/hashicorp/terraform/dag"
|
||||
|
@ -227,14 +228,7 @@ func (n *NodeDestroyDeposedResourceInstanceObject) Execute(ctx EvalContext, op w
|
|||
// Always write the resource back to the state deposed. If it
|
||||
// was successfully destroyed it will be pruned. If it was not, it will
|
||||
// be caught on the next run.
|
||||
writeStateDeposed := &EvalWriteStateDeposed{
|
||||
Addr: addr,
|
||||
Key: n.DeposedKey,
|
||||
ProviderAddr: n.ResolvedProvider,
|
||||
ProviderSchema: &providerSchema,
|
||||
State: &state,
|
||||
}
|
||||
diags = diags.Append(writeStateDeposed.Eval(ctx))
|
||||
diags = diags.Append(n.writeResourceInstanceState(ctx, state))
|
||||
if diags.HasErrors() {
|
||||
return diags
|
||||
}
|
||||
|
@ -273,3 +267,46 @@ type graphNodeDeposer struct {
|
|||
func (n *graphNodeDeposer) SetPreallocatedDeposedKey(key states.DeposedKey) {
|
||||
n.PreallocatedDeposedKey = key
|
||||
}
|
||||
|
||||
func (n *NodeDestroyDeposedResourceInstanceObject) writeResourceInstanceState(ctx EvalContext, obj *states.ResourceInstanceObject) error {
|
||||
absAddr := n.Addr
|
||||
key := n.DeposedKey
|
||||
state := ctx.State()
|
||||
|
||||
if key == states.NotDeposed {
|
||||
// should never happen
|
||||
return fmt.Errorf("can't save deposed object for %s without a deposed key; this is a bug in Terraform that should be reported", absAddr)
|
||||
}
|
||||
|
||||
if obj == nil {
|
||||
// No need to encode anything: we'll just write it directly.
|
||||
state.SetResourceInstanceDeposed(absAddr, key, nil, n.ResolvedProvider)
|
||||
log.Printf("[TRACE] writeResourceInstanceStateDeposed: removing state object for %s deposed %s", absAddr, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
_, providerSchema, err := GetProvider(ctx, n.ResolvedProvider)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if providerSchema == nil {
|
||||
// Should never happen, unless our state object is nil
|
||||
panic("writeResourceInstanceStateDeposed used with no ProviderSchema object")
|
||||
}
|
||||
|
||||
schema, currentVersion := providerSchema.SchemaForResourceAddr(absAddr.ContainingResource().Resource)
|
||||
if schema == nil {
|
||||
// It shouldn't be possible to get this far in any real scenario
|
||||
// without a schema, but we might end up here in contrived tests that
|
||||
// fail to set up their world properly.
|
||||
return fmt.Errorf("failed to encode %s in state: no resource type schema available", absAddr)
|
||||
}
|
||||
src, err := obj.Encode(schema.ImpliedType(), currentVersion)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode %s in state: %s", absAddr, err)
|
||||
}
|
||||
|
||||
log.Printf("[TRACE] writeResourceInstanceStateDeposed: writing state object for %s deposed %s", absAddr, key)
|
||||
state.SetResourceInstanceDeposed(absAddr, key, src, n.ResolvedProvider)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -128,3 +128,47 @@ func TestNodeDestroyDeposedResourceInstanceObject_Execute(t *testing.T) {
|
|||
t.Fatalf("resources left in state after destroy")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeDestroyDeposedResourceInstanceObject_WriteResourceInstanceState(t *testing.T) {
|
||||
state := states.NewState()
|
||||
ctx := new(MockEvalContext)
|
||||
ctx.StateState = state.SyncWrapper()
|
||||
ctx.PathPath = addrs.RootModuleInstance
|
||||
mockProvider := mockProviderWithResourceTypeSchema("aws_instance", &configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"id": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
ctx.ProviderProvider = mockProvider
|
||||
ctx.ProviderSchemaSchema = mockProvider.GetSchemaReturn
|
||||
|
||||
obj := &states.ResourceInstanceObject{
|
||||
Value: cty.ObjectVal(map[string]cty.Value{
|
||||
"id": cty.StringVal("i-abc123"),
|
||||
}),
|
||||
Status: states.ObjectReady,
|
||||
}
|
||||
node := &NodeDestroyDeposedResourceInstanceObject{
|
||||
NodeAbstractResourceInstance: &NodeAbstractResourceInstance{
|
||||
NodeAbstractResource: NodeAbstractResource{
|
||||
ResolvedProvider: mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`),
|
||||
},
|
||||
Addr: mustResourceInstanceAddr("aws_instance.foo"),
|
||||
},
|
||||
DeposedKey: states.NewDeposedKey(),
|
||||
}
|
||||
err := node.writeResourceInstanceState(ctx, obj)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err.Error())
|
||||
}
|
||||
|
||||
checkStateString(t, state, `
|
||||
aws_instance.foo: (1 deposed)
|
||||
ID = <not created>
|
||||
provider = provider["registry.terraform.io/hashicorp/aws"]
|
||||
Deposed ID 1 = i-abc123
|
||||
`)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package terraform
|
||||
|
||||
// UpdateStateHook calls the PostStateUpdate hook with the current state.
|
||||
func UpdateStateHook(ctx EvalContext) error {
|
||||
// In principle we could grab the lock here just long enough to take a
|
||||
// deep copy and then pass that to our hooks below, but we'll instead
|
||||
// hold the hook for the duration to avoid the potential confusing
|
||||
// situation of us racing to call PostStateUpdate concurrently with
|
||||
// different state snapshots.
|
||||
stateSync := ctx.State()
|
||||
state := stateSync.Lock().DeepCopy()
|
||||
defer stateSync.Unlock()
|
||||
|
||||
// Call the hook
|
||||
err := ctx.Hook(func(h Hook) (HookAction, error) {
|
||||
return h.PostStateUpdate(state)
|
||||
})
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package terraform
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
"github.com/hashicorp/terraform/addrs"
|
||||
"github.com/hashicorp/terraform/states"
|
||||
)
|
||||
|
||||
func TestUpdateStateHook(t *testing.T) {
|
||||
mockHook := new(MockHook)
|
||||
|
||||
state := states.NewState()
|
||||
state.Module(addrs.RootModuleInstance).SetLocalValue("foo", cty.StringVal("hello"))
|
||||
|
||||
ctx := new(MockEvalContext)
|
||||
ctx.HookHook = mockHook
|
||||
ctx.StateState = state.SyncWrapper()
|
||||
|
||||
if err := UpdateStateHook(ctx); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if !mockHook.PostStateUpdateCalled {
|
||||
t.Fatal("should call PostStateUpdate")
|
||||
}
|
||||
if mockHook.PostStateUpdateState.LocalValue(addrs.LocalValue{Name: "foo"}.Absolute(addrs.RootModuleInstance)) != cty.StringVal("hello") {
|
||||
t.Fatalf("wrong state passed to hook: %s", spew.Sdump(mockHook.PostStateUpdateState))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue