terraform: Refresh supports new data sources

This commit is contained in:
Mitchell Hashimoto 2017-01-21 11:22:40 -08:00
parent 9c16489887
commit 38286fe491
No known key found for this signature in database
GPG Key ID: 744E147AA52F5B0A
7 changed files with 384 additions and 5 deletions

View File

@ -233,6 +233,15 @@ func (c *Context) Graph(typ GraphType, opts *ContextGraphOpts) (*Graph, error) {
Validate: opts.Validate,
}).Build(RootModulePath)
case GraphTypeRefresh:
return (&RefreshGraphBuilder{
Module: c.module,
State: c.state,
Providers: c.components.ResourceProviders(),
Targets: c.targets,
Validate: opts.Validate,
}).Build(RootModulePath)
case GraphTypeLegacy:
return c.graphBuilder(opts).Build(RootModulePath)
}
@ -569,8 +578,15 @@ func (c *Context) Refresh() (*State, error) {
// Copy our own state
c.state = c.state.DeepCopy()
// Build the graph
graph, err := c.Graph(GraphTypeLegacy, nil)
// Used throughout below
X_legacyGraph := experiment.Enabled(experiment.X_legacyGraph)
// Build the graph.
graphType := GraphTypeLegacy
if !X_legacyGraph {
graphType = GraphTypeRefresh
}
graph, err := c.Graph(graphType, nil)
if err != nil {
return nil, err
}

View File

@ -10,6 +10,7 @@ type GraphType byte
const (
GraphTypeInvalid GraphType = 0
GraphTypeLegacy GraphType = iota
GraphTypeRefresh
GraphTypePlan
GraphTypePlanDestroy
GraphTypeApply
@ -22,5 +23,6 @@ var GraphTypeMap = map[string]GraphType{
"apply": GraphTypeApply,
"plan": GraphTypePlan,
"plan-destroy": GraphTypePlanDestroy,
"refresh": GraphTypeRefresh,
"legacy": GraphTypeLegacy,
}

View File

@ -520,7 +520,7 @@ func TestContext2Refresh_outputPartial(t *testing.T) {
}
}
func TestContext2Refresh_state(t *testing.T) {
func TestContext2Refresh_stateBasic(t *testing.T) {
p := testProvider("aws")
m := testModule(t, "refresh-basic")
state := &State{
@ -529,6 +529,7 @@ func TestContext2Refresh_state(t *testing.T) {
Path: rootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.web": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "bar",
},
@ -737,6 +738,21 @@ func TestContext2Refresh_unknownProvider(t *testing.T) {
ctx := testContext2(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{},
State: &State{
Modules: []*ModuleState{
&ModuleState{
Path: rootModulePath,
Resources: map[string]*ResourceState{
"aws_instance.web": &ResourceState{
Type: "aws_instance",
Primary: &InstanceState{
ID: "foo",
},
},
},
},
},
},
})
if _, err := ctx.Refresh(); err == nil {

View File

@ -0,0 +1,121 @@
package terraform
import (
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module"
"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 {
// Module is the root module for the graph to build.
Module *module.Tree
// State is the current state
State *State
// Providers is the list of providers supported.
Providers []string
// Targets are resources to target
Targets []string
// DisableReduce, if true, will not reduce the graph. Great for testing.
DisableReduce bool
// Validate will do structural validation of the graph.
Validate bool
}
// See GraphBuilder
func (b *RefreshGraphBuilder) Build(path []string) (*Graph, error) {
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,
}
}
concreteResource := func(a *NodeAbstractResource) dag.Vertex {
return &NodeRefreshableResource{
NodeAbstractResource: a,
}
}
steps := []GraphTransformer{
// Creates all the resources represented in the state
&StateTransformer{
Concrete: concreteResource,
State: b.State,
},
// Creates all the data resources that aren't in the state
&ConfigTransformer{
Concrete: concreteResource,
Module: b.Module,
Unique: true,
ModeFilter: true,
Mode: config.DataResourceMode,
},
// Attach the state
&AttachStateTransformer{State: b.State},
// Attach the configuration to any resources
&AttachResourceConfigTransformer{Module: b.Module},
// Add root variables
&RootVariableTransformer{Module: b.Module},
// Create all the providers
&MissingProviderTransformer{Providers: b.Providers, Concrete: concreteProvider},
&ProviderTransformer{},
&DisableProviderTransformer{},
&ParentProviderTransformer{},
&AttachProviderConfigTransformer{Module: b.Module},
// Add the outputs
&OutputTransformer{Module: b.Module},
// Add module variables
&ModuleVariableTransformer{Module: b.Module},
// Connect so that the references are ready for targeting. We'll
// have to connect again later for providers and so on.
&ReferenceTransformer{},
// Target
&TargetsTransformer{Targets: b.Targets},
// Single root
&RootTransformer{},
}
if !b.DisableReduce {
// Perform the transitive reduction to make our graph a bit
// more sane if possible (it usually is possible).
steps = append(steps, &TransitiveReductionTransformer{})
}
return steps
}

View File

@ -4,9 +4,9 @@ package terraform
import "fmt"
const _GraphType_name = "GraphTypeInvalidGraphTypeLegacyGraphTypePlanGraphTypePlanDestroyGraphTypeApply"
const _GraphType_name = "GraphTypeInvalidGraphTypeLegacyGraphTypeRefreshGraphTypePlanGraphTypePlanDestroyGraphTypeApply"
var _GraphType_index = [...]uint8{0, 16, 31, 44, 64, 78}
var _GraphType_index = [...]uint8{0, 16, 31, 47, 60, 80, 94}
func (i GraphType) String() string {
if i >= GraphType(len(_GraphType_index)-1) {

View File

@ -0,0 +1,222 @@
package terraform
import (
"fmt"
"github.com/hashicorp/terraform/config"
)
// NodeRefreshableResource represents a resource that is "applyable":
// it is ready to be applied and is represented by a diff.
type NodeRefreshableResource struct {
*NodeAbstractResource
}
// GraphNodeDestroyer
func (n *NodeRefreshableResource) DestroyAddr() *ResourceAddress {
return n.Addr
}
// GraphNodeEvalable
func (n *NodeRefreshableResource) EvalTree() EvalNode {
// Eval info is different depending on what kind of resource this is
switch mode := n.Addr.Mode; mode {
case config.ManagedResourceMode:
return n.evalTreeManagedResource()
case config.DataResourceMode:
return n.evalTreeDataResource()
default:
panic(fmt.Errorf("unsupported resource mode %s", mode))
}
}
func (n *NodeRefreshableResource) evalTreeManagedResource() 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,
}
// Declare a bunch of variables that are used for state during
// evaluation. Most of this are written to by-address below.
var provider ResourceProvider
var state *InstanceState
// This happened during initial development. All known cases were
// fixed and tested but as a sanity check let's assert here.
if n.ResourceState == 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{
Name: n.ProvidedBy()[0],
Output: &provider,
},
&EvalReadState{
Name: stateId,
Output: &state,
},
&EvalRefresh{
Info: info,
Provider: &provider,
State: &state,
Output: &state,
},
&EvalWriteState{
Name: stateId,
ResourceType: n.ResourceState.Type,
Provider: n.ResourceState.Provider,
Dependencies: n.ResourceState.Dependencies,
State: &state,
},
},
}
}
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{},
},
}
}

View File

@ -45,8 +45,10 @@ func (t *AttachStateTransformer) Transform(g *Graph) error {
}
// Attach the first resource state we get
log.Printf("SEARCH: %s", addr)
found := false
for _, result := range results {
log.Printf("WTF: %s %#v", addr, result)
if rs, ok := result.Value.(*ResourceState); ok {
log.Printf(
"[DEBUG] Attaching resource state to %q: %s",