From abc6418d80fc9535fb0c69790a6dc9a0ce7a2adf Mon Sep 17 00:00:00 2001 From: Ryan VanGundy Date: Thu, 19 Jun 2025 18:57:22 -0400 Subject: [PATCH 1/4] feat: add parallelism parameter to terraform components --- api/v1alpha1/blueprint_types.go | 19 ++++-- api/v1alpha1/blueprint_types_test.go | 77 ++++++++++++++++++++++++ api/v1alpha1/utils.go | 4 ++ docs/reference/blueprint.md | 2 + pkg/stack/windsor_stack.go | 14 ++++- pkg/stack/windsor_stack_test.go | 88 ++++++++++++++++++++++++++++ 6 files changed, 197 insertions(+), 7 deletions(-) diff --git a/api/v1alpha1/blueprint_types.go b/api/v1alpha1/blueprint_types.go index c49bd89a4..037186c2f 100644 --- a/api/v1alpha1/blueprint_types.go +++ b/api/v1alpha1/blueprint_types.go @@ -121,6 +121,10 @@ type TerraformComponent struct { // Destroy determines if the component should be destroyed during down operations. // Defaults to true if not specified. Destroy *bool `yaml:"destroy,omitempty"` + + // Parallelism limits the number of concurrent operations as Terraform walks the graph. + // This corresponds to the -parallelism flag in terraform apply/destroy commands. + Parallelism *int `yaml:"parallelism,omitempty"` } // Kustomization defines a kustomization config in a blueprint. @@ -236,11 +240,12 @@ func (b *Blueprint) DeepCopy() *Blueprint { maps.Copy(valuesCopy, component.Values) terraformComponentsCopy[i] = TerraformComponent{ - Source: component.Source, - Path: component.Path, - FullPath: component.FullPath, - Values: valuesCopy, - Destroy: component.Destroy, + Source: component.Source, + Path: component.Path, + FullPath: component.FullPath, + Values: valuesCopy, + Destroy: component.Destroy, + Parallelism: component.Parallelism, } } @@ -378,6 +383,10 @@ func (b *Blueprint) Merge(overlay *Blueprint) { mergedComponent.Destroy = overlayComponent.Destroy } + if overlayComponent.Parallelism != nil { + mergedComponent.Parallelism = overlayComponent.Parallelism + } + b.TerraformComponents = append(b.TerraformComponents, mergedComponent) } else { b.TerraformComponents = append(b.TerraformComponents, overlayComponent) diff --git a/api/v1alpha1/blueprint_types_test.go b/api/v1alpha1/blueprint_types_test.go index 33b8aad17..c8b9d6c00 100644 --- a/api/v1alpha1/blueprint_types_test.go +++ b/api/v1alpha1/blueprint_types_test.go @@ -397,6 +397,83 @@ func TestBlueprint_Merge(t *testing.T) { } }) + t.Run("ParallelismFieldMerge", func(t *testing.T) { + tests := []struct { + name string + dst *int + overlay *int + expected *int + }{ + { + name: "BothNil", + dst: nil, + overlay: nil, + expected: nil, + }, + { + name: "DstNilOverlaySet", + dst: nil, + overlay: ptrInt(5), + expected: ptrInt(5), + }, + { + name: "DstSetOverlayNil", + dst: ptrInt(10), + overlay: nil, + expected: ptrInt(10), + }, + { + name: "BothSet", + dst: ptrInt(10), + overlay: ptrInt(5), + expected: ptrInt(5), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dst := &Blueprint{ + TerraformComponents: []TerraformComponent{ + { + Source: "source1", + Path: "module/path1", + Parallelism: tt.dst, + }, + }, + } + + overlay := &Blueprint{ + TerraformComponents: []TerraformComponent{ + { + Source: "source1", + Path: "module/path1", + Parallelism: tt.overlay, + }, + }, + } + + dst.Merge(overlay) + + if len(dst.TerraformComponents) != 1 { + t.Fatalf("Expected 1 TerraformComponent, but got %d", len(dst.TerraformComponents)) + } + + component := dst.TerraformComponents[0] + if tt.expected == nil { + if component.Parallelism != nil { + t.Errorf("Expected Parallelism to be nil, but got %v", component.Parallelism) + } + } else { + if component.Parallelism == nil { + t.Errorf("Expected Parallelism to be %v, but got nil", *tt.expected) + } else if *component.Parallelism != *tt.expected { + t.Errorf("Expected Parallelism to be %v, but got %v", *tt.expected, *component.Parallelism) + } + } + }) + } + }) + t.Run("OverlayComponentWithDifferentSource", func(t *testing.T) { base := &Blueprint{ TerraformComponents: []TerraformComponent{{Path: "mod", Source: "A"}}, diff --git a/api/v1alpha1/utils.go b/api/v1alpha1/utils.go index 434578466..f6f3f0089 100644 --- a/api/v1alpha1/utils.go +++ b/api/v1alpha1/utils.go @@ -8,3 +8,7 @@ func ptrString(s string) *string { func ptrBool(b bool) *bool { return &b } + +func ptrInt(i int) *int { + return &i +} diff --git a/docs/reference/blueprint.md b/docs/reference/blueprint.md index ffd3cdea7..18280ec79 100644 --- a/docs/reference/blueprint.md +++ b/docs/reference/blueprint.md @@ -109,6 +109,7 @@ terraform: # A Terraform module defined within the current blueprint source - path: apps/my-infra + parallelism: 5 ``` | Field | Type | Description | @@ -117,6 +118,7 @@ terraform: | `path` | `string` | Path of the Terraform module relative to the `terraform/` folder. | | `values` | `map[string]any` | Configuration values for the module. | | `variables`| `map[string]TerraformVariable` | Input variables for the module. | +| `parallelism`| `int` | Limits the number of concurrent operations as Terraform walks the graph. Corresponds to the `-parallelism` flag. | ### Kustomization For more information on Flux Kustomizations, which are sets of resources and configurations applied to a Kubernetes cluster, visit [Flux Kustomizations Documentation](https://fluxcd.io/flux/components/kustomize/kustomizations/). Most parameters are not necessary to define. diff --git a/pkg/stack/windsor_stack.go b/pkg/stack/windsor_stack.go index 1acf55f86..532eaa017 100644 --- a/pkg/stack/windsor_stack.go +++ b/pkg/stack/windsor_stack.go @@ -91,7 +91,12 @@ func (s *WindsorStack) Up() error { return fmt.Errorf("error planning Terraform changes in %s: %w", component.FullPath, err) } - _, err = s.shell.ExecProgress(fmt.Sprintf("🌎 Applying Terraform changes in %s", component.Path), "terraform", "apply") + // Build terraform apply command with optional parallelism flag + applyArgs := []string{"apply"} + if component.Parallelism != nil { + applyArgs = append(applyArgs, fmt.Sprintf("-parallelism=%d", *component.Parallelism)) + } + _, err = s.shell.ExecProgress(fmt.Sprintf("🌎 Applying Terraform changes in %s", component.Path), "terraform", applyArgs...) if err != nil { return fmt.Errorf("error applying Terraform changes in %s: %w", component.FullPath, err) } @@ -164,7 +169,12 @@ func (s *WindsorStack) Down() error { return fmt.Errorf("error planning Terraform destruction in %s: %w", component.FullPath, err) } - _, err = s.shell.ExecProgress(fmt.Sprintf("🗑️ Destroying Terraform resources in %s", component.Path), "terraform", "destroy", "-auto-approve") + // Build terraform destroy command with optional parallelism flag + destroyArgs := []string{"destroy", "-auto-approve"} + if component.Parallelism != nil { + destroyArgs = append(destroyArgs, fmt.Sprintf("-parallelism=%d", *component.Parallelism)) + } + _, err = s.shell.ExecProgress(fmt.Sprintf("🗑️ Destroying Terraform resources in %s", component.Path), "terraform", destroyArgs...) if err != nil { return fmt.Errorf("error destroying Terraform resources in %s: %w", component.FullPath, err) } diff --git a/pkg/stack/windsor_stack_test.go b/pkg/stack/windsor_stack_test.go index f69879022..6bb573f94 100644 --- a/pkg/stack/windsor_stack_test.go +++ b/pkg/stack/windsor_stack_test.go @@ -331,6 +331,48 @@ func TestWindsorStack_Up(t *testing.T) { t.Fatalf("Expected error to contain %q, got %q", expectedError, err.Error()) } }) + + t.Run("SuccessWithParallelism", func(t *testing.T) { + stack, mocks := setup(t) + + // Set up components with parallelism + mocks.Blueprint.GetTerraformComponentsFunc = func() []blueprintv1alpha1.TerraformComponent { + return []blueprintv1alpha1.TerraformComponent{ + { + Source: "source1", + Path: "module/path1", + FullPath: filepath.Join(os.Getenv("WINDSOR_PROJECT_ROOT"), ".windsor", ".tf_modules", "remote", "path"), + Parallelism: ptrInt(5), + }, + } + } + + // Track terraform commands to verify parallelism flag + var capturedCommands [][]string + mocks.Shell.ExecProgressFunc = func(message string, command string, args ...string) (string, error) { + if command == "terraform" { + capturedCommands = append(capturedCommands, append([]string{command}, args...)) + } + return "", nil + } + + // When the stack is brought up + if err := stack.Up(); err != nil { + t.Errorf("Expected Up to return nil, got %v", err) + } + + // Then terraform apply should be called with parallelism flag + foundApplyWithParallelism := false + for _, cmd := range capturedCommands { + if len(cmd) >= 3 && cmd[1] == "apply" && cmd[2] == "-parallelism=5" { + foundApplyWithParallelism = true + break + } + } + if !foundApplyWithParallelism { + t.Errorf("Expected terraform apply command with -parallelism=5, but it was not found in captured commands: %v", capturedCommands) + } + }) } func TestWindsorStack_Down(t *testing.T) { @@ -530,6 +572,48 @@ func TestWindsorStack_Down(t *testing.T) { } }) + t.Run("SuccessWithParallelism", func(t *testing.T) { + stack, mocks := setup(t) + + // Set up components with parallelism + mocks.Blueprint.GetTerraformComponentsFunc = func() []blueprintv1alpha1.TerraformComponent { + return []blueprintv1alpha1.TerraformComponent{ + { + Source: "source1", + Path: "module/path1", + FullPath: filepath.Join(os.Getenv("WINDSOR_PROJECT_ROOT"), ".windsor", ".tf_modules", "remote", "path"), + Parallelism: ptrInt(3), + }, + } + } + + // Track terraform commands to verify parallelism flag + var capturedCommands [][]string + mocks.Shell.ExecProgressFunc = func(message string, command string, args ...string) (string, error) { + if command == "terraform" { + capturedCommands = append(capturedCommands, append([]string{command}, args...)) + } + return "", nil + } + + // When the stack is brought down + if err := stack.Down(); err != nil { + t.Errorf("Expected Down to return nil, got %v", err) + } + + // Then terraform destroy should be called with parallelism flag + foundDestroyWithParallelism := false + for _, cmd := range capturedCommands { + if len(cmd) >= 4 && cmd[1] == "destroy" && cmd[2] == "-auto-approve" && cmd[3] == "-parallelism=3" { + foundDestroyWithParallelism = true + break + } + } + if !foundDestroyWithParallelism { + t.Errorf("Expected terraform destroy command with -parallelism=3, but it was not found in captured commands: %v", capturedCommands) + } + }) + t.Run("SkipComponentWithDestroyFalse", func(t *testing.T) { stack, mocks := setup(t) mocks.Blueprint.GetTerraformComponentsFunc = func() []blueprintv1alpha1.TerraformComponent { @@ -556,3 +640,7 @@ func TestWindsorStack_Down(t *testing.T) { func ptrBool(b bool) *bool { return &b } + +func ptrInt(i int) *int { + return &i +} From 9169ac457a5bc2813ae7a998ab4ffe726c7eab58 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy Date: Tue, 22 Jul 2025 01:24:17 -0400 Subject: [PATCH 2/4] Include parallelism in terraform env --- pkg/env/terraform_env.go | 28 +++++++++++++++++++++------- pkg/stack/windsor_stack_test.go | 2 +- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/pkg/env/terraform_env.go b/pkg/env/terraform_env.go index bc1098e81..c0b1b070f 100644 --- a/pkg/env/terraform_env.go +++ b/pkg/env/terraform_env.go @@ -133,9 +133,9 @@ func (e *TerraformEnvPrinter) Print() error { return e.BaseEnvPrinter.Print(envVars) } -// GenerateTerraformArgs constructs Terraform CLI argument lists and environment variables for the specified project and module paths. -// It resolves configuration root, locates relevant tfvars files, generates backend configuration arguments, and assembles -// all required CLI and environment variable values for Terraform operations. Returns a TerraformArgs struct or error. +// GenerateTerraformArgs constructs Terraform CLI arguments and environment variables for given project and module paths. +// Resolves config root, locates tfvars files, generates backend config args, and assembles all CLI/env values needed +// for Terraform operations. Returns a TerraformArgs struct or error. func (e *TerraformEnvPrinter) GenerateTerraformArgs(projectPath, modulePath string) (*TerraformArgs, error) { configRoot, err := e.configHandler.GetConfigRoot() if err != nil { @@ -182,8 +182,7 @@ func (e *TerraformEnvPrinter) GenerateTerraformArgs(projectPath, modulePath stri planArgs := []string{fmt.Sprintf("-out=%s", tfPlanPath)} planArgs = append(planArgs, varFileArgs...) - applyArgs := []string{tfPlanPath} - + applyArgs := []string{} refreshArgs := []string{} refreshArgs = append(refreshArgs, varFileArgs...) @@ -193,14 +192,29 @@ func (e *TerraformEnvPrinter) GenerateTerraformArgs(projectPath, modulePath stri destroyArgs := []string{"-auto-approve"} destroyArgs = append(destroyArgs, varFileArgs...) + var parallelismArg string + if e.blueprintHandler != nil { + components := e.blueprintHandler.GetTerraformComponents() + for _, component := range components { + if component.Path == projectPath && component.Parallelism != nil { + parallelismArg = fmt.Sprintf(" -parallelism=%d", *component.Parallelism) + applyArgs = append(applyArgs, fmt.Sprintf("-parallelism=%d", *component.Parallelism)) + destroyArgs = append(destroyArgs, fmt.Sprintf("-parallelism=%d", *component.Parallelism)) + break + } + } + } + + applyArgs = append(applyArgs, tfPlanPath) + terraformVars := make(map[string]string) terraformVars["TF_DATA_DIR"] = strings.TrimSpace(tfDataDir) terraformVars["TF_CLI_ARGS_init"] = strings.TrimSpace(fmt.Sprintf("-backend=true -force-copy %s", strings.Join(backendConfigArgsForEnv, " "))) terraformVars["TF_CLI_ARGS_plan"] = strings.TrimSpace(fmt.Sprintf("-out=\"%s\" %s", tfPlanPath, strings.Join(varFileArgsForEnv, " "))) - terraformVars["TF_CLI_ARGS_apply"] = strings.TrimSpace(fmt.Sprintf("\"%s\"", tfPlanPath)) + terraformVars["TF_CLI_ARGS_apply"] = strings.TrimSpace(fmt.Sprintf("\"%s\"%s", tfPlanPath, parallelismArg)) terraformVars["TF_CLI_ARGS_refresh"] = strings.TrimSpace(strings.Join(varFileArgsForEnv, " ")) terraformVars["TF_CLI_ARGS_import"] = strings.TrimSpace(strings.Join(varFileArgsForEnv, " ")) - terraformVars["TF_CLI_ARGS_destroy"] = strings.TrimSpace(strings.Join(varFileArgsForEnv, " ")) + terraformVars["TF_CLI_ARGS_destroy"] = strings.TrimSpace(fmt.Sprintf("%s%s", strings.Join(varFileArgsForEnv, " "), parallelismArg)) terraformVars["TF_VAR_context_path"] = strings.TrimSpace(filepath.ToSlash(configRoot)) terraformVars["TF_VAR_context_id"] = strings.TrimSpace(e.configHandler.GetString("id", "")) diff --git a/pkg/stack/windsor_stack_test.go b/pkg/stack/windsor_stack_test.go index 5337b4e70..ba460c631 100644 --- a/pkg/stack/windsor_stack_test.go +++ b/pkg/stack/windsor_stack_test.go @@ -329,7 +329,7 @@ func TestWindsorStack_Up(t *testing.T) { // Then terraform apply should be called with parallelism flag foundApplyWithParallelism := false for _, cmd := range capturedCommands { - if len(cmd) >= 5 && cmd[2] == "apply" && cmd[4] == "-parallelism=5" { + if len(cmd) >= 5 && cmd[2] == "apply" && cmd[3] == "-parallelism=5" { foundApplyWithParallelism = true break } From 23a54d942ab1c9092111d7c8cd79cd2899061321 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy Date: Tue, 22 Jul 2025 01:37:12 -0400 Subject: [PATCH 3/4] Test coverage --- pkg/env/terraform_env_test.go | 228 ++++++++++++++++++++++++++++++++ pkg/stack/windsor_stack_test.go | 42 ------ 2 files changed, 228 insertions(+), 42 deletions(-) diff --git a/pkg/env/terraform_env_test.go b/pkg/env/terraform_env_test.go index f58d27b75..d1de6734f 100644 --- a/pkg/env/terraform_env_test.go +++ b/pkg/env/terraform_env_test.go @@ -1664,3 +1664,231 @@ func TestTerraformEnv_DependencyResolution(t *testing.T) { } }) } + +func TestTerraformEnv_GenerateTerraformArgs(t *testing.T) { + t.Run("GeneratesCorrectArgsWithoutParallelism", func(t *testing.T) { + mocks := setupTerraformEnvMocks(t) + + printer := &TerraformEnvPrinter{ + BaseEnvPrinter: *NewBaseEnvPrinter(mocks.Injector), + } + + if err := printer.Initialize(); err != nil { + t.Fatalf("Failed to initialize printer: %v", err) + } + + // When generating terraform args without parallelism + args, err := printer.GenerateTerraformArgs("test/path", "test/module") + + // Then no error should be returned + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + // And apply args should contain only the plan file path + if len(args.ApplyArgs) != 1 { + t.Errorf("Expected 1 apply arg, got %d: %v", len(args.ApplyArgs), args.ApplyArgs) + } + + // And destroy args should contain only auto-approve + expectedDestroyArgs := []string{"-auto-approve"} + if !reflect.DeepEqual(args.DestroyArgs, expectedDestroyArgs) { + t.Errorf("Expected destroy args %v, got %v", expectedDestroyArgs, args.DestroyArgs) + } + + // And environment variables should not contain parallelism + if strings.Contains(args.TerraformVars["TF_CLI_ARGS_apply"], "parallelism") { + t.Errorf("Apply args should not contain parallelism: %s", args.TerraformVars["TF_CLI_ARGS_apply"]) + } + if strings.Contains(args.TerraformVars["TF_CLI_ARGS_destroy"], "parallelism") { + t.Errorf("Destroy args should not contain parallelism: %s", args.TerraformVars["TF_CLI_ARGS_destroy"]) + } + }) + + t.Run("GeneratesCorrectArgsWithParallelism", func(t *testing.T) { + mocks := setupTerraformEnvMocks(t) + + // Set up blueprint handler with parallelism component + mockBlueprint := blueprint.NewMockBlueprintHandler(mocks.Injector) + parallelism := 5 + mockBlueprint.GetTerraformComponentsFunc = func() []blueprintv1alpha1.TerraformComponent { + return []blueprintv1alpha1.TerraformComponent{ + { + Path: "test/path", + Parallelism: ¶llelism, + }, + } + } + mocks.Injector.Register("blueprintHandler", mockBlueprint) + + printer := &TerraformEnvPrinter{ + BaseEnvPrinter: *NewBaseEnvPrinter(mocks.Injector), + } + + if err := printer.Initialize(); err != nil { + t.Fatalf("Failed to initialize printer: %v", err) + } + + // When generating terraform args with parallelism + args, err := printer.GenerateTerraformArgs("test/path", "test/module") + + // Then no error should be returned + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + // And apply args should contain parallelism flag before plan file + expectedApplyArgs := []string{"-parallelism=5"} + if len(args.ApplyArgs) < 1 || args.ApplyArgs[0] != expectedApplyArgs[0] { + t.Errorf("Expected apply args to start with %v, got %v", expectedApplyArgs, args.ApplyArgs) + } + + // And destroy args should contain parallelism flag + foundParallelismInDestroy := false + for _, arg := range args.DestroyArgs { + if arg == "-parallelism=5" { + foundParallelismInDestroy = true + break + } + } + if !foundParallelismInDestroy { + t.Errorf("Expected destroy args to contain -parallelism=5, got %v", args.DestroyArgs) + } + + // And environment variables should contain parallelism + if !strings.Contains(args.TerraformVars["TF_CLI_ARGS_apply"], " -parallelism=5") { + t.Errorf("Apply env var should contain parallelism: %s", args.TerraformVars["TF_CLI_ARGS_apply"]) + } + if !strings.Contains(args.TerraformVars["TF_CLI_ARGS_destroy"], "-parallelism=5") { + t.Errorf("Destroy env var should contain parallelism: %s", args.TerraformVars["TF_CLI_ARGS_destroy"]) + } + }) + + t.Run("ParallelismOnlyAppliedToMatchingComponent", func(t *testing.T) { + mocks := setupTerraformEnvMocks(t) + + // Set up blueprint handler with parallelism for different component + mockBlueprint := blueprint.NewMockBlueprintHandler(mocks.Injector) + parallelism := 10 + mockBlueprint.GetTerraformComponentsFunc = func() []blueprintv1alpha1.TerraformComponent { + return []blueprintv1alpha1.TerraformComponent{ + { + Path: "other/path", + Parallelism: ¶llelism, + }, + { + Path: "test/path", + // No parallelism set + }, + } + } + mocks.Injector.Register("blueprintHandler", mockBlueprint) + + printer := &TerraformEnvPrinter{ + BaseEnvPrinter: *NewBaseEnvPrinter(mocks.Injector), + } + + if err := printer.Initialize(); err != nil { + t.Fatalf("Failed to initialize printer: %v", err) + } + + // When generating terraform args for component without parallelism + args, err := printer.GenerateTerraformArgs("test/path", "test/module") + + // Then no error should be returned + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + // And apply args should not contain parallelism flag + for _, arg := range args.ApplyArgs { + if strings.Contains(arg, "parallelism") { + t.Errorf("Apply args should not contain parallelism for non-matching component: %v", args.ApplyArgs) + } + } + + // And environment variables should not contain parallelism + if strings.Contains(args.TerraformVars["TF_CLI_ARGS_apply"], "parallelism") { + t.Errorf("Apply env var should not contain parallelism: %s", args.TerraformVars["TF_CLI_ARGS_apply"]) + } + }) + + t.Run("HandlesNilBlueprintHandler", func(t *testing.T) { + mocks := setupTerraformEnvMocks(t) + + printer := &TerraformEnvPrinter{ + BaseEnvPrinter: *NewBaseEnvPrinter(mocks.Injector), + } + + // Initialize with the base dependencies but don't fail on missing blueprint handler + if err := printer.BaseEnvPrinter.Initialize(); err != nil { + t.Fatalf("Failed to initialize base printer: %v", err) + } + + // Set blueprint handler to nil explicitly + printer.blueprintHandler = nil + + // When generating terraform args with nil blueprint handler + args, err := printer.GenerateTerraformArgs("test/path", "test/module") + + // Then no error should be returned + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + // And no parallelism should be applied + for _, arg := range args.ApplyArgs { + if strings.Contains(arg, "parallelism") { + t.Errorf("Apply args should not contain parallelism with nil blueprint handler: %v", args.ApplyArgs) + } + } + }) + + t.Run("CorrectArgumentOrdering", func(t *testing.T) { + mocks := setupTerraformEnvMocks(t) + + // Set up blueprint handler with parallelism + mockBlueprint := blueprint.NewMockBlueprintHandler(mocks.Injector) + parallelism := 3 + mockBlueprint.GetTerraformComponentsFunc = func() []blueprintv1alpha1.TerraformComponent { + return []blueprintv1alpha1.TerraformComponent{ + { + Path: "test/path", + Parallelism: ¶llelism, + }, + } + } + mocks.Injector.Register("blueprintHandler", mockBlueprint) + + printer := &TerraformEnvPrinter{ + BaseEnvPrinter: *NewBaseEnvPrinter(mocks.Injector), + } + + if err := printer.Initialize(); err != nil { + t.Fatalf("Failed to initialize printer: %v", err) + } + + // When generating terraform args + args, err := printer.GenerateTerraformArgs("test/path", "test/module") + + // Then no error should be returned + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + // And apply args should have parallelism flag before plan file path + if len(args.ApplyArgs) < 2 { + t.Fatalf("Expected at least 2 apply args, got %d: %v", len(args.ApplyArgs), args.ApplyArgs) + } + + if args.ApplyArgs[0] != "-parallelism=3" { + t.Errorf("Expected first apply arg to be -parallelism=3, got %s", args.ApplyArgs[0]) + } + + // Plan file should be last argument + lastArg := args.ApplyArgs[len(args.ApplyArgs)-1] + if !strings.Contains(lastArg, "terraform.tfplan") { + t.Errorf("Expected last apply arg to contain terraform.tfplan, got %s", lastArg) + } + }) +} diff --git a/pkg/stack/windsor_stack_test.go b/pkg/stack/windsor_stack_test.go index ba460c631..679356efd 100644 --- a/pkg/stack/windsor_stack_test.go +++ b/pkg/stack/windsor_stack_test.go @@ -296,48 +296,6 @@ func TestWindsorStack_Up(t *testing.T) { t.Fatalf("Expected error to contain %q, got %q", expectedError, err.Error()) } }) - - t.Run("SuccessWithParallelism", func(t *testing.T) { - stack, mocks := setup(t) - - // Set up components with parallelism - mocks.Blueprint.GetTerraformComponentsFunc = func() []blueprintv1alpha1.TerraformComponent { - return []blueprintv1alpha1.TerraformComponent{ - { - Source: "source1", - Path: "module/path1", - FullPath: filepath.Join(os.Getenv("WINDSOR_PROJECT_ROOT"), ".windsor", ".tf_modules", "remote", "path"), - Parallelism: ptrInt(5), - }, - } - } - - // Track terraform commands to verify parallelism flag - var capturedCommands [][]string - mocks.Shell.ExecProgressFunc = func(message string, command string, args ...string) (string, error) { - if command == "terraform" { - capturedCommands = append(capturedCommands, append([]string{command}, args...)) - } - return "", nil - } - - // When the stack is brought up - if err := stack.Up(); err != nil { - t.Errorf("Expected Up to return nil, got %v", err) - } - - // Then terraform apply should be called with parallelism flag - foundApplyWithParallelism := false - for _, cmd := range capturedCommands { - if len(cmd) >= 5 && cmd[2] == "apply" && cmd[3] == "-parallelism=5" { - foundApplyWithParallelism = true - break - } - } - if !foundApplyWithParallelism { - t.Errorf("Expected terraform apply command with -parallelism=5, but it was not found in captured commands: %v", capturedCommands) - } - }) } func TestWindsorStack_Down(t *testing.T) { From 869c63b23966a513048d26ff2f5f7c80c1a0c930 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy Date: Tue, 22 Jul 2025 01:38:26 -0400 Subject: [PATCH 4/4] Remove extra test --- pkg/stack/windsor_stack_test.go | 45 --------------------------------- 1 file changed, 45 deletions(-) diff --git a/pkg/stack/windsor_stack_test.go b/pkg/stack/windsor_stack_test.go index 679356efd..0a0d7bacf 100644 --- a/pkg/stack/windsor_stack_test.go +++ b/pkg/stack/windsor_stack_test.go @@ -467,49 +467,4 @@ func TestWindsorStack_Down(t *testing.T) { t.Fatalf("Expected error to contain %q, got %q", expectedError, err.Error()) } }) - - t.Run("ErrorRemovingBackendOverride", func(t *testing.T) { - stack, mocks := setup(t) - mocks.Shims.Remove = func(_ string) error { - return fmt.Errorf("mock error removing backend override") - } - - // And when Down is called - err := stack.Down() - // Then the expected error is contained in err - expectedError := "error removing backend_override.tf" - if !strings.Contains(err.Error(), expectedError) { - t.Fatalf("Expected error to contain %q, got %q", expectedError, err.Error()) - } - }) - - t.Run("SkipComponentWithDestroyFalse", func(t *testing.T) { - stack, mocks := setup(t) - mocks.Blueprint.GetTerraformComponentsFunc = func() []blueprintv1alpha1.TerraformComponent { - return []blueprintv1alpha1.TerraformComponent{ - { - Source: "source1", - Path: "module/path1", - FullPath: filepath.Join(os.Getenv("WINDSOR_PROJECT_ROOT"), ".windsor", ".tf_modules", "remote", "path"), - Destroy: ptrBool(false), - }, - } - } - - // And when Down is called - err := stack.Down() - // Then no error should occur - if err != nil { - t.Errorf("Expected Down to return nil, got %v", err) - } - }) -} - -// Helper functions to create pointers for basic types -func ptrBool(b bool) *bool { - return &b -} - -func ptrInt(i int) *int { - return &i }