remove refresh!

Delete all the code associated with the Refresh walk
This commit is contained in:
James Bardin 2020-09-21 21:53:55 -04:00
parent 915d4e4b45
commit 906d399189
15 changed files with 17 additions and 1345 deletions

View File

@ -311,16 +311,6 @@ func (c *Context) Graph(typ GraphType, opts *ContextGraphOpts) (*Graph, tfdiags.
Validate: opts.Validate,
}).Build(addrs.RootModuleInstance)
case GraphTypeRefresh:
return (&RefreshGraphBuilder{
Config: c.config,
State: c.state,
Components: c.components,
Schemas: c.schemas,
Targets: c.targets,
Validate: opts.Validate,
}).Build(addrs.RootModuleInstance)
case GraphTypeEval:
return (&EvalGraphBuilder{
Config: c.config,

View File

@ -8,9 +8,7 @@ package terraform
type GraphType byte
const (
GraphTypeInvalid GraphType = 0
GraphTypeLegacy GraphType = iota
GraphTypeRefresh
GraphTypeInvalid GraphType = iota
GraphTypePlan
GraphTypePlanDestroy
GraphTypeApply
@ -25,8 +23,6 @@ var GraphTypeMap = map[string]GraphType{
"apply": GraphTypeApply,
"plan": GraphTypePlan,
"plan-destroy": GraphTypePlanDestroy,
"refresh": GraphTypeRefresh,
"legacy": GraphTypeLegacy,
"validate": GraphTypeValidate,
"eval": GraphTypeEval,
}

View File

@ -1,190 +0,0 @@
package terraform
import (
"log"
"github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/tfdiags"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/dag"
)
// RefreshGraphBuilder implements GraphBuilder and is responsible for building
// a graph for refreshing (updating the Terraform state).
//
// The primary difference between this graph and others:
//
// * Based on the state since it represents the only resources that
// need to be refreshed.
//
// * Ignores lifecycle options since no lifecycle events occur here. This
// simplifies the graph significantly since complex transforms such as
// create-before-destroy can be completely ignored.
//
type RefreshGraphBuilder struct {
// Config is the configuration tree.
Config *configs.Config
// State is the prior state
State *states.State
// Components is a factory for the plug-in components (providers and
// provisioners) available for use.
Components contextComponentFactory
// Schemas is the repository of schemas we will draw from to analyse
// the configuration.
Schemas *Schemas
// Targets are resources to target
Targets []addrs.Targetable
// Validate will do structural validation of the graph.
Validate bool
}
// See GraphBuilder
func (b *RefreshGraphBuilder) Build(path addrs.ModuleInstance) (*Graph, tfdiags.Diagnostics) {
return (&BasicGraphBuilder{
Steps: b.Steps(),
Validate: b.Validate,
Name: "RefreshGraphBuilder",
}).Build(path)
}
// See GraphBuilder
func (b *RefreshGraphBuilder) Steps() []GraphTransformer {
// Custom factory for creating providers.
concreteProvider := func(a *NodeAbstractProvider) dag.Vertex {
return &NodeApplyableProvider{
NodeAbstractProvider: a,
}
}
concreteManagedResource := func(a *NodeAbstractResource) dag.Vertex {
return &nodeExpandRefreshableManagedResource{
NodeAbstractResource: a,
}
}
concreteManagedResourceInstance := func(a *NodeAbstractResourceInstance) dag.Vertex {
return &NodeRefreshableManagedResourceInstance{
NodeAbstractResourceInstance: a,
}
}
concreteResourceInstanceDeposed := func(a *NodeAbstractResourceInstance, key states.DeposedKey) dag.Vertex {
// The "Plan" node type also handles refreshing behavior.
return &NodePlanDeposedResourceInstanceObject{
NodeAbstractResourceInstance: a,
DeposedKey: key,
}
}
concreteDataResource := func(a *NodeAbstractResource) dag.Vertex {
return &nodeExpandRefreshableDataResource{
NodeAbstractResource: a,
}
}
steps := []GraphTransformer{
// Creates all the managed resources that aren't in the state, but only if
// we have a state already. No resources in state means there's not
// anything to refresh.
func() GraphTransformer {
if b.State.HasResources() {
return &ConfigTransformer{
Concrete: concreteManagedResource,
Config: b.Config,
Unique: true,
ModeFilter: true,
Mode: addrs.ManagedResourceMode,
}
}
log.Println("[TRACE] No managed resources in state during refresh; skipping managed resource transformer")
return nil
}(),
// Creates all the data resources that aren't in the state. This will also
// add any orphans from scaling in as destroy nodes.
&ConfigTransformer{
Concrete: concreteDataResource,
Config: b.Config,
Unique: true,
ModeFilter: true,
Mode: addrs.DataResourceMode,
},
// Add any fully-orphaned resources from config (ones that have been
// removed completely, not ones that are just orphaned due to a scaled-in
// count.
&OrphanResourceInstanceTransformer{
Concrete: concreteManagedResourceInstance,
State: b.State,
Config: b.Config,
},
// We also need nodes for any deposed instance objects present in the
// state, so we can check if they still exist. (This intentionally
// skips creating nodes for _current_ objects, since ConfigTransformer
// created nodes that will do that during DynamicExpand.)
&StateTransformer{
ConcreteDeposed: concreteResourceInstanceDeposed,
State: b.State,
},
// Attach the state
&AttachStateTransformer{State: b.State},
// Attach the configuration to any resources
&AttachResourceConfigTransformer{Config: b.Config},
// Add root variables
&RootVariableTransformer{Config: b.Config},
// Add the local values
&LocalTransformer{Config: b.Config},
// Add the outputs
&OutputTransformer{Config: b.Config},
// Add module variables
&ModuleVariableTransformer{Config: b.Config},
TransformProviders(b.Components.ResourceProviders(), concreteProvider, b.Config),
// Must attach schemas before ReferenceTransformer so that we can
// analyze the configuration to find references.
&AttachSchemaTransformer{Schemas: b.Schemas, Config: b.Config},
// Create expansion nodes for all of the module calls. This must
// come after all other transformers that create nodes representing
// objects that can belong to modules.
&ModuleExpansionTransformer{Config: b.Config},
// Connect so that the references are ready for targeting. We'll
// have to connect again later for providers and so on.
&ReferenceTransformer{},
&AttachDependenciesTransformer{},
&attachDataResourceDependenciesTransformer{},
// Target
&TargetsTransformer{
Targets: b.Targets,
},
// Close opened plugin connections
&CloseProviderTransformer{},
// Close root module
&CloseRootModuleTransformer{},
// Perform the transitive reduction to make our graph a bit
// more sane if possible (it usually is possible).
&TransitiveReductionTransformer{},
}
return steps
}

View File

@ -1,79 +0,0 @@
package terraform
import (
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/states"
)
func TestRefreshGraphBuilder_configOrphans(t *testing.T) {
m := testModule(t, "refresh-config-orphan")
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
deposedKey := states.DeposedKey("00000001")
testSetResourceInstanceDeposed(root, "test_object.foo[0]", `{"id":"foo"}`, `provider["registry.terraform.io/hashicorp/test"]`, deposedKey)
testSetResourceInstanceDeposed(root, "test_object.foo[1]", `{"id":"bar"}`, `provider["registry.terraform.io/hashicorp/test"]`, deposedKey)
testSetResourceInstanceDeposed(root, "test_object.foo[2]", `{"id":"baz"}`, `provider["registry.terraform.io/hashicorp/test"]`, deposedKey)
// NOTE: Real-world data resources don't get deposed
testSetResourceInstanceDeposed(root, "data.test_object.foo[0]", `{"id":"foo"}`, `provider["registry.terraform.io/hashicorp/test"]`, deposedKey)
testSetResourceInstanceDeposed(root, "data.test_object.foo[1]", `{"id":"bar"}`, `provider["registry.terraform.io/hashicorp/test"]`, deposedKey)
testSetResourceInstanceDeposed(root, "data.test_object.foo[2]", `{"id":"baz"}`, `provider["registry.terraform.io/hashicorp/test"]`, deposedKey)
b := &RefreshGraphBuilder{
Config: m,
State: state,
Components: simpleMockComponentFactory(),
Schemas: simpleTestSchemas(),
}
g, err := b.Build(addrs.RootModuleInstance)
if err != nil {
t.Fatalf("Error building graph: %s", err)
}
actual := strings.TrimSpace(g.StringWithNodeTypes())
expected := strings.TrimSpace(`
data.test_object.foo[0] - *terraform.NodeRefreshableManagedResourceInstance
provider["registry.terraform.io/hashicorp/test"] - *terraform.NodeApplyableProvider
data.test_object.foo[0] (deposed 00000001) - *terraform.NodePlanDeposedResourceInstanceObject
provider["registry.terraform.io/hashicorp/test"] - *terraform.NodeApplyableProvider
data.test_object.foo[1] - *terraform.NodeRefreshableManagedResourceInstance
provider["registry.terraform.io/hashicorp/test"] - *terraform.NodeApplyableProvider
data.test_object.foo[1] (deposed 00000001) - *terraform.NodePlanDeposedResourceInstanceObject
provider["registry.terraform.io/hashicorp/test"] - *terraform.NodeApplyableProvider
data.test_object.foo[2] - *terraform.NodeRefreshableManagedResourceInstance
provider["registry.terraform.io/hashicorp/test"] - *terraform.NodeApplyableProvider
data.test_object.foo[2] (deposed 00000001) - *terraform.NodePlanDeposedResourceInstanceObject
provider["registry.terraform.io/hashicorp/test"] - *terraform.NodeApplyableProvider
provider["registry.terraform.io/hashicorp/test"] - *terraform.NodeApplyableProvider
provider["registry.terraform.io/hashicorp/test"] (close) - *terraform.graphNodeCloseProvider
data.test_object.foo[0] - *terraform.NodeRefreshableManagedResourceInstance
data.test_object.foo[0] (deposed 00000001) - *terraform.NodePlanDeposedResourceInstanceObject
data.test_object.foo[1] - *terraform.NodeRefreshableManagedResourceInstance
data.test_object.foo[1] (deposed 00000001) - *terraform.NodePlanDeposedResourceInstanceObject
data.test_object.foo[2] - *terraform.NodeRefreshableManagedResourceInstance
data.test_object.foo[2] (deposed 00000001) - *terraform.NodePlanDeposedResourceInstanceObject
test_object.foo (expand) - *terraform.nodeExpandRefreshableManagedResource
test_object.foo[0] (deposed 00000001) - *terraform.NodePlanDeposedResourceInstanceObject
test_object.foo[1] (deposed 00000001) - *terraform.NodePlanDeposedResourceInstanceObject
test_object.foo[2] (deposed 00000001) - *terraform.NodePlanDeposedResourceInstanceObject
root - *terraform.nodeCloseModule
provider["registry.terraform.io/hashicorp/test"] (close) - *terraform.graphNodeCloseProvider
test_object.foo (expand) - *terraform.nodeExpandRefreshableManagedResource
provider["registry.terraform.io/hashicorp/test"] - *terraform.NodeApplyableProvider
test_object.foo[0] (deposed 00000001) - *terraform.NodePlanDeposedResourceInstanceObject
provider["registry.terraform.io/hashicorp/test"] - *terraform.NodeApplyableProvider
test_object.foo[1] (deposed 00000001) - *terraform.NodePlanDeposedResourceInstanceObject
provider["registry.terraform.io/hashicorp/test"] - *terraform.NodeApplyableProvider
test_object.foo[2] (deposed 00000001) - *terraform.NodePlanDeposedResourceInstanceObject
provider["registry.terraform.io/hashicorp/test"] - *terraform.NodeApplyableProvider
`)
if expected != actual {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s\ndiff:\n%s", actual, expected, cmp.Diff(expected, actual))
}
}

View File

@ -10,7 +10,6 @@ const (
walkApply
walkPlan
walkPlanDestroy
walkRefresh
walkValidate
walkDestroy
walkImport

View File

@ -9,18 +9,16 @@ func _() {
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[GraphTypeInvalid-0]
_ = x[GraphTypeLegacy-1]
_ = x[GraphTypeRefresh-2]
_ = x[GraphTypePlan-3]
_ = x[GraphTypePlanDestroy-4]
_ = x[GraphTypeApply-5]
_ = x[GraphTypeValidate-6]
_ = x[GraphTypeEval-7]
_ = x[GraphTypePlan-1]
_ = x[GraphTypePlanDestroy-2]
_ = x[GraphTypeApply-3]
_ = x[GraphTypeValidate-4]
_ = x[GraphTypeEval-5]
}
const _GraphType_name = "GraphTypeInvalidGraphTypeLegacyGraphTypeRefreshGraphTypePlanGraphTypePlanDestroyGraphTypeApplyGraphTypeValidateGraphTypeEval"
const _GraphType_name = "GraphTypeInvalidGraphTypePlanGraphTypePlanDestroyGraphTypeApplyGraphTypeValidateGraphTypeEval"
var _GraphType_index = [...]uint8{0, 16, 31, 47, 60, 80, 94, 111, 124}
var _GraphType_index = [...]uint8{0, 16, 29, 49, 63, 80, 93}
func (i GraphType) String() string {
if i >= GraphType(len(_GraphType_index)-1) {

View File

@ -1,283 +0,0 @@
package terraform
import (
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/dag"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/tfdiags"
)
type nodeExpandRefreshableDataResource struct {
*NodeAbstractResource
}
var (
_ GraphNodeDynamicExpandable = (*nodeExpandRefreshableDataResource)(nil)
_ GraphNodeReferenceable = (*nodeExpandRefreshableDataResource)(nil)
_ GraphNodeReferencer = (*nodeExpandRefreshableDataResource)(nil)
_ GraphNodeConfigResource = (*nodeExpandRefreshableDataResource)(nil)
_ GraphNodeAttachResourceConfig = (*nodeExpandRefreshableDataResource)(nil)
)
func (n *nodeExpandRefreshableDataResource) Name() string {
return n.NodeAbstractResource.Name() + " (expand)"
}
func (n *nodeExpandRefreshableDataResource) References() []*addrs.Reference {
return (&NodeRefreshableManagedResource{NodeAbstractResource: n.NodeAbstractResource}).References()
}
func (n *nodeExpandRefreshableDataResource) DynamicExpand(ctx EvalContext) (*Graph, error) {
var g Graph
expander := ctx.InstanceExpander()
for _, module := range expander.ExpandModule(n.Addr.Module) {
g.Add(&NodeRefreshableDataResource{
NodeAbstractResource: n.NodeAbstractResource,
Addr: n.Addr.Resource.Absolute(module),
})
}
return &g, nil
}
// NodeRefreshableDataResource represents a resource that is "refreshable".
type NodeRefreshableDataResource struct {
*NodeAbstractResource
Addr addrs.AbsResource
}
var (
_ GraphNodeModuleInstance = (*NodeRefreshableDataResource)(nil)
_ GraphNodeDynamicExpandable = (*NodeRefreshableDataResource)(nil)
_ GraphNodeReferenceable = (*NodeRefreshableDataResource)(nil)
_ GraphNodeReferencer = (*NodeRefreshableDataResource)(nil)
_ GraphNodeConfigResource = (*NodeRefreshableDataResource)(nil)
_ GraphNodeAttachResourceConfig = (*NodeRefreshableDataResource)(nil)
_ GraphNodeAttachProviderMetaConfigs = (*NodeAbstractResource)(nil)
)
func (n *NodeRefreshableDataResource) Path() addrs.ModuleInstance {
return n.Addr.Module
}
// GraphNodeDynamicExpandable
func (n *NodeRefreshableDataResource) DynamicExpand(ctx EvalContext) (*Graph, error) {
var diags tfdiags.Diagnostics
expander := ctx.InstanceExpander()
switch {
case n.Config.Count != nil:
count, countDiags := evaluateCountExpressionValue(n.Config.Count, ctx)
diags = diags.Append(countDiags)
if countDiags.HasErrors() {
return nil, diags.Err()
}
if !count.IsKnown() {
// If the count isn't known yet, we'll skip refreshing and try expansion
// again during the plan walk.
return nil, nil
}
c, _ := count.AsBigFloat().Int64()
expander.SetResourceCount(n.Addr.Module, n.Addr.Resource, int(c))
case n.Config.ForEach != nil:
forEachVal, forEachDiags := evaluateForEachExpressionValue(n.Config.ForEach, ctx)
diags = diags.Append(forEachDiags)
if forEachDiags.HasErrors() {
return nil, diags.Err()
}
if !forEachVal.IsKnown() {
// If the for_each isn't known yet, we'll skip refreshing and try expansion
// again during the plan walk.
return nil, nil
}
expander.SetResourceForEach(n.Addr.Module, n.Addr.Resource, forEachVal.AsValueMap())
default:
expander.SetResourceSingle(n.Addr.Module, n.Addr.Resource)
}
// Next we need to potentially rename an instance address in the state
// if we're transitioning whether "count" is set at all.
fixResourceCountSetTransition(ctx, n.ResourceAddr(), n.Config.Count != nil)
instanceAddrs := expander.ExpandResource(n.Addr)
// Our graph transformers require access to the full state, so we'll
// temporarily lock it while we work on this.
state := ctx.State().Lock()
defer ctx.State().Unlock()
// The concrete resource factory we'll use
concreteResource := func(a *NodeAbstractResourceInstance) dag.Vertex {
// Add the config and state since we don't do that via transforms
a.Config = n.Config
a.ResolvedProvider = n.ResolvedProvider
a.ProviderMetas = n.ProviderMetas
a.dependsOn = n.dependsOn
a.forceDependsOn = n.forceDependsOn
a.Targets = n.Targets
return &NodeRefreshableDataResourceInstance{
NodeAbstractResourceInstance: a,
}
}
// We also need a destroyable resource for orphans that are a result of a
// scaled-in count.
concreteResourceDestroyable := func(a *NodeAbstractResourceInstance) dag.Vertex {
// Add the config and provider since we don't do that via transforms
a.Config = n.Config
a.ResolvedProvider = n.ResolvedProvider
return &NodeDestroyableDataResourceInstance{
NodeAbstractResourceInstance: a,
}
}
// Start creating the steps
steps := []GraphTransformer{
// Expand the count.
&ResourceCountTransformer{
Concrete: concreteResource,
Schema: n.Schema,
Addr: n.ResourceAddr(),
InstanceAddrs: instanceAddrs,
},
// Add the count orphans. As these are orphaned refresh nodes, we add them
// directly as NodeDestroyableDataResource.
&OrphanResourceInstanceCountTransformer{
Concrete: concreteResourceDestroyable,
Addr: n.Addr,
InstanceAddrs: instanceAddrs,
State: state,
},
// Attach the state
&AttachStateTransformer{State: state},
// Targeting
&TargetsTransformer{Targets: n.Targets},
// Connect references so ordering is correct
&ReferenceTransformer{},
// Make sure there is a single root
&RootTransformer{},
}
// Build the graph
b := &BasicGraphBuilder{
Steps: steps,
Validate: true,
Name: "NodeRefreshableDataResource",
}
graph, diags := b.Build(nil)
return graph, diags.ErrWithWarnings()
}
// NodeRefreshableDataResourceInstance represents a single resource instance
// that is refreshable.
type NodeRefreshableDataResourceInstance struct {
*NodeAbstractResourceInstance
}
// GraphNodeExecutable
func (n *NodeRefreshableDataResourceInstance) Execute(ctx EvalContext, op walkOperation) error {
addr := n.ResourceInstanceAddr()
// These variables are the state for the eval sequence below, and are
// updated through pointers.
var change *plans.ResourceInstanceChange
var state *states.ResourceInstanceObject
provider, providerSchema, err := GetProvider(ctx, n.ResolvedProvider)
if err != nil {
return err
}
// EvalReadState
readStateReq := &EvalReadState{
Addr: addr.Resource,
Provider: &provider,
ProviderSchema: &providerSchema,
Output: &state,
}
_, err = readStateReq.Eval(ctx)
if err != nil {
return err
}
// EvalReadDataRefresh will _attempt_ to read the data source, but
// may generate an incomplete planned object if the configuration
// includes values that won't be known until apply.
readDataRefreshReq := &evalReadDataRefresh{
evalReadData{
Addr: addr.Resource,
Config: n.Config,
Provider: &provider,
ProviderAddr: n.ResolvedProvider,
ProviderMetas: n.ProviderMetas,
ProviderSchema: &providerSchema,
OutputChange: &change,
State: &state,
dependsOn: n.dependsOn,
forceDependsOn: n.forceDependsOn,
},
}
_, err = readDataRefreshReq.Eval(ctx)
if err != nil {
return err
}
if change == nil {
// EvalWriteState
writeStateRequest := EvalWriteState{
Addr: addr.Resource,
ProviderAddr: n.ResolvedProvider,
State: &state,
ProviderSchema: &providerSchema,
}
_, err := writeStateRequest.Eval(ctx)
if err != nil {
return err
}
err = UpdateStateHook(ctx)
if err != nil {
return err
}
} else {
// EvalWriteDiff
writeDiffReq := &EvalWriteDiff{
Addr: addr.Resource,
Change: &change,
ProviderSchema: &providerSchema,
}
_, err = writeDiffReq.Eval(ctx)
if err != nil {
return err
}
// EvalWriteState
writeStateRequest := EvalWriteState{
Addr: addr.Resource,
ProviderAddr: n.ResolvedProvider,
State: &state,
ProviderSchema: &providerSchema,
}
_, err := writeStateRequest.Eval(ctx)
if err != nil {
return err
}
}
return err
}

View File

@ -1,178 +0,0 @@
package terraform
import (
"testing"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/instances"
)
func TestNodeRefreshableDataResourceDynamicExpand_scaleOut(t *testing.T) {
m := testModule(t, "refresh-data-scale-inout")
state := MustShimLegacyState(&State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"data.aws_instance.foo.0": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "foo",
},
},
},
"data.aws_instance.foo.1": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "bar",
},
},
},
},
},
},
})
addr := addrs.RootModule.Resource(addrs.DataResourceMode, "aws_instance", "foo")
n := &NodeRefreshableDataResource{
NodeAbstractResource: &NodeAbstractResource{
Addr: addr,
Config: m.Module.DataResources["data.aws_instance.foo"],
},
Addr: addr.Absolute(addrs.RootModuleInstance),
}
g, err := n.DynamicExpand(&MockEvalContext{
PathPath: addrs.RootModuleInstance,
StateState: state.SyncWrapper(),
InstanceExpanderExpander: instances.NewExpander(),
// DynamicExpand will call EvaluateExpr to evaluate the "count"
// expression, which is just a literal number 3 in the fixture config
// and so we'll just hard-code this here too.
EvaluateExprResult: cty.NumberIntVal(3),
})
if err != nil {
t.Fatalf("error on DynamicExpand: %s", err)
}
actual := g.StringWithNodeTypes()
expected := `data.aws_instance.foo[0] - *terraform.NodeRefreshableDataResourceInstance
data.aws_instance.foo[1] - *terraform.NodeRefreshableDataResourceInstance
data.aws_instance.foo[2] - *terraform.NodeRefreshableDataResourceInstance
root - terraform.graphNodeRoot
data.aws_instance.foo[0] - *terraform.NodeRefreshableDataResourceInstance
data.aws_instance.foo[1] - *terraform.NodeRefreshableDataResourceInstance
data.aws_instance.foo[2] - *terraform.NodeRefreshableDataResourceInstance
`
if expected != actual {
t.Fatalf("Expected:\n%s\nGot:\n%s", expected, actual)
}
}
func TestNodeRefreshableDataResourceDynamicExpand_scaleIn(t *testing.T) {
m := testModule(t, "refresh-data-scale-inout")
state := MustShimLegacyState(&State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"data.aws_instance.foo.0": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "foo",
},
},
},
"data.aws_instance.foo.1": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "bar",
},
},
},
"data.aws_instance.foo.2": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "baz",
},
},
},
"data.aws_instance.foo.3": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "qux",
},
},
},
},
},
},
})
addr := addrs.RootModule.Resource(addrs.DataResourceMode, "aws_instance", "foo")
n := &NodeRefreshableDataResource{
NodeAbstractResource: &NodeAbstractResource{
Addr: addr,
Config: m.Module.DataResources["data.aws_instance.foo"],
ResolvedProvider: addrs.AbsProviderConfig{
Provider: addrs.NewDefaultProvider("aws"),
Module: addrs.RootModule,
},
},
Addr: addr.Absolute(addrs.RootModuleInstance),
}
g, err := n.DynamicExpand(&MockEvalContext{
PathPath: addrs.RootModuleInstance,
StateState: state.SyncWrapper(),
InstanceExpanderExpander: instances.NewExpander(),
// DynamicExpand will call EvaluateExpr to evaluate the "count"
// expression, which is just a literal number 3 in the fixture config
// and so we'll just hard-code this here too.
EvaluateExprResult: cty.NumberIntVal(3),
})
if err != nil {
t.Fatalf("error on DynamicExpand: %s", err)
}
actual := g.StringWithNodeTypes()
expected := `data.aws_instance.foo[0] - *terraform.NodeRefreshableDataResourceInstance
data.aws_instance.foo[1] - *terraform.NodeRefreshableDataResourceInstance
data.aws_instance.foo[2] - *terraform.NodeRefreshableDataResourceInstance
data.aws_instance.foo[3] - *terraform.NodeDestroyableDataResourceInstance
root - terraform.graphNodeRoot
data.aws_instance.foo[0] - *terraform.NodeRefreshableDataResourceInstance
data.aws_instance.foo[1] - *terraform.NodeRefreshableDataResourceInstance
data.aws_instance.foo[2] - *terraform.NodeRefreshableDataResourceInstance
data.aws_instance.foo[3] - *terraform.NodeDestroyableDataResourceInstance
`
if expected != actual {
t.Fatalf("Expected:\n%s\nGot:\n%s", expected, actual)
}
var destroyableDataResource *NodeDestroyableDataResourceInstance
for _, v := range g.Vertices() {
if r, ok := v.(*NodeDestroyableDataResourceInstance); ok {
destroyableDataResource = r
}
}
if destroyableDataResource == nil {
t.Fatal("failed to find a destroyableDataResource")
}
if destroyableDataResource.ResolvedProvider.Provider.Type == "" {
t.Fatal("NodeDestroyableDataResourceInstance missing provider config")
}
}

View File

@ -153,7 +153,7 @@ func (n *nodeModuleVariable) Execute(ctx EvalContext, op walkOperation) error {
var err error
switch op {
case walkRefresh, walkPlan, walkApply, walkDestroy, walkImport:
case walkPlan, walkApply, walkDestroy, walkImport:
vals, err = n.EvalModuleCallArgument(ctx, false)
if err != nil {
return err

View File

@ -200,7 +200,7 @@ func (n *NodeApplyableOutput) References() []*addrs.Reference {
func (n *NodeApplyableOutput) Execute(ctx EvalContext, op walkOperation) error {
switch op {
// Everything except walkImport
case walkEval, walkRefresh, walkPlan, walkApply, walkValidate, walkDestroy, walkPlanDestroy:
case walkEval, walkPlan, walkApply, walkValidate, walkDestroy, walkPlanDestroy:
// This has to run before we have a state lock, since evaluation also
// reads the state
val, diags := ctx.EvaluateExpr(n.Config.Expr, cty.DynamicPseudoType, nil)

View File

@ -33,7 +33,7 @@ func (n *NodeApplyableProvider) Execute(ctx EvalContext, op walkOperation) error
switch op {
case walkValidate:
return n.ValidateProvider(ctx, provider)
case walkRefresh, walkPlan, walkApply, walkDestroy:
case walkPlan, walkApply, walkDestroy:
return n.ConfigureProvider(ctx, provider, false)
case walkImport:
return n.ConfigureProvider(ctx, provider, true)

View File

@ -73,46 +73,6 @@ func (n *NodePlanDeposedResourceInstanceObject) EvalTree() EvalNode {
seq := &EvalSequence{Nodes: make([]EvalNode, 0, 5)}
// During the refresh walk we will ensure that our record of the deposed
// object is up-to-date. If it was already deleted outside of Terraform
// then this will remove it from state and thus avoid us planning a
// destroy for it during the subsequent plan walk.
seq.Nodes = append(seq.Nodes, &EvalOpFilter{
Ops: []walkOperation{walkRefresh},
Node: &EvalSequence{
Nodes: []EvalNode{
&EvalGetProvider{
Addr: n.ResolvedProvider,
Output: &provider,
Schema: &providerSchema,
},
&EvalReadStateDeposed{
Addr: addr.Resource,
Provider: &provider,
ProviderSchema: &providerSchema,
Key: n.DeposedKey,
Output: &state,
},
&EvalRefresh{
Addr: addr.Resource,
ProviderAddr: n.ResolvedProvider,
Provider: &provider,
ProviderMetas: n.ProviderMetas,
ProviderSchema: &providerSchema,
State: &state,
Output: &state,
},
&EvalWriteStateDeposed{
Addr: addr.Resource,
Key: n.DeposedKey,
ProviderAddr: n.ResolvedProvider,
ProviderSchema: &providerSchema,
State: &state,
},
},
},
})
// During the plan walk we always produce a planned destroy change, because
// destroying is the only supported action for deposed objects.
var change *plans.ResourceInstanceChange

View File

@ -1,381 +0,0 @@
package terraform
import (
"fmt"
"log"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/providers"
"github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/dag"
"github.com/hashicorp/terraform/tfdiags"
)
// nodeExpandRefreshableResource handles the first layer of resource
// expansion durin refresh. We need this extra layer so DynamicExpand is called
// twice for the resource, the first to expand the Resource for each module
// instance, and the second to expand each ResourceInstance for the expanded
// Resources.
type nodeExpandRefreshableManagedResource struct {
*NodeAbstractResource
// We attach dependencies to the Resource during refresh, since the
// instances are instantiated during DynamicExpand.
Dependencies []addrs.ConfigResource
}
var (
_ GraphNodeDynamicExpandable = (*nodeExpandRefreshableManagedResource)(nil)
_ GraphNodeReferenceable = (*nodeExpandRefreshableManagedResource)(nil)
_ GraphNodeReferencer = (*nodeExpandRefreshableManagedResource)(nil)
_ GraphNodeConfigResource = (*nodeExpandRefreshableManagedResource)(nil)
_ GraphNodeAttachResourceConfig = (*nodeExpandRefreshableManagedResource)(nil)
_ GraphNodeAttachDependencies = (*nodeExpandRefreshableManagedResource)(nil)
)
func (n *nodeExpandRefreshableManagedResource) Name() string {
return n.NodeAbstractResource.Name() + " (expand)"
}
// GraphNodeAttachDependencies
func (n *nodeExpandRefreshableManagedResource) AttachDependencies(deps []addrs.ConfigResource) {
n.Dependencies = deps
}
func (n *nodeExpandRefreshableManagedResource) References() []*addrs.Reference {
return (&NodeRefreshableManagedResource{NodeAbstractResource: n.NodeAbstractResource}).References()
}
func (n *nodeExpandRefreshableManagedResource) DynamicExpand(ctx EvalContext) (*Graph, error) {
var g Graph
expander := ctx.InstanceExpander()
for _, module := range expander.ExpandModule(n.Addr.Module) {
g.Add(&NodeRefreshableManagedResource{
NodeAbstractResource: n.NodeAbstractResource,
Addr: n.Addr.Resource.Absolute(module),
Dependencies: n.Dependencies,
})
}
return &g, nil
}
// NodeRefreshableManagedResource represents a resource that is expandable into
// NodeRefreshableManagedResourceInstance. Resource count orphans are also added.
type NodeRefreshableManagedResource struct {
*NodeAbstractResource
Addr addrs.AbsResource
// We attach dependencies to the Resource during refresh, since the
// instances are instantiated during DynamicExpand.
Dependencies []addrs.ConfigResource
}
var (
_ GraphNodeModuleInstance = (*NodeRefreshableManagedResource)(nil)
_ GraphNodeDynamicExpandable = (*NodeRefreshableManagedResource)(nil)
_ GraphNodeReferenceable = (*NodeRefreshableManagedResource)(nil)
_ GraphNodeReferencer = (*NodeRefreshableManagedResource)(nil)
_ GraphNodeConfigResource = (*NodeRefreshableManagedResource)(nil)
_ GraphNodeAttachResourceConfig = (*NodeRefreshableManagedResource)(nil)
)
func (n *NodeRefreshableManagedResource) Path() addrs.ModuleInstance {
return n.Addr.Module
}
// GraphNodeDynamicExpandable
func (n *NodeRefreshableManagedResource) DynamicExpand(ctx EvalContext) (*Graph, error) {
var diags tfdiags.Diagnostics
expander := ctx.InstanceExpander()
// Inform our instance expander about our expansion results, and then use
// it to calculate the instance addresses we'll expand for.
switch {
case n.Config.Count != nil:
count, countDiags := evaluateCountExpression(n.Config.Count, ctx)
diags = diags.Append(countDiags)
if countDiags.HasErrors() {
return nil, diags.Err()
}
expander.SetResourceCount(n.Addr.Module, n.Addr.Resource, count)
case n.Config.ForEach != nil:
forEachMap, forEachDiags := evaluateForEachExpression(n.Config.ForEach, ctx)
if forEachDiags.HasErrors() {
return nil, diags.Err()
}
expander.SetResourceForEach(n.Addr.Module, n.Addr.Resource, forEachMap)
default:
expander.SetResourceSingle(n.Addr.Module, n.Addr.Resource)
}
// Next we need to potentially rename an instance address in the state
// if we're transitioning whether "count" is set at all.
fixResourceCountSetTransition(ctx, n.Addr.Config(), n.Config.Count != nil)
instanceAddrs := expander.ExpandResource(n.Addr)
// Our graph transformers require access to the full state, so we'll
// temporarily lock it while we work on this.
state := ctx.State().Lock()
defer ctx.State().Unlock()
// The concrete resource factory we'll use
concreteResource := func(a *NodeAbstractResourceInstance) dag.Vertex {
// Add the config and state since we don't do that via transforms
a.Config = n.Config
a.ResolvedProvider = n.ResolvedProvider
a.Dependencies = n.Dependencies
a.ProviderMetas = n.ProviderMetas
return &NodeRefreshableManagedResourceInstance{
NodeAbstractResourceInstance: a,
}
}
// Start creating the steps
steps := []GraphTransformer{
// Expand the count.
&ResourceCountTransformer{
Concrete: concreteResource,
Schema: n.Schema,
Addr: n.Addr.Config(),
InstanceAddrs: instanceAddrs,
},
// Add the count orphans to make sure these resources are accounted for
// during a scale in.
&OrphanResourceInstanceCountTransformer{
Concrete: concreteResource,
Addr: n.Addr,
InstanceAddrs: instanceAddrs,
State: state,
},
// Attach the state
&AttachStateTransformer{State: state},
// Targeting
&TargetsTransformer{Targets: n.Targets},
// Connect references so ordering is correct
&ReferenceTransformer{},
// Make sure there is a single root
&RootTransformer{},
}
// Build the graph
b := &BasicGraphBuilder{
Steps: steps,
Validate: true,
Name: "NodeRefreshableManagedResource",
}
graph, diags := b.Build(nil)
return graph, diags.ErrWithWarnings()
}
// NodeRefreshableManagedResourceInstance represents a resource that is "applyable":
// it is ready to be applied and is represented by a diff.
type NodeRefreshableManagedResourceInstance struct {
*NodeAbstractResourceInstance
}
var (
_ GraphNodeModuleInstance = (*NodeRefreshableManagedResourceInstance)(nil)
_ GraphNodeReferenceable = (*NodeRefreshableManagedResourceInstance)(nil)
_ GraphNodeReferencer = (*NodeRefreshableManagedResourceInstance)(nil)
_ GraphNodeDestroyer = (*NodeRefreshableManagedResourceInstance)(nil)
_ GraphNodeConfigResource = (*NodeRefreshableManagedResourceInstance)(nil)
_ GraphNodeResourceInstance = (*NodeRefreshableManagedResourceInstance)(nil)
_ GraphNodeAttachResourceConfig = (*NodeRefreshableManagedResourceInstance)(nil)
_ GraphNodeAttachResourceState = (*NodeRefreshableManagedResourceInstance)(nil)
_ GraphNodeExecutable = (*NodeRefreshableManagedResourceInstance)(nil)
)
// GraphNodeDestroyer
func (n *NodeRefreshableManagedResourceInstance) DestroyAddr() *addrs.AbsResourceInstance {
addr := n.ResourceInstanceAddr()
return &addr
}
// GraphNodeEvalable
func (n *NodeRefreshableManagedResourceInstance) Execute(ctx EvalContext, op walkOperation) error {
addr := n.ResourceInstanceAddr()
// Eval info is different depending on what kind of resource this is
switch addr.Resource.Resource.Mode {
case addrs.ManagedResourceMode:
if n.instanceState == nil {
log.Printf("[TRACE] NodeRefreshableManagedResourceInstance: %s has no existing state to refresh", addr)
_, err := n.evalTreeManagedResourceNoState().Eval(ctx)
return err
}
log.Printf("[TRACE] NodeRefreshableManagedResourceInstance: %s will be refreshed", addr)
_, err := n.evalTreeManagedResource().Eval(ctx)
return err
case addrs.DataResourceMode:
// Get the data source node. If we don't have a configuration
// then it is an orphan so we destroy it (remove it from the state).
var dn GraphNodeExecutable
if n.Config != nil {
dn = &NodeRefreshableDataResourceInstance{
NodeAbstractResourceInstance: n.NodeAbstractResourceInstance,
}
} else {
dn = &NodeDestroyableDataResourceInstance{
NodeAbstractResourceInstance: n.NodeAbstractResourceInstance,
}
}
return dn.Execute(ctx, op)
default:
panic(fmt.Errorf("unsupported resource mode %s", addr.Resource.Resource.Mode))
}
}
func (n *NodeRefreshableManagedResourceInstance) evalTreeManagedResource() EvalNode {
addr := n.ResourceInstanceAddr()
// Declare a bunch of variables that are used for state during
// evaluation. Most of this are written to by-address below.
var provider providers.Interface
var providerSchema *ProviderSchema
var state *states.ResourceInstanceObject
// This happened during initial development. All known cases were
// fixed and tested but as a sanity check let's assert here.
if n.instanceState == nil {
err := fmt.Errorf(
"No resource state attached for addr: %s\n\n"+
"This is a bug. Please report this to Terraform with your configuration\n"+
"and state attached. Please be careful to scrub any sensitive information.",
addr)
return &EvalReturnError{Error: &err}
}
return &EvalSequence{
Nodes: []EvalNode{
&EvalGetProvider{
Addr: n.ResolvedProvider,
Output: &provider,
Schema: &providerSchema,
},
&EvalReadState{
Addr: addr.Resource,
Provider: &provider,
ProviderSchema: &providerSchema,
Output: &state,
},
&EvalRefreshDependencies{
State: &state,
Dependencies: &n.Dependencies,
},
&EvalRefresh{
Addr: addr.Resource,
ProviderAddr: n.ResolvedProvider,
Provider: &provider,
ProviderMetas: n.ProviderMetas,
ProviderSchema: &providerSchema,
State: &state,
Output: &state,
},
&EvalWriteState{
Addr: addr.Resource,
ProviderAddr: n.ResolvedProvider,
ProviderSchema: &providerSchema,
State: &state,
Dependencies: &n.Dependencies,
},
},
}
}
// evalTreeManagedResourceNoState produces an EvalSequence for refresh resource
// nodes that don't have state attached. An example of where this functionality
// is useful is when a resource that already exists in state is being scaled
// out, ie: has its resource count increased. In this case, the scaled out node
// needs to be available to other nodes (namely data sources) that may depend
// on it for proper interpolation, or confusing "index out of range" errors can
// occur.
//
// The steps in this sequence are very similar to the steps carried out in
// plan, but nothing is done with the diff after it is created - it is dropped,
// and its changes are not counted in the UI.
func (n *NodeRefreshableManagedResourceInstance) evalTreeManagedResourceNoState() EvalNode {
addr := n.ResourceInstanceAddr()
// Declare a bunch of variables that are used for state during
// evaluation. Most of this are written to by-address below.
var provider providers.Interface
var providerSchema *ProviderSchema
var change *plans.ResourceInstanceChange
var state *states.ResourceInstanceObject
return &EvalSequence{
Nodes: []EvalNode{
&EvalGetProvider{
Addr: n.ResolvedProvider,
Output: &provider,
Schema: &providerSchema,
},
&EvalReadState{
Addr: addr.Resource,
Provider: &provider,
ProviderSchema: &providerSchema,
Output: &state,
},
&EvalDiff{
Addr: addr.Resource,
Config: n.Config,
Provider: &provider,
ProviderAddr: n.ResolvedProvider,
ProviderSchema: &providerSchema,
State: &state,
OutputChange: &change,
OutputState: &state,
Stub: true,
},
&EvalWriteState{
Addr: addr.Resource,
ProviderAddr: n.ResolvedProvider,
ProviderSchema: &providerSchema,
State: &state,
Dependencies: &n.Dependencies,
},
// We must also save the planned change, so that expressions in
// other nodes, such as provider configurations and data resources,
// can work with the planned new value.
//
// This depends on the fact that Context.Refresh creates a
// temporary new empty changeset for the duration of its graph
// walk, and so this recorded change will be discarded immediately
// after the refresh walk completes.
&EvalWriteDiff{
Addr: addr.Resource,
Change: &change,
ProviderSchema: &providerSchema,
},
},
}
}

View File

@ -1,159 +0,0 @@
package terraform
import (
"testing"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/instances"
)
func TestNodeRefreshableManagedResourceDynamicExpand_scaleOut(t *testing.T) {
m := testModule(t, "refresh-resource-scale-inout")
state := MustShimLegacyState(&State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.foo.0": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "foo",
},
},
},
"aws_instance.foo.1": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "bar",
},
},
},
},
},
},
}).SyncWrapper()
cfgAddr := addrs.RootModule.Resource(addrs.ManagedResourceMode, "aws_instance", "foo")
n := &NodeRefreshableManagedResource{
NodeAbstractResource: &NodeAbstractResource{
Addr: cfgAddr,
Config: m.Module.ManagedResources["aws_instance.foo"],
},
Addr: cfgAddr.Absolute(addrs.RootModuleInstance),
}
g, err := n.DynamicExpand(&MockEvalContext{
PathPath: addrs.RootModuleInstance,
StateState: state,
InstanceExpanderExpander: instances.NewExpander(),
// DynamicExpand will call EvaluateExpr to evaluate the "count"
// expression, which is just a literal number 3 in the fixture config
// and so we'll just hard-code this here too.
EvaluateExprResult: cty.NumberIntVal(3),
})
if err != nil {
t.Fatalf("error attempting DynamicExpand: %s", err)
}
actual := g.StringWithNodeTypes()
expected := `aws_instance.foo[0] - *terraform.NodeRefreshableManagedResourceInstance
aws_instance.foo[1] - *terraform.NodeRefreshableManagedResourceInstance
aws_instance.foo[2] - *terraform.NodeRefreshableManagedResourceInstance
root - terraform.graphNodeRoot
aws_instance.foo[0] - *terraform.NodeRefreshableManagedResourceInstance
aws_instance.foo[1] - *terraform.NodeRefreshableManagedResourceInstance
aws_instance.foo[2] - *terraform.NodeRefreshableManagedResourceInstance
`
if expected != actual {
t.Fatalf("Expected:\n%s\nGot:\n%s", expected, actual)
}
}
func TestNodeRefreshableManagedResourceDynamicExpand_scaleIn(t *testing.T) {
m := testModule(t, "refresh-resource-scale-inout")
state := MustShimLegacyState(&State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.foo.0": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "foo",
},
},
},
"aws_instance.foo.1": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "bar",
},
},
},
"aws_instance.foo.2": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "baz",
},
},
},
"aws_instance.foo.3": &ResourceState{
Type: "aws_instance",
Deposed: []*InstanceState{
&InstanceState{
ID: "qux",
},
},
},
},
},
},
}).SyncWrapper()
cfgAddr := addrs.RootModule.Resource(addrs.ManagedResourceMode, "aws_instance", "foo")
n := &NodeRefreshableManagedResource{
NodeAbstractResource: &NodeAbstractResource{
Addr: cfgAddr,
Config: m.Module.ManagedResources["aws_instance.foo"],
},
Addr: cfgAddr.Absolute(addrs.RootModuleInstance),
}
g, err := n.DynamicExpand(&MockEvalContext{
PathPath: addrs.RootModuleInstance,
StateState: state,
InstanceExpanderExpander: instances.NewExpander(),
// DynamicExpand will call EvaluateExpr to evaluate the "count"
// expression, which is just a literal number 3 in the fixture config
// and so we'll just hard-code this here too.
EvaluateExprResult: cty.NumberIntVal(3),
})
if err != nil {
t.Fatalf("error attempting DynamicExpand: %s", err)
}
actual := g.StringWithNodeTypes()
expected := `aws_instance.foo[0] - *terraform.NodeRefreshableManagedResourceInstance
aws_instance.foo[1] - *terraform.NodeRefreshableManagedResourceInstance
aws_instance.foo[2] - *terraform.NodeRefreshableManagedResourceInstance
aws_instance.foo[3] - *terraform.NodeRefreshableManagedResourceInstance
root - terraform.graphNodeRoot
aws_instance.foo[0] - *terraform.NodeRefreshableManagedResourceInstance
aws_instance.foo[1] - *terraform.NodeRefreshableManagedResourceInstance
aws_instance.foo[2] - *terraform.NodeRefreshableManagedResourceInstance
aws_instance.foo[3] - *terraform.NodeRefreshableManagedResourceInstance
`
if expected != actual {
t.Fatalf("Expected:\n%s\nGot:\n%s", expected, actual)
}
}

View File

@ -12,16 +12,15 @@ func _() {
_ = x[walkApply-1]
_ = x[walkPlan-2]
_ = x[walkPlanDestroy-3]
_ = x[walkRefresh-4]
_ = x[walkValidate-5]
_ = x[walkDestroy-6]
_ = x[walkImport-7]
_ = x[walkEval-8]
_ = x[walkValidate-4]
_ = x[walkDestroy-5]
_ = x[walkImport-6]
_ = x[walkEval-7]
}
const _walkOperation_name = "walkInvalidwalkApplywalkPlanwalkPlanDestroywalkRefreshwalkValidatewalkDestroywalkImportwalkEval"
const _walkOperation_name = "walkInvalidwalkApplywalkPlanwalkPlanDestroywalkValidatewalkDestroywalkImportwalkEval"
var _walkOperation_index = [...]uint8{0, 11, 20, 28, 43, 54, 66, 77, 87, 95}
var _walkOperation_index = [...]uint8{0, 11, 20, 28, 43, 55, 66, 76, 84}
func (i walkOperation) String() string {
if i >= walkOperation(len(_walkOperation_index)-1) {