From 994f5ce773fccfd8e18e30a8a85b80687e6fc490 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 15 Sep 2016 18:30:11 -0700 Subject: [PATCH] terraform: ReferenceTransform to connect references --- terraform/graph_builder_apply.go | 3 + terraform/graph_builder_apply_test.go | 14 ++- terraform/node_resource.go | 30 +++++ .../graph-builder-apply-basic/main.tf | 6 + terraform/transform_reference.go | 112 ++++++++++++++++++ 5 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 terraform/transform_reference.go diff --git a/terraform/graph_builder_apply.go b/terraform/graph_builder_apply.go index d5be10162..1deea69c5 100644 --- a/terraform/graph_builder_apply.go +++ b/terraform/graph_builder_apply.go @@ -63,6 +63,9 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer { &MissingProvisionerTransformer{Provisioners: b.Provisioners}, &ProvisionerTransformer{}, + // Connect references so ordering is correct + &ReferenceTransformer{}, + // Attach the configurations &AttachConfigTransformer{Module: b.Module}, diff --git a/terraform/graph_builder_apply_test.go b/terraform/graph_builder_apply_test.go index 78c5dd61c..6c384a9f4 100644 --- a/terraform/graph_builder_apply_test.go +++ b/terraform/graph_builder_apply_test.go @@ -27,6 +27,15 @@ func TestApplyGraphBuilder(t *testing.T) { }, }, }, + + "aws_instance.other": &InstanceDiff{ + Attributes: map[string]*ResourceAttrDiff{ + "name": &ResourceAttrDiff{ + Old: "", + New: "foo", + }, + }, + }, }, }, @@ -72,6 +81,9 @@ func TestApplyGraphBuilder(t *testing.T) { const testApplyGraphBuilderStr = ` aws_instance.create provider.aws +aws_instance.other + aws_instance.create + provider.aws module.child.aws_instance.create module.child.provider.aws provisioner.exec @@ -80,6 +92,6 @@ module.child.provider.aws provider.aws provisioner.exec root - aws_instance.create + aws_instance.other module.child.aws_instance.create ` diff --git a/terraform/node_resource.go b/terraform/node_resource.go index 27c6b30f3..0503f0a7b 100644 --- a/terraform/node_resource.go +++ b/terraform/node_resource.go @@ -23,6 +23,36 @@ func (n *NodeApplyableResource) Path() []string { return n.Addr.Path } +// GraphNodeReferenceable +func (n *NodeApplyableResource) ReferenceableName() []string { + if n.Config == nil { + return nil + } + + return []string{n.Config.Id()} +} + +// GraphNodeReferencer +func (n *NodeApplyableResource) References() []string { + // Let's make this a little shorter so it is easier to reference + c := n.Config + if c == nil { + return nil + } + + // Grab all the references + var result []string + result = append(result, c.DependsOn...) + result = append(result, ReferencesFromConfig(c.RawCount)...) + result = append(result, ReferencesFromConfig(c.RawConfig)...) + for _, p := range c.Provisioners { + result = append(result, ReferencesFromConfig(p.ConnInfo)...) + result = append(result, ReferencesFromConfig(p.RawConfig)...) + } + + return result +} + // GraphNodeProviderConsumer func (n *NodeApplyableResource) ProvidedBy() []string { // If we have a config we prefer that above all else diff --git a/terraform/test-fixtures/graph-builder-apply-basic/main.tf b/terraform/test-fixtures/graph-builder-apply-basic/main.tf index 0f6991c53..d077a4ae5 100644 --- a/terraform/test-fixtures/graph-builder-apply-basic/main.tf +++ b/terraform/test-fixtures/graph-builder-apply-basic/main.tf @@ -1,3 +1,9 @@ module "child" { source = "./child" } + +resource "aws_instance" "create" {} + +resource "aws_instance" "other" { + foo = "${aws_instance.create.bar}" +} diff --git a/terraform/transform_reference.go b/terraform/transform_reference.go new file mode 100644 index 000000000..d0c15c118 --- /dev/null +++ b/terraform/transform_reference.go @@ -0,0 +1,112 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/dag" +) + +// GraphNodeReferenceable must be implemented by any node that represents +// a Terraform thing that can be referenced (resource, module, etc.). +type GraphNodeReferenceable interface { + // ReferenceableName is the name by which this can be referenced. + // This can be either just the type, or include the field. Example: + // "aws_instance.bar" or "aws_instance.bar.id". + ReferenceableName() []string +} + +// GraphNodeReferencer must be implemented by nodes that reference other +// Terraform items and therefore depend on them. +type GraphNodeReferencer interface { + // References are the list of things that this node references. This + // can include fields or just the type, just like GraphNodeReferenceable + // above. + References() []string +} + +// ReferenceTransformer is a GraphTransformer that connects all the +// nodes that reference each other in order to form the proper ordering. +type ReferenceTransformer struct{} + +func (t *ReferenceTransformer) Transform(g *Graph) error { + // Build the mapping of reference => vertex for efficient lookups. + refMap := make(map[string][]dag.Vertex) + for _, v := range g.Vertices() { + // We're only looking for referenceable nodes + rn, ok := v.(GraphNodeReferenceable) + if !ok { + continue + } + + // If this node represents a sub path then we prefix + var prefix string + if pn, ok := v.(GraphNodeSubPath); ok { + if path := normalizeModulePath(pn.Path()); len(path) > 1 { + prefix = modulePrefixStr(path[1:]) + "." + } + } + + // Go through and cache them + for _, n := range rn.ReferenceableName() { + n = prefix + n + refMap[n] = append(refMap[n], v) + } + } + + // Find the things that reference things and connect them + for _, v := range g.Vertices() { + rn, ok := v.(GraphNodeReferencer) + if !ok { + continue + } + + // If this node represents a sub path then we prefix + var prefix string + if pn, ok := v.(GraphNodeSubPath); ok { + if path := normalizeModulePath(pn.Path()); len(path) > 1 { + prefix = modulePrefixStr(path[1:]) + "." + } + } + + for _, n := range rn.References() { + n = prefix + n + if parents, ok := refMap[n]; ok { + for _, parent := range parents { + g.Connect(dag.BasicEdge(v, parent)) + } + } + } + } + + return nil +} + +// ReferencesFromConfig returns the references that a configuration has +// based on the interpolated variables in a configuration. +func ReferencesFromConfig(c *config.RawConfig) []string { + var result []string + for _, v := range c.Variables { + if r := ReferenceFromInterpolatedVar(v); r != "" { + result = append(result, r) + } + + } + + return result +} + +// ReferenceFromInterpolatedVar returns the reference from this variable, +// or an empty string if there is no reference. +func ReferenceFromInterpolatedVar(v config.InterpolatedVariable) string { + switch v := v.(type) { + case *config.ModuleVariable: + return fmt.Sprintf("module.%s.output.%s", v.Name, v.Field) + case *config.ResourceVariable: + return v.ResourceId() + case *config.UserVariable: + return fmt.Sprintf("var.%s", v.Name) + default: + return "" + } +}