From dc9b9eee4471fe0626fbce7f1bd8fd693b9ee926 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 13 Sep 2016 17:11:34 -0700 Subject: [PATCH] terraform: connect providers in the apply graph --- terraform/graph.go | 61 +++++++++++++++++++++ terraform/graph_builder_apply.go | 4 ++ terraform/graph_builder_apply_test.go | 5 +- terraform/node_resource.go | 31 +++++++++-- terraform/resource_address.go | 12 +++++ terraform/transform_diff.go | 78 +++++++++++++++++++++++++-- 6 files changed, 182 insertions(+), 9 deletions(-) diff --git a/terraform/graph.go b/terraform/graph.go index e75d93663..f20ad4448 100644 --- a/terraform/graph.go +++ b/terraform/graph.go @@ -28,6 +28,11 @@ type Graph struct { // RootModuleName Path []string + // annotations are the annotations that are added to vertices. Annotations + // are arbitrary metadata taht is used for various logic. Annotations + // should have unique keys that are referenced via constants. + annotations map[dag.Vertex]map[string]interface{} + // dependableMap is a lookaside table for fast lookups for connecting // dependencies by their GraphNodeDependable value to avoid O(n^3)-like // situations and turn them into O(1) with respect to the number of new @@ -37,6 +42,29 @@ type Graph struct { once sync.Once } +// Annotations returns the annotations that are configured for the +// given vertex. The map is guaranteed to be non-nil but may be empty. +// +// The returned map may be modified to modify the annotations of the +// vertex. +func (g *Graph) Annotations(v dag.Vertex) map[string]interface{} { + g.once.Do(g.init) + + // If this vertex isn't in the graph, then just return an empty map + if !g.HasVertex(v) { + return map[string]interface{}{} + } + + // Get the map, if it doesn't exist yet then initialize it + m, ok := g.annotations[v] + if !ok { + m = make(map[string]interface{}) + g.annotations[v] = m + } + + return m +} + // Add is the same as dag.Graph.Add. func (g *Graph) Add(v dag.Vertex) dag.Vertex { g.once.Do(g.init) @@ -51,6 +79,14 @@ func (g *Graph) Add(v dag.Vertex) dag.Vertex { } } + // If this initializes annotations, then do that + if av, ok := v.(GraphNodeAnnotationInit); ok { + as := g.Annotations(v) + for k, v := range av.AnnotationInit() { + as[k] = v + } + } + return v } @@ -65,12 +101,17 @@ func (g *Graph) Remove(v dag.Vertex) dag.Vertex { } } + // Remove the annotations + delete(g.annotations, v) + // Call upwards to remove it from the actual graph return g.Graph.Remove(v) } // Replace is the same as dag.Graph.Replace func (g *Graph) Replace(o, n dag.Vertex) bool { + g.once.Do(g.init) + // Go through and update our lookaside to point to the new vertex for k, v := range g.dependableMap { if v == o { @@ -82,6 +123,12 @@ func (g *Graph) Replace(o, n dag.Vertex) bool { } } + // Move the annotation if it exists + if m, ok := g.annotations[o]; ok { + g.annotations[n] = m + delete(g.annotations, o) + } + return g.Graph.Replace(o, n) } @@ -153,6 +200,10 @@ func (g *Graph) Walk(walker GraphWalker) error { } func (g *Graph) init() { + if g.annotations == nil { + g.annotations = make(map[dag.Vertex]map[string]interface{}) + } + if g.dependableMap == nil { g.dependableMap = make(map[string]dag.Vertex) } @@ -237,6 +288,16 @@ func (g *Graph) walk(walker GraphWalker) error { return g.AcyclicGraph.Walk(walkFn) } +// GraphNodeAnnotationInit is an interface that allows a node to +// initialize it's annotations. +// +// AnnotationInit will be called _once_ when the node is added to a +// graph for the first time and is expected to return it's initial +// annotations. +type GraphNodeAnnotationInit interface { + AnnotationInit() map[string]interface{} +} + // GraphNodeDependable is an interface which says that a node can be // depended on (an edge can be placed between this node and another) according // to the well-known name returned by DependableName. diff --git a/terraform/graph_builder_apply.go b/terraform/graph_builder_apply.go index f168a6a3d..18dba7ab8 100644 --- a/terraform/graph_builder_apply.go +++ b/terraform/graph_builder_apply.go @@ -39,6 +39,10 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer { // Creates all the nodes represented in the diff. &DiffTransformer{Diff: b.Diff}, + // Create all the providers + &MissingProviderTransformer{Providers: b.Providers}, + &ProviderTransformer{}, + // Single root &RootTransformer{}, } diff --git a/terraform/graph_builder_apply_test.go b/terraform/graph_builder_apply_test.go index 45d4c95bc..2a9cde191 100644 --- a/terraform/graph_builder_apply_test.go +++ b/terraform/graph_builder_apply_test.go @@ -33,7 +33,8 @@ func TestApplyGraphBuilder(t *testing.T) { } b := &ApplyGraphBuilder{ - Diff: diff, + Diff: diff, + Providers: []string{"aws"}, } g, err := b.Build(RootModulePath) @@ -54,4 +55,6 @@ func TestApplyGraphBuilder(t *testing.T) { const testApplyGraphBuilderStr = ` aws_instance.create + provider.aws +provider.aws ` diff --git a/terraform/node_resource.go b/terraform/node_resource.go index 86dd245f2..7529418e7 100644 --- a/terraform/node_resource.go +++ b/terraform/node_resource.go @@ -1,10 +1,33 @@ package terraform -// NodeResource is a graph node for referencing a resource. -type NodeResource struct { - Addr *ResourceAddress // Addr is the address for this resource +import ( + "github.com/hashicorp/terraform/config" +) + +// NodeApplyableResource represents a resource that is "applyable": +// it is ready to be applied and is represented by a diff. +type NodeApplyableResource struct { + Addr *ResourceAddress // Addr is the address for this resource + Config *config.Resource // Config is the resource in the config + ResourceState *ResourceState // ResourceState is the ResourceState for this } -func (n *NodeResource) Name() string { +func (n *NodeApplyableResource) Name() string { return n.Addr.String() } + +// GraphNodeProviderConsumer +func (n *NodeApplyableResource) ProvidedBy() []string { + // If we have a config we prefer that above all else + if n.Config != nil { + return []string{resourceProvider(n.Config.Type, n.Config.Provider)} + } + + // If we have state, then we will use the provider from there + if n.ResourceState != nil { + return []string{n.ResourceState.Provider} + } + + // Use our type + return []string{resourceProvider(n.Addr.Type, "")} +} diff --git a/terraform/resource_address.go b/terraform/resource_address.go index 3d15c7f99..e6bd61615 100644 --- a/terraform/resource_address.go +++ b/terraform/resource_address.go @@ -85,6 +85,17 @@ func (r *ResourceAddress) String() string { return strings.Join(result, ".") } +// parseResourceAddressConfig creates a resource address from a config.Resource +func parseResourceAddressConfig(r *config.Resource) (*ResourceAddress, error) { + return &ResourceAddress{ + Type: r.Type, + Name: r.Name, + Index: -1, + InstanceType: TypePrimary, + Mode: r.Mode, + }, nil +} + // parseResourceAddressInternal parses the somewhat bespoke resource // identifier used in states and diffs, such as "instance.name.0". func parseResourceAddressInternal(s string) (*ResourceAddress, error) { @@ -101,6 +112,7 @@ func parseResourceAddressInternal(s string) (*ResourceAddress, error) { Name: parts[1], Index: -1, InstanceType: TypePrimary, + Mode: config.ManagedResourceMode, } // If we have more parts, then we have an index. Parse that. diff --git a/terraform/transform_diff.go b/terraform/transform_diff.go index e51834629..c154fa7f8 100644 --- a/terraform/transform_diff.go +++ b/terraform/transform_diff.go @@ -2,6 +2,8 @@ package terraform import ( "fmt" + + "github.com/hashicorp/terraform/config/module" ) // DiffTransformer is a GraphTransformer that adds the elements of @@ -9,8 +11,16 @@ import ( // // This transform is used for example by the ApplyGraphBuilder to ensure // that only resources that are being modified are represented in the graph. +// +// Config and State is still required for the DiffTransformer for annotations +// since the Diff doesn't contain all the information required to build the +// complete graph (such as create-before-destroy information). The graph +// is built based on the diff first, though, ensuring that only resources +// that are being modified are present in the graph. type DiffTransformer struct { - Diff *Diff + Diff *Diff + Config *module.Tree + State *State } func (t *DiffTransformer) Transform(g *Graph) error { @@ -20,6 +30,7 @@ func (t *DiffTransformer) Transform(g *Graph) error { } // Go through all the modules in the diff. + var nodes []*NodeApplyableResource for _, m := range t.Diff.Modules { // TODO: If this is a destroy diff then add a module destroy node @@ -38,16 +49,75 @@ func (t *DiffTransformer) Transform(g *Graph) error { // reference this resource. addr, err := parseResourceAddressInternal(name) if err != nil { - return fmt.Errorf( - "Error parsing internal name, this is a bug: %q", name) + panic(fmt.Sprintf( + "Error parsing internal name, this is a bug: %q", name)) } + // Very important: add the module path for this resource to + // the address. Remove "root" from it. + addr.Path = m.Path[1:] + // Add the resource to the graph - g.Add(&NodeResource{ + nodes = append(nodes, &NodeApplyableResource{ Addr: addr, }) } } + // NOTE: Lots of room for performance optimizations below. For + // resource-heavy diffs this part alone is probably pretty slow. + + // Annotate all nodes with their config and state + for _, n := range nodes { + // Grab the configuration at this path. + if t := t.Config.Child(n.Addr.Path); t != nil { + for _, r := range t.Config().Resources { + // Get a resource address so we can compare + addr, err := parseResourceAddressConfig(r) + if err != nil { + panic(fmt.Sprintf( + "Error parsing config address, this is a bug: %#v", r)) + } + addr.Path = n.Addr.Path + + // If this is not the same resource, then continue + if !addr.Equals(n.Addr) { + continue + } + + // Same resource! Mark it and exit + n.Config = r + break + } + } + + // Grab the state at this path + if ms := t.State.ModuleByPath(normalizeModulePath(n.Addr.Path)); ms != nil { + for name, rs := range ms.Resources { + // Parse the name for comparison + addr, err := parseResourceAddressInternal(name) + if err != nil { + panic(fmt.Sprintf( + "Error parsing internal name, this is a bug: %q", name)) + } + addr.Path = n.Addr.Path + + // If this is not the same resource, then continue + if !addr.Equals(n.Addr) { + continue + } + + // Same resource! + n.ResourceState = rs + break + } + } + } + + // Add all the nodes to the graph + for _, n := range nodes { + g.Add(n) + } + return nil }