Skip to content

Commit

Permalink
feat: add --include-output-dependencies flag.
Browse files Browse the repository at this point in the history
Signed-off-by: i4k <[email protected]>
  • Loading branch information
i4ki committed Jan 9, 2025
1 parent ffd8680 commit 155f450
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 29 deletions.
106 changes: 89 additions & 17 deletions cmd/terramate/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"path"
"path/filepath"
"slices"
"sort"
"strings"
"time"

Expand Down Expand Up @@ -147,11 +148,13 @@ type cliSpec struct {
RunOrder bool `default:"false" help:"Sort listed stacks by order of execution"`

changeDetectionFlags
outputsSharingFlags
} `cmd:"" help:"List stacks."`

Run struct {
runCommandFlags `envprefix:"TM_ARG_RUN_"`
runSafeguardsCliSpec
outputsSharingFlags
} `cmd:"" help:"Run command in the stacks"`

Generate struct {
Expand All @@ -168,6 +171,7 @@ type cliSpec struct {
Run struct {
runScriptFlags `envprefix:"TM_ARG_RUN_"`
runSafeguardsCliSpec
outputsSharingFlags
} `cmd:"" help:"Run a Terramate Script in stacks."`
} `cmd:"" help:"Use Terramate Scripts"`

Expand Down Expand Up @@ -304,6 +308,11 @@ type changeDetectionFlags struct {
DisableChangeDetection []string `help:"Disable specific change detection modes" enum:"git-untracked,git-uncommitted"`
}

type outputsSharingFlags struct {
IncludeOutputDependencies bool `help:"Include stacks that are dependencies of the selected stacks. (requires outputs-sharing experiment enabled)"`
OnlyOutputDependencies bool `help:"Only include stacks that are dependencies of the selected stacks. (requires outputs-sharing experiment enabled)"`
}

type cloudTargetFlags struct {
Target string `env:"TARGET" help:"Set the deployment target for stacks synchronized to Terramate Cloud."`
FromTarget string `env:"FROM_TARGET" help:"Migrate stacks from given deployment target."`
Expand Down Expand Up @@ -1151,7 +1160,7 @@ func (c *cli) triggerStackByFilter() {
stackFilter := cloud.StatusFilters{
StackStatus: statusFilter,
}
stacksReport, err := c.listStacks(false, "", stackFilter, false)
stacksReport, err := c.listStacks(false, outputsSharingFlags{}, cloudstack.AnyTarget, stackFilter, false)
if err != nil {
fatalWithDetailf(err, "unable to list stacks")
}
Expand Down Expand Up @@ -1224,7 +1233,7 @@ func (c *cli) triggerStack(basePath string) {
stacks = append(stacks, st.Sortable())
} else {
var err error
stacksReport, err := c.listStacks(false, cloudstack.AnyTarget, cloud.NoStatusFilters(), false)
stacksReport, err := c.listStacks(false, outputsSharingFlags{}, cloudstack.AnyTarget, cloud.NoStatusFilters(), false)
if err != nil {
fatalWithDetailf(err, "computing selected stacks")
}
Expand Down Expand Up @@ -1437,12 +1446,16 @@ func (c *cli) setupChangeDetection(enable []string, disable []string) {
}
}

func (c *cli) listStacks(isChanged bool, target string, stackFilters cloud.StatusFilters, checkRepo bool) (*stack.Report, error) {
func (c *cli) listStacks(isChanged bool, extra outputsSharingFlags, target string, stackFilters cloud.StatusFilters, checkRepo bool) (*stack.Report, error) {
var (
err error
report *stack.Report
)

if extra.IncludeOutputDependencies && !c.cfg().HasExperiment(hcl.SharingIsCaringExperimentName) {
fatal(errors.E("--include-output-dependencies requires the '%s' experiment enabled", hcl.SharingIsCaringExperimentName))
}

mgr := c.stackManager()

if isChanged {
Expand Down Expand Up @@ -1503,10 +1516,69 @@ func (c *cli) listStacks(isChanged bool, target string, stackFilters cloud.Statu
report.Stacks = stacks
}

if extra.IncludeOutputDependencies {
report.Stacks = c.addOutputDependencies(report.Stacks)
sort.Sort(report.Stacks)
}

c.prj.git.repoChecks = report.Checks
return report, nil
}

func (c *cli) addOutputDependencies(stacks []stack.Entry) []stack.Entry {
stacksMap := map[string]stack.Entry{}
for _, stack := range stacks {
stacksMap[stack.Stack.Dir.String()] = stack
}

rootcfg := c.cfg()
depIDs := map[string]struct{}{}
depOrigins := map[string][]string{} // id -> stack paths
for _, st := range stacks {
evalctx := c.setupEvalContext(st.Stack, map[string]string{})
cfg, _ := rootcfg.Lookup(st.Stack.Dir)
for _, inputcfg := range cfg.Node.Inputs {
input, err := config.EvalInput(evalctx, inputcfg)
if err != nil {
fatalWithDetailf(err, "evaluating input %s", input.Name)
}
depIDs[input.FromStackID] = struct{}{}
depOrigins[input.FromStackID] = append(depOrigins[input.FromStackID], st.Stack.Dir.String())
}
}

mgr := c.stackManager()
outputsMap := map[string]stack.Entry{}
for depID := range depIDs {
st, found, err := mgr.StackByID(depID)
if err != nil {
fatalWithDetailf(err, "loading output dependencies of selected stacks")
}
if !found {
fatalWithDetailf(errors.E("dependency stack %s not found", depID), "loading output dependencies of selected stacks")
}
var reason string
depsOf := depOrigins[depID]
if len(depsOf) == 1 {
reason = stdfmt.Sprintf("Output dependency of stack %s", depsOf[0])
} else {
reason = stdfmt.Sprintf("Output dependency of stacks %s", strings.Join(depsOf, ", "))
}
outputsMap[st.Dir.String()] = stack.Entry{
Stack: st,
Reason: reason,
}
}

for _, dep := range outputsMap {
if _, found := stacksMap[dep.Stack.Dir.String()]; !found {
// TODO(i4k): test duplicates
stacks = append(stacks, dep)
}
}
return stacks
}

func (c *cli) scanCreate() {
scanFlags := 0
if c.parsedArgs.Create.AllTerraform {
Expand Down Expand Up @@ -1965,7 +2037,7 @@ func (c *cli) printStacks() {
DriftStatus: parseDriftStatusFilter(driftStatusStr),
}

report, err := c.listStacks(c.parsedArgs.Changed, c.parsedArgs.List.Target, cloudFilters, false)
report, err := c.listStacks(c.parsedArgs.Changed, c.parsedArgs.List.outputsSharingFlags, c.parsedArgs.List.Target, cloudFilters, false)
if err != nil {
fatal(err)
}
Expand Down Expand Up @@ -2044,7 +2116,7 @@ func parseDriftStatusFilter(filterStr string) drift.FilterStatus {
}

func (c *cli) printRuntimeEnv() {
report, err := c.listStacks(c.parsedArgs.Changed, cloudstack.AnyTarget, cloud.NoStatusFilters(), false)
report, err := c.listStacks(c.parsedArgs.Changed, outputsSharingFlags{}, cloudstack.AnyTarget, cloud.NoStatusFilters(), false)
if err != nil {
fatalWithDetailf(err, "listing stacks")
}
Expand Down Expand Up @@ -2189,19 +2261,19 @@ func generateDot(
}

func (c *cli) generateDebug() {
// TODO(KATCIPIS): When we introduce config defined on root context
// we need to know blocks that have root context, since they should
// not be filtered by stack selection.
stacks, err := c.computeSelectedStacks(false, cloudstack.AnyTarget, cloud.NoStatusFilters())
report, err := c.listStacks(c.parsedArgs.Changed, outputsSharingFlags{}, cloudstack.AnyTarget, cloud.NoStatusFilters(), false)
if err != nil {
fatalWithDetailf(err, "generate debug: selecting stacks")
}

selectedStacks := map[prj.Path]struct{}{}
for _, stack := range stacks {
log.Debug().Msgf("selected stack: %s", stack.Dir())
for _, entry := range report.Stacks {
stackdir := entry.Stack.HostDir(c.cfg())
if stackdir == c.wd() || strings.HasPrefix(stackdir, c.wd()+string(filepath.Separator)) {
log.Debug().Msgf("selected stack: %s", entry.Stack.Dir)

selectedStacks[stack.Dir()] = struct{}{}
selectedStacks[entry.Stack.Dir] = struct{}{}
}
}

results, err := generate.Load(c.cfg(), c.vendorDir())
Expand Down Expand Up @@ -2236,7 +2308,7 @@ func (c *cli) generateDebug() {
}

func (c *cli) printStacksGlobals() {
report, err := c.listStacks(c.parsedArgs.Changed, cloudstack.AnyTarget, cloud.NoStatusFilters(), false)
report, err := c.listStacks(c.parsedArgs.Changed, outputsSharingFlags{}, cloudstack.AnyTarget, cloud.NoStatusFilters(), false)
if err != nil {
fatalWithDetailf(err, "listing stacks globals: listing stacks")
}
Expand Down Expand Up @@ -2265,7 +2337,7 @@ func (c *cli) printMetadata() {
Str("action", "cli.printMetadata()").
Logger()

report, err := c.listStacks(c.parsedArgs.Changed, cloudstack.AnyTarget, cloud.NoStatusFilters(), false)
report, err := c.listStacks(c.parsedArgs.Changed, outputsSharingFlags{}, cloudstack.AnyTarget, cloud.NoStatusFilters(), false)
if err != nil {
fatalWithDetailf(err, "loading metadata: listing stacks")
}
Expand Down Expand Up @@ -2324,7 +2396,7 @@ func (c *cli) checkGenCode() bool {
}

func (c *cli) ensureStackID() {
report, err := c.listStacks(false, cloudstack.AnyTarget, cloud.NoStatusFilters(), false)
report, err := c.listStacks(false, outputsSharingFlags{}, cloudstack.AnyTarget, cloud.NoStatusFilters(), false)
if err != nil {
fatalWithDetailf(err, "listing stacks")
}
Expand Down Expand Up @@ -2584,8 +2656,8 @@ func (c *cli) friendlyFmtDir(dir string) (string, bool) {
return prj.FriendlyFmtDir(c.rootdir(), c.wd(), dir)
}

func (c *cli) computeSelectedStacks(ensureCleanRepo bool, target string, stackFilters cloud.StatusFilters) (config.List[*config.SortableStack], error) {
report, err := c.listStacks(c.parsedArgs.Changed, target, stackFilters, true)
func (c *cli) computeSelectedStacks(ensureCleanRepo bool, extraListFlags outputsSharingFlags, target string, stackFilters cloud.StatusFilters) (config.List[*config.SortableStack], error) {
report, err := c.listStacks(c.parsedArgs.Changed, extraListFlags, target, stackFilters, true)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/terramate/cli/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ func (c *cli) runOnStacks() {
stackFilter := parseStatusFilter(c.parsedArgs.Run.Status)
deploymentFilter := parseDeploymentStatusFilter(c.parsedArgs.Run.DeploymentStatus)
driftFilter := parseDriftStatusFilter(c.parsedArgs.Run.DriftStatus)
stacks, err = c.computeSelectedStacks(true, c.parsedArgs.Run.Target, cloud.StatusFilters{
stacks, err = c.computeSelectedStacks(true, c.parsedArgs.Run.outputsSharingFlags, c.parsedArgs.Run.Target, cloud.StatusFilters{
StackStatus: stackFilter,
DeploymentStatus: deploymentFilter,
DriftStatus: driftFilter,
Expand Down
2 changes: 1 addition & 1 deletion cmd/terramate/cli/script_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import (
func (c *cli) printScriptInfo() {
labels := c.parsedArgs.Script.Info.Cmds

stacks, err := c.computeSelectedStacks(false, cloudstack.AnyTarget, cloud.NoStatusFilters())
stacks, err := c.computeSelectedStacks(false, outputsSharingFlags{}, cloudstack.AnyTarget, cloud.NoStatusFilters())
if err != nil {
fatalWithDetailf(err, "computing selected stacks")
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/terramate/cli/script_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func (c *cli) runScript() {
statusFilter := parseStatusFilter(c.parsedArgs.Script.Run.Status)
deploymentFilter := parseDeploymentStatusFilter(c.parsedArgs.Script.Run.DeploymentStatus)
driftFilter := parseDriftStatusFilter(c.parsedArgs.Script.Run.DriftStatus)
stacks, err = c.computeSelectedStacks(true, c.parsedArgs.Script.Run.Target, cloud.StatusFilters{
stacks, err = c.computeSelectedStacks(true, c.parsedArgs.Script.Run.outputsSharingFlags, c.parsedArgs.Script.Run.Target, cloud.StatusFilters{
StackStatus: statusFilter,
DeploymentStatus: deploymentFilter,
DriftStatus: driftFilter,
Expand Down
18 changes: 18 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,24 @@ func (root *Root) StacksByTagsFilters(filters []string) (project.Paths, error) {
}).Paths(), nil
}

// StackByID returns the stack with the given ID.
func (root *Root) StackByID(id string) (*Stack, bool, error) {
stacks := root.tree.stacks(func(tree *Tree) bool {
return tree.IsStack() && tree.Node.Stack.ID == id
})
if len(stacks) == 0 {
return nil, false, nil
}
if len(stacks) > 1 {
printer.Stderr.Warnf("multiple stacks with the same ID %q found.", id)
}
stack, err := stacks[0].Stack()
if err != nil {
return nil, true, err
}
return stack, true, nil
}

// LoadSubTree loads a subtree located at cfgdir into the current tree.
func (root *Root) LoadSubTree(cfgdir project.Path) error {
var parent project.Path
Expand Down
Loading

0 comments on commit 155f450

Please sign in to comment.