From ea42deb66c84ddf88f5aee62b5c78423991ec6ea Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 9 Feb 2015 10:14:09 -0800 Subject: [PATCH] terraform: provisioner transforms --- terraform/graph_builder.go | 10 ++ terraform/graph_config_node.go | 10 ++ .../transform-provisioner-basic/main.tf | 3 + .../transform-provisioner-prune/main.tf | 3 + terraform/transform_provisioner.go | 124 ++++++++++++++++++ terraform/transform_provisioner_test.go | 95 ++++++++++++++ 6 files changed, 245 insertions(+) create mode 100644 terraform/test-fixtures/transform-provisioner-basic/main.tf create mode 100644 terraform/test-fixtures/transform-provisioner-prune/main.tf create mode 100644 terraform/transform_provisioner.go create mode 100644 terraform/transform_provisioner_test.go diff --git a/terraform/graph_builder.go b/terraform/graph_builder.go index 435883009..429968183 100644 --- a/terraform/graph_builder.go +++ b/terraform/graph_builder.go @@ -67,19 +67,29 @@ func (b *BuiltinGraphBuilder) Build(path []string) (*Graph, error) { // to build a complete graph. func (b *BuiltinGraphBuilder) Steps() []GraphTransformer { return []GraphTransformer{ + // Create all our resources from the configuration and state &ConfigTransformer{Module: b.Root}, &OrphanTransformer{State: b.State, Module: b.Root}, &TaintedTransformer{State: b.State}, + + // Provider-related transformations &MissingProviderTransformer{Providers: b.Providers}, &ProviderTransformer{}, &PruneProviderTransformer{}, + + // Provisioner-related transformations + + // Run our vertex-level transforms &VertexTransformer{ Transforms: []GraphVertexTransformer{ + // Expand any statically expanded nodes, such as module graphs &ExpandTransform{ Builder: b, }, }, }, + + // Make sure we create one root &RootTransformer{}, } } diff --git a/terraform/graph_config_node.go b/terraform/graph_config_node.go index 536f9fb2f..b0f960e8e 100644 --- a/terraform/graph_config_node.go +++ b/terraform/graph_config_node.go @@ -147,3 +147,13 @@ func (n *GraphNodeConfigResource) EvalTree() EvalNode { func (n *GraphNodeConfigResource) ProvidedBy() string { return resourceProvider(n.Resource.Type) } + +// GraphNodeProvisionerConsumer +func (n *GraphNodeConfigResource) ProvisionedBy() []string { + result := make([]string, len(n.Resource.Provisioners)) + for i, p := range n.Resource.Provisioners { + result[i] = p.Type + } + + return result +} diff --git a/terraform/test-fixtures/transform-provisioner-basic/main.tf b/terraform/test-fixtures/transform-provisioner-basic/main.tf new file mode 100644 index 000000000..3898ac4db --- /dev/null +++ b/terraform/test-fixtures/transform-provisioner-basic/main.tf @@ -0,0 +1,3 @@ +resource "aws_instance" "web" { + provisioner "shell" {} +} diff --git a/terraform/test-fixtures/transform-provisioner-prune/main.tf b/terraform/test-fixtures/transform-provisioner-prune/main.tf new file mode 100644 index 000000000..c78a6ecac --- /dev/null +++ b/terraform/test-fixtures/transform-provisioner-prune/main.tf @@ -0,0 +1,3 @@ +resource "aws_instance" "web" { + provisioner "foo" {} +} diff --git a/terraform/transform_provisioner.go b/terraform/transform_provisioner.go new file mode 100644 index 000000000..049123d00 --- /dev/null +++ b/terraform/transform_provisioner.go @@ -0,0 +1,124 @@ +package terraform + +import ( + "fmt" + + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform/dag" +) + +// GraphNodeProvisioner is an interface that nodes that can be a provisioner +// must implement. The ProvisionerName returned is the name of the provisioner +// they satisfy. +type GraphNodeProvisioner interface { + ProvisionerName() string +} + +// GraphNodeProvisionerConsumer is an interface that nodes that require +// a provisioner must implement. ProvisionedBy must return the name of the +// provisioner to use. +type GraphNodeProvisionerConsumer interface { + ProvisionedBy() []string +} + +// ProvisionerTransformer is a GraphTransformer that maps resources to +// provisioners within the graph. This will error if there are any resources +// that don't map to proper resources. +type ProvisionerTransformer struct{} + +func (t *ProvisionerTransformer) Transform(g *Graph) error { + // Go through the other nodes and match them to provisioners they need + var err error + m := provisionerVertexMap(g) + for _, v := range g.Vertices() { + if pv, ok := v.(GraphNodeProvisionerConsumer); ok { + for _, provisionerName := range pv.ProvisionedBy() { + target := m[provisionerName] + if target == nil { + err = multierror.Append(err, fmt.Errorf( + "%s: provisioner %s couldn't be found", + dag.VertexName(v), provisionerName)) + continue + } + + g.Connect(dag.BasicEdge(v, target)) + } + } + } + + return err +} + +// MissingProvisionerTransformer is a GraphTransformer that adds nodes +// for missing provisioners into the graph. Specifically, it creates provisioner +// configuration nodes for all the provisioners that we support. These are +// pruned later during an optimization pass. +type MissingProvisionerTransformer struct { + // Provisioners is the list of provisioners we support. + Provisioners []string +} + +func (t *MissingProvisionerTransformer) Transform(g *Graph) error { + m := provisionerVertexMap(g) + for _, p := range t.Provisioners { + if _, ok := m[p]; ok { + // This provisioner already exists as a configured node + continue + } + + // Add our own missing provisioner node to the graph + g.Add(&graphNodeMissingProvisioner{ProvisionerNameValue: p}) + } + + return nil +} + +// PruneProvisionerTransformer is a GraphTransformer that prunes all the +// provisioners that aren't needed from the graph. A provisioner is unneeded if +// no resource or module is using that provisioner. +type PruneProvisionerTransformer struct{} + +func (t *PruneProvisionerTransformer) Transform(g *Graph) error { + for _, v := range g.Vertices() { + // We only care about the provisioners + if _, ok := v.(GraphNodeProvisioner); !ok { + continue + } + + // Does anything depend on this? If not, then prune it. + if s := g.UpEdges(v); s.Len() == 0 { + g.Remove(v) + } + } + + return nil +} + +type graphNodeMissingProvisioner struct { + ProvisionerNameValue string +} + +func (n *graphNodeMissingProvisioner) Name() string { + return fmt.Sprintf("provisioner.%s", n.ProvisionerNameValue) +} + +// GraphNodeEvalable impl. +func (n *graphNodeMissingProvisioner) EvalTree() EvalNode { + return nil + //return ProvisionerEvalTree(n.ProvisionerNameValue, nil) +} + +func (n *graphNodeMissingProvisioner) ProvisionerName() string { + return n.ProvisionerNameValue +} + +func provisionerVertexMap(g *Graph) map[string]dag.Vertex { + m := make(map[string]dag.Vertex) + for _, v := range g.Vertices() { + if pv, ok := v.(GraphNodeProvisioner); ok { + m[pv.ProvisionerName()] = v + } + } + + return m +} diff --git a/terraform/transform_provisioner_test.go b/terraform/transform_provisioner_test.go new file mode 100644 index 000000000..f38a0a045 --- /dev/null +++ b/terraform/transform_provisioner_test.go @@ -0,0 +1,95 @@ +package terraform + +import ( + "strings" + "testing" + + "github.com/hashicorp/terraform/dag" +) + +func TestMissingProvisionerTransformer(t *testing.T) { + mod := testModule(t, "transform-provisioner-basic") + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + transform := &MissingProvisionerTransformer{Provisioners: []string{"foo"}} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformMissingProvisionerBasicStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestPruneProvisionerTransformer(t *testing.T) { + mod := testModule(t, "transform-provisioner-prune") + + g := Graph{Path: RootModulePath} + { + tf := &ConfigTransformer{Module: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + transform := &MissingProvisionerTransformer{ + Provisioners: []string{"foo", "bar"}} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + transform := &ProvisionerTransformer{} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + transform := &PruneProvisionerTransformer{} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformPruneProvisionerBasicStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + +func TestGraphNodeMissingProvisioner_impl(t *testing.T) { + var _ dag.Vertex = new(graphNodeMissingProvisioner) + var _ dag.NamedVertex = new(graphNodeMissingProvisioner) + var _ GraphNodeProvisioner = new(graphNodeMissingProvisioner) +} + +func TestGraphNodeMissingProvisioner_ProvisionerName(t *testing.T) { + n := &graphNodeMissingProvisioner{ProvisionerNameValue: "foo"} + if v := n.ProvisionerName(); v != "foo" { + t.Fatalf("bad: %#v", v) + } +} + +const testTransformMissingProvisionerBasicStr = ` +aws_instance.web +provisioner.foo +` + +const testTransformPruneProvisionerBasicStr = ` +aws_instance.web + provisioner.foo +provisioner.foo +`