Skip to content

Commit 844b161

Browse files
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.
1 parent 135f142 commit 844b161

File tree

5 files changed

+291
-70
lines changed

5 files changed

+291
-70
lines changed

internal/command/graph.go

Lines changed: 128 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ package command
55

66
import (
77
"fmt"
8+
"sort"
89
"strings"
910

11+
"github.com/hashicorp/terraform/internal/addrs"
1012
"github.com/hashicorp/terraform/internal/backend"
1113
"github.com/hashicorp/terraform/internal/command/arguments"
1214
"github.com/hashicorp/terraform/internal/dag"
@@ -115,11 +117,23 @@ func (c *GraphCommand) Run(args []string) int {
115117
}
116118

117119
if graphTypeStr == "" {
118-
switch {
119-
case lr.Plan != nil:
120+
if planFile == nil {
121+
// Simple resource dependency mode:
122+
// This is based on the plan graph but we then further reduce it down
123+
// to just resource dependency relationships, assuming that in most
124+
// cases the most important thing is what order we'll visit the
125+
// resources in.
126+
fullG, graphDiags := lr.Core.PlanGraphForUI(lr.Config, lr.InputState, plans.NormalMode)
127+
diags = diags.Append(graphDiags)
128+
if graphDiags.HasErrors() {
129+
c.showDiagnostics(diags)
130+
return 1
131+
}
132+
133+
g := fullG.ResourceGraph()
134+
return c.resourceOnlyGraph(g)
135+
} else {
120136
graphTypeStr = "apply"
121-
default:
122-
graphTypeStr = "plan"
123137
}
124138
}
125139

@@ -189,36 +203,139 @@ func (c *GraphCommand) Run(args []string) int {
189203
return 1
190204
}
191205

192-
c.Ui.Output(graphStr)
206+
_, err = c.Streams.Stdout.File.WriteString(graphStr)
207+
if err != nil {
208+
c.Ui.Error(fmt.Sprintf("Failed to write graph to stdout: %s", err))
209+
return 1
210+
}
193211

194212
return 0
195213
}
196214

215+
func (c *GraphCommand) resourceOnlyGraph(graph addrs.DirectedGraph[addrs.ConfigResource]) int {
216+
out := c.Streams.Stdout.File
217+
fmt.Fprintln(out, "digraph G {")
218+
// Horizontal presentation is easier to read because our nodes tend
219+
// to be much wider than they are tall. The leftmost nodes in the output
220+
// are those Terraform would visit first.
221+
fmt.Fprintln(out, " rankdir = \"RL\";")
222+
fmt.Fprintln(out, " node [shape = rect, fontname = \"sans-serif\"];")
223+
224+
// To help relate the output back to the configuration it came from,
225+
// and to make the individual node labels more reasonably sized when
226+
// deeply nested inside modules, we'll cluster the nodes together by
227+
// the module they belong to and then show only the local resource
228+
// address in the individual nodes. We'll accomplish that by sorting
229+
// the nodes first by module, so we can then notice the transitions.
230+
allAddrs := graph.AllNodes()
231+
if len(allAddrs) == 0 {
232+
fmt.Fprintln(out, " /* This configuration does not contain any resources. */")
233+
fmt.Fprintln(out, " /* For a more detailed graph, try: terraform graph -type=plan */")
234+
}
235+
addrsOrder := make([]addrs.ConfigResource, 0, len(allAddrs))
236+
for _, addr := range allAddrs {
237+
addrsOrder = append(addrsOrder, addr)
238+
}
239+
sort.Slice(addrsOrder, func(i, j int) bool {
240+
iAddr, jAddr := addrsOrder[i], addrsOrder[j]
241+
iModStr, jModStr := iAddr.Module.String(), jAddr.Module.String()
242+
switch {
243+
case iModStr != jModStr:
244+
return iModStr < jModStr
245+
default:
246+
iRes, jRes := iAddr.Resource, jAddr.Resource
247+
switch {
248+
case iRes.Mode != jRes.Mode:
249+
return iRes.Mode == addrs.DataResourceMode
250+
case iRes.Type != jRes.Type:
251+
return iRes.Type < jRes.Type
252+
default:
253+
return iRes.Name < jRes.Name
254+
}
255+
}
256+
})
257+
258+
currentMod := addrs.RootModule
259+
for _, addr := range addrsOrder {
260+
if !addr.Module.Equal(currentMod) {
261+
// We need a new subgraph, then.
262+
// Experimentally it seems like nested clusters tend to make it
263+
// hard for dot to converge on a good layout, so we'll stick with
264+
// just one level of clusters for now but could revise later based
265+
// on feedback.
266+
if !currentMod.IsRoot() {
267+
fmt.Fprintln(out, " }")
268+
}
269+
currentMod = addr.Module
270+
fmt.Fprintf(out, " subgraph \"cluster_%s\" {\n", currentMod.String())
271+
fmt.Fprintf(out, " label = %q\n", currentMod.String())
272+
fmt.Fprintf(out, " fontname = %q\n", "sans-serif")
273+
}
274+
if currentMod.IsRoot() {
275+
fmt.Fprintf(out, " %q [label=%q];\n", addr.String(), addr.Resource.String())
276+
} else {
277+
fmt.Fprintf(out, " %q [label=%q];\n", addr.String(), addr.Resource.String())
278+
}
279+
}
280+
if !currentMod.IsRoot() {
281+
fmt.Fprintln(out, " }")
282+
}
283+
284+
// Now we'll emit all of the edges.
285+
// We use addrsOrder for both levels to ensure a consistent ordering between
286+
// runs without further sorting, which means we visit more nodes than we
287+
// really need to but this output format is only really useful for relatively
288+
// small graphs anyway, so this should be fine.
289+
for _, sourceAddr := range addrsOrder {
290+
deps := graph.DirectDependenciesOf(sourceAddr)
291+
for _, targetAddr := range addrsOrder {
292+
if !deps.Has(targetAddr) {
293+
continue
294+
}
295+
fmt.Fprintf(out, " %q -> %q;\n", sourceAddr.String(), targetAddr.String())
296+
}
297+
}
298+
299+
fmt.Fprintln(out, "}")
300+
return 0
301+
}
302+
197303
func (c *GraphCommand) Help() string {
198304
helpText := `
199305
Usage: terraform [global options] graph [options]
200306
201307
Produces a representation of the dependency graph between different
202308
objects in the current configuration and state.
203309
310+
By default the graph shows a summary only of the relationships between
311+
resources in the configuration, since those are the main objects that
312+
have side-effects whose ordering is significant. You can generate more
313+
detailed graphs reflecting Terraform's actual evaluation strategy
314+
by specifying the -type=TYPE option to select an operation type.
315+
204316
The graph is presented in the DOT language. The typical program that can
205317
read this format is GraphViz, but many web services are also available
206318
to read this format.
207319
208320
Options:
209321
210322
-plan=tfplan Render graph using the specified plan file instead of the
211-
configuration in the current directory.
323+
configuration in the current directory. Implies -type=apply.
212324
213325
-draw-cycles Highlight any cycles in the graph with colored edges.
214-
This helps when diagnosing cycle errors.
326+
This helps when diagnosing cycle errors. This option is
327+
supported only when illustrating a real evaluation graph,
328+
selected using the -type=TYPE option.
215329
216-
-type=plan Type of graph to output. Can be: plan, plan-refresh-only,
217-
plan-destroy, or apply. By default Terraform chooses
218-
"plan", or "apply" if you also set the -plan=... option.
330+
-type=TYPE Type of operation graph to output. Can be: plan,
331+
plan-refresh-only, plan-destroy, or apply. By default
332+
Terraform just summarizes the relationships between the
333+
resources in your configuration, without any particular
334+
operation in mind. Full operation graphs are more detailed
335+
but therefore often harder to read.
219336
220337
-module-depth=n (deprecated) In prior versions of Terraform, specified the
221-
depth of modules to show in the output.
338+
depth of modules to show in the output.
222339
`
223340
return strings.TrimSpace(helpText)
224341
}

0 commit comments

Comments
 (0)