From 7c014b84b68bce935fd9e1f1550ea8dc43477045 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 22 Jan 2017 16:05:10 -0800 Subject: [PATCH] terraform: handle count fields for data sources --- terraform/graph_builder_plan.go | 4 +- terraform/graph_builder_refresh.go | 10 +- terraform/node_data_refresh.go | 209 ++++++++++++++++++++++ terraform/node_resource_abstract_count.go | 27 +++ terraform/node_resource_plan.go | 31 +--- terraform/node_resource_refresh.go | 142 +-------------- 6 files changed, 255 insertions(+), 168 deletions(-) create mode 100644 terraform/node_data_refresh.go create mode 100644 terraform/node_resource_abstract_count.go diff --git a/terraform/graph_builder_plan.go b/terraform/graph_builder_plan.go index 35961ee46..4d8f487c6 100644 --- a/terraform/graph_builder_plan.go +++ b/terraform/graph_builder_plan.go @@ -56,7 +56,9 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer { concreteResource := func(a *NodeAbstractResource) dag.Vertex { return &NodePlannableResource{ - NodeAbstractResource: a, + NodeAbstractCountResource: &NodeAbstractCountResource{ + NodeAbstractResource: a, + }, } } diff --git a/terraform/graph_builder_refresh.go b/terraform/graph_builder_refresh.go index a7689a996..8fed21d61 100644 --- a/terraform/graph_builder_refresh.go +++ b/terraform/graph_builder_refresh.go @@ -62,6 +62,14 @@ func (b *RefreshGraphBuilder) Steps() []GraphTransformer { } } + concreteDataResource := func(a *NodeAbstractResource) dag.Vertex { + return &NodeRefreshableDataResource{ + NodeAbstractCountResource: &NodeAbstractCountResource{ + NodeAbstractResource: a, + }, + } + } + steps := []GraphTransformer{ // Creates all the resources represented in the state &StateTransformer{ @@ -71,7 +79,7 @@ func (b *RefreshGraphBuilder) Steps() []GraphTransformer { // Creates all the data resources that aren't in the state &ConfigTransformer{ - Concrete: concreteResource, + Concrete: concreteDataResource, Module: b.Module, Unique: true, ModeFilter: true, diff --git a/terraform/node_data_refresh.go b/terraform/node_data_refresh.go new file mode 100644 index 000000000..ae64d844a --- /dev/null +++ b/terraform/node_data_refresh.go @@ -0,0 +1,209 @@ +package terraform + +import ( + "github.com/hashicorp/terraform/dag" +) + +// NodeRefreshableDataResource represents a resource that is "plannable": +// it is ready to be planned in order to create a diff. +type NodeRefreshableDataResource struct { + *NodeAbstractCountResource +} + +// GraphNodeDynamicExpandable +func (n *NodeRefreshableDataResource) DynamicExpand(ctx EvalContext) (*Graph, error) { + // Grab the state which we read + state, lock := ctx.State() + lock.RLock() + defer lock.RUnlock() + + // Expand the resource count which must be available by now from EvalTree + count, err := n.Config.Count() + if err != nil { + return nil, err + } + + // The concrete resource factory we'll use + concreteResource := func(a *NodeAbstractResource) dag.Vertex { + // Add the config and state since we don't do that via transforms + a.Config = n.Config + + return &NodeRefreshableDataResourceInstance{ + NodeAbstractResource: a, + } + } + + // Start creating the steps + steps := []GraphTransformer{ + // Expand the count. + &ResourceCountTransformer{ + Concrete: concreteResource, + Count: count, + Addr: n.ResourceAddr(), + }, + + // Attach the state + &AttachStateTransformer{State: state}, + + // Targeting + &TargetsTransformer{ParsedTargets: 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", + } + + return b.Build(ctx.Path()) +} + +// NodeRefreshableDataResourceInstance represents a _single_ resource instance +// that is refreshable. +type NodeRefreshableDataResourceInstance struct { + *NodeAbstractResource +} + +// GraphNodeEvalable +func (n *NodeRefreshableDataResourceInstance) EvalTree() EvalNode { + addr := n.NodeAbstractResource.Addr + + // stateId is the ID to put into the state + stateId := addr.stateId() + + // Build the instance info. More of this will be populated during eval + info := &InstanceInfo{ + Id: stateId, + Type: addr.Type, + } + + // Get the state if we have it, if not we build it + rs := n.ResourceState + if rs == nil { + rs = &ResourceState{} + } + + // If the config isn't empty we update the state + if n.Config != nil { + // Determine the dependencies for the state. We use some older + // code for this that we've used for a long time. + var stateDeps []string + { + oldN := &graphNodeExpandedResource{ + Resource: n.Config, + Index: addr.Index, + } + stateDeps = oldN.StateDependencies() + } + + rs = &ResourceState{ + Type: n.Config.Type, + Provider: n.Config.Provider, + Dependencies: stateDeps, + } + } + + // Build the resource for eval + resource := &Resource{ + Name: addr.Name, + Type: addr.Type, + CountIndex: addr.Index, + } + if resource.CountIndex < 0 { + resource.CountIndex = 0 + } + + // Declare a bunch of variables that are used for state during + // evaluation. Most of this are written to by-address below. + var config *ResourceConfig + var diff *InstanceDiff + var provider ResourceProvider + var state *InstanceState + + return &EvalSequence{ + Nodes: []EvalNode{ + // Always destroy the existing state first, since we must + // make sure that values from a previous read will not + // get interpolated if we end up needing to defer our + // loading until apply time. + &EvalWriteState{ + Name: stateId, + ResourceType: rs.Type, + Provider: rs.Provider, + Dependencies: rs.Dependencies, + State: &state, // state is nil here + }, + + &EvalInterpolate{ + Config: n.Config.RawConfig.Copy(), + Resource: resource, + Output: &config, + }, + + // The rest of this pass can proceed only if there are no + // computed values in our config. + // (If there are, we'll deal with this during the plan and + // apply phases.) + &EvalIf{ + If: func(ctx EvalContext) (bool, error) { + if config.ComputedKeys != nil && len(config.ComputedKeys) > 0 { + return true, EvalEarlyExitError{} + } + + // If the config explicitly has a depends_on for this + // data source, assume the intention is to prevent + // refreshing ahead of that dependency. + if len(n.Config.DependsOn) > 0 { + return true, EvalEarlyExitError{} + } + + return true, nil + }, + + Then: EvalNoop{}, + }, + + // The remainder of this pass is the same as running + // a "plan" pass immediately followed by an "apply" pass, + // populating the state early so it'll be available to + // provider configurations that need this data during + // refresh/plan. + &EvalGetProvider{ + Name: n.ProvidedBy()[0], + Output: &provider, + }, + + &EvalReadDataDiff{ + Info: info, + Config: &config, + Provider: &provider, + Output: &diff, + OutputState: &state, + }, + + &EvalReadDataApply{ + Info: info, + Diff: &diff, + Provider: &provider, + Output: &state, + }, + + &EvalWriteState{ + Name: stateId, + ResourceType: rs.Type, + Provider: rs.Provider, + Dependencies: rs.Dependencies, + State: &state, + }, + + &EvalUpdateStateHook{}, + }, + } +} diff --git a/terraform/node_resource_abstract_count.go b/terraform/node_resource_abstract_count.go new file mode 100644 index 000000000..131ebccf9 --- /dev/null +++ b/terraform/node_resource_abstract_count.go @@ -0,0 +1,27 @@ +package terraform + +// NodeAbstractCountResource should be embedded instead of NodeAbstractResource +// if the resource has a `count` value that needs to be expanded. +// +// The embedder should implement `DynamicExpand` to process the count. +type NodeAbstractCountResource struct { + *NodeAbstractResource +} + +// GraphNodeEvalable +func (n *NodeAbstractCountResource) EvalTree() EvalNode { + return &EvalSequence{ + Nodes: []EvalNode{ + // The EvalTree for a plannable resource primarily involves + // interpolating the count since it can contain variables + // we only just received access to. + // + // With the interpolated count, we can then DynamicExpand + // into the proper number of instances. + &EvalInterpolate{Config: n.Config.RawCount}, + + &EvalCountCheckComputed{Resource: n.Config}, + &EvalCountFixZeroOneBoundary{Resource: n.Config}, + }, + } +} diff --git a/terraform/node_resource_plan.go b/terraform/node_resource_plan.go index c4694d493..52bbf88a1 100644 --- a/terraform/node_resource_plan.go +++ b/terraform/node_resource_plan.go @@ -7,34 +7,7 @@ import ( // NodePlannableResource represents a resource that is "plannable": // it is ready to be planned in order to create a diff. type NodePlannableResource struct { - *NodeAbstractResource - - // Set by GraphNodeTargetable and used during DynamicExpand to - // forward targets downwards. - targets []ResourceAddress -} - -// GraphNodeTargetable -func (n *NodePlannableResource) SetTargets(targets []ResourceAddress) { - n.targets = targets -} - -// GraphNodeEvalable -func (n *NodePlannableResource) EvalTree() EvalNode { - return &EvalSequence{ - Nodes: []EvalNode{ - // The EvalTree for a plannable resource primarily involves - // interpolating the count since it can contain variables - // we only just received access to. - // - // With the interpolated count, we can then DynamicExpand - // into the proper number of instances. - &EvalInterpolate{Config: n.Config.RawCount}, - - &EvalCountCheckComputed{Resource: n.Config}, - &EvalCountFixZeroOneBoundary{Resource: n.Config}, - }, - } + *NodeAbstractCountResource } // GraphNodeDynamicExpandable @@ -91,7 +64,7 @@ func (n *NodePlannableResource) DynamicExpand(ctx EvalContext) (*Graph, error) { &AttachStateTransformer{State: state}, // Targeting - &TargetsTransformer{ParsedTargets: n.targets}, + &TargetsTransformer{ParsedTargets: n.Targets}, // Connect references so ordering is correct &ReferenceTransformer{}, diff --git a/terraform/node_resource_refresh.go b/terraform/node_resource_refresh.go index 018c77cfb..3adfffdf9 100644 --- a/terraform/node_resource_refresh.go +++ b/terraform/node_resource_refresh.go @@ -24,7 +24,11 @@ func (n *NodeRefreshableResource) EvalTree() EvalNode { case config.ManagedResourceMode: return n.evalTreeManagedResource() case config.DataResourceMode: - return n.evalTreeDataResource() + dn := &NodeRefreshableDataResourceInstance{ + NodeAbstractResource: n.NodeAbstractResource, + } + + return dn.EvalTree() default: panic(fmt.Errorf("unsupported resource mode %s", mode)) } @@ -84,139 +88,3 @@ func (n *NodeRefreshableResource) evalTreeManagedResource() EvalNode { }, } } - -func (n *NodeRefreshableResource) evalTreeDataResource() EvalNode { - addr := n.NodeAbstractResource.Addr - - // stateId is the ID to put into the state - stateId := addr.stateId() - - // Build the instance info. More of this will be populated during eval - info := &InstanceInfo{ - Id: stateId, - Type: addr.Type, - } - - // Get the state if we have it, if not we build it - rs := n.ResourceState - if rs == nil { - rs = &ResourceState{} - } - - // If the config isn't empty we update the state - if n.Config != nil { - // Determine the dependencies for the state. We use some older - // code for this that we've used for a long time. - var stateDeps []string - { - oldN := &graphNodeExpandedResource{ - Resource: n.Config, - Index: addr.Index, - } - stateDeps = oldN.StateDependencies() - } - - rs = &ResourceState{ - Type: n.Config.Type, - Provider: n.Config.Provider, - Dependencies: stateDeps, - } - } - - // Build the resource for eval - resource := &Resource{ - Name: addr.Name, - Type: addr.Type, - CountIndex: addr.Index, - } - if resource.CountIndex < 0 { - resource.CountIndex = 0 - } - - // Declare a bunch of variables that are used for state during - // evaluation. Most of this are written to by-address below. - var config *ResourceConfig - var diff *InstanceDiff - var provider ResourceProvider - var state *InstanceState - - return &EvalSequence{ - Nodes: []EvalNode{ - // Always destroy the existing state first, since we must - // make sure that values from a previous read will not - // get interpolated if we end up needing to defer our - // loading until apply time. - &EvalWriteState{ - Name: stateId, - ResourceType: rs.Type, - Provider: rs.Provider, - Dependencies: rs.Dependencies, - State: &state, // state is nil here - }, - - &EvalInterpolate{ - Config: n.Config.RawConfig.Copy(), - Resource: resource, - Output: &config, - }, - - // The rest of this pass can proceed only if there are no - // computed values in our config. - // (If there are, we'll deal with this during the plan and - // apply phases.) - &EvalIf{ - If: func(ctx EvalContext) (bool, error) { - if config.ComputedKeys != nil && len(config.ComputedKeys) > 0 { - return true, EvalEarlyExitError{} - } - - // If the config explicitly has a depends_on for this - // data source, assume the intention is to prevent - // refreshing ahead of that dependency. - if len(n.Config.DependsOn) > 0 { - return true, EvalEarlyExitError{} - } - - return true, nil - }, - - Then: EvalNoop{}, - }, - - // The remainder of this pass is the same as running - // a "plan" pass immediately followed by an "apply" pass, - // populating the state early so it'll be available to - // provider configurations that need this data during - // refresh/plan. - &EvalGetProvider{ - Name: n.ProvidedBy()[0], - Output: &provider, - }, - - &EvalReadDataDiff{ - Info: info, - Config: &config, - Provider: &provider, - Output: &diff, - OutputState: &state, - }, - - &EvalReadDataApply{ - Info: info, - Diff: &diff, - Provider: &provider, - Output: &state, - }, - - &EvalWriteState{ - Name: stateId, - ResourceType: rs.Type, - Provider: rs.Provider, - Dependencies: rs.Dependencies, - State: &state, - }, - - &EvalUpdateStateHook{}, - }, - } -}