diff --git a/e2etests/core/run_test.go b/e2etests/core/run_test.go index 93b4839f9..8b9632582 100644 --- a/e2etests/core/run_test.go +++ b/e2etests/core/run_test.go @@ -40,12 +40,13 @@ func TestCLIRunOrder(t *testing.T) { t.Parallel() type testcase struct { - name string - layout []string - filterTags []string - filterNoTags []string - workingDir string - want RunExpected + name string + layout []string + fsOrderingDisabled bool + filterTags []string + filterNoTags []string + workingDir string + want RunExpected } for _, tc := range []testcase{ @@ -542,6 +543,22 @@ func TestCLIRunOrder(t *testing.T) { ), }, }, + { + name: "stack-b after stack-a after parent (fs ordering disabled)", + layout: []string{ + `s:z-parent/stack-b:after=["/z-parent/stack-a"]`, + `s:z-parent/stack-a`, + `s:z-parent`, + }, + fsOrderingDisabled: true, + want: RunExpected{ + Stdout: nljoin( + "/parent", + "/parent/stack-a", + "/parent/stack-b", + ), + }, + }, { name: "implicit order with tags - Zied case", layout: []string{ @@ -919,6 +936,16 @@ func TestCLIRunOrder(t *testing.T) { t.Parallel() copiedLayout := make([]string, len(tc.layout)) copy(copiedLayout, tc.layout) + if tc.fsOrderingDisabled { + copiedLayout = append(copiedLayout, + fmt.Sprintf("file:disable_fs_ordering:%s", Terramate( + Config( + Block("order_of_execution", + Bool("nested", false), + ), + ), + ).String())) + } if runtime.GOOS != "windows" { copiedLayout = append(copiedLayout, diff --git a/hcl/hcl.go b/hcl/hcl.go index 885d9d0a8..f4f4bf34f 100644 --- a/hcl/hcl.go +++ b/hcl/hcl.go @@ -182,6 +182,11 @@ type RunConfig struct { Env *RunEnv } +// OrderOfExecutionConfig represents the order_of_execution block. +type OrderOfExecutionConfig struct { + Nested *bool // If filesystem order is enabled. +} + // RunEnv represents Terramate run environment. type RunEnv struct { // Attributes is the collection of attribute definitions within the env block. @@ -262,6 +267,7 @@ type RootConfig struct { Generate *GenerateRootConfig ChangeDetection *ChangeDetectionConfig Run *RunConfig + OrderOfExecution *OrderOfExecutionConfig Cloud *CloudConfig Experiments []string DisableSafeguards safeguard.Keywords @@ -1759,7 +1765,16 @@ func (p *TerramateParser) parseRootConfig(cfg *RootConfig, block *ast.MergedBloc } } - errs.AppendWrap(ErrTerramateSchema, block.ValidateSubBlocks("git", "generate", "change_detection", "run", "cloud", "targets", "telemetry")) + errs.AppendWrap(ErrTerramateSchema, block.ValidateSubBlocks( + "git", + "generate", + "change_detection", + "run", + "order_of_execution", + "cloud", + "targets", + "telemetry", + )) gitBlock, ok := block.Blocks[ast.NewEmptyLabelBlockType("git")] if ok { @@ -1771,6 +1786,12 @@ func (p *TerramateParser) parseRootConfig(cfg *RootConfig, block *ast.MergedBloc errs.Append(parseRunConfig(cfg, runBlock)) } + orderExecBlock, ok := block.Blocks[ast.NewEmptyLabelBlockType("order_of_execution")] + if ok { + cfg.OrderOfExecution = &OrderOfExecutionConfig{} + errs.Append(parseOrderOfExecutionConfig(cfg.OrderOfExecution, orderExecBlock)) + } + cloudBlock, ok := block.Blocks[ast.NewEmptyLabelBlockType("cloud")] if ok { cfg.Cloud = &CloudConfig{} @@ -1850,6 +1871,36 @@ func parseRunConfig(cfg *RootConfig, runBlock *ast.MergedBlock) error { return errs.AsError() } +func parseOrderOfExecutionConfig(cfg *OrderOfExecutionConfig, orderExecBlock *ast.MergedBlock) error { + errs := errors.L() + for _, attr := range orderExecBlock.Attributes { + value, err := attr.Expr.Value(nil) + if err != nil { + errs.Append(errors.E(err, "failed to evaluate terramate.config.order_of_execution.%s attribute", attr.Name)) + continue + } + + switch attr.Name { + case "nested": + if value.Type() != cty.Bool { + errs.Append(attrErr(attr, + "terramate.config.order_of_execution.nested is not a bool but %q", + value.Type().FriendlyName(), + )) + continue + } + t := value.True() + cfg.Nested = &t + default: + errs.Append(errors.E(attr.NameRange, + "unrecognized attribute terramate.config.order_of_execution.%s", + attr.Name, + )) + } + } + return errs.AsError() +} + func parseGenerateRootConfig(cfg *GenerateRootConfig, generateBlock *ast.MergedBlock) error { errs := errors.L() diff --git a/hcl/hcl_test.go b/hcl/hcl_test.go index 6a236c987..332035d23 100644 --- a/hcl/hcl_test.go +++ b/hcl/hcl_test.go @@ -17,6 +17,7 @@ import ( errtest "github.com/terramate-io/terramate/test/errors" . "github.com/terramate-io/terramate/test/hclutils" "github.com/terramate-io/terramate/test/hclutils/info" + . "github.com/terramate-io/terramate/test/hclwrite/hclutils" ) type ( @@ -43,6 +44,8 @@ type ( ) func TestHCLParserTerramateBlock(t *testing.T) { + truth := true + falsy := false for _, tc := range []testcase{ { name: "unrecognized blocks", @@ -594,6 +597,84 @@ func TestHCLParserTerramateBlock(t *testing.T) { }, }, }, + { + name: "terramate.config.order_of_execution.nested=false", + input: []cfgfile{ + { + filename: "cfg.tm", + body: Terramate( + Config( + Block("order_of_execution", + Bool("nested", false), + ), + ), + ).String(), + }, + }, + want: want{ + config: hcl.Config{ + Terramate: &hcl.Terramate{ + Config: &hcl.RootConfig{ + OrderOfExecution: &hcl.OrderOfExecutionConfig{ + Nested: &falsy, + }, + }, + }, + }, + }, + }, + { + name: "empty terramate.config.order_of_execution block", + input: []cfgfile{ + { + filename: "cfg.tm", + body: ` + terramate { + config { + order_of_execution { + + } + } + } + `, + }, + }, + want: want{ + config: hcl.Config{ + Terramate: &hcl.Terramate{ + Config: &hcl.RootConfig{ + OrderOfExecution: &hcl.OrderOfExecutionConfig{}, + }, + }, + }, + }, + }, + { + name: "terramate.config.order_of_execution.nested=true", + input: []cfgfile{ + { + filename: "cfg.tm", + body: Terramate( + Config( + Block("order_of_execution", + Bool("nested", true), + ), + ), + ).String(), + }, + }, + want: want{ + config: hcl.Config{ + Terramate: &hcl.Terramate{ + Config: &hcl.RootConfig{ + OrderOfExecution: &hcl.OrderOfExecutionConfig{ + Nested: &truth, + }, + }, + }, + }, + }, + }, } { testParser(t, tc) } diff --git a/run/order.go b/run/order.go index 196c83a94..09bf09aa0 100644 --- a/run/order.go +++ b/run/order.go @@ -106,28 +106,30 @@ func buildValidStackDAG[S ~[]E, E any]( Str("root", root.HostDir()). Logger() - isParentStack := func(s1, s2 *config.Stack) bool { - return s1.Dir.HasPrefix(s2.Dir.String() + "/") - } + if isFsOrderingEnabled(root) { + isParentStack := func(s1, s2 *config.Stack) bool { + return s1.Dir.HasPrefix(s2.Dir.String() + "/") + } - getStackDir := func(s E) string { - return getStack(s).Dir.String() - } + getStackDir := func(s E) string { + return getStack(s).Dir.String() + } - slices.SortStableFunc(items, func(a, b E) int { - return strings.Compare(getStack(a).Dir.String(), getStack(b).Dir.String()) - }) + slices.SortStableFunc(items, func(a, b E) int { + return strings.Compare(getStack(a).Dir.String(), getStack(b).Dir.String()) + }) - for _, a := range items { - for _, b := range items { - if getStack(a).Dir == getStack(b).Dir { - continue - } + for _, a := range items { + for _, b := range items { + if getStack(a).Dir == getStack(b).Dir { + continue + } - if isParentStack(getStack(a), getStack(b)) { - logger.Debug().Msgf("stack %q runs before %q since it is its parent", getStackDir(a), getStackDir(b)) + if isParentStack(getStack(a), getStack(b)) { + logger.Debug().Msgf("stack %q runs before %q since it is its parent", getStackDir(a), getStackDir(b)) - getStack(b).AppendBefore(getStack(a).Dir.String()) + getStack(b).AppendBefore(getStack(a).Dir.String()) + } } } } @@ -298,3 +300,14 @@ func toids(values config.List[*config.SortableStack]) []dag.ID { } return ids } + +func isFsOrderingEnabled(root *config.Root) bool { + cfg := root.Tree().Node + if cfg.Terramate != nil && + cfg.Terramate.Config != nil && + cfg.Terramate.Config.OrderOfExecution != nil && + cfg.Terramate.Config.OrderOfExecution.Nested != nil { + return *cfg.Terramate.Config.OrderOfExecution.Nested + } + return true +}