github.com/cycloidio/terraform@v1.1.10-0.20220513142504-76d5c768dc63/command/graph.go (about)

     1  package command
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	"github.com/cycloidio/terraform/backend"
     8  	"github.com/cycloidio/terraform/dag"
     9  	"github.com/cycloidio/terraform/plans"
    10  	"github.com/cycloidio/terraform/plans/planfile"
    11  	"github.com/cycloidio/terraform/terraform"
    12  	"github.com/cycloidio/terraform/tfdiags"
    13  )
    14  
    15  // GraphCommand is a Command implementation that takes a Terraform
    16  // configuration and outputs the dependency tree in graphical form.
    17  type GraphCommand struct {
    18  	Meta
    19  }
    20  
    21  func (c *GraphCommand) Run(args []string) int {
    22  	var drawCycles bool
    23  	var graphTypeStr string
    24  	var moduleDepth int
    25  	var verbose bool
    26  	var planPath string
    27  
    28  	args = c.Meta.process(args)
    29  	cmdFlags := c.Meta.defaultFlagSet("graph")
    30  	cmdFlags.BoolVar(&drawCycles, "draw-cycles", false, "draw-cycles")
    31  	cmdFlags.StringVar(&graphTypeStr, "type", "", "type")
    32  	cmdFlags.IntVar(&moduleDepth, "module-depth", -1, "module-depth")
    33  	cmdFlags.BoolVar(&verbose, "verbose", false, "verbose")
    34  	cmdFlags.StringVar(&planPath, "plan", "", "plan")
    35  	cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
    36  	if err := cmdFlags.Parse(args); err != nil {
    37  		c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error()))
    38  		return 1
    39  	}
    40  
    41  	configPath, err := ModulePath(cmdFlags.Args())
    42  	if err != nil {
    43  		c.Ui.Error(err.Error())
    44  		return 1
    45  	}
    46  
    47  	// Check for user-supplied plugin path
    48  	if c.pluginPath, err = c.loadPluginPath(); err != nil {
    49  		c.Ui.Error(fmt.Sprintf("Error loading plugin path: %s", err))
    50  		return 1
    51  	}
    52  
    53  	// Try to load plan if path is specified
    54  	var planFile *planfile.Reader
    55  	if planPath != "" {
    56  		planFile, err = c.PlanFile(planPath)
    57  		if err != nil {
    58  			c.Ui.Error(err.Error())
    59  			return 1
    60  		}
    61  	}
    62  
    63  	var diags tfdiags.Diagnostics
    64  
    65  	backendConfig, backendDiags := c.loadBackendConfig(configPath)
    66  	diags = diags.Append(backendDiags)
    67  	if diags.HasErrors() {
    68  		c.showDiagnostics(diags)
    69  		return 1
    70  	}
    71  
    72  	// Load the backend
    73  	b, backendDiags := c.Backend(&BackendOpts{
    74  		Config: backendConfig,
    75  	})
    76  	diags = diags.Append(backendDiags)
    77  	if backendDiags.HasErrors() {
    78  		c.showDiagnostics(diags)
    79  		return 1
    80  	}
    81  
    82  	// We require a local backend
    83  	local, ok := b.(backend.Local)
    84  	if !ok {
    85  		c.showDiagnostics(diags) // in case of any warnings in here
    86  		c.Ui.Error(ErrUnsupportedLocalOp)
    87  		return 1
    88  	}
    89  
    90  	// This is a read-only command
    91  	c.ignoreRemoteVersionConflict(b)
    92  
    93  	// Build the operation
    94  	opReq := c.Operation(b)
    95  	opReq.ConfigDir = configPath
    96  	opReq.ConfigLoader, err = c.initConfigLoader()
    97  	opReq.PlanFile = planFile
    98  	opReq.AllowUnsetVariables = true
    99  	if err != nil {
   100  		diags = diags.Append(err)
   101  		c.showDiagnostics(diags)
   102  		return 1
   103  	}
   104  
   105  	// Get the context
   106  	lr, _, ctxDiags := local.LocalRun(opReq)
   107  	diags = diags.Append(ctxDiags)
   108  	if ctxDiags.HasErrors() {
   109  		c.showDiagnostics(diags)
   110  		return 1
   111  	}
   112  
   113  	if graphTypeStr == "" {
   114  		switch {
   115  		case lr.Plan != nil:
   116  			graphTypeStr = "apply"
   117  		default:
   118  			graphTypeStr = "plan"
   119  		}
   120  	}
   121  
   122  	var g *terraform.Graph
   123  	var graphDiags tfdiags.Diagnostics
   124  	switch graphTypeStr {
   125  	case "plan":
   126  		g, graphDiags = lr.Core.PlanGraphForUI(lr.Config, lr.InputState, plans.NormalMode)
   127  	case "plan-refresh-only":
   128  		g, graphDiags = lr.Core.PlanGraphForUI(lr.Config, lr.InputState, plans.RefreshOnlyMode)
   129  	case "plan-destroy":
   130  		g, graphDiags = lr.Core.PlanGraphForUI(lr.Config, lr.InputState, plans.DestroyMode)
   131  	case "apply":
   132  		plan := lr.Plan
   133  
   134  		// Historically "terraform graph" would allow the nonsensical request to
   135  		// render an apply graph without a plan, so we continue to support that
   136  		// here, though perhaps one day this should be an error.
   137  		if lr.Plan == nil {
   138  			plan = &plans.Plan{
   139  				Changes:      plans.NewChanges(),
   140  				UIMode:       plans.NormalMode,
   141  				PriorState:   lr.InputState,
   142  				PrevRunState: lr.InputState,
   143  			}
   144  		}
   145  
   146  		g, graphDiags = lr.Core.ApplyGraphForUI(plan, lr.Config)
   147  	case "eval", "validate":
   148  		// Terraform v0.12 through v1.0 supported both of these, but the
   149  		// graph variants for "eval" and "validate" are purely implementation
   150  		// details and don't reveal anything (user-model-wise) that you can't
   151  		// see in the plan graph.
   152  		graphDiags = graphDiags.Append(tfdiags.Sourceless(
   153  			tfdiags.Error,
   154  			"Graph type no longer available",
   155  			fmt.Sprintf("The graph type %q is no longer available. Use -type=plan instead to get a similar result.", graphTypeStr),
   156  		))
   157  	default:
   158  		graphDiags = graphDiags.Append(tfdiags.Sourceless(
   159  			tfdiags.Error,
   160  			"Unsupported graph type",
   161  			`The -type=... argument must be either "plan", "plan-refresh-only", "plan-destroy", or "apply".`,
   162  		))
   163  	}
   164  	diags = diags.Append(graphDiags)
   165  	if graphDiags.HasErrors() {
   166  		c.showDiagnostics(diags)
   167  		return 1
   168  	}
   169  
   170  	graphStr, err := terraform.GraphDot(g, &dag.DotOpts{
   171  		DrawCycles: drawCycles,
   172  		MaxDepth:   moduleDepth,
   173  		Verbose:    verbose,
   174  	})
   175  	if err != nil {
   176  		c.Ui.Error(fmt.Sprintf("Error converting graph: %s", err))
   177  		return 1
   178  	}
   179  
   180  	if diags.HasErrors() {
   181  		// For this command we only show diagnostics if there are errors,
   182  		// because printing out naked warnings could upset a naive program
   183  		// consuming our dot output.
   184  		c.showDiagnostics(diags)
   185  		return 1
   186  	}
   187  
   188  	c.Ui.Output(graphStr)
   189  
   190  	return 0
   191  }
   192  
   193  func (c *GraphCommand) Help() string {
   194  	helpText := `
   195  Usage: terraform [global options] graph [options]
   196  
   197    Produces a representation of the dependency graph between different
   198    objects in the current configuration and state.
   199  
   200    The graph is presented in the DOT language. The typical program that can
   201    read this format is GraphViz, but many web services are also available
   202    to read this format.
   203  
   204  Options:
   205  
   206    -plan=tfplan     Render graph using the specified plan file instead of the
   207                     configuration in the current directory.
   208  
   209    -draw-cycles     Highlight any cycles in the graph with colored edges.
   210                     This helps when diagnosing cycle errors.
   211  
   212    -type=plan       Type of graph to output. Can be: plan, plan-refresh-only,
   213                     plan-destroy, or apply. By default Terraform chooses
   214  				   "plan", or "apply" if you also set the -plan=... option.
   215  
   216    -module-depth=n  (deprecated) In prior versions of Terraform, specified the
   217  				   depth of modules to show in the output.
   218  `
   219  	return strings.TrimSpace(helpText)
   220  }
   221  
   222  func (c *GraphCommand) Synopsis() string {
   223  	return "Generate a Graphviz graph of the steps in an operation"
   224  }