From 38286fe49160f054f567990fc6a4dd2e9d9d5182 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 21 Jan 2017 11:22:40 -0800 Subject: [PATCH] terraform: Refresh supports new data sources --- terraform/context.go | 20 ++- terraform/context_graph_type.go | 2 + terraform/context_refresh_test.go | 18 ++- terraform/graph_builder_refresh.go | 121 +++++++++++++++ terraform/graphtype_string.go | 4 +- terraform/node_resource_refresh.go | 222 ++++++++++++++++++++++++++++ terraform/transform_attach_state.go | 2 + 7 files changed, 384 insertions(+), 5 deletions(-) create mode 100644 terraform/graph_builder_refresh.go create mode 100644 terraform/node_resource_refresh.go diff --git a/terraform/context.go b/terraform/context.go index 2ebeed097..7115f7560 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -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 } diff --git a/terraform/context_graph_type.go b/terraform/context_graph_type.go index a204969ff..c63db7ea5 100644 --- a/terraform/context_graph_type.go +++ b/terraform/context_graph_type.go @@ -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, } diff --git a/terraform/context_refresh_test.go b/terraform/context_refresh_test.go index 8aa279fe0..b3f0039a2 100644 --- a/terraform/context_refresh_test.go +++ b/terraform/context_refresh_test.go @@ -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 { diff --git a/terraform/graph_builder_refresh.go b/terraform/graph_builder_refresh.go new file mode 100644 index 000000000..a7689a996 --- /dev/null +++ b/terraform/graph_builder_refresh.go @@ -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 +} diff --git a/terraform/graphtype_string.go b/terraform/graphtype_string.go index 8e644abb9..ccf9da711 100644 --- a/terraform/graphtype_string.go +++ b/terraform/graphtype_string.go @@ -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) { diff --git a/terraform/node_resource_refresh.go b/terraform/node_resource_refresh.go new file mode 100644 index 000000000..018c77cfb --- /dev/null +++ b/terraform/node_resource_refresh.go @@ -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{}, + }, + } +} diff --git a/terraform/transform_attach_state.go b/terraform/transform_attach_state.go index 623f843de..6e3b80cfd 100644 --- a/terraform/transform_attach_state.go +++ b/terraform/transform_attach_state.go @@ -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",