Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions cli/commands/dag/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ package dag

import (
"github.com/gruntwork-io/terragrunt/cli/commands/dag/graph"
"github.com/gruntwork-io/terragrunt/cli/flags"
"github.com/gruntwork-io/terragrunt/internal/cli"
"github.com/gruntwork-io/terragrunt/options"
"github.com/gruntwork-io/terragrunt/pkg/log"
Expand All @@ -15,13 +14,11 @@ const (
)

func NewCommand(l log.Logger, opts *options.TerragruntOptions) *cli.Command {
prefix := flags.Prefix{CommandName}

return &cli.Command{
Name: CommandName,
Usage: "Interact with the Directed Acyclic Graph (DAG).",
Subcommands: cli.Commands{
graph.NewCommand(l, opts, prefix),
graph.NewCommand(l, opts),
},
Action: cli.ShowCommandHelp,
}
Expand Down
51 changes: 29 additions & 22 deletions cli/commands/dag/graph/cli.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
// Package graph implements the terragrunt dag graph command which generates a visual
// representation of the Terragrunt dependency graph in DOT language format.
//
// Alias for 'list --format=dot --dag --dependencies --external'.
package graph

import (
"github.com/gruntwork-io/terragrunt/cli/commands/common/graph"
"github.com/gruntwork-io/terragrunt/cli/commands/common/runall"
"github.com/gruntwork-io/terragrunt/cli/commands/run"
"github.com/gruntwork-io/terragrunt/cli/flags"
"github.com/gruntwork-io/terragrunt/cli/commands/list"
runCmd "github.com/gruntwork-io/terragrunt/cli/commands/run"
"github.com/gruntwork-io/terragrunt/cli/flags/shared"
"github.com/gruntwork-io/terragrunt/internal/cli"
"github.com/gruntwork-io/terragrunt/internal/runner"
"github.com/gruntwork-io/terragrunt/options"
"github.com/gruntwork-io/terragrunt/pkg/log"
)
Expand All @@ -17,32 +17,39 @@ const (
CommandName = "graph"
)

func NewCommand(l log.Logger, opts *options.TerragruntOptions, _ flags.Prefix) *cli.Command {
cmd := &cli.Command{
func NewCommand(l log.Logger, opts *options.TerragruntOptions) *cli.Command {
// Build flags: queue flags + backend/feature flags + filter flag
cmdFlags := shared.NewQueueFlags(opts, nil)
cmdFlags = append(cmdFlags, runCmd.NewBackendFlags(l, opts, nil)...)
cmdFlags = append(cmdFlags, runCmd.NewFeatureFlags(l, opts, nil)...)
cmdFlags = append(cmdFlags, shared.NewFilterFlag(opts))

return &cli.Command{
Name: CommandName,
Usage: "Graph the Directed Acyclic Graph (DAG) in DOT language.",
Usage: "Graph the Directed Acyclic Graph (DAG) in DOT language. Alias for 'list --format=dot --dag --dependencies --external'.",
UsageText: "terragrunt dag graph",
Flags: cmdFlags,
Action: func(ctx *cli.Context) error {
return Run(ctx, l, opts)
},
Flags: run.NewFlags(l, opts, nil),
}

cmd = runall.WrapCommand(l, opts, cmd, run.Run, true)
cmd = graph.WrapCommand(l, opts, cmd, run.Run, true)

return cmd
}

func Run(ctx *cli.Context, l log.Logger, opts *options.TerragruntOptions) error {
stack, err := runner.FindStackInSubfolders(ctx, l, opts)
if err != nil {
return err
}

if err := stack.GetStack().Units.WriteDot(l, opts.Writer, opts); err != nil {
l.Warnf("Failed to graph dot: %v", err)
listOpts := list.NewOptions(opts)
listOpts.Format = list.FormatDot
listOpts.Mode = list.ModeDAG
listOpts.Dependencies = true
listOpts.Hidden = true

// By default, graph includes external dependencies.
// Respect queue flags to override this behavior.
if opts.IgnoreExternalDependencies {
listOpts.External = false
} else {
// Default to true, or explicitly set if --queue-include-external is used
listOpts.External = true
}

return nil
return list.Run(ctx, l, listOpts)
}
2 changes: 1 addition & 1 deletion cli/commands/list/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func NewFlags(opts *Options, prefix flags.Prefix) cli.Flags {
Name: FormatFlagName,
EnvVars: tgPrefix.EnvVars(FormatFlagName),
Destination: &opts.Format,
Usage: "Output format for list results. Valid values: text, tree, long.",
Usage: "Output format for list results. Valid values: text, tree, long, dot.",
DefaultText: FormatText,
}),
flags.NewFlag(&cli.BoolFlag{
Expand Down
102 changes: 94 additions & 8 deletions cli/commands/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package list

import (
"context"
"fmt"
"os"
"path/filepath"
"sort"
Expand Down Expand Up @@ -104,6 +105,8 @@ func Run(ctx context.Context, l log.Logger, opts *Options) error {
return outputTree(l, opts, listedComponents, opts.Mode)
case FormatLong:
return outputLong(l, opts, listedComponents)
case FormatDot:
return outputDot(l, opts, listedComponents)
default:
// This should never happen, because of validation in the command.
// If it happens, we want to throw so we can fix the validation.
Expand Down Expand Up @@ -131,10 +134,10 @@ func shouldDiscoverDependencies(opts *Options) bool {
type ListedComponents []*ListedComponent

type ListedComponent struct {
Type component.Kind
Path string

Type component.Kind
Path string
Dependencies []*ListedComponent
Excluded bool
}

// Contains checks to see if the given path is in the listed components.
Expand Down Expand Up @@ -168,11 +171,17 @@ func discoveredToListed(components component.Components, opts *Options) (ListedC
continue
}

excluded := false

if opts.QueueConstructAs != "" {
if unit, ok := c.(*component.Unit); ok {
if cfg := unit.Config(); cfg != nil && cfg.Exclude != nil {
if cfg.Exclude.IsActionListed(opts.QueueConstructAs) {
continue
if opts.Format != FormatDot {
continue
}

excluded = true
}
}
}
Expand All @@ -186,8 +195,9 @@ func discoveredToListed(components component.Components, opts *Options) (ListedC
}

listedCfg := &ListedComponent{
Type: c.Kind(),
Path: relPath,
Type: c.Kind(),
Path: relPath,
Excluded: excluded,
}

if len(c.Dependencies()) == 0 {
Expand All @@ -206,9 +216,22 @@ func discoveredToListed(components component.Components, opts *Options) (ListedC
continue
}

depExcluded := false

if opts.QueueConstructAs != "" {
if depUnit, ok := dep.(*component.Unit); ok {
if depCfg := depUnit.Config(); depCfg != nil && depCfg.Exclude != nil {
if depCfg.Exclude.IsActionListed(opts.QueueConstructAs) {
depExcluded = true
}
}
}
}

listedCfg.Dependencies[i] = &ListedComponent{
Type: dep.Kind(),
Path: relDepPath,
Type: dep.Kind(),
Path: relDepPath,
Excluded: depExcluded,
}
}

Expand Down Expand Up @@ -442,6 +465,11 @@ func outputTree(l log.Logger, opts *Options, components ListedComponents, sort s
return renderTree(opts, components, s, sort)
}

// outputDot outputs the discovered components in GraphViz DOT format.
func outputDot(_ log.Logger, opts *Options, components ListedComponents) error {
return renderDot(opts, components)
}

type TreeStyler struct {
entryStyle lipgloss.Style
rootStyle lipgloss.Style
Expand Down Expand Up @@ -674,3 +702,61 @@ func getLongestPathLen(components ListedComponents) int {

return longest
}

// renderDot renders the components in GraphViz DOT format.
func renderDot(opts *Options, components ListedComponents) error {
if _, err := opts.Writer.Write([]byte("digraph {\n")); err != nil {
return errors.New(err)
}

sortedComponents := make(ListedComponents, len(components))
copy(sortedComponents, components)
sort.Slice(sortedComponents, func(i, j int) bool {
return sortedComponents[i].Path < sortedComponents[j].Path
})

for _, component := range sortedComponents {
if len(component.Dependencies) > 1 {
sort.Slice(component.Dependencies, func(i, j int) bool {
return component.Dependencies[i].Path < component.Dependencies[j].Path
})
}
}

for _, component := range sortedComponents {
style := ""
if component.Excluded {
style = "[color=red]"
}

if _, writeErr := opts.Writer.Write(
fmt.Appendf(
nil,
"\t\"%s\" %s;\n",
component.Path,
style,
),
); writeErr != nil {
return errors.New(writeErr)
}

for _, dep := range component.Dependencies {
if _, writeErr := opts.Writer.Write(
fmt.Appendf(
nil,
"\t\"%s\" -> \"%s\";\n",
component.Path,
dep.Path,
),
); writeErr != nil {
return errors.New(writeErr)
}
}
}

if _, err := opts.Writer.Write([]byte("}\n")); err != nil {
return errors.New(err)
}

return nil
}
Loading