From 38e127275ca84ad6d47666fe605470d9a05539e5 Mon Sep 17 00:00:00 2001 From: Chris Ell Date: Tue, 14 Dec 2021 01:26:18 -0500 Subject: [PATCH] Implements the graph command (#257) * Implements the graph command * Adds version gating, additional tests --- tfexec/graph.go | 85 +++++++++++++++++++++++++++ tfexec/graph_test.go | 76 ++++++++++++++++++++++++ tfexec/internal/e2etest/graph_test.go | 35 +++++++++++ tfexec/options.go | 26 ++++++++ tfexec/version.go | 2 + 5 files changed, 224 insertions(+) create mode 100644 tfexec/graph.go create mode 100644 tfexec/graph_test.go create mode 100644 tfexec/internal/e2etest/graph_test.go diff --git a/tfexec/graph.go b/tfexec/graph.go new file mode 100644 index 00000000..73396280 --- /dev/null +++ b/tfexec/graph.go @@ -0,0 +1,85 @@ +package tfexec + +import ( + "context" + "fmt" + "os/exec" + "strings" +) + +type graphConfig struct { + plan string + drawCycles bool + graphType string +} + +var defaultGraphOptions = graphConfig{} + +type GraphOption interface { + configureGraph(*graphConfig) +} + +func (opt *GraphPlanOption) configureGraph(conf *graphConfig) { + conf.plan = opt.file +} + +func (opt *DrawCyclesOption) configureGraph(conf *graphConfig) { + conf.drawCycles = opt.drawCycles +} + +func (opt *GraphTypeOption) configureGraph(conf *graphConfig) { + conf.graphType = opt.graphType +} + +func (tf *Terraform) Graph(ctx context.Context, opts ...GraphOption) (string, error) { + graphCmd, err := tf.graphCmd(ctx, opts...) + if err != nil { + return "", err + } + var outBuf strings.Builder + graphCmd.Stdout = &outBuf + err = tf.runTerraformCmd(ctx, graphCmd) + if err != nil { + return "", err + } + + return outBuf.String(), nil + +} + +func (tf *Terraform) graphCmd(ctx context.Context, opts ...GraphOption) (*exec.Cmd, error) { + c := defaultGraphOptions + + for _, o := range opts { + o.configureGraph(&c) + } + + args := []string{"graph"} + + if c.plan != "" { + // plan was a positional arguement prior to Terraform 0.15.0. Ensure proper use by checking version. + if err := tf.compatible(ctx, tf0_15_0, nil); err == nil { + args = append(args, "-plan="+c.plan) + } else { + args = append(args, c.plan) + } + } + + if c.drawCycles { + err := tf.compatible(ctx, tf0_5_0, nil) + if err != nil { + return nil, fmt.Errorf("-draw-cycles was first introduced in Terraform 0.5.0: %w", err) + } + args = append(args, "-draw-cycles") + } + + if c.graphType != "" { + err := tf.compatible(ctx, tf0_8_0, nil) + if err != nil { + return nil, fmt.Errorf("-graph-type was first introduced in Terraform 0.8.0: %w", err) + } + args = append(args, "-type="+c.graphType) + } + + return tf.buildTerraformCmd(ctx, nil, args...), nil +} diff --git a/tfexec/graph_test.go b/tfexec/graph_test.go new file mode 100644 index 00000000..02c683e3 --- /dev/null +++ b/tfexec/graph_test.go @@ -0,0 +1,76 @@ +package tfexec + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-exec/tfexec/internal/testutil" +) + +func TestGraphCmd(t *testing.T) { + td := t.TempDir() + + tf, err := NewTerraform(td, tfVersion(t, testutil.Latest013)) + if err != nil { + t.Fatal(err) + } + + // empty env, to avoid environ mismatch in testing + tf.SetEnv(map[string]string{}) + + t.Run("defaults", func(t *testing.T) { + graphCmd, _ := tf.graphCmd(context.Background()) + + assertCmd(t, []string{ + "graph", + }, nil, graphCmd) + }) + + t.Run("override all defaults", func(t *testing.T) { + graphCmd, _ := tf.graphCmd(context.Background(), + GraphPlan("teststate"), + DrawCycles(true), + GraphType("output")) + + assertCmd(t, []string{ + "graph", + "teststate", + "-draw-cycles", + "-type=output", + }, nil, graphCmd) + }) +} + +func TestGraphCmd15(t *testing.T) { + td := t.TempDir() + + tf, err := NewTerraform(td, tfVersion(t, testutil.Latest015)) + if err != nil { + t.Fatal(err) + } + + // empty env, to avoid environ mismatch in testing + tf.SetEnv(map[string]string{}) + + t.Run("defaults", func(t *testing.T) { + graphCmd, _ := tf.graphCmd(context.Background()) + + assertCmd(t, []string{ + "graph", + }, nil, graphCmd) + }) + + t.Run("override all defaults", func(t *testing.T) { + graphCmd, _ := tf.graphCmd(context.Background(), + GraphPlan("teststate"), + DrawCycles(true), + GraphType("output")) + + assertCmd(t, []string{ + "graph", + "-plan=teststate", + "-draw-cycles", + "-type=output", + }, nil, graphCmd) + }) +} diff --git a/tfexec/internal/e2etest/graph_test.go b/tfexec/internal/e2etest/graph_test.go new file mode 100644 index 00000000..216134e4 --- /dev/null +++ b/tfexec/internal/e2etest/graph_test.go @@ -0,0 +1,35 @@ +package e2etest + +import ( + "context" + "strings" + "testing" + + "github.com/hashicorp/go-version" + + "github.com/hashicorp/terraform-exec/tfexec" +) + +func TestGraph(t *testing.T) { + runTest(t, "basic", func(t *testing.T, tfv *version.Version, tf *tfexec.Terraform) { + err := tf.Init(context.Background()) + if err != nil { + t.Fatalf("error running Init in test directory: %s", err) + } + + err = tf.Apply(context.Background()) + if err != nil { + t.Fatalf("error running Apply: %s", err) + } + + graphOutput, err := tf.Graph(context.Background()) + if err != nil { + t.Fatalf("error running Graph: %s", err) + } + + // Graph output differs slightly between versions, but resource subgraph remains consistent + if !strings.Contains(graphOutput, `"[root] null_resource.foo" [label = "null_resource.foo", shape = "box"]`) { + t.Fatalf("error running Graph. Graph output does not contain expected strings. Returned: %s", graphOutput) + } + }) +} diff --git a/tfexec/options.go b/tfexec/options.go index 4c3e3567..ad3cc65c 100644 --- a/tfexec/options.go +++ b/tfexec/options.go @@ -118,6 +118,15 @@ func Destroy(destroy bool) *DestroyFlagOption { return &DestroyFlagOption{destroy} } +type DrawCyclesOption struct { + drawCycles bool +} + +// DrawCycles represents the -draw-cycles flag. +func DrawCycles(drawCycles bool) *DrawCyclesOption { + return &DrawCyclesOption{drawCycles} +} + type DryRunOption struct { dryRun bool } @@ -222,6 +231,15 @@ func Parallelism(n int) *ParallelismOption { return &ParallelismOption{n} } +type GraphPlanOption struct { + file string +} + +// GraphPlan represents the -plan flag which is a specified plan file string +func GraphPlan(file string) *GraphPlanOption { + return &GraphPlanOption{file} +} + type PlatformOption struct { platform string } @@ -344,6 +362,14 @@ func Target(resource string) *TargetOption { return &TargetOption{resource} } +type GraphTypeOption struct { + graphType string +} + +func GraphType(graphType string) *GraphTypeOption { + return &GraphTypeOption{graphType} +} + type UpdateOption struct { update bool } diff --git a/tfexec/version.go b/tfexec/version.go index 2c60ea43..9978ae28 100644 --- a/tfexec/version.go +++ b/tfexec/version.go @@ -15,8 +15,10 @@ import ( var ( tf0_4_1 = version.Must(version.NewVersion("0.4.1")) + tf0_5_0 = version.Must(version.NewVersion("0.5.0")) tf0_6_13 = version.Must(version.NewVersion("0.6.13")) tf0_7_7 = version.Must(version.NewVersion("0.7.7")) + tf0_8_0 = version.Must(version.NewVersion("0.8.0")) tf0_10_0 = version.Must(version.NewVersion("0.10.0")) tf0_12_0 = version.Must(version.NewVersion("0.12.0")) tf0_13_0 = version.Must(version.NewVersion("0.13.0"))