terraform: handle count fields for data sources

This commit is contained in:
Mitchell Hashimoto 2017-01-22 16:05:10 -08:00
parent 38286fe491
commit 7c014b84b6
No known key found for this signature in database
GPG Key ID: 744E147AA52F5B0A
6 changed files with 255 additions and 168 deletions

View File

@ -56,7 +56,9 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
concreteResource := func(a *NodeAbstractResource) dag.Vertex {
return &NodePlannableResource{
NodeAbstractCountResource: &NodeAbstractCountResource{
NodeAbstractResource: a,
},
}
}

View File

@ -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,

View File

@ -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{},
},
}
}

View File

@ -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},
},
}
}

View File

@ -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{},

View File

@ -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{},
},
}
}