core: lifecycle for data resources
This implements the main behavior of data resources, including both the early read in cases where the configuration is non-computed and the split plan/apply read for cases where full configuration can't be known until apply time.
This commit is contained in:
parent
1da560b653
commit
36054470e4
|
@ -0,0 +1,112 @@
|
||||||
|
package terraform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EvalReadDataDiff is an EvalNode implementation that executes a data
|
||||||
|
// resource's ReadDataDiff method to discover what attributes it exports.
|
||||||
|
type EvalReadDataDiff struct {
|
||||||
|
Provider *ResourceProvider
|
||||||
|
Output **InstanceDiff
|
||||||
|
OutputState **InstanceState
|
||||||
|
Config **ResourceConfig
|
||||||
|
Info *InstanceInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *EvalReadDataDiff) Eval(ctx EvalContext) (interface{}, error) {
|
||||||
|
// TODO: test
|
||||||
|
provider := *n.Provider
|
||||||
|
config := *n.Config
|
||||||
|
|
||||||
|
err := ctx.Hook(func(h Hook) (HookAction, error) {
|
||||||
|
return h.PreDiff(n.Info, nil)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
diff, err := provider.ReadDataDiff(n.Info, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if diff == nil {
|
||||||
|
diff = new(InstanceDiff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// id is always computed, because we're always "creating a new resource"
|
||||||
|
diff.init()
|
||||||
|
diff.Attributes["id"] = &ResourceAttrDiff{
|
||||||
|
Old: "",
|
||||||
|
NewComputed: true,
|
||||||
|
RequiresNew: true,
|
||||||
|
Type: DiffAttrOutput,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ctx.Hook(func(h Hook) (HookAction, error) {
|
||||||
|
return h.PostDiff(n.Info, diff)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
*n.Output = diff
|
||||||
|
|
||||||
|
if n.OutputState != nil {
|
||||||
|
state := &InstanceState{}
|
||||||
|
*n.OutputState = state
|
||||||
|
|
||||||
|
// Apply the diff to the returned state, so the state includes
|
||||||
|
// any attribute values that are not computed.
|
||||||
|
if !diff.Empty() && n.OutputState != nil {
|
||||||
|
*n.OutputState = state.MergeDiff(diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EvalReadDataApply is an EvalNode implementation that executes a data
|
||||||
|
// resource's ReadDataApply method to read data from the data source.
|
||||||
|
type EvalReadDataApply struct {
|
||||||
|
Provider *ResourceProvider
|
||||||
|
Output **InstanceState
|
||||||
|
Diff **InstanceDiff
|
||||||
|
Info *InstanceInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *EvalReadDataApply) Eval(ctx EvalContext) (interface{}, error) {
|
||||||
|
// TODO: test
|
||||||
|
provider := *n.Provider
|
||||||
|
diff := *n.Diff
|
||||||
|
|
||||||
|
// For the purpose of external hooks we present a data apply as a
|
||||||
|
// "Refresh" rather than an "Apply" because creating a data source
|
||||||
|
// is presented to users/callers as a "read" operation.
|
||||||
|
err := ctx.Hook(func(h Hook) (HookAction, error) {
|
||||||
|
// We don't have a state yet, so we'll just give the hook an
|
||||||
|
// empty one to work with.
|
||||||
|
return h.PreRefresh(n.Info, &InstanceState{})
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
state, err := provider.ReadDataApply(n.Info, diff)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: %s", n.Info.Id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ctx.Hook(func(h Hook) (HookAction, error) {
|
||||||
|
return h.PostRefresh(n.Info, state)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.Output != nil {
|
||||||
|
*n.Output = state
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
|
@ -586,11 +586,232 @@ func (n *graphNodeExpandedResource) managedResourceEvalNodes(resource *Resource,
|
||||||
|
|
||||||
func (n *graphNodeExpandedResource) dataResourceEvalNodes(resource *Resource, info *InstanceInfo, resourceConfig *ResourceConfig) []EvalNode {
|
func (n *graphNodeExpandedResource) dataResourceEvalNodes(resource *Resource, info *InstanceInfo, resourceConfig *ResourceConfig) []EvalNode {
|
||||||
//var diff *InstanceDiff
|
//var diff *InstanceDiff
|
||||||
//var provider ResourceProvider
|
var provider ResourceProvider
|
||||||
//var state *InstanceState
|
var config *ResourceConfig
|
||||||
|
var diff *InstanceDiff
|
||||||
|
var state *InstanceState
|
||||||
|
|
||||||
nodes := make([]EvalNode, 0, 5)
|
nodes := make([]EvalNode, 0, 5)
|
||||||
|
|
||||||
|
// Refresh the resource
|
||||||
|
// TODO: Interpolate and then check if the config has any computed stuff.
|
||||||
|
// If it doesn't, then do the diff/apply/writestate steps here so we
|
||||||
|
// can get this data resource populated early enough for its values to
|
||||||
|
// be visible during plan.
|
||||||
|
nodes = append(nodes, &EvalOpFilter{
|
||||||
|
Ops: []walkOperation{walkRefresh},
|
||||||
|
Node: &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: n.stateId(),
|
||||||
|
ResourceType: n.Resource.Type,
|
||||||
|
Provider: n.Resource.Provider,
|
||||||
|
Dependencies: n.StateDependencies(),
|
||||||
|
State: &state, // state is nil here
|
||||||
|
},
|
||||||
|
|
||||||
|
&EvalInterpolate{
|
||||||
|
Config: n.Resource.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{}
|
||||||
|
}
|
||||||
|
|
||||||
|
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: n.stateId(),
|
||||||
|
ResourceType: n.Resource.Type,
|
||||||
|
Provider: n.Resource.Provider,
|
||||||
|
Dependencies: n.StateDependencies(),
|
||||||
|
State: &state,
|
||||||
|
},
|
||||||
|
|
||||||
|
&EvalUpdateStateHook{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Diff the resource
|
||||||
|
nodes = append(nodes, &EvalOpFilter{
|
||||||
|
Ops: []walkOperation{walkPlan},
|
||||||
|
Node: &EvalSequence{
|
||||||
|
Nodes: []EvalNode{
|
||||||
|
|
||||||
|
&EvalReadState{
|
||||||
|
Name: n.stateId(),
|
||||||
|
Output: &state,
|
||||||
|
},
|
||||||
|
|
||||||
|
// If we already have a state (created either during refresh
|
||||||
|
// or on a previous apply) then we don't need to do any
|
||||||
|
// more work on it during apply.
|
||||||
|
&EvalIf{
|
||||||
|
If: func(ctx EvalContext) (bool, error) {
|
||||||
|
if state != nil {
|
||||||
|
return true, EvalEarlyExitError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
Then: EvalNoop{},
|
||||||
|
},
|
||||||
|
|
||||||
|
&EvalInterpolate{
|
||||||
|
Config: n.Resource.RawConfig.Copy(),
|
||||||
|
Resource: resource,
|
||||||
|
Output: &config,
|
||||||
|
},
|
||||||
|
|
||||||
|
&EvalGetProvider{
|
||||||
|
Name: n.ProvidedBy()[0],
|
||||||
|
Output: &provider,
|
||||||
|
},
|
||||||
|
|
||||||
|
&EvalReadDataDiff{
|
||||||
|
Info: info,
|
||||||
|
Config: &config,
|
||||||
|
Provider: &provider,
|
||||||
|
Output: &diff,
|
||||||
|
OutputState: &state,
|
||||||
|
},
|
||||||
|
|
||||||
|
&EvalWriteState{
|
||||||
|
Name: n.stateId(),
|
||||||
|
ResourceType: n.Resource.Type,
|
||||||
|
Provider: n.Resource.Provider,
|
||||||
|
Dependencies: n.StateDependencies(),
|
||||||
|
State: &state,
|
||||||
|
},
|
||||||
|
|
||||||
|
&EvalWriteDiff{
|
||||||
|
Name: n.stateId(),
|
||||||
|
Diff: &diff,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Apply
|
||||||
|
nodes = append(nodes, &EvalOpFilter{
|
||||||
|
Ops: []walkOperation{walkApply, walkDestroy},
|
||||||
|
Node: &EvalSequence{
|
||||||
|
Nodes: []EvalNode{
|
||||||
|
// Get the saved diff for apply
|
||||||
|
&EvalReadDiff{
|
||||||
|
Name: n.stateId(),
|
||||||
|
Diff: &diff,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Stop here if we don't actually have a diff
|
||||||
|
&EvalIf{
|
||||||
|
If: func(ctx EvalContext) (bool, error) {
|
||||||
|
if diff == nil {
|
||||||
|
return true, EvalEarlyExitError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(diff.Attributes) == 0 {
|
||||||
|
return true, EvalEarlyExitError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
Then: EvalNoop{},
|
||||||
|
},
|
||||||
|
|
||||||
|
// We need to re-interpolate the config here, rather than
|
||||||
|
// just using the diff's values directly, because we've
|
||||||
|
// potentially learned more variable values during the
|
||||||
|
// apply pass that weren't known when the diff was produced.
|
||||||
|
&EvalInterpolate{
|
||||||
|
Config: n.Resource.RawConfig.Copy(),
|
||||||
|
Resource: resource,
|
||||||
|
Output: &config,
|
||||||
|
},
|
||||||
|
|
||||||
|
&EvalGetProvider{
|
||||||
|
Name: n.ProvidedBy()[0],
|
||||||
|
Output: &provider,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Make a new diff with our newly-interpolated config.
|
||||||
|
&EvalReadDataDiff{
|
||||||
|
Info: info,
|
||||||
|
Config: &config,
|
||||||
|
Provider: &provider,
|
||||||
|
Output: &diff,
|
||||||
|
},
|
||||||
|
|
||||||
|
&EvalReadDataApply{
|
||||||
|
Info: info,
|
||||||
|
Diff: &diff,
|
||||||
|
Provider: &provider,
|
||||||
|
Output: &state,
|
||||||
|
},
|
||||||
|
|
||||||
|
&EvalWriteState{
|
||||||
|
Name: n.stateId(),
|
||||||
|
ResourceType: n.Resource.Type,
|
||||||
|
Provider: n.Resource.Provider,
|
||||||
|
Dependencies: n.StateDependencies(),
|
||||||
|
State: &state,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Clear the diff now that we've applied it, so
|
||||||
|
// later nodes won't see a diff that's now a no-op.
|
||||||
|
&EvalWriteDiff{
|
||||||
|
Name: n.stateId(),
|
||||||
|
Diff: nil,
|
||||||
|
},
|
||||||
|
|
||||||
|
&EvalUpdateStateHook{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
return nodes
|
return nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue