From d0b7a4a072d4829bfcc3eae4d0a1dd2b03761c2d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 21 Jan 2017 10:24:03 -0800 Subject: [PATCH 1/9] terraform: StateFilter handles cases where ResourceState has no type This was possible with test fixtures but it is also conceiably possible with older states or corrupted states. We can also extract the type from the key so we do that now so that StateFilter is more robust. --- terraform/state_filter.go | 23 ++++++++++++------- terraform/state_filter_test.go | 9 ++++++++ .../single-minimal-resource.tfstate | 18 +++++++++++++++ 3 files changed, 42 insertions(+), 8 deletions(-) create mode 100644 terraform/test-fixtures/state-filter/single-minimal-resource.tfstate diff --git a/terraform/state_filter.go b/terraform/state_filter.go index 1b41a3b7e..9d3a5fb32 100644 --- a/terraform/state_filter.go +++ b/terraform/state_filter.go @@ -85,15 +85,22 @@ func (f *StateFilter) filterSingle(a *ResourceAddress) []*StateFilterResult { // the modules to find relevant resources. for _, m := range modules { for n, r := range m.Resources { - if f.relevant(a, r) { - // The name in the state contains valuable information. Parse. - key, err := ParseResourceStateKey(n) - if err != nil { - // If we get an error parsing, then just ignore it - // out of the state. - continue - } + // The name in the state contains valuable information. Parse. + key, err := ParseResourceStateKey(n) + if err != nil { + // If we get an error parsing, then just ignore it + // out of the state. + continue + } + // Older states and test fixtures often don't contain the + // type directly on the ResourceState. We add this so StateFilter + // is a bit more robust. + if r.Type == "" { + r.Type = key.Type + } + + if f.relevant(a, r) { if a.Name != "" && a.Name != key.Name { // Name doesn't match continue diff --git a/terraform/state_filter_test.go b/terraform/state_filter_test.go index a0dcba0fe..6fbcfa716 100644 --- a/terraform/state_filter_test.go +++ b/terraform/state_filter_test.go @@ -38,6 +38,15 @@ func TestStateFilterFilter(t *testing.T) { }, }, + "single resource from minimal state": { + "single-minimal-resource.tfstate", + []string{"aws_instance.web"}, + []string{ + "*terraform.ResourceState: aws_instance.web", + "*terraform.InstanceState: aws_instance.web", + }, + }, + "single resource with similar names": { "small_test_instance.tfstate", []string{"test_instance.foo"}, diff --git a/terraform/test-fixtures/state-filter/single-minimal-resource.tfstate b/terraform/test-fixtures/state-filter/single-minimal-resource.tfstate new file mode 100644 index 000000000..b6963ee31 --- /dev/null +++ b/terraform/test-fixtures/state-filter/single-minimal-resource.tfstate @@ -0,0 +1,18 @@ +{ + "version": 1, + "serial": 12, + "modules": [ + { + "path": [ + "root" + ], + "resources": { + "aws_instance.web": { + "primary": { + "id": "onprem" + } + } + } + } + ] +} From 9c1648988734b9556e9d22040d7029c0077681f2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 22 Jan 2017 12:57:19 -0800 Subject: [PATCH 2/9] terraform: ConfigTransformer has Unique and mode filters --- .../transform-config-mode-data/main.tf | 3 + terraform/transform_config.go | 43 ++++++++++- terraform/transform_config_test.go | 75 +++++++++++++++++++ 3 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 terraform/test-fixtures/transform-config-mode-data/main.tf diff --git a/terraform/test-fixtures/transform-config-mode-data/main.tf b/terraform/test-fixtures/transform-config-mode-data/main.tf new file mode 100644 index 000000000..3c3e7e50d --- /dev/null +++ b/terraform/test-fixtures/transform-config-mode-data/main.tf @@ -0,0 +1,3 @@ +data "aws_ami" "foo" {} + +resource "aws_instance" "web" {} diff --git a/terraform/transform_config.go b/terraform/transform_config.go index c2dad20c9..61bce8532 100644 --- a/terraform/transform_config.go +++ b/terraform/transform_config.go @@ -4,7 +4,9 @@ import ( "errors" "fmt" "log" + "sync" + "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/dag" ) @@ -23,10 +25,25 @@ import ( type ConfigTransformer struct { Concrete ConcreteResourceNodeFunc + // Module is the module to add resources from. Module *module.Tree + + // Unique will only add resources that aren't already present in the graph. + Unique bool + + // Mode will only add resources that match the given mode + ModeFilter bool + Mode config.ResourceMode + + l sync.Mutex + uniqueMap map[string]struct{} } func (t *ConfigTransformer) Transform(g *Graph) error { + // Lock since we use some internal state + t.l.Lock() + defer t.l.Unlock() + // If no module is given, we don't do anything if t.Module == nil { return nil @@ -37,6 +54,18 @@ func (t *ConfigTransformer) Transform(g *Graph) error { return errors.New("module must be loaded for ConfigTransformer") } + // Reset the uniqueness map. If we're tracking uniques, then populate + // it with addresses. + t.uniqueMap = make(map[string]struct{}) + defer func() { t.uniqueMap = nil }() + if t.Unique { + for _, v := range g.Vertices() { + if rn, ok := v.(GraphNodeResource); ok { + t.uniqueMap[rn.ResourceAddr().String()] = struct{}{} + } + } + } + // Start the transformation process return t.transform(g, t.Module) } @@ -66,13 +95,13 @@ func (t *ConfigTransformer) transformSingle(g *Graph, m *module.Tree) error { log.Printf("[TRACE] ConfigTransformer: Starting for path: %v", m.Path()) // Get the configuration for this module - config := m.Config() + conf := m.Config() // Build the path we're at path := m.Path() // Write all the resources out - for _, r := range config.Resources { + for _, r := range conf.Resources { // Build the resource address addr, err := parseResourceAddressConfig(r) if err != nil { @@ -81,6 +110,16 @@ func (t *ConfigTransformer) transformSingle(g *Graph, m *module.Tree) error { } addr.Path = path + // If this is already in our uniqueness map, don't add it again + if _, ok := t.uniqueMap[addr.String()]; ok { + continue + } + + // Remove non-matching modes + if t.ModeFilter && addr.Mode != t.Mode { + continue + } + // Build the abstract node and the concrete one abstract := &NodeAbstractResource{Addr: addr} var node dag.Vertex = abstract diff --git a/terraform/transform_config_test.go b/terraform/transform_config_test.go index 31bb7c8e0..1b2c27cbe 100644 --- a/terraform/transform_config_test.go +++ b/terraform/transform_config_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + "github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config/module" ) @@ -48,6 +49,80 @@ func TestConfigTransformer(t *testing.T) { } } +func TestConfigTransformer_mode(t *testing.T) { + g := Graph{Path: RootModulePath} + tf := &ConfigTransformer{ + Module: testModule(t, "transform-config-mode-data"), + ModeFilter: true, + Mode: config.DataResourceMode, + } + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(` +data.aws_ami.foo +`) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestConfigTransformer_nonUnique(t *testing.T) { + addr, err := ParseResourceAddress("aws_instance.web") + if err != nil { + t.Fatalf("bad: %s", err) + } + + g := Graph{Path: RootModulePath} + g.Add(&NodeAbstractResource{Addr: addr}) + tf := &ConfigTransformer{Module: testModule(t, "graph-basic")} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(` +aws_instance.web +aws_instance.web +aws_load_balancer.weblb +aws_security_group.firewall +openstack_floating_ip.random +`) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestConfigTransformer_unique(t *testing.T) { + addr, err := ParseResourceAddress("aws_instance.web") + if err != nil { + t.Fatalf("bad: %s", err) + } + + g := Graph{Path: RootModulePath} + g.Add(&NodeAbstractResource{Addr: addr}) + tf := &ConfigTransformer{ + Module: testModule(t, "graph-basic"), + Unique: true, + } + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(` +aws_instance.web +aws_load_balancer.weblb +aws_security_group.firewall +openstack_floating_ip.random +`) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + const testConfigTransformerGraphBasicStr = ` aws_instance.web aws_load_balancer.weblb From 38286fe49160f054f567990fc6a4dd2e9d9d5182 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 21 Jan 2017 11:22:40 -0800 Subject: [PATCH 3/9] 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", From 7c014b84b68bce935fd9e1f1550ea8dc43477045 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 22 Jan 2017 16:05:10 -0800 Subject: [PATCH 4/9] 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{}, - }, - } -} From 66f6f70cdbc2b2d11d18f8352d5f6f2ef84d8328 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 23 Jan 2017 21:48:22 -0800 Subject: [PATCH 5/9] terraform: input graph --- terraform/context.go | 6 +++++- terraform/context_graph_type.go | 2 ++ terraform/graph_builder_plan.go | 25 +++++++++++++++++-------- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/terraform/context.go b/terraform/context.go index 7115f7560..04f43c855 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -216,6 +216,9 @@ func (c *Context) Graph(typ GraphType, opts *ContextGraphOpts) (*Graph, error) { Validate: opts.Validate, }).Build(RootModulePath) + case GraphTypeInput: + // The input graph is just a slightly modified plan graph + fallthrough case GraphTypePlan: return (&PlanGraphBuilder{ Module: c.module, @@ -223,6 +226,7 @@ func (c *Context) Graph(typ GraphType, opts *ContextGraphOpts) (*Graph, error) { Providers: c.components.ResourceProviders(), Targets: c.targets, Validate: opts.Validate, + Input: typ == GraphTypeInput, }).Build(RootModulePath) case GraphTypePlanDestroy: @@ -411,7 +415,7 @@ func (c *Context) Input(mode InputMode) error { if mode&InputModeProvider != 0 { // Build the graph - graph, err := c.Graph(GraphTypeLegacy, nil) + graph, err := c.Graph(GraphTypeInput, nil) if err != nil { return err } diff --git a/terraform/context_graph_type.go b/terraform/context_graph_type.go index c63db7ea5..0dff25f15 100644 --- a/terraform/context_graph_type.go +++ b/terraform/context_graph_type.go @@ -14,6 +14,7 @@ const ( GraphTypePlan GraphTypePlanDestroy GraphTypeApply + GraphTypeInput ) // GraphTypeMap is a mapping of human-readable string to GraphType. This @@ -21,6 +22,7 @@ const ( // graph types. var GraphTypeMap = map[string]GraphType{ "apply": GraphTypeApply, + "input": GraphTypeInput, "plan": GraphTypePlan, "plan-destroy": GraphTypePlanDestroy, "refresh": GraphTypeRefresh, diff --git a/terraform/graph_builder_plan.go b/terraform/graph_builder_plan.go index 4d8f487c6..f53024270 100644 --- a/terraform/graph_builder_plan.go +++ b/terraform/graph_builder_plan.go @@ -34,6 +34,14 @@ type PlanGraphBuilder struct { // Validate will do structural validation of the graph. Validate bool + + // Input, if true, modifies this graph for inputs. There isn't a + // dedicated input graph because asking for input is identical to + // planning except for the operations done. You still need to know WHAT + // you're going to plan since you only need to ask for input for things + // that are necessary for planning. This requirement makes the graphs + // very similar. + Input bool } // See GraphBuilder @@ -54,17 +62,18 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer { } } - concreteResource := func(a *NodeAbstractResource) dag.Vertex { - return &NodePlannableResource{ - NodeAbstractCountResource: &NodeAbstractCountResource{ + var concreteResource, concreteResourceOrphan ConcreteResourceNodeFunc + if !b.Input { + concreteResource = func(a *NodeAbstractResource) dag.Vertex { + return &NodePlannableResource{ NodeAbstractResource: a, - }, + } } - } - concreteResourceOrphan := func(a *NodeAbstractResource) dag.Vertex { - return &NodePlannableResourceOrphan{ - NodeAbstractResource: a, + concreteResourceOrphan = func(a *NodeAbstractResource) dag.Vertex { + return &NodePlannableResourceOrphan{ + NodeAbstractResource: a, + } } } From 14270750051aeb6dfdea687ecc57a6fd07edcd9c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 25 Jan 2017 12:24:48 -0800 Subject: [PATCH 6/9] terraform: wip moving validation to new graph --- terraform/context.go | 25 +++++++-- terraform/context_graph_type.go | 2 + terraform/graph_builder_input.go | 27 ++++++++++ terraform/graph_builder_plan.go | 83 ++++++++++++++++++----------- terraform/graph_builder_validate.go | 34 ++++++++++++ terraform/node_resource_validate.go | 66 +++++++++++++++++++++++ 6 files changed, 202 insertions(+), 35 deletions(-) create mode 100644 terraform/graph_builder_input.go create mode 100644 terraform/graph_builder_validate.go create mode 100644 terraform/node_resource_validate.go diff --git a/terraform/context.go b/terraform/context.go index 04f43c855..cf18c74c2 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -219,15 +219,32 @@ func (c *Context) Graph(typ GraphType, opts *ContextGraphOpts) (*Graph, error) { case GraphTypeInput: // The input graph is just a slightly modified plan graph fallthrough + case GraphTypeValidate: + // The validate graph is just a slightly modified plan graph + fallthrough case GraphTypePlan: - return (&PlanGraphBuilder{ + // Create the plan graph builder + p := &PlanGraphBuilder{ Module: c.module, State: c.state, Providers: c.components.ResourceProviders(), Targets: c.targets, Validate: opts.Validate, - Input: typ == GraphTypeInput, - }).Build(RootModulePath) + } + + // Some special cases for other graph types shared with plan currently + var b GraphBuilder = p + switch typ { + case GraphTypeInput: + b = InputGraphBuilder(p) + case GraphTypeValidate: + // We need to set the provisioners so those can be validated + p.Provisioners = c.components.ResourceProvisioners() + + b = ValidateGraphBuilder(p) + } + + return b.Build(RootModulePath) case GraphTypePlanDestroy: return (&DestroyPlanGraphBuilder{ @@ -661,7 +678,7 @@ func (c *Context) Validate() ([]string, []error) { // We also validate the graph generated here, but this graph doesn't // necessarily match the graph that Plan will generate, so we'll validate the // graph again later after Planning. - graph, err := c.Graph(GraphTypeLegacy, nil) + graph, err := c.Graph(GraphTypeValidate, nil) if err != nil { return nil, []error{err} } diff --git a/terraform/context_graph_type.go b/terraform/context_graph_type.go index 0dff25f15..084f0105d 100644 --- a/terraform/context_graph_type.go +++ b/terraform/context_graph_type.go @@ -15,6 +15,7 @@ const ( GraphTypePlanDestroy GraphTypeApply GraphTypeInput + GraphTypeValidate ) // GraphTypeMap is a mapping of human-readable string to GraphType. This @@ -27,4 +28,5 @@ var GraphTypeMap = map[string]GraphType{ "plan-destroy": GraphTypePlanDestroy, "refresh": GraphTypeRefresh, "legacy": GraphTypeLegacy, + "validate": GraphTypeValidate, } diff --git a/terraform/graph_builder_input.go b/terraform/graph_builder_input.go new file mode 100644 index 000000000..0df48cdb8 --- /dev/null +++ b/terraform/graph_builder_input.go @@ -0,0 +1,27 @@ +package terraform + +import ( + "github.com/hashicorp/terraform/dag" +) + +// InputGraphBuilder creates the graph for the input operation. +// +// Unlike other graph builders, this is a function since it currently modifies +// and is based on the PlanGraphBuilder. The PlanGraphBuilder passed in will be +// modified and should not be used for any other operations. +func InputGraphBuilder(p *PlanGraphBuilder) GraphBuilder { + // We're going to customize the concrete functions + p.CustomConcrete = true + + // Set the provider to the normal provider. This will ask for input. + p.ConcreteProvider = func(a *NodeAbstractProvider) dag.Vertex { + return &NodeApplyableProvider{ + NodeAbstractProvider: a, + } + } + + // We purposely don't set any more concrete fields since the remainder + // should be no-ops. + + return p +} diff --git a/terraform/graph_builder_plan.go b/terraform/graph_builder_plan.go index f53024270..ecb8f4ae5 100644 --- a/terraform/graph_builder_plan.go +++ b/terraform/graph_builder_plan.go @@ -1,6 +1,8 @@ package terraform import ( + "sync" + "github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/dag" ) @@ -26,6 +28,9 @@ type PlanGraphBuilder struct { // Providers is the list of providers supported. Providers []string + // Provisioners is the list of provisioners supported. + Provisioners []string + // Targets are resources to target Targets []string @@ -35,13 +40,15 @@ type PlanGraphBuilder struct { // Validate will do structural validation of the graph. Validate bool - // Input, if true, modifies this graph for inputs. There isn't a - // dedicated input graph because asking for input is identical to - // planning except for the operations done. You still need to know WHAT - // you're going to plan since you only need to ask for input for things - // that are necessary for planning. This requirement makes the graphs - // very similar. - Input bool + // CustomConcrete can be set to customize the node types created + // for various parts of the plan. This is useful in order to customize + // the plan behavior. + CustomConcrete bool + ConcreteProvider ConcreteProviderNodeFunc + ConcreteResource ConcreteResourceNodeFunc + ConcreteResourceOrphan ConcreteResourceNodeFunc + + once sync.Once } // See GraphBuilder @@ -55,32 +62,12 @@ func (b *PlanGraphBuilder) Build(path []string) (*Graph, error) { // See GraphBuilder func (b *PlanGraphBuilder) Steps() []GraphTransformer { - // Custom factory for creating providers. - concreteProvider := func(a *NodeAbstractProvider) dag.Vertex { - return &NodeApplyableProvider{ - NodeAbstractProvider: a, - } - } - - var concreteResource, concreteResourceOrphan ConcreteResourceNodeFunc - if !b.Input { - concreteResource = func(a *NodeAbstractResource) dag.Vertex { - return &NodePlannableResource{ - NodeAbstractResource: a, - } - } - - concreteResourceOrphan = func(a *NodeAbstractResource) dag.Vertex { - return &NodePlannableResourceOrphan{ - NodeAbstractResource: a, - } - } - } + b.once.Do(b.init) steps := []GraphTransformer{ // Creates all the resources represented in the config &ConfigTransformer{ - Concrete: concreteResource, + Concrete: b.ConcreteResource, Module: b.Module, }, @@ -89,7 +76,7 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer { // Add orphan resources &OrphanResourceTransformer{ - Concrete: concreteResourceOrphan, + Concrete: b.ConcreteResourceOrphan, State: b.State, Module: b.Module, }, @@ -104,12 +91,21 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer { &RootVariableTransformer{Module: b.Module}, // Create all the providers - &MissingProviderTransformer{Providers: b.Providers, Concrete: concreteProvider}, + &MissingProviderTransformer{Providers: b.Providers, Concrete: b.ConcreteProvider}, &ProviderTransformer{}, &DisableProviderTransformer{}, &ParentProviderTransformer{}, &AttachProviderConfigTransformer{Module: b.Module}, + // Provisioner-related transformations. Only add these if requested. + GraphTransformIf( + func() bool { return b.Provisioners != nil }, + GraphTransformMulti( + &MissingProvisionerTransformer{Provisioners: b.Provisioners}, + &ProvisionerTransformer{}, + ), + ), + // Add module variables &ModuleVariableTransformer{Module: b.Module}, @@ -132,3 +128,28 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer { return steps } + +func (b *PlanGraphBuilder) init() { + // Do nothing if the user requests customizing the fields + if b.CustomConcrete { + return + } + + b.ConcreteProvider = func(a *NodeAbstractProvider) dag.Vertex { + return &NodeApplyableProvider{ + NodeAbstractProvider: a, + } + } + + b.ConcreteResource = func(a *NodeAbstractResource) dag.Vertex { + return &NodePlannableResource{ + NodeAbstractResource: a, + } + } + + b.ConcreteResourceOrphan = func(a *NodeAbstractResource) dag.Vertex { + return &NodePlannableResourceOrphan{ + NodeAbstractResource: a, + } + } +} diff --git a/terraform/graph_builder_validate.go b/terraform/graph_builder_validate.go new file mode 100644 index 000000000..1881f95f2 --- /dev/null +++ b/terraform/graph_builder_validate.go @@ -0,0 +1,34 @@ +package terraform + +import ( + "github.com/hashicorp/terraform/dag" +) + +// ValidateGraphBuilder creates the graph for the validate operation. +// +// ValidateGraphBuilder is based on the PlanGraphBuilder. We do this so that +// we only have to validate what we'd normally plan anyways. The +// PlanGraphBuilder given will be modified so it shouldn't be used for anything +// else after calling this function. +func ValidateGraphBuilder(p *PlanGraphBuilder) GraphBuilder { + // We're going to customize the concrete functions + p.CustomConcrete = true + + // Set the provider to the normal provider. This will ask for input. + p.ConcreteProvider = func(a *NodeAbstractProvider) dag.Vertex { + return &NodeApplyableProvider{ + NodeAbstractProvider: a, + } + } + + p.ConcreteResource = func(a *NodeAbstractResource) dag.Vertex { + return &NodeValidatableResource{ + NodeAbstractResource: a, + } + } + + // We purposely don't set any other concrete types since they don't + // require validation. + + return p +} diff --git a/terraform/node_resource_validate.go b/terraform/node_resource_validate.go new file mode 100644 index 000000000..bbdc6f544 --- /dev/null +++ b/terraform/node_resource_validate.go @@ -0,0 +1,66 @@ +package terraform + +// NodeValidatableResource represents a resource that is used for validation +// only. +type NodeValidatableResource struct { + *NodeAbstractResource +} + +// GraphNodeEvalable +func (n *NodeValidatableResource) EvalTree() EvalNode { + addr := n.NodeAbstractResource.Addr + + // 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 provider ResourceProvider + + seq := &EvalSequence{ + Nodes: []EvalNode{ + &EvalGetProvider{ + Name: n.ProvidedBy()[0], + Output: &provider, + }, + &EvalInterpolate{ + Config: n.Config.RawConfig.Copy(), + Resource: resource, + Output: &config, + }, + &EvalValidateResource{ + Provider: &provider, + Config: &config, + ResourceName: n.Config.Name, + ResourceType: n.Config.Type, + ResourceMode: n.Config.Mode, + }, + }, + } + + // Validate all the provisioners + for _, p := range n.Config.Provisioners { + var provisioner ResourceProvisioner + seq.Nodes = append(seq.Nodes, &EvalGetProvisioner{ + Name: p.Type, + Output: &provisioner, + }, &EvalInterpolate{ + Config: p.RawConfig.Copy(), + Resource: resource, + Output: &config, + }, &EvalValidateProvisioner{ + Provisioner: &provisioner, + Config: &config, + }) + } + + return seq +} From 86cbcff962b760bd056c226744992da3a53a9eb9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 25 Jan 2017 12:32:09 -0800 Subject: [PATCH 7/9] terraform: fixup a merge issue We forgot to wrap some changes from the old branch into the new style --- terraform/graph_builder_plan.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/terraform/graph_builder_plan.go b/terraform/graph_builder_plan.go index ecb8f4ae5..275cb32f3 100644 --- a/terraform/graph_builder_plan.go +++ b/terraform/graph_builder_plan.go @@ -143,7 +143,9 @@ func (b *PlanGraphBuilder) init() { b.ConcreteResource = func(a *NodeAbstractResource) dag.Vertex { return &NodePlannableResource{ - NodeAbstractResource: a, + NodeAbstractCountResource: &NodeAbstractCountResource{ + NodeAbstractResource: a, + }, } } From 4a9cafcd67e62355325341724740b5e0f0e79c49 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 25 Jan 2017 12:39:08 -0800 Subject: [PATCH 8/9] terraform: expand count on resources during validation --- terraform/graph_builder_validate.go | 4 +- terraform/node_resource_validate.go | 66 ++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/terraform/graph_builder_validate.go b/terraform/graph_builder_validate.go index 1881f95f2..645ec7be9 100644 --- a/terraform/graph_builder_validate.go +++ b/terraform/graph_builder_validate.go @@ -23,7 +23,9 @@ func ValidateGraphBuilder(p *PlanGraphBuilder) GraphBuilder { p.ConcreteResource = func(a *NodeAbstractResource) dag.Vertex { return &NodeValidatableResource{ - NodeAbstractResource: a, + NodeAbstractCountResource: &NodeAbstractCountResource{ + NodeAbstractResource: a, + }, } } diff --git a/terraform/node_resource_validate.go b/terraform/node_resource_validate.go index bbdc6f544..8e99075c4 100644 --- a/terraform/node_resource_validate.go +++ b/terraform/node_resource_validate.go @@ -1,13 +1,77 @@ package terraform +import ( + "github.com/hashicorp/terraform/dag" +) + // NodeValidatableResource represents a resource that is used for validation // only. type NodeValidatableResource struct { + *NodeAbstractCountResource +} + +// GraphNodeDynamicExpandable +func (n *NodeValidatableResource) 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 &NodeValidatableResourceInstance{ + 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: "NodeValidatableResource", + } + + return b.Build(ctx.Path()) +} + +// This represents a _single_ resource instance to validate. +type NodeValidatableResourceInstance struct { *NodeAbstractResource } // GraphNodeEvalable -func (n *NodeValidatableResource) EvalTree() EvalNode { +func (n *NodeValidatableResourceInstance) EvalTree() EvalNode { addr := n.NodeAbstractResource.Addr // Build the resource for eval From ae6bf241ec10b6d6eb01bb85a88b241855c19ffe Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 25 Jan 2017 21:00:45 -0800 Subject: [PATCH 9/9] terraform: validate self references --- terraform/eval_validate_selfref.go | 74 +++++++++++++++++ terraform/eval_validate_selfref_test.go | 99 +++++++++++++++++++++++ terraform/node_resource_abstract_count.go | 14 ++++ terraform/node_resource_validate.go | 12 +++ 4 files changed, 199 insertions(+) create mode 100644 terraform/eval_validate_selfref.go create mode 100644 terraform/eval_validate_selfref_test.go diff --git a/terraform/eval_validate_selfref.go b/terraform/eval_validate_selfref.go new file mode 100644 index 000000000..ae4436a2e --- /dev/null +++ b/terraform/eval_validate_selfref.go @@ -0,0 +1,74 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/config" +) + +// EvalValidateResourceSelfRef is an EvalNode implementation that validates that +// a configuration doesn't contain a reference to the resource itself. +// +// This must be done prior to interpolating configuration in order to avoid +// any infinite loop scenarios. +type EvalValidateResourceSelfRef struct { + Addr **ResourceAddress + Config **config.RawConfig +} + +func (n *EvalValidateResourceSelfRef) Eval(ctx EvalContext) (interface{}, error) { + addr := *n.Addr + conf := *n.Config + + // Go through the variables and find self references + var errs []error + for k, raw := range conf.Variables { + rv, ok := raw.(*config.ResourceVariable) + if !ok { + continue + } + + // Build an address from the variable + varAddr := &ResourceAddress{ + Path: addr.Path, + Mode: rv.Mode, + Type: rv.Type, + Name: rv.Name, + Index: rv.Index, + InstanceType: TypePrimary, + } + + // If the variable access is a multi-access (*), then we just + // match the index so that we'll match our own addr if everything + // else matches. + if rv.Multi && rv.Index == -1 { + varAddr.Index = addr.Index + } + + // This is a weird thing where ResourceAddres has index "-1" when + // index isn't set at all. This means index "0" for resource access. + // So, if we have this scenario, just set our varAddr to -1 so it + // matches. + if addr.Index == -1 && varAddr.Index == 0 { + varAddr.Index = -1 + } + + // If the addresses match, then this is a self reference + if varAddr.Equals(addr) && varAddr.Index == addr.Index { + errs = append(errs, fmt.Errorf( + "%s: self reference not allowed: %q", + addr, k)) + } + } + + // If no errors, no errors! + if len(errs) == 0 { + return nil, nil + } + + // Wrap the errors in the proper wrapper so we can handle validation + // formatting properly upstream. + return nil, &EvalValidateError{ + Errors: errs, + } +} diff --git a/terraform/eval_validate_selfref_test.go b/terraform/eval_validate_selfref_test.go new file mode 100644 index 000000000..f5e70cb66 --- /dev/null +++ b/terraform/eval_validate_selfref_test.go @@ -0,0 +1,99 @@ +package terraform + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/config" +) + +func TestEvalValidateResourceSelfRef(t *testing.T) { + cases := []struct { + Name string + Addr string + Config map[string]interface{} + Err bool + }{ + { + "no interpolations", + "aws_instance.foo", + map[string]interface{}{ + "foo": "bar", + }, + false, + }, + + { + "non self reference", + "aws_instance.foo", + map[string]interface{}{ + "foo": "${aws_instance.bar.id}", + }, + false, + }, + + { + "self reference", + "aws_instance.foo", + map[string]interface{}{ + "foo": "hello ${aws_instance.foo.id}", + }, + true, + }, + + { + "self reference other index", + "aws_instance.foo", + map[string]interface{}{ + "foo": "hello ${aws_instance.foo.4.id}", + }, + false, + }, + + { + "self reference same index", + "aws_instance.foo[4]", + map[string]interface{}{ + "foo": "hello ${aws_instance.foo.4.id}", + }, + true, + }, + + { + "self reference multi", + "aws_instance.foo[4]", + map[string]interface{}{ + "foo": "hello ${aws_instance.foo.*.id}", + }, + true, + }, + + { + "self reference multi single", + "aws_instance.foo", + map[string]interface{}{ + "foo": "hello ${aws_instance.foo.*.id}", + }, + true, + }, + } + + for i, tc := range cases { + t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { + addr, err := ParseResourceAddress(tc.Addr) + if err != nil { + t.Fatalf("err: %s", err) + } + conf := config.TestRawConfig(t, tc.Config) + + n := &EvalValidateResourceSelfRef{Addr: &addr, Config: &conf} + result, err := n.Eval(nil) + if result != nil { + t.Fatal("result should always be nil") + } + if (err != nil) != tc.Err { + t.Fatalf("err: %s", err) + } + }) + } +} diff --git a/terraform/node_resource_abstract_count.go b/terraform/node_resource_abstract_count.go index 131ebccf9..9b4df757f 100644 --- a/terraform/node_resource_abstract_count.go +++ b/terraform/node_resource_abstract_count.go @@ -6,6 +6,10 @@ package terraform // The embedder should implement `DynamicExpand` to process the count. type NodeAbstractCountResource struct { *NodeAbstractResource + + // Validate, if true, will perform the validation for the count. + // This should only be turned on for the "validate" operation. + Validate bool } // GraphNodeEvalable @@ -21,6 +25,16 @@ func (n *NodeAbstractCountResource) EvalTree() EvalNode { &EvalInterpolate{Config: n.Config.RawCount}, &EvalCountCheckComputed{Resource: n.Config}, + + // If validation is enabled, perform the validation + &EvalIf{ + If: func(ctx EvalContext) (bool, error) { + return n.Validate, nil + }, + + Then: &EvalValidateCount{Resource: n.Config}, + }, + &EvalCountFixZeroOneBoundary{Resource: n.Config}, }, } diff --git a/terraform/node_resource_validate.go b/terraform/node_resource_validate.go index 8e99075c4..3ea5bfd12 100644 --- a/terraform/node_resource_validate.go +++ b/terraform/node_resource_validate.go @@ -10,6 +10,14 @@ type NodeValidatableResource struct { *NodeAbstractCountResource } +// GraphNodeEvalable +func (n *NodeValidatableResource) EvalTree() EvalNode { + // Ensure we're validating + c := n.NodeAbstractCountResource + c.Validate = true + return c.EvalTree() +} + // GraphNodeDynamicExpandable func (n *NodeValidatableResource) DynamicExpand(ctx EvalContext) (*Graph, error) { // Grab the state which we read @@ -91,6 +99,10 @@ func (n *NodeValidatableResourceInstance) EvalTree() EvalNode { seq := &EvalSequence{ Nodes: []EvalNode{ + &EvalValidateResourceSelfRef{ + Addr: &addr, + Config: &n.Config.RawConfig, + }, &EvalGetProvider{ Name: n.ProvidedBy()[0], Output: &provider,