@@ -5,8 +5,10 @@ package command
55
66import (
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+
197303func (c * GraphCommand ) Help () string {
198304 helpText := `
199305Usage: 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
208320Options:
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