command/graph: Simpler resource-only graph by default

Unless a user specifically requests a real operation graph using the
-type option, we'll by default present a simplified graph which only
represents the relationships between resources, since resources are the
main side-effects and so the ordering of these is more interesting than
the ordering of Terraform's internal implementation details.
apparentlymart-patch-2
Martin Atkins 3 years ago
parent 135f14275b
commit 844b1616e8

@ -5,8 +5,10 @@ package command
import (
"fmt"
"sort"
"strings"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/command/arguments"
"github.com/hashicorp/terraform/internal/dag"
@ -115,11 +117,23 @@ func (c *GraphCommand) Run(args []string) int {
}
if graphTypeStr == "" {
switch {
case lr.Plan != nil:
if planFile == nil {
// Simple resource dependency mode:
// This is based on the plan graph but we then further reduce it down
// to just resource dependency relationships, assuming that in most
// cases the most important thing is what order we'll visit the
// resources in.
fullG, graphDiags := lr.Core.PlanGraphForUI(lr.Config, lr.InputState, plans.NormalMode)
diags = diags.Append(graphDiags)
if graphDiags.HasErrors() {
c.showDiagnostics(diags)
return 1
}
g := fullG.ResourceGraph()
return c.resourceOnlyGraph(g)
} else {
graphTypeStr = "apply"
default:
graphTypeStr = "plan"
}
}
@ -189,11 +203,103 @@ func (c *GraphCommand) Run(args []string) int {
return 1
}
c.Ui.Output(graphStr)
_, err = c.Streams.Stdout.File.WriteString(graphStr)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to write graph to stdout: %s", err))
return 1
}
return 0
}
func (c *GraphCommand) resourceOnlyGraph(graph addrs.DirectedGraph[addrs.ConfigResource]) int {
out := c.Streams.Stdout.File
fmt.Fprintln(out, "digraph G {")
// Horizontal presentation is easier to read because our nodes tend
// to be much wider than they are tall. The leftmost nodes in the output
// are those Terraform would visit first.
fmt.Fprintln(out, " rankdir = \"RL\";")
fmt.Fprintln(out, " node [shape = rect, fontname = \"sans-serif\"];")
// To help relate the output back to the configuration it came from,
// and to make the individual node labels more reasonably sized when
// deeply nested inside modules, we'll cluster the nodes together by
// the module they belong to and then show only the local resource
// address in the individual nodes. We'll accomplish that by sorting
// the nodes first by module, so we can then notice the transitions.
allAddrs := graph.AllNodes()
if len(allAddrs) == 0 {
fmt.Fprintln(out, " /* This configuration does not contain any resources. */")
fmt.Fprintln(out, " /* For a more detailed graph, try: terraform graph -type=plan */")
}
addrsOrder := make([]addrs.ConfigResource, 0, len(allAddrs))
for _, addr := range allAddrs {
addrsOrder = append(addrsOrder, addr)
}
sort.Slice(addrsOrder, func(i, j int) bool {
iAddr, jAddr := addrsOrder[i], addrsOrder[j]
iModStr, jModStr := iAddr.Module.String(), jAddr.Module.String()
switch {
case iModStr != jModStr:
return iModStr < jModStr
default:
iRes, jRes := iAddr.Resource, jAddr.Resource
switch {
case iRes.Mode != jRes.Mode:
return iRes.Mode == addrs.DataResourceMode
case iRes.Type != jRes.Type:
return iRes.Type < jRes.Type
default:
return iRes.Name < jRes.Name
}
}
})
currentMod := addrs.RootModule
for _, addr := range addrsOrder {
if !addr.Module.Equal(currentMod) {
// We need a new subgraph, then.
// Experimentally it seems like nested clusters tend to make it
// hard for dot to converge on a good layout, so we'll stick with
// just one level of clusters for now but could revise later based
// on feedback.
if !currentMod.IsRoot() {
fmt.Fprintln(out, " }")
}
currentMod = addr.Module
fmt.Fprintf(out, " subgraph \"cluster_%s\" {\n", currentMod.String())
fmt.Fprintf(out, " label = %q\n", currentMod.String())
fmt.Fprintf(out, " fontname = %q\n", "sans-serif")
}
if currentMod.IsRoot() {
fmt.Fprintf(out, " %q [label=%q];\n", addr.String(), addr.Resource.String())
} else {
fmt.Fprintf(out, " %q [label=%q];\n", addr.String(), addr.Resource.String())
}
}
if !currentMod.IsRoot() {
fmt.Fprintln(out, " }")
}
// Now we'll emit all of the edges.
// We use addrsOrder for both levels to ensure a consistent ordering between
// runs without further sorting, which means we visit more nodes than we
// really need to but this output format is only really useful for relatively
// small graphs anyway, so this should be fine.
for _, sourceAddr := range addrsOrder {
deps := graph.DirectDependenciesOf(sourceAddr)
for _, targetAddr := range addrsOrder {
if !deps.Has(targetAddr) {
continue
}
fmt.Fprintf(out, " %q -> %q;\n", sourceAddr.String(), targetAddr.String())
}
}
fmt.Fprintln(out, "}")
return 0
}
func (c *GraphCommand) Help() string {
helpText := `
Usage: terraform [global options] graph [options]
@ -201,6 +307,12 @@ Usage: terraform [global options] graph [options]
Produces a representation of the dependency graph between different
objects in the current configuration and state.
By default the graph shows a summary only of the relationships between
resources in the configuration, since those are the main objects that
have side-effects whose ordering is significant. You can generate more
detailed graphs reflecting Terraform's actual evaluation strategy
by specifying the -type=TYPE option to select an operation type.
The graph is presented in the DOT language. The typical program that can
read this format is GraphViz, but many web services are also available
to read this format.
@ -208,17 +320,22 @@ Usage: terraform [global options] graph [options]
Options:
-plan=tfplan Render graph using the specified plan file instead of the
configuration in the current directory.
configuration in the current directory. Implies -type=apply.
-draw-cycles Highlight any cycles in the graph with colored edges.
This helps when diagnosing cycle errors.
This helps when diagnosing cycle errors. This option is
supported only when illustrating a real evaluation graph,
selected using the -type=TYPE option.
-type=plan Type of graph to output. Can be: plan, plan-refresh-only,
plan-destroy, or apply. By default Terraform chooses
"plan", or "apply" if you also set the -plan=... option.
-type=TYPE Type of operation graph to output. Can be: plan,
plan-refresh-only, plan-destroy, or apply. By default
Terraform just summarizes the relationships between the
resources in your configuration, without any particular
operation in mind. Full operation graphs are more detailed
but therefore often harder to read.
-module-depth=n (deprecated) In prior versions of Terraform, specified the
depth of modules to show in the output.
depth of modules to show in the output.
`
return strings.TrimSpace(helpText)
}

@ -4,39 +4,49 @@
package command
import (
"context"
"os"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/mitchellh/cli"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs/configload"
"github.com/hashicorp/terraform/internal/configs/configschema"
"github.com/hashicorp/terraform/internal/initwd"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/registry"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/terminal"
)
func TestGraph(t *testing.T) {
func TestGraph_planPhase(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath("graph"), td)
defer testChdir(t, td)()
ui := new(cli.MockUi)
streams, closeStreams := terminal.StreamsForTesting(t)
c := &GraphCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(applyFixtureProvider()),
Ui: ui,
Streams: streams,
},
}
args := []string{}
args := []string{"-type=plan"}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
output := ui.OutputWriter.String()
if !strings.Contains(output, `provider[\"registry.terraform.io/hashicorp/test\"]`) {
t.Fatalf("doesn't look like digraph: %s", output)
output := closeStreams(t)
if !strings.Contains(output.Stdout(), `provider[\"registry.terraform.io/hashicorp/test\"]`) {
t.Fatalf("doesn't look like digraph:\n%s\n\nstderr:\n%s", output.Stdout(), output.Stderr())
}
}
@ -58,52 +68,112 @@ func TestGraph_multipleArgs(t *testing.T) {
}
}
func TestGraph_noArgs(t *testing.T) {
func TestGraph_noConfig(t *testing.T) {
td := t.TempDir()
testCopyDir(t, testFixturePath("graph"), td)
os.MkdirAll(td, 0755)
defer testChdir(t, td)()
ui := new(cli.MockUi)
streams, closeStreams := terminal.StreamsForTesting(t)
defer closeStreams(t)
ui := cli.NewMockUi()
c := &GraphCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(applyFixtureProvider()),
Ui: ui,
Streams: streams,
},
}
args := []string{}
// Running the graph command without a config should not panic,
// but this may be an error at some point in the future.
args := []string{"-type", "apply"}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
output := ui.OutputWriter.String()
if !strings.Contains(output, `provider[\"registry.terraform.io/hashicorp/test\"]`) {
t.Fatalf("doesn't look like digraph: %s", output)
}
}
func TestGraph_noConfig(t *testing.T) {
td := t.TempDir()
os.MkdirAll(td, 0755)
defer testChdir(t, td)()
func TestGraph_resourcesOnly(t *testing.T) {
wd := tempWorkingDirFixture(t, "graph-interesting")
defer testChdir(t, wd.RootModuleDir())()
// The graph-interesting fixture has a child module, so we'll need to
// run the module installer just to get the working directory set up
// properly, as if the user has run "terraform init". This is really
// just building the working directory's index of module directories.
loader, cleanupLoader := configload.NewLoaderForTests(t)
t.Cleanup(cleanupLoader)
err := os.MkdirAll(".terraform/modules", 0700)
if err != nil {
t.Fatal(err)
}
inst := initwd.NewModuleInstaller(".terraform/modules", loader, registry.NewClient(nil, nil))
_, instDiags := inst.InstallModules(context.Background(), ".", "tests", true, false, initwd.ModuleInstallHooksImpl{})
if instDiags.HasErrors() {
t.Fatal(instDiags.Err())
}
p := testProvider()
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"foo": {
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"arg": {
Type: cty.String,
Optional: true,
},
},
},
},
},
}
ui := new(cli.MockUi)
ui := cli.NewMockUi()
streams, closeStreams := terminal.StreamsForTesting(t)
c := &GraphCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(applyFixtureProvider()),
Ui: ui,
testingOverrides: &testingOverrides{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("foo"): providers.FactoryFixed(p),
},
},
Ui: ui,
Streams: streams,
},
}
// Running the graph command without a config should not panic,
// but this may be an error at some point in the future.
args := []string{"-type", "apply"}
// A "resources only" graph is the default behavior, with no extra arguments.
args := []string{}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
output := closeStreams(t)
t.Fatalf("unexpected error: \n%s", output.Stderr())
}
output := closeStreams(t)
gotGraph := strings.TrimSpace(output.Stdout())
wantGraph := strings.TrimSpace(`
digraph G {
rankdir = "RL";
node [shape = rect, fontname = "sans-serif"];
"foo.bar" [label="foo.bar"];
"foo.baz" [label="foo.baz"];
"foo.boop" [label="foo.boop"];
subgraph "cluster_module.child" {
label = "module.child"
fontname = "sans-serif"
"module.child.foo.bleep" [label="foo.bleep"];
}
"foo.baz" -> "foo.bar";
"foo.boop" -> "module.child.foo.bleep";
"module.child.foo.bleep" -> "foo.bar";
}
`)
if diff := cmp.Diff(wantGraph, gotGraph); diff != "" {
t.Fatalf("wrong result\n%s", diff)
}
}
func TestGraph_plan(t *testing.T) {
func TestGraph_applyPhaseSavedPlan(t *testing.T) {
testCwd(t)
plan := &plans.Plan{
@ -140,11 +210,13 @@ func TestGraph_plan(t *testing.T) {
planPath := testPlanFile(t, configSnap, states.NewState(), plan)
ui := new(cli.MockUi)
streams, closeStreams := terminal.StreamsForTesting(t)
ui := cli.NewMockUi()
c := &GraphCommand{
Meta: Meta{
testingOverrides: metaOverridesForProvider(applyFixtureProvider()),
Ui: ui,
Streams: streams,
},
}
@ -155,8 +227,8 @@ func TestGraph_plan(t *testing.T) {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
output := ui.OutputWriter.String()
if !strings.Contains(output, `provider[\"registry.terraform.io/hashicorp/test\"]`) {
t.Fatalf("doesn't look like digraph: %s", output)
output := closeStreams(t)
if !strings.Contains(output.Stdout(), `provider[\"registry.terraform.io/hashicorp/test\"]`) {
t.Fatalf("doesn't look like digraph:\n%s\n\nstderr:\n%s", output.Stdout(), output.Stderr())
}
}

@ -0,0 +1,12 @@
variable "in" {
type = string
}
resource "foo" "bleep" {
arg = var.in
}
output "out" {
value = foo.bleep.arg
}

@ -0,0 +1,20 @@
resource "foo" "bar" {
}
locals {
foo_bar_baz = foo.bar.baz
}
resource "foo" "baz" {
arg = local.foo_bar_baz
}
module "child" {
source = "./child"
in = local.foo_bar_baz
}
resource "foo" "boop" {
arg = module.child.out
}

@ -7,50 +7,50 @@ description: >-
# Command: graph
The `terraform graph` command is used to generate a visual
representation of either a configuration or execution plan.
The output is in the DOT format, which can be used by
[GraphViz](http://www.graphviz.org) to generate charts.
The `terraform graph` command produces descriptions of the relationships
between objects in a Terraform configuration, using
[the DOT language](https://en.wikipedia.org/wiki/DOT_(graph_description_language)).
## Usage
Usage: `terraform graph [options]`
Outputs the visual execution graph of Terraform resources according to
either the current configuration or an execution plan.
By default the result is a simplified graph which describes only the dependency
ordering of the resources (`resource` and `data` blocks) in the configuration.
The graph is outputted in DOT format. The typical program that can
read this format is GraphViz, but many web services are also available
to read this format.
The `-type` flag can be used to control the type of graph shown. Terraform
creates different graphs for different operations. See the options below
for the list of types supported. The default type is "plan" if a
configuration is given, and "apply" if a plan file is passed as an
argument.
The `-type=...` option optionally selects from a number of other graph types
which have more detail, at the expense of also exposing some of the
implementation details of the Terraform language runtime.
Options:
* `-plan=tfplan` - Render graph using the specified plan file instead of the
configuration in the current directory.
* `-draw-cycles` - Highlight any cycles in the graph with colored edges.
This helps when diagnosing cycle errors.
* `-plan=tfplan` - Produce a graph for applying the given plan. Implies `-type=apply`.
* `-type=plan` - Type of graph to output. Can be: `plan`, `plan-refresh-only`, `plan-destroy`, or `apply`.
* `-draw-cycles` - Highlight any cycles in the graph with colored edges.
This helps when diagnosing cycle errors. This option is supported only when
selecting one of the real graph operaton types using the `-type=...`
option.
* `-module-depth=n` - (deprecated) In prior versions of Terraform, specified the
depth of modules to show in the output.
* `-type=...` - Selects a specific operation type to show the graph of, instead
of the default resources-only simplified graph.
Can be: `plan`, `plan-refresh-only`, `plan-destroy`, or `apply`.
## Generating Images
The output of `terraform graph` is in the DOT format, which can
easily be converted to an image by making use of `dot` provided
by GraphViz:
The graph output uses
[the DOT language](https://en.wikipedia.org/wiki/DOT_(graph_description_language)),
which is a machine-readable graph description language which originated in
[Graphviz](https://graphviz.org/). You can use the Graphviz `dot` command
to present the resulting graph description as an image. There are also various
third-party online graph rendering services which accept this format.
If you have the Graphviz `dot` command already installed, you can render
a PNG image by piping into that command:
```shellsession
$ terraform graph | dot -Tsvg > graph.svg
$ terraform graph -type=plan | dot -Tpng >graph.png
```
Here is an example graph output:
![Graph Example](/img/docs/graph-example.png)
The following is an example result:
![A visualization of the plan graph of a hypothetical Terraform configuration, produced by dot](/img/docs/graph-example.png)

Loading…
Cancel
Save