diff --git a/cmd/terramate/cli/cli.go b/cmd/terramate/cli/cli.go index 95e964585..09b7082d3 100644 --- a/cmd/terramate/cli/cli.go +++ b/cmd/terramate/cli/cli.go @@ -147,11 +147,13 @@ type cliSpec struct { RunOrder bool `default:"false" help:"Sort listed stacks by order of execution"` changeDetectionFlags + extraListFlags } `cmd:"" help:"List stacks."` Run struct { runCommandFlags `envprefix:"TM_ARG_RUN_"` runSafeguardsCliSpec + extraListFlags } `cmd:"" help:"Run command in the stacks"` Generate struct { @@ -168,6 +170,7 @@ type cliSpec struct { Run struct { runScriptFlags `envprefix:"TM_ARG_RUN_"` runSafeguardsCliSpec + extraListFlags } `cmd:"" help:"Run a Terramate Script in stacks."` } `cmd:"" help:"Use Terramate Scripts"` @@ -304,6 +307,11 @@ type changeDetectionFlags struct { DisableChangeDetection []string `help:"Disable specific change detection modes" enum:"git-untracked,git-uncommitted"` } +type extraListFlags 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."` @@ -1151,7 +1159,7 @@ func (c *cli) triggerStackByFilter() { stackFilter := cloud.StatusFilters{ StackStatus: statusFilter, } - stacksReport, err := c.listStacks(false, "", stackFilter, false) + stacksReport, err := c.listStacks(false, extraListFlags{}, cloudstack.AnyTarget, stackFilter, false) if err != nil { fatalWithDetailf(err, "unable to list stacks") } @@ -1224,7 +1232,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, extraListFlags{}, cloudstack.AnyTarget, cloud.NoStatusFilters(), false) if err != nil { fatalWithDetailf(err, "computing selected stacks") } @@ -1437,7 +1445,7 @@ 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 extraListFlags, target string, stackFilters cloud.StatusFilters, checkRepo bool) (*stack.Report, error) { var ( err error report *stack.Report @@ -1503,10 +1511,67 @@ func (c *cli) listStacks(isChanged bool, target string, stackFilters cloud.Statu report.Stacks = stacks } + if extra.IncludeOutputDependencies { + report.Stacks = c.addOutputDependencies(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()) + } + } + + outputsMap := map[string]stack.Entry{} + for depID := range depIDs { + st, found, err := rootcfg.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 { @@ -1965,7 +2030,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.extraListFlags, c.parsedArgs.List.Target, cloudFilters, false) if err != nil { fatal(err) } @@ -2044,7 +2109,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, extraListFlags{}, cloudstack.AnyTarget, cloud.NoStatusFilters(), false) if err != nil { fatalWithDetailf(err, "listing stacks") } @@ -2189,19 +2254,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, extraListFlags{}, 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()) @@ -2236,7 +2301,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, extraListFlags{}, cloudstack.AnyTarget, cloud.NoStatusFilters(), false) if err != nil { fatalWithDetailf(err, "listing stacks globals: listing stacks") } @@ -2265,7 +2330,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, extraListFlags{}, cloudstack.AnyTarget, cloud.NoStatusFilters(), false) if err != nil { fatalWithDetailf(err, "loading metadata: listing stacks") } @@ -2324,7 +2389,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, extraListFlags{}, cloudstack.AnyTarget, cloud.NoStatusFilters(), false) if err != nil { fatalWithDetailf(err, "listing stacks") } @@ -2584,8 +2649,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 extraListFlags, 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 } diff --git a/cmd/terramate/cli/run.go b/cmd/terramate/cli/run.go index 700a3661c..7fcef1c56 100644 --- a/cmd/terramate/cli/run.go +++ b/cmd/terramate/cli/run.go @@ -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.extraListFlags, c.parsedArgs.Run.Target, cloud.StatusFilters{ StackStatus: stackFilter, DeploymentStatus: deploymentFilter, DriftStatus: driftFilter, diff --git a/cmd/terramate/cli/script_info.go b/cmd/terramate/cli/script_info.go index 12309ffe2..d90314183 100644 --- a/cmd/terramate/cli/script_info.go +++ b/cmd/terramate/cli/script_info.go @@ -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, extraListFlags{}, cloudstack.AnyTarget, cloud.NoStatusFilters()) if err != nil { fatalWithDetailf(err, "computing selected stacks") } diff --git a/cmd/terramate/cli/script_run.go b/cmd/terramate/cli/script_run.go index 7e072669d..564b80112 100644 --- a/cmd/terramate/cli/script_run.go +++ b/cmd/terramate/cli/script_run.go @@ -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.extraListFlags, c.parsedArgs.Script.Run.Target, cloud.StatusFilters{ StackStatus: statusFilter, DeploymentStatus: deploymentFilter, DriftStatus: driftFilter, diff --git a/config/config.go b/config/config.go index 4a3e1beee..4e32e203d 100644 --- a/config/config.go +++ b/config/config.go @@ -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