diff --git a/cmd/build_id.go b/cmd/build_id.go index f867869c4..ed0636fa7 100644 --- a/cmd/build_id.go +++ b/cmd/build_id.go @@ -28,20 +28,20 @@ Examples: RunE: func(cmd *cobra.Command, args []string) error { injector := cmd.Context().Value(injectorKey).(di.Injector) - execCtx := &runtime.Runtime{ + rt := &runtime.Runtime{ Injector: injector, } - execCtx, err := runtime.NewRuntime(execCtx) + rt, err := runtime.NewRuntime(rt) if err != nil { return fmt.Errorf("failed to initialize context: %w", err) } var buildID string if buildIdNewFlag { - buildID, err = execCtx.GenerateBuildID() + buildID, err = rt.GenerateBuildID() } else { - buildID, err = execCtx.GetBuildID() + buildID, err = rt.GetBuildID() } if err != nil { return fmt.Errorf("failed to manage build ID: %w", err) diff --git a/cmd/bundle.go b/cmd/bundle.go index b98826d48..bc2e33693 100644 --- a/cmd/bundle.go +++ b/cmd/bundle.go @@ -6,8 +6,8 @@ import ( "github.com/spf13/cobra" "github.com/windsorcli/cli/pkg/composer" "github.com/windsorcli/cli/pkg/composer/artifact" - "github.com/windsorcli/cli/pkg/runtime" "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/runtime" ) // bundleCmd represents the bundle command @@ -35,26 +35,25 @@ Examples: RunE: func(cmd *cobra.Command, args []string) error { injector := cmd.Context().Value(injectorKey).(di.Injector) - execCtx := &runtime.Runtime{ + rt := &runtime.Runtime{ Injector: injector, } - execCtx, err := runtime.NewRuntime(execCtx) + rt, err := runtime.NewRuntime(rt) if err != nil { return fmt.Errorf("failed to initialize context: %w", err) } - composerCtx := &composer.ComposerRuntime{ - Runtime: *execCtx, - } - + var override *composer.Composer if existingArtifactBuilder := injector.Resolve("artifactBuilder"); existingArtifactBuilder != nil { if artifactBuilder, ok := existingArtifactBuilder.(artifact.Artifact); ok { - composerCtx.ArtifactBuilder = artifactBuilder + override = &composer.Composer{ + ArtifactBuilder: artifactBuilder, + } } } - comp := composer.NewComposer(composerCtx) + comp := composer.NewComposer(rt, override) tag, _ := cmd.Flags().GetString("tag") outputPath, _ := cmd.Flags().GetString("output") diff --git a/cmd/check.go b/cmd/check.go index 063e9a56c..2b7ba3757 100644 --- a/cmd/check.go +++ b/cmd/check.go @@ -27,28 +27,28 @@ var checkCmd = &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { injector := cmd.Context().Value(injectorKey).(di.Injector) - execCtx := &runtime.Runtime{ + rt := &runtime.Runtime{ Injector: injector, } - execCtx, err := runtime.NewRuntime(execCtx) + rt, err := runtime.NewRuntime(rt) if err != nil { return fmt.Errorf("failed to initialize context: %w", err) } - if err := execCtx.Shell.CheckTrustedDirectory(); err != nil { + if err := rt.Shell.CheckTrustedDirectory(); err != nil { return fmt.Errorf("not in a trusted directory. If you are in a Windsor project, run 'windsor init' to approve") } - if err := execCtx.ConfigHandler.LoadConfig(); err != nil { + if err := rt.ConfigHandler.LoadConfig(); err != nil { return err } - if !execCtx.ConfigHandler.IsLoaded() { + if !rt.ConfigHandler.IsLoaded() { return fmt.Errorf("Nothing to check. Have you run \033[1mwindsor init\033[0m?") } - if err := execCtx.CheckTools(); err != nil { + if err := rt.CheckTools(); err != nil { return err } @@ -72,29 +72,29 @@ var checkNodeHealthCmd = &cobra.Command{ nodeHealthTimeout = constants.DefaultNodeHealthCheckTimeout } - execCtx := &runtime.Runtime{ + rt := &runtime.Runtime{ Injector: injector, } - execCtx, err := runtime.NewRuntime(execCtx) + rt, err := runtime.NewRuntime(rt) if err != nil { return fmt.Errorf("failed to initialize context: %w", err) } - if err := execCtx.Shell.CheckTrustedDirectory(); err != nil { + if err := rt.Shell.CheckTrustedDirectory(); err != nil { return fmt.Errorf("not in a trusted directory. If you are in a Windsor project, run 'windsor init' to approve") } - if err := execCtx.ConfigHandler.LoadConfig(); err != nil { + if err := rt.ConfigHandler.LoadConfig(); err != nil { return err } - if !execCtx.ConfigHandler.IsLoaded() { + if !rt.ConfigHandler.IsLoaded() { return fmt.Errorf("Nothing to check. Have you run \033[1mwindsor init\033[0m?") } provisionerCtx := &provisioner.ProvisionerRuntime{ - Runtime: *execCtx, + Runtime: *rt, } prov := provisioner.NewProvisioner(provisionerCtx) diff --git a/cmd/context.go b/cmd/context.go index 254415117..cb51cbeb0 100644 --- a/cmd/context.go +++ b/cmd/context.go @@ -17,20 +17,20 @@ var getContextCmd = &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { injector := cmd.Context().Value(injectorKey).(di.Injector) - execCtx := &runtime.Runtime{ + rt := &runtime.Runtime{ Injector: injector, } - execCtx, err := runtime.NewRuntime(execCtx) + rt, err := runtime.NewRuntime(rt) if err != nil { return fmt.Errorf("failed to initialize context: %w", err) } - if err := execCtx.ConfigHandler.LoadConfig(); err != nil { + if err := rt.ConfigHandler.LoadConfig(); err != nil { return fmt.Errorf("failed to load config: %w", err) } - contextName := execCtx.ConfigHandler.GetContext() + contextName := rt.ConfigHandler.GetContext() fmt.Fprintln(cmd.OutOrStdout(), contextName) return nil @@ -47,24 +47,24 @@ var setContextCmd = &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { injector := cmd.Context().Value(injectorKey).(di.Injector) - execCtx := &runtime.Runtime{ + rt := &runtime.Runtime{ Injector: injector, } - execCtx, err := runtime.NewRuntime(execCtx) + rt, err := runtime.NewRuntime(rt) if err != nil { return fmt.Errorf("failed to initialize context: %w", err) } - if err := execCtx.ConfigHandler.LoadConfig(); err != nil { + if err := rt.ConfigHandler.LoadConfig(); err != nil { return fmt.Errorf("failed to load config: %w", err) } - if _, err := execCtx.Shell.WriteResetToken(); err != nil { + if _, err := rt.Shell.WriteResetToken(); err != nil { return fmt.Errorf("failed to write reset token: %w", err) } - if err := execCtx.ConfigHandler.SetContext(args[0]); err != nil { + if err := rt.ConfigHandler.SetContext(args[0]); err != nil { return fmt.Errorf("failed to set context: %w", err) } diff --git a/cmd/down.go b/cmd/down.go index 875b5191d..78936c9ef 100644 --- a/cmd/down.go +++ b/cmd/down.go @@ -29,7 +29,7 @@ var downCmd = &cobra.Command{ return err } - if err := proj.Context.Shell.CheckTrustedDirectory(); err != nil { + if err := proj.Runtime.Shell.CheckTrustedDirectory(); err != nil { return fmt.Errorf("not in a trusted directory. If you are in a Windsor project, run 'windsor init' to approve") } diff --git a/cmd/env.go b/cmd/env.go index 593345114..b0b0db345 100644 --- a/cmd/env.go +++ b/cmd/env.go @@ -27,28 +27,28 @@ var envCmd = &cobra.Command{ injector := cmd.Context().Value(injectorKey).(di.Injector) - execCtx := &runtime.Runtime{ + rt := &runtime.Runtime{ Injector: injector, } - execCtx, err := runtime.NewRuntime(execCtx) + rt, err := runtime.NewRuntime(rt) if err != nil { return fmt.Errorf("failed to initialize context: %w", err) } - if err := execCtx.Shell.CheckTrustedDirectory(); err != nil { + if err := rt.Shell.CheckTrustedDirectory(); err != nil { return fmt.Errorf("not in a trusted directory. If you are in a Windsor project, run 'windsor init' to approve") } - if err := execCtx.HandleSessionReset(); err != nil { + if err := rt.HandleSessionReset(); err != nil { return err } - if err := execCtx.ConfigHandler.LoadConfig(); err != nil { + if err := rt.ConfigHandler.LoadConfig(); err != nil { return err } - if err := execCtx.LoadEnvironment(decrypt); err != nil { + if err := rt.LoadEnvironment(decrypt); err != nil { if hook || !verbose { return nil } @@ -62,15 +62,15 @@ var envCmd = &cobra.Command{ } if hook { - if execCtx.Shell != nil && len(execCtx.GetEnvVars()) > 0 { - outputFunc(execCtx.Shell.RenderEnvVars(execCtx.GetEnvVars(), true)) + if rt.Shell != nil && len(rt.GetEnvVars()) > 0 { + outputFunc(rt.Shell.RenderEnvVars(rt.GetEnvVars(), true)) } - if execCtx.Shell != nil && len(execCtx.GetAliases()) > 0 { - outputFunc(execCtx.Shell.RenderAliases(execCtx.GetAliases())) + if rt.Shell != nil && len(rt.GetAliases()) > 0 { + outputFunc(rt.Shell.RenderAliases(rt.GetAliases())) } } else { - if execCtx.Shell != nil && len(execCtx.GetEnvVars()) > 0 { - outputFunc(execCtx.Shell.RenderEnvVars(execCtx.GetEnvVars(), false)) + if rt.Shell != nil && len(rt.GetEnvVars()) > 0 { + outputFunc(rt.Shell.RenderEnvVars(rt.GetEnvVars(), false)) } } diff --git a/cmd/exec.go b/cmd/exec.go index 332c92bca..a8b0b4c0a 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -25,35 +25,35 @@ var execCmd = &cobra.Command{ injector := cmd.Context().Value(injectorKey).(di.Injector) - execCtx := &runtime.Runtime{ + rt := &runtime.Runtime{ Injector: injector, } - execCtx, err := runtime.NewRuntime(execCtx) + rt, err := runtime.NewRuntime(rt) if err != nil { return fmt.Errorf("failed to initialize context: %w", err) } - if err := execCtx.Shell.CheckTrustedDirectory(); err != nil { + if err := rt.Shell.CheckTrustedDirectory(); err != nil { return fmt.Errorf("not in a trusted directory. If you are in a Windsor project, run 'windsor init' to approve") } - if err := execCtx.HandleSessionReset(); err != nil { + if err := rt.HandleSessionReset(); err != nil { return err } - if err := execCtx.ConfigHandler.LoadConfig(); err != nil { + if err := rt.ConfigHandler.LoadConfig(); err != nil { return err } - if err := execCtx.LoadEnvironment(true); err != nil { + if err := rt.LoadEnvironment(true); err != nil { if !verbose { return nil } return fmt.Errorf("failed to load environment: %w", err) } - for key, value := range execCtx.GetEnvVars() { + for key, value := range rt.GetEnvVars() { if err := os.Setenv(key, value); err != nil { return fmt.Errorf("failed to set environment variable %s: %w", key, err) } @@ -65,7 +65,7 @@ var execCmd = &cobra.Command{ commandArgs = args[1:] } - if _, err := execCtx.Shell.Exec(command, commandArgs...); err != nil { + if _, err := rt.Shell.Exec(command, commandArgs...); err != nil { return fmt.Errorf("failed to execute command: %w", err) } diff --git a/cmd/hook.go b/cmd/hook.go index 69f2ed2ea..a8169d324 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -17,16 +17,16 @@ var hookCmd = &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { injector := cmd.Context().Value(injectorKey).(di.Injector) - execCtx := &runtime.Runtime{ + rt := &runtime.Runtime{ Injector: injector, } - execCtx, err := runtime.NewRuntime(execCtx) + rt, err := runtime.NewRuntime(rt) if err != nil { return fmt.Errorf("failed to initialize context: %w", err) } - if err := execCtx.Shell.InstallHook(args[0]); err != nil { + if err := rt.Shell.InstallHook(args[0]); err != nil { return fmt.Errorf("error installing hook: %w", err) } diff --git a/cmd/init.go b/cmd/init.go index 2ccf8a94b..a24858f7d 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -6,9 +6,9 @@ import ( "strings" "github.com/spf13/cobra" - "github.com/windsorcli/cli/pkg/runtime" "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/project" + "github.com/windsorcli/cli/pkg/runtime" ) // ============================================================================= @@ -43,16 +43,16 @@ var initCmd = &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { injector := cmd.Context().Value(injectorKey).(di.Injector) - baseCtx := &runtime.Runtime{ + rt := &runtime.Runtime{ Injector: injector, } - baseCtx, err := runtime.NewRuntime(baseCtx) + rt, err := runtime.NewRuntime(rt) if err != nil { return fmt.Errorf("failed to initialize context: %w", err) } - if err := baseCtx.Shell.AddCurrentDirToTrustedFile(); err != nil { + if err := rt.Shell.AddCurrentDirToTrustedFile(); err != nil { return fmt.Errorf("failed to add current directory to trusted file: %w", err) } @@ -60,7 +60,7 @@ var initCmd = &cobra.Command{ if len(args) > 0 { contextName = args[0] } else { - currentContext := baseCtx.ConfigHandler.GetContext() + currentContext := rt.ConfigHandler.GetContext() if currentContext != "" && currentContext != "local" { contextName = currentContext } @@ -114,7 +114,7 @@ var initCmd = &cobra.Command{ } } - proj, err := project.NewProject(injector, contextName, baseCtx) + proj, err := project.NewProject(injector, contextName, rt) if err != nil { return err } @@ -131,7 +131,7 @@ var initCmd = &cobra.Command{ } hasSetFlags := len(initSetFlags) > 0 - if err := proj.Context.ConfigHandler.SaveConfig(hasSetFlags); err != nil { + if err := proj.Runtime.ConfigHandler.SaveConfig(hasSetFlags); err != nil { return fmt.Errorf("failed to save configuration: %w", err) } diff --git a/cmd/install.go b/cmd/install.go index 4649e5f10..66cb47beb 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -22,7 +22,7 @@ var installCmd = &cobra.Command{ return err } - if err := proj.Context.Shell.CheckTrustedDirectory(); err != nil { + if err := proj.Runtime.Shell.CheckTrustedDirectory(); err != nil { return fmt.Errorf("not in a trusted directory. If you are in a Windsor project, run 'windsor init' to approve") } diff --git a/cmd/push.go b/cmd/push.go index 36a31c59e..cbee814d5 100644 --- a/cmd/push.go +++ b/cmd/push.go @@ -7,8 +7,8 @@ import ( "github.com/spf13/cobra" "github.com/windsorcli/cli/pkg/composer" "github.com/windsorcli/cli/pkg/composer/artifact" - "github.com/windsorcli/cli/pkg/runtime" "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/runtime" ) // pushCmd represents the push command @@ -38,26 +38,25 @@ Examples: injector := cmd.Context().Value(injectorKey).(di.Injector) - execCtx := &runtime.Runtime{ + rt := &runtime.Runtime{ Injector: injector, } - execCtx, err := runtime.NewRuntime(execCtx) + rt, err := runtime.NewRuntime(rt) if err != nil { return fmt.Errorf("failed to initialize context: %w", err) } - composerCtx := &composer.ComposerRuntime{ - Runtime: *execCtx, - } - + var override *composer.Composer if existingArtifactBuilder := injector.Resolve("artifactBuilder"); existingArtifactBuilder != nil { if artifactBuilder, ok := existingArtifactBuilder.(artifact.Artifact); ok { - composerCtx.ArtifactBuilder = artifactBuilder + override = &composer.Composer{ + ArtifactBuilder: artifactBuilder, + } } } - comp := composer.NewComposer(composerCtx) + comp := composer.NewComposer(rt, override) registryURL, err := comp.Push(args[0]) if err != nil { diff --git a/cmd/up.go b/cmd/up.go index ad2429875..f43561053 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -26,7 +26,7 @@ var upCmd = &cobra.Command{ return err } - if err := proj.Context.Shell.CheckTrustedDirectory(); err != nil { + if err := proj.Runtime.Shell.CheckTrustedDirectory(); err != nil { return fmt.Errorf("not in a trusted directory. If you are in a Windsor project, run 'windsor init' to approve") } diff --git a/cmd/up_test.go b/cmd/up_test.go index 8fa7c5a0e..c46b32df9 100644 --- a/cmd/up_test.go +++ b/cmd/up_test.go @@ -287,6 +287,7 @@ func TestUpCmd(t *testing.T) { // Then an error should occur if err == nil { t.Error("Expected error, got nil") + return } if !strings.Contains(err.Error(), "error installing blueprint") { t.Errorf("Expected install error, got: %v", err) diff --git a/pkg/composer/artifact/artifact.go b/pkg/composer/artifact/artifact.go index 7d959fa61..68b3a1a77 100644 --- a/pkg/composer/artifact/artifact.go +++ b/pkg/composer/artifact/artifact.go @@ -18,8 +18,8 @@ import ( "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/static" "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/windsorcli/cli/pkg/runtime" "github.com/windsorcli/cli/pkg/runtime/shell" - "github.com/windsorcli/cli/pkg/di" ) // The ArtifactBuilder creates tar.gz artifacts from prepared build directories. @@ -86,7 +86,6 @@ type BlueprintMetadataInput struct { // Artifact defines the interface for artifact creation operations type Artifact interface { - Initialize(injector di.Injector) error Bundle() error Write(outputPath string, tag string) (string, error) Push(registryBase string, repoName string, tag string) error @@ -117,43 +116,31 @@ type ArtifactBuilder struct { shell shell.Shell tarballPath string metadata BlueprintMetadataInput - ociCache map[string][]byte // Cache for downloaded OCI artifacts + ociCache map[string][]byte } // ============================================================================= // Constructor // ============================================================================= -// NewArtifactBuilder creates a new ArtifactBuilder instance with default configuration. -// Initializes an empty file map for storing artifact contents and sets up default shims -// for system operations. The returned builder is ready for dependency injection and file operations. -func NewArtifactBuilder() *ArtifactBuilder { - return &ArtifactBuilder{ +// NewArtifactBuilder creates a new ArtifactBuilder instance with the provided shell dependency. +// If overrides are provided, any non-nil component in the override ArtifactBuilder will be used instead of creating a default. +// The shell is used for retrieving git provenance and builder information during metadata generation. +func NewArtifactBuilder(rt *runtime.Runtime) *ArtifactBuilder { + builder := &ArtifactBuilder{ shims: NewShims(), files: make(map[string]FileInfo), ociCache: make(map[string][]byte), + shell: rt.Shell, } + + return builder } // ============================================================================= // Public Methods // ============================================================================= -// Initialize sets up the ArtifactBuilder with dependency injection and resolves required dependencies. -// Extracts the shell dependency from the injector for git operations and command execution. -// The shell is used for retrieving git provenance and builder information during metadata generation. -// Returns an error if the shell cannot be resolved from the injector. -func (a *ArtifactBuilder) Initialize(injector di.Injector) error { - if injector != nil { - shell, ok := injector.Resolve("shell").(shell.Shell) - if !ok { - return fmt.Errorf("failed to resolve shell from injector") - } - a.shell = shell - } - return nil -} - // Write bundles all files and creates a compressed tar.gz artifact file with optional tag override. // Accepts optional tag in "name:version" format to override metadata.yaml values. // Tag takes precedence over existing metadata. If no metadata.yaml exists, tag is required. diff --git a/pkg/composer/artifact/artifact_test.go b/pkg/composer/artifact/artifact_test.go index 81a74dad3..bb8ad2b70 100644 --- a/pkg/composer/artifact/artifact_test.go +++ b/pkg/composer/artifact/artifact_test.go @@ -18,6 +18,7 @@ import ( "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/types" "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/runtime" "github.com/windsorcli/cli/pkg/runtime/shell" ) @@ -41,6 +42,7 @@ type ArtifactMocks struct { Injector di.Injector Shell *shell.MockShell Shims *Shims + Runtime *runtime.Runtime } // mockTarWriter provides a mock implementation of TarWriter for testing @@ -154,6 +156,12 @@ func setupArtifactMocks(t *testing.T, opts ...*ArtifactSetupOptions) *ArtifactMo return os.Create(fullPath) } + // Create runtime + rt := &runtime.Runtime{ + Injector: injector, + Shell: mockShell, + } + // Cleanup function t.Cleanup(func() { os.Chdir(tmpDir) @@ -163,6 +171,7 @@ func setupArtifactMocks(t *testing.T, opts ...*ArtifactSetupOptions) *ArtifactMo Injector: injector, Shell: mockShell, Shims: shims, + Runtime: rt, } } @@ -249,11 +258,20 @@ func setupShims(t *testing.T) *Shims { // ============================================================================= func TestArtifactBuilder_NewArtifactBuilder(t *testing.T) { + setup := func(t *testing.T) (*ArtifactBuilder, *ArtifactMocks) { + t.Helper() + mocks := setupArtifactMocks(t) + builder := NewArtifactBuilder(mocks.Runtime) + builder.shims = mocks.Shims + return builder, mocks + } + t.Run("CreatesBuilderWithDefaults", func(t *testing.T) { // Given no preconditions // When creating a new artifact builder - builder := NewArtifactBuilder() + mocks := setupArtifactMocks(t) + builder := NewArtifactBuilder(mocks.Runtime) // Then the builder should be properly initialized if builder == nil { @@ -268,76 +286,19 @@ func TestArtifactBuilder_NewArtifactBuilder(t *testing.T) { t.Error("Expected files map to be initialized") } - // And dependency fields should be nil until Initialize() is called - if builder.shell != nil { - t.Error("Expected shell to be nil before Initialize()") + // And shell should be set (passed to constructor) + if builder.shell == nil { + t.Error("Expected shell to be set") } }) -} - -// ============================================================================= -// Test Public Methods -// ============================================================================= - -func TestArtifactBuilder_Initialize(t *testing.T) { - setup := func(t *testing.T) (*ArtifactBuilder, *ArtifactMocks) { - t.Helper() - mocks := setupArtifactMocks(t) - builder := NewArtifactBuilder() - builder.shims = mocks.Shims - return builder, mocks - } t.Run("Success", func(t *testing.T) { // Given a builder and mocks - builder, mocks := setup(t) - - // When calling Initialize - err := builder.Initialize(mocks.Injector) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected nil error, got %v", err) - } - - // And shell should be injected - if builder.shell == nil { - t.Error("Expected shell to be set after Initialize()") - } - }) - - t.Run("SuccessWithNilInjector", func(t *testing.T) { - // Given a builder builder, _ := setup(t) - // When calling Initialize with nil injector - err := builder.Initialize(nil) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected nil error, got %v", err) - } - - // And shell should remain nil - if builder.shell != nil { - t.Error("Expected shell to remain nil with nil injector") - } - }) - - t.Run("ErrorWhenShellNotFound", func(t *testing.T) { - // Given a builder and injector without shell - builder, mocks := setup(t) - mocks.Injector.Register("shell", "not-a-shell") - - // When calling Initialize - err := builder.Initialize(mocks.Injector) - - // Then an error should be returned - if err == nil { - t.Error("Expected error when shell not found") - } - if !strings.Contains(err.Error(), "failed to resolve shell") { - t.Errorf("Expected shell resolution error, got: %v", err) + // Then shell should be set + if builder.shell == nil { + t.Error("Expected shell to be set") } }) } @@ -346,7 +307,7 @@ func TestArtifactBuilder_AddFile(t *testing.T) { setup := func(t *testing.T) *ArtifactBuilder { t.Helper() mocks := setupArtifactMocks(t) - builder := NewArtifactBuilder() + builder := NewArtifactBuilder(mocks.Runtime) builder.shims = mocks.Shims return builder } @@ -413,9 +374,8 @@ func TestArtifactBuilder_Create(t *testing.T) { setup := func(t *testing.T) (*ArtifactBuilder, *ArtifactMocks) { t.Helper() mocks := setupArtifactMocks(t) - builder := NewArtifactBuilder() + builder := NewArtifactBuilder(mocks.Runtime) builder.shims = mocks.Shims - builder.Initialize(mocks.Injector) return builder, mocks } @@ -826,9 +786,8 @@ func TestArtifactBuilder_Push(t *testing.T) { setup := func(t *testing.T) (*ArtifactBuilder, *ArtifactMocks) { t.Helper() mocks := setupArtifactMocks(t) - builder := NewArtifactBuilder() + builder := NewArtifactBuilder(mocks.Runtime) builder.shims = mocks.Shims - builder.Initialize(mocks.Injector) return builder, mocks } @@ -1494,7 +1453,7 @@ func TestArtifactBuilder_resolveOutputPath(t *testing.T) { setup := func(t *testing.T) (*ArtifactBuilder, *ArtifactMocks) { t.Helper() mocks := setupArtifactMocks(t) - builder := NewArtifactBuilder() + builder := NewArtifactBuilder(mocks.Runtime) builder.shims = mocks.Shims return builder, mocks } @@ -1574,9 +1533,8 @@ func TestArtifactBuilder_createTarballInMemory(t *testing.T) { setup := func(t *testing.T) (*ArtifactBuilder, *ArtifactMocks) { t.Helper() mocks := setupArtifactMocks(t) - builder := NewArtifactBuilder() + builder := NewArtifactBuilder(mocks.Runtime) builder.shims = mocks.Shims - builder.Initialize(mocks.Injector) return builder, mocks } @@ -1693,9 +1651,8 @@ func TestArtifactBuilder_generateMetadataWithNameVersion(t *testing.T) { setup := func(t *testing.T) (*ArtifactBuilder, *ArtifactMocks) { t.Helper() mocks := setupArtifactMocks(t) - builder := NewArtifactBuilder() + builder := NewArtifactBuilder(mocks.Runtime) builder.shims = mocks.Shims - builder.Initialize(mocks.Injector) return builder, mocks } @@ -1790,9 +1747,8 @@ func TestArtifactBuilder_getGitProvenance(t *testing.T) { setup := func(t *testing.T) (*ArtifactBuilder, *ArtifactMocks) { t.Helper() mocks := setupArtifactMocks(t) - builder := NewArtifactBuilder() + builder := NewArtifactBuilder(mocks.Runtime) builder.shims = mocks.Shims - builder.Initialize(mocks.Injector) return builder, mocks } @@ -1930,9 +1886,8 @@ func TestArtifactBuilder_getBuilderInfo(t *testing.T) { setup := func(t *testing.T) (*ArtifactBuilder, *ArtifactMocks) { t.Helper() mocks := setupArtifactMocks(t) - builder := NewArtifactBuilder() + builder := NewArtifactBuilder(mocks.Runtime) builder.shims = mocks.Shims - builder.Initialize(mocks.Injector) return builder, mocks } @@ -2057,9 +2012,8 @@ func TestArtifactBuilder_createOCIArtifactImage(t *testing.T) { setup := func(t *testing.T) (*ArtifactBuilder, *ArtifactMocks) { t.Helper() mocks := setupArtifactMocks(t) - builder := NewArtifactBuilder() + builder := NewArtifactBuilder(mocks.Runtime) builder.shims = mocks.Shims - builder.Initialize(mocks.Injector) return builder, mocks } @@ -2282,11 +2236,8 @@ func (m *mockLayer) MediaType() (types.MediaType, error) { return "", nil } func TestArtifactBuilder_parseOCIRef(t *testing.T) { setup := func(t *testing.T) (*ArtifactBuilder, *ArtifactMocks) { mocks := setupArtifactMocks(t) - builder := NewArtifactBuilder() + builder := NewArtifactBuilder(mocks.Runtime) builder.shims = mocks.Shims - if err := builder.Initialize(mocks.Injector); err != nil { - t.Fatalf("failed to initialize ArtifactBuilder: %v", err) - } return builder, mocks } @@ -2399,11 +2350,8 @@ func TestArtifactBuilder_parseOCIRef(t *testing.T) { func TestArtifactBuilder_downloadOCIArtifact(t *testing.T) { setup := func(t *testing.T) (*ArtifactBuilder, *ArtifactMocks) { mocks := setupArtifactMocks(t) - builder := NewArtifactBuilder() + builder := NewArtifactBuilder(mocks.Runtime) builder.shims = mocks.Shims - if err := builder.Initialize(mocks.Injector); err != nil { - t.Fatalf("failed to initialize ArtifactBuilder: %v", err) - } return builder, mocks } @@ -2507,7 +2455,7 @@ func TestArtifactBuilder_downloadOCIArtifact(t *testing.T) { func TestArtifactBuilder_Pull(t *testing.T) { setup := func(t *testing.T) (*ArtifactBuilder, *ArtifactMocks) { mocks := setupArtifactMocks(t) - builder := NewArtifactBuilder() + builder := NewArtifactBuilder(mocks.Runtime) builder.shims = mocks.Shims // Set up OCI mocks @@ -2532,9 +2480,6 @@ func TestArtifactBuilder_Pull(t *testing.T) { return []byte("test artifact data"), nil } - if err := builder.Initialize(mocks.Injector); err != nil { - t.Fatalf("failed to initialize ArtifactBuilder: %v", err) - } return builder, mocks } @@ -2735,14 +2680,11 @@ func TestArtifactBuilder_Pull(t *testing.T) { t.Run("CachingPreventsRedundantDownloads", func(t *testing.T) { // Given an ArtifactBuilder with mocked dependencies - builder := NewArtifactBuilder() + mocks := setupArtifactMocks(t) + builder := NewArtifactBuilder(mocks.Runtime) injector := di.NewInjector() shell := shell.NewMockShell() injector.Register("shell", shell) - err := builder.Initialize(injector) - if err != nil { - t.Fatalf("failed to initialize builder: %v", err) - } // And download counter to track calls downloadCount := 0 @@ -2814,14 +2756,11 @@ func TestArtifactBuilder_Pull(t *testing.T) { t.Run("CachingWorksWithMixedNewAndCachedArtifacts", func(t *testing.T) { // Given an ArtifactBuilder with mocked dependencies - builder := NewArtifactBuilder() + mocks := setupArtifactMocks(t) + builder := NewArtifactBuilder(mocks.Runtime) injector := di.NewInjector() shell := shell.NewMockShell() injector.Register("shell", shell) - err := builder.Initialize(injector) - if err != nil { - t.Fatalf("failed to initialize builder: %v", err) - } // And download counter to track calls downloadCount := 0 @@ -2897,11 +2836,8 @@ func TestArtifactBuilder_Bundle(t *testing.T) { setup := func(t *testing.T) (*ArtifactBuilder, *ArtifactMocks) { t.Helper() mocks := setupArtifactMocks(t) - builder := NewArtifactBuilder() + builder := NewArtifactBuilder(mocks.Runtime) builder.shims = mocks.Shims - if err := builder.Initialize(mocks.Injector); err != nil { - t.Fatalf("Failed to initialize builder: %v", err) - } return builder, mocks } @@ -3036,7 +2972,8 @@ func (m *mockReference) Scope(action string) string { return "" } func TestArtifactBuilder_GetTemplateData(t *testing.T) { t.Run("InvalidOCIReference", func(t *testing.T) { // Given an artifact builder - builder := NewArtifactBuilder() + mocks := setupArtifactMocks(t) + builder := NewArtifactBuilder(mocks.Runtime) // When calling GetTemplateData with invalid reference templateData, err := builder.GetTemplateData("invalid-ref") @@ -3055,7 +2992,8 @@ func TestArtifactBuilder_GetTemplateData(t *testing.T) { t.Run("ErrorParsingOCIReference", func(t *testing.T) { // Given an artifact builder with mock shims - builder := NewArtifactBuilder() + mocks := setupArtifactMocks(t) + builder := NewArtifactBuilder(mocks.Runtime) builder.shims = &Shims{ ParseReference: func(ref string, opts ...name.Option) (name.Reference, error) { return nil, fmt.Errorf("parse error") @@ -3079,7 +3017,8 @@ func TestArtifactBuilder_GetTemplateData(t *testing.T) { t.Run("UsesCachedArtifact", func(t *testing.T) { // Given an artifact builder with cached data - builder := NewArtifactBuilder() + mocks := setupArtifactMocks(t) + builder := NewArtifactBuilder(mocks.Runtime) // Create test tar.gz data testData := createTestTarGz(t, map[string][]byte{ @@ -3143,7 +3082,8 @@ func TestArtifactBuilder_GetTemplateData(t *testing.T) { t.Run("FiltersOnlyJsonnetFiles", func(t *testing.T) { // Given an artifact builder with cached data containing multiple file types - builder := NewArtifactBuilder() + mocks := setupArtifactMocks(t) + builder := NewArtifactBuilder(mocks.Runtime) // Create test tar.gz data with mixed file types testData := createTestTarGz(t, map[string][]byte{ @@ -3216,7 +3156,8 @@ func TestArtifactBuilder_GetTemplateData(t *testing.T) { t.Run("ErrorWhenMissingMetadata", func(t *testing.T) { // Given an artifact builder with cached data missing metadata.yaml - builder := NewArtifactBuilder() + mocks := setupArtifactMocks(t) + builder := NewArtifactBuilder(mocks.Runtime) // Create test tar.gz data without metadata.yaml testData := createTestTarGz(t, map[string][]byte{ @@ -3250,7 +3191,8 @@ func TestArtifactBuilder_GetTemplateData(t *testing.T) { t.Run("ErrorWhenMissingBlueprintJsonnet", func(t *testing.T) { // Given an artifact builder with cached data missing _template/blueprint.jsonnet - builder := NewArtifactBuilder() + mocks := setupArtifactMocks(t) + builder := NewArtifactBuilder(mocks.Runtime) // Create test tar.gz data without _template/blueprint.jsonnet testData := createTestTarGz(t, map[string][]byte{ @@ -3292,7 +3234,8 @@ func TestArtifactBuilder_GetTemplateData(t *testing.T) { t.Run("SuccessWithOptionalSchema", func(t *testing.T) { // Given an artifact builder with cached data missing optional schema.yaml - builder := NewArtifactBuilder() + mocks := setupArtifactMocks(t) + builder := NewArtifactBuilder(mocks.Runtime) // Create test tar.gz data without schema.yaml testData := createTestTarGz(t, map[string][]byte{ @@ -3354,7 +3297,8 @@ func TestArtifactBuilder_GetTemplateData(t *testing.T) { t.Run("SuccessWithRequiredFiles", func(t *testing.T) { // Given an artifact builder with cached data containing required files - builder := NewArtifactBuilder() + mocks := setupArtifactMocks(t) + builder := NewArtifactBuilder(mocks.Runtime) // Create test tar.gz data with required files testData := createTestTarGz(t, map[string][]byte{ @@ -3544,11 +3488,8 @@ func TestArtifactBuilder_findMatchingProcessor(t *testing.T) { setup := func(t *testing.T) (*ArtifactBuilder, *ArtifactMocks) { t.Helper() mocks := setupArtifactMocks(t) - builder := NewArtifactBuilder() + builder := NewArtifactBuilder(mocks.Runtime) builder.shims = mocks.Shims - if err := builder.Initialize(mocks.Injector); err != nil { - t.Fatalf("Failed to initialize builder: %v", err) - } return builder, mocks } @@ -3620,11 +3561,8 @@ func TestArtifactBuilder_shouldSkipTerraformFile(t *testing.T) { setup := func(t *testing.T) (*ArtifactBuilder, *ArtifactMocks) { t.Helper() mocks := setupArtifactMocks(t) - builder := NewArtifactBuilder() + builder := NewArtifactBuilder(mocks.Runtime) builder.shims = mocks.Shims - if err := builder.Initialize(mocks.Injector); err != nil { - t.Fatalf("Failed to initialize builder: %v", err) - } return builder, mocks } @@ -3757,11 +3695,8 @@ func TestArtifactBuilder_walkAndProcessFiles(t *testing.T) { setup := func(t *testing.T) (*ArtifactBuilder, *ArtifactMocks) { t.Helper() mocks := setupArtifactMocks(t) - builder := NewArtifactBuilder() + builder := NewArtifactBuilder(mocks.Runtime) builder.shims = mocks.Shims - if err := builder.Initialize(mocks.Injector); err != nil { - t.Fatalf("Failed to initialize builder: %v", err) - } return builder, mocks } diff --git a/pkg/composer/blueprint/blueprint_handler.go b/pkg/composer/blueprint/blueprint_handler.go index 8a5a9c9c7..8f2926a05 100644 --- a/pkg/composer/blueprint/blueprint_handler.go +++ b/pkg/composer/blueprint/blueprint_handler.go @@ -15,9 +15,7 @@ import ( "github.com/goccy/go-yaml" "github.com/windsorcli/cli/pkg/composer/artifact" "github.com/windsorcli/cli/pkg/constants" - "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/runtime/config" - "github.com/windsorcli/cli/pkg/runtime/shell" + "github.com/windsorcli/cli/pkg/runtime" "github.com/fluxcd/pkg/apis/kustomize" blueprintv1alpha1 "github.com/windsorcli/cli/api/v1alpha1" @@ -32,7 +30,6 @@ import ( // infrastructure definitions, enabling consistent and reproducible infrastructure deployments. type BlueprintHandler interface { - Initialize() error LoadBlueprint() error Write(overwrite ...bool) error GetTerraformComponents() []blueprintv1alpha1.TerraformComponent @@ -42,12 +39,9 @@ type BlueprintHandler interface { type BaseBlueprintHandler struct { BlueprintHandler - injector di.Injector - configHandler config.ConfigHandler - shell shell.Shell + runtime *runtime.Runtime + artifactBuilder artifact.Artifact blueprint blueprintv1alpha1.Blueprint - projectRoot string - templateRoot string featureEvaluator *FeatureEvaluator shims *Shims kustomizeData map[string]any @@ -55,62 +49,61 @@ type BaseBlueprintHandler struct { configLoaded bool } -// NewBlueprintHandler creates a new instance of BaseBlueprintHandler. -// It initializes the handler with the provided dependency injector. -func NewBlueprintHandler(injector di.Injector) *BaseBlueprintHandler { - return &BaseBlueprintHandler{ - injector: injector, - featureEvaluator: NewFeatureEvaluator(injector), +// NewBlueprintHandler creates a new instance of BaseBlueprintHandler with the provided dependencies. +// If overrides are provided, any non-nil component in the override BaseBlueprintHandler will be used instead of creating a default. +func NewBlueprintHandler(rt *runtime.Runtime, artifactBuilder artifact.Artifact, opts ...*BaseBlueprintHandler) (*BaseBlueprintHandler, error) { + handler := &BaseBlueprintHandler{ + runtime: rt, + artifactBuilder: artifactBuilder, + featureEvaluator: NewFeatureEvaluator(rt), shims: NewShims(), kustomizeData: make(map[string]any), featureSubstitutions: make(map[string]map[string]string), } + + if len(opts) > 0 && opts[0] != nil { + overrides := opts[0] + if overrides.featureEvaluator != nil { + handler.featureEvaluator = overrides.featureEvaluator + } + } + + return handler, nil } // ============================================================================= // Public Methods // ============================================================================= -// Initialize resolves and assigns dependencies for BaseBlueprintHandler using the provided dependency injector. -// It sets configHandler and shell, determines the project root directory. -// Returns an error if any dependency resolution or initialization step fails. -func (b *BaseBlueprintHandler) Initialize() error { - configHandler, ok := b.injector.Resolve("configHandler").(config.ConfigHandler) - if !ok { - return fmt.Errorf("error resolving configHandler") - } - b.configHandler = configHandler - - shell, ok := b.injector.Resolve("shell").(shell.Shell) - if !ok { - return fmt.Errorf("error resolving shell") - } - b.shell = shell - - projectRoot, err := b.shell.GetProjectRoot() - if err != nil { - return fmt.Errorf("error getting project root: %w", err) - } - b.projectRoot = projectRoot - b.templateRoot = filepath.Join(projectRoot, "contexts", "_template") - - if err := b.featureEvaluator.Initialize(); err != nil { - return fmt.Errorf("error initializing feature evaluator: %w", err) - } - - return nil -} - // LoadBlueprint loads all blueprint data into memory, establishing defaults from either templates // or OCI artifacts, then applies any local blueprint.yaml overrides to ensure the correct precedence. // All sources are processed and merged into the in-memory runtime state. // Returns an error if any required paths are inaccessible or any loading operation fails. func (b *BaseBlueprintHandler) LoadBlueprint() error { - if _, err := b.shims.Stat(b.templateRoot); err == nil { - if _, err := b.GetLocalTemplateData(); err != nil { + if _, err := b.shims.Stat(b.runtime.TemplateRoot); err == nil { + templateData, err := b.GetLocalTemplateData() + if err != nil { return fmt.Errorf("failed to get local template data: %w", err) } + if len(templateData) == 0 { + configRoot := b.runtime.ConfigRoot + if configRoot == "" { + return fmt.Errorf("blueprint.yaml not found at %s", filepath.Join(configRoot, "blueprint.yaml")) + } + blueprintPath := filepath.Join(configRoot, "blueprint.yaml") + if _, err := b.shims.Stat(blueprintPath); err != nil { + return fmt.Errorf("blueprint.yaml not found at %s", blueprintPath) + } + } } else { + configRoot := b.runtime.ConfigRoot + blueprintPath := filepath.Join(configRoot, "blueprint.yaml") + if _, err := b.shims.Stat(blueprintPath); err == nil { + if err := b.loadConfig(); err != nil { + return fmt.Errorf("failed to load blueprint config: %w", err) + } + return nil + } effectiveBlueprintURL := constants.GetEffectiveBlueprintURL() ociInfo, err := artifact.ParseOCIReference(effectiveBlueprintURL) if err != nil { @@ -119,17 +112,12 @@ func (b *BaseBlueprintHandler) LoadBlueprint() error { if ociInfo == nil { return fmt.Errorf("invalid default blueprint reference: %s", effectiveBlueprintURL) } - artifactBuilder := b.injector.Resolve("artifactBuilder") - if artifactBuilder == nil { - return fmt.Errorf("artifact builder not available") + if b.artifactBuilder == nil { + return fmt.Errorf("blueprint.yaml not found at %s and artifact builder not available", blueprintPath) } - ab, ok := artifactBuilder.(artifact.Artifact) - if !ok { - return fmt.Errorf("artifact builder has wrong type") - } - templateData, err := ab.GetTemplateData(ociInfo.URL) + templateData, err := b.artifactBuilder.GetTemplateData(ociInfo.URL) if err != nil { - return fmt.Errorf("failed to get template data from default blueprint: %w", err) + return fmt.Errorf("blueprint.yaml not found at %s and failed to get template data from default blueprint: %w", blueprintPath, err) } blueprintData := make(map[string]any) for key, value := range templateData { @@ -142,29 +130,23 @@ func (b *BaseBlueprintHandler) LoadBlueprint() error { sources := b.getSources() if len(sources) > 0 { - artifactBuilder := b.injector.Resolve("artifactBuilder") - if artifactBuilder != nil { - if ab, ok := artifactBuilder.(artifact.Artifact); ok { - var ociURLs []string - for _, source := range sources { - if strings.HasPrefix(source.Url, "oci://") { - ociURLs = append(ociURLs, source.Url) - } + if b.artifactBuilder != nil { + var ociURLs []string + for _, source := range sources { + if strings.HasPrefix(source.Url, "oci://") { + ociURLs = append(ociURLs, source.Url) } - if len(ociURLs) > 0 { - _, err := ab.Pull(ociURLs) - if err != nil { - return fmt.Errorf("failed to load OCI sources: %w", err) - } + } + if len(ociURLs) > 0 { + _, err := b.artifactBuilder.Pull(ociURLs) + if err != nil { + return fmt.Errorf("failed to load OCI sources: %w", err) } } } } - configRoot, err := b.configHandler.GetConfigRoot() - if err != nil { - return fmt.Errorf("error getting config root: %w", err) - } + configRoot := b.runtime.ConfigRoot blueprintPath := filepath.Join(configRoot, "blueprint.yaml") if _, err := b.shims.Stat(blueprintPath); err == nil { @@ -187,9 +169,9 @@ func (b *BaseBlueprintHandler) Write(overwrite ...bool) error { shouldOverwrite = overwrite[0] } - configRoot, err := b.configHandler.GetConfigRoot() - if err != nil { - return fmt.Errorf("error getting config root: %w", err) + configRoot := b.runtime.ConfigRoot + if configRoot == "" { + return fmt.Errorf("error getting config root: config root is empty") } yamlPath := filepath.Join(configRoot, "blueprint.yaml") @@ -290,22 +272,22 @@ func (b *BaseBlueprintHandler) Generate() *blueprintv1alpha1.Blueprint { // values taking precedence. Returns nil if no templates exist. Keys are relative file paths, // values are file contents. func (b *BaseBlueprintHandler) GetLocalTemplateData() (map[string][]byte, error) { - if _, err := b.shims.Stat(b.templateRoot); os.IsNotExist(err) { + if _, err := b.shims.Stat(b.runtime.TemplateRoot); os.IsNotExist(err) { return nil, nil } templateData := make(map[string][]byte) - if err := b.walkAndCollectTemplates(b.templateRoot, templateData); err != nil { + if err := b.walkAndCollectTemplates(b.runtime.TemplateRoot, templateData); err != nil { return nil, fmt.Errorf("failed to collect templates: %w", err) } if schemaData, exists := templateData["schema"]; exists { - if err := b.configHandler.LoadSchemaFromBytes(schemaData); err != nil { + if err := b.runtime.ConfigHandler.LoadSchemaFromBytes(schemaData); err != nil { return nil, fmt.Errorf("failed to load schema: %w", err) } } - contextValues, err := b.configHandler.GetContextValues() + contextValues, err := b.runtime.ConfigHandler.GetContextValues() if err != nil { return nil, fmt.Errorf("failed to load context values: %w", err) } @@ -322,7 +304,7 @@ func (b *BaseBlueprintHandler) GetLocalTemplateData() (map[string][]byte, error) } if len(b.blueprint.TerraformComponents) > 0 || len(b.blueprint.Kustomizations) > 0 { - contextName := b.configHandler.GetContext() + contextName := b.runtime.ConfigHandler.GetContext() if contextName != "" { b.blueprint.Metadata.Name = contextName b.blueprint.Metadata.Description = fmt.Sprintf("Blueprint for %s context", contextName) @@ -362,9 +344,9 @@ func (b *BaseBlueprintHandler) GetLocalTemplateData() (map[string][]byte, error) // Returns an error if blueprint.yaml does not exist. // Template processing is now handled by the pkg/template package. func (b *BaseBlueprintHandler) loadConfig() error { - configRoot, err := b.configHandler.GetConfigRoot() - if err != nil { - return fmt.Errorf("error getting config root: %w", err) + configRoot := b.runtime.ConfigRoot + if configRoot == "" { + return fmt.Errorf("error getting config root: config root is empty") } yamlPath := filepath.Join(configRoot, "blueprint.yaml") @@ -501,7 +483,7 @@ func (b *BaseBlueprintHandler) walkAndCollectTemplates(templateDir string, templ } else if strings.HasSuffix(entry.Name(), ".jsonnet") || entry.Name() == "schema.yaml" || entry.Name() == "blueprint.yaml" || - (strings.HasPrefix(filepath.Dir(entryPath), filepath.Join(b.templateRoot, "features")) && strings.HasSuffix(entry.Name(), ".yaml")) { + (strings.HasPrefix(filepath.Dir(entryPath), filepath.Join(b.runtime.TemplateRoot, "features")) && strings.HasSuffix(entry.Name(), ".yaml")) { content, err := b.shims.ReadFile(filepath.Clean(entryPath)) if err != nil { return fmt.Errorf("failed to read template file %s: %w", entryPath, err) @@ -512,7 +494,7 @@ func (b *BaseBlueprintHandler) walkAndCollectTemplates(templateDir string, templ } else if entry.Name() == "blueprint.yaml" { templateData["blueprint"] = content } else { - relPath, err := filepath.Rel(b.templateRoot, entryPath) + relPath, err := filepath.Rel(b.runtime.TemplateRoot, entryPath) if err != nil { return fmt.Errorf("failed to calculate relative path for %s: %w", entryPath, err) } @@ -546,7 +528,7 @@ func (b *BaseBlueprintHandler) processFeatures(templateData map[string][]byte, c return nil } - evaluator := NewFeatureEvaluator(b.injector) + evaluator := b.featureEvaluator sort.Slice(features, func(i, j int) bool { return features[i].Metadata.Name < features[j].Metadata.Name @@ -660,7 +642,7 @@ func (b *BaseBlueprintHandler) loadFeatures(templateData map[string][]byte) ([]b if err != nil { return nil, fmt.Errorf("failed to parse feature %s: %w", path, err) } - feature.Path = filepath.Join(b.templateRoot, path) + feature.Path = filepath.Join(b.runtime.TemplateRoot, path) features = append(features, *feature) } } @@ -743,7 +725,7 @@ func (b *BaseBlueprintHandler) resolveComponentSources(blueprint *blueprintv1alp // for local modules. It processes all components in the blueprint, checking source types to // determine appropriate path resolution strategies and updating component paths accordingly. func (b *BaseBlueprintHandler) resolveComponentPaths(blueprint *blueprintv1alpha1.Blueprint) { - projectRoot := b.projectRoot + projectRoot := b.runtime.ProjectRoot resolvedComponents := make([]blueprintv1alpha1.TerraformComponent, len(blueprint.TerraformComponents)) copy(resolvedComponents, blueprint.TerraformComponents) @@ -878,8 +860,8 @@ func (b *BaseBlueprintHandler) resolvePatchFromPath(patchPath, defaultNamespace } } - configRoot, err := b.configHandler.GetConfigRoot() - if err == nil { + configRoot := b.runtime.ConfigRoot + if configRoot != "" { patchFilePath := filepath.Join(configRoot, "kustomize", patchPath) if data, err := b.shims.ReadFile(patchFilePath); err == nil { if basePatchData == nil { @@ -889,7 +871,7 @@ func (b *BaseBlueprintHandler) resolvePatchFromPath(patchPath, defaultNamespace var userPatchData map[string]any if err := b.shims.YamlUnmarshal(data, &userPatchData); err == nil { - maps.Copy(basePatchData, userPatchData) + basePatchData = b.deepMergeMaps(basePatchData, userPatchData) } else { target = b.extractTargetFromPatchContent(string(data), defaultNamespace) return string(data), target @@ -1058,7 +1040,7 @@ func (b *BaseBlueprintHandler) deepMergeMaps(base, overlay map[string]any) map[s // Uses development URL if dev flag is enabled, otherwise falls back to git remote origin URL. // In dev mode, always overrides the URL even if it's already set. func (b *BaseBlueprintHandler) setRepositoryDefaults() error { - devMode := b.configHandler.GetBool("dev") + devMode := b.runtime.ConfigHandler.GetBool("dev") if devMode { url := b.getDevelopmentRepositoryURL() @@ -1073,7 +1055,7 @@ func (b *BaseBlueprintHandler) setRepositoryDefaults() error { return nil } - gitURL, err := b.shell.ExecSilent("git", "config", "--get", "remote.origin.url") + gitURL, err := b.runtime.Shell.ExecSilent("git", "config", "--get", "remote.origin.url") if err == nil && gitURL != "" { b.blueprint.Repository.Url = b.normalizeGitURL(strings.TrimSpace(gitURL)) return nil @@ -1087,7 +1069,7 @@ func (b *BaseBlueprintHandler) setRepositoryDefaults() error { // All evaluated values are converted to strings as required by Flux postBuild substitution. func (b *BaseBlueprintHandler) evaluateSubstitutions(substitutions map[string]string, config map[string]any, featurePath string) (map[string]string, error) { result := make(map[string]string) - evaluator := NewFeatureEvaluator(b.injector) + evaluator := b.featureEvaluator for key, value := range substitutions { if strings.Contains(value, "${") { @@ -1125,18 +1107,17 @@ func (b *BaseBlueprintHandler) normalizeGitURL(url string) string { // getDevelopmentRepositoryURL generates a development repository URL from configuration. // Returns URL in format: http://git./git/ func (b *BaseBlueprintHandler) getDevelopmentRepositoryURL() string { - domain := b.configHandler.GetString("dns.domain", "test") + domain := b.runtime.ConfigHandler.GetString("dns.domain", "test") if domain == "" { return "" } - projectRoot, err := b.shell.GetProjectRoot() - if err != nil { + if b.runtime.ProjectRoot == "" { return "" } - folder := b.shims.FilepathBase(projectRoot) - if folder == "" { + folder := b.shims.FilepathBase(b.runtime.ProjectRoot) + if folder == "" || folder == "." { return "" } diff --git a/pkg/composer/blueprint/blueprint_handler_helper_test.go b/pkg/composer/blueprint/blueprint_handler_helper_test.go index 06a877f17..02a92c3b5 100644 --- a/pkg/composer/blueprint/blueprint_handler_helper_test.go +++ b/pkg/composer/blueprint/blueprint_handler_helper_test.go @@ -6,6 +6,7 @@ import ( "testing" blueprintv1alpha1 "github.com/windsorcli/cli/api/v1alpha1" + "github.com/windsorcli/cli/pkg/runtime" "github.com/windsorcli/cli/pkg/runtime/config" ) @@ -17,9 +18,11 @@ func TestBaseBlueprintHandler_getKustomizations(t *testing.T) { t.Run("NoKustomizations", func(t *testing.T) { // Given a blueprint handler with no kustomizations handler := &BaseBlueprintHandler{ - shims: NewShims(), - configHandler: config.NewMockConfigHandler(), - projectRoot: "/tmp", + shims: NewShims(), + runtime: &runtime.Runtime{ + ProjectRoot: "/tmp", + ConfigHandler: config.NewMockConfigHandler(), + }, blueprint: blueprintv1alpha1.Blueprint{ Kustomizations: nil, }, @@ -37,9 +40,11 @@ func TestBaseBlueprintHandler_getKustomizations(t *testing.T) { t.Run("KustomizationWithNoPatches", func(t *testing.T) { // Given a blueprint handler with a kustomization that has no patches handler := &BaseBlueprintHandler{ - shims: NewShims(), - configHandler: config.NewMockConfigHandler(), - projectRoot: "/tmp", + shims: NewShims(), + runtime: &runtime.Runtime{ + ProjectRoot: "/tmp", + ConfigHandler: config.NewMockConfigHandler(), + }, blueprint: blueprintv1alpha1.Blueprint{ Metadata: blueprintv1alpha1.Metadata{ Name: "test-blueprint", @@ -81,9 +86,11 @@ func TestBaseBlueprintHandler_getKustomizations(t *testing.T) { }, } handler := &BaseBlueprintHandler{ - shims: NewShims(), - configHandler: config.NewMockConfigHandler(), - projectRoot: "/tmp", + shims: NewShims(), + runtime: &runtime.Runtime{ + ProjectRoot: "/tmp", + ConfigHandler: config.NewMockConfigHandler(), + }, blueprint: blueprintv1alpha1.Blueprint{ Metadata: blueprintv1alpha1.Metadata{ Name: "test-blueprint", @@ -115,9 +122,11 @@ func TestBaseBlueprintHandler_getKustomizations(t *testing.T) { t.Run("KustomizationWithDiscoveredPatches", func(t *testing.T) { // Given a blueprint handler with a kustomization that will have discovered patches handler := &BaseBlueprintHandler{ - shims: NewShims(), - configHandler: config.NewMockConfigHandler(), - projectRoot: "/tmp", + shims: NewShims(), + runtime: &runtime.Runtime{ + ProjectRoot: "/tmp", + ConfigHandler: config.NewMockConfigHandler(), + }, blueprint: blueprintv1alpha1.Blueprint{ Metadata: blueprintv1alpha1.Metadata{ Name: "test-blueprint", @@ -161,7 +170,7 @@ data: } // Override project root for this test - handler.projectRoot = tempDir + handler.runtime.ProjectRoot = tempDir // When getting kustomizations result := handler.getKustomizations() @@ -186,9 +195,11 @@ data: }, } handler := &BaseBlueprintHandler{ - shims: NewShims(), - configHandler: config.NewMockConfigHandler(), - projectRoot: "/tmp", + shims: NewShims(), + runtime: &runtime.Runtime{ + ProjectRoot: "/tmp", + ConfigHandler: config.NewMockConfigHandler(), + }, blueprint: blueprintv1alpha1.Blueprint{ Metadata: blueprintv1alpha1.Metadata{ Name: "test-blueprint", @@ -222,7 +233,7 @@ data: } // Override project root for this test - handler.projectRoot = tempDir + handler.runtime.ProjectRoot = tempDir // When getting kustomizations result := handler.getKustomizations() @@ -246,9 +257,11 @@ data: t.Run("KustomizationWithPatchDiscoveryError", func(t *testing.T) { // Given a blueprint handler with a kustomization that will have patch discovery errors handler := &BaseBlueprintHandler{ - shims: NewShims(), - configHandler: config.NewMockConfigHandler(), - projectRoot: "/tmp", + shims: NewShims(), + runtime: &runtime.Runtime{ + ProjectRoot: "/tmp", + ConfigHandler: config.NewMockConfigHandler(), + }, blueprint: blueprintv1alpha1.Blueprint{ Metadata: blueprintv1alpha1.Metadata{ Name: "test-blueprint", @@ -279,7 +292,7 @@ data: } // Override project root for this test - handler.projectRoot = tempDir + handler.runtime.ProjectRoot = tempDir // When getting kustomizations result := handler.getKustomizations() @@ -296,9 +309,11 @@ data: t.Run("MultipleKustomizations", func(t *testing.T) { // Given a blueprint handler with multiple kustomizations handler := &BaseBlueprintHandler{ - shims: NewShims(), - configHandler: config.NewMockConfigHandler(), - projectRoot: "/tmp", + shims: NewShims(), + runtime: &runtime.Runtime{ + ProjectRoot: "/tmp", + ConfigHandler: config.NewMockConfigHandler(), + }, blueprint: blueprintv1alpha1.Blueprint{ Metadata: blueprintv1alpha1.Metadata{ Name: "test-blueprint", @@ -339,7 +354,7 @@ data: } // Override project root for this test - handler.projectRoot = tempDir + handler.runtime.ProjectRoot = tempDir // When getting kustomizations result := handler.getKustomizations() diff --git a/pkg/composer/blueprint/blueprint_handler_private_test.go b/pkg/composer/blueprint/blueprint_handler_private_test.go index b58f6afa7..7fe805386 100644 --- a/pkg/composer/blueprint/blueprint_handler_private_test.go +++ b/pkg/composer/blueprint/blueprint_handler_private_test.go @@ -7,7 +7,8 @@ import ( "testing" blueprintv1alpha1 "github.com/windsorcli/cli/api/v1alpha1" - "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/composer/artifact" + "github.com/windsorcli/cli/pkg/runtime" "github.com/windsorcli/cli/pkg/runtime/config" "github.com/windsorcli/cli/pkg/runtime/shell" ) @@ -15,10 +16,19 @@ import ( func TestBaseBlueprintHandler_resolvePatchFromPath(t *testing.T) { setup := func(t *testing.T) *BaseBlueprintHandler { t.Helper() - injector := di.NewInjector() - handler := NewBlueprintHandler(injector) + mockConfigHandler := config.NewMockConfigHandler() + mockShell := shell.NewMockShell() + mockArtifactBuilder := artifact.NewMockArtifact() + rt := &runtime.Runtime{ + ConfigHandler: mockConfigHandler, + Shell: mockShell, + } + handler, err := NewBlueprintHandler(rt, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } handler.shims = NewShims() - handler.configHandler = config.NewMockConfigHandler() + handler.runtime.ConfigHandler = config.NewMockConfigHandler() return handler } @@ -64,7 +74,7 @@ func TestBaseBlueprintHandler_resolvePatchFromPath(t *testing.T) { t.Run("WithNoData", func(t *testing.T) { // Given a handler with no data handler := setup(t) - handler.configHandler.(*config.MockConfigHandler).GetConfigRootFunc = func() (string, error) { + handler.runtime.ConfigHandler.(*config.MockConfigHandler).GetConfigRootFunc = func() (string, error) { return "", fmt.Errorf("config root error") } // When resolving patch from path @@ -146,9 +156,7 @@ func TestBaseBlueprintHandler_resolvePatchFromPath(t *testing.T) { }, }, } - handler.configHandler.(*config.MockConfigHandler).GetConfigRootFunc = func() (string, error) { - return "/test/config", nil - } + handler.runtime.ConfigRoot = "/test/config" handler.shims.ReadFile = func(name string) ([]byte, error) { return []byte(`apiVersion: v1 kind: ConfigMap @@ -197,8 +205,17 @@ data: func TestBaseBlueprintHandler_extractTargetFromPatchData(t *testing.T) { setup := func(t *testing.T) *BaseBlueprintHandler { t.Helper() - injector := di.NewInjector() - handler := NewBlueprintHandler(injector) + mockConfigHandler := config.NewMockConfigHandler() + mockShell := shell.NewMockShell() + mockArtifactBuilder := artifact.NewMockArtifact() + rt := &runtime.Runtime{ + ConfigHandler: mockConfigHandler, + Shell: mockShell, + } + handler, err := NewBlueprintHandler(rt, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } return handler } @@ -353,8 +370,17 @@ func TestBaseBlueprintHandler_extractTargetFromPatchData(t *testing.T) { func TestBaseBlueprintHandler_extractTargetFromPatchContent(t *testing.T) { setup := func(t *testing.T) *BaseBlueprintHandler { t.Helper() - injector := di.NewInjector() - handler := NewBlueprintHandler(injector) + mockConfigHandler := config.NewMockConfigHandler() + mockShell := shell.NewMockShell() + mockArtifactBuilder := artifact.NewMockArtifact() + rt := &runtime.Runtime{ + ConfigHandler: mockConfigHandler, + Shell: mockShell, + } + handler, err := NewBlueprintHandler(rt, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } return handler } @@ -443,8 +469,17 @@ kind: ConfigMap func TestBaseBlueprintHandler_deepMergeMaps(t *testing.T) { setup := func(t *testing.T) *BaseBlueprintHandler { t.Helper() - injector := di.NewInjector() - handler := NewBlueprintHandler(injector) + mockConfigHandler := config.NewMockConfigHandler() + mockShell := shell.NewMockShell() + mockArtifactBuilder := artifact.NewMockArtifact() + rt := &runtime.Runtime{ + ConfigHandler: mockConfigHandler, + Shell: mockShell, + } + handler, err := NewBlueprintHandler(rt, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } return handler } @@ -989,7 +1024,11 @@ func TestBaseBlueprintHandler_parseFeature(t *testing.T) { setup := func(t *testing.T) *BaseBlueprintHandler { t.Helper() mocks := setupMocks(t) - handler := NewBlueprintHandler(mocks.Injector) + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } handler.shims = mocks.Shims return handler } @@ -1139,7 +1178,11 @@ func TestBaseBlueprintHandler_loadFeatures(t *testing.T) { setup := func(t *testing.T) *BaseBlueprintHandler { t.Helper() mocks := setupMocks(t) - handler := NewBlueprintHandler(mocks.Injector) + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } handler.shims = mocks.Shims return handler } @@ -1309,7 +1352,11 @@ func TestBaseBlueprintHandler_processFeatures(t *testing.T) { setup := func(t *testing.T) *BaseBlueprintHandler { t.Helper() mocks := setupMocks(t) - handler := NewBlueprintHandler(mocks.Injector) + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } handler.shims = mocks.Shims return handler } @@ -1811,10 +1858,14 @@ func TestBaseBlueprintHandler_setRepositoryDefaults(t *testing.T) { setup := func(t *testing.T) *BaseBlueprintHandler { t.Helper() mocks := setupMocks(t) - handler := NewBlueprintHandler(mocks.Injector) + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } handler.shims = mocks.Shims - handler.configHandler = mocks.ConfigHandler - handler.shell = mocks.Shell + handler.runtime.ConfigHandler = mocks.ConfigHandler + handler.runtime.Shell = mocks.Shell return handler } @@ -1822,7 +1873,7 @@ func TestBaseBlueprintHandler_setRepositoryDefaults(t *testing.T) { handler := setup(t) handler.blueprint.Repository.Url = "https://github.com/existing/repo" - mockConfigHandler := handler.configHandler.(*config.MockConfigHandler) + mockConfigHandler := handler.runtime.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { if key == "dev" { return false @@ -1843,7 +1894,7 @@ func TestBaseBlueprintHandler_setRepositoryDefaults(t *testing.T) { t.Run("UsesDevelopmentURLWhenDevFlagEnabled", func(t *testing.T) { handler := setup(t) - mockConfigHandler := handler.configHandler.(*config.MockConfigHandler) + mockConfigHandler := handler.runtime.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { if key == "dev" { return true @@ -1857,10 +1908,7 @@ func TestBaseBlueprintHandler_setRepositoryDefaults(t *testing.T) { return "" } - mockShell := handler.shell.(*shell.MockShell) - mockShell.GetProjectRootFunc = func() (string, error) { - return "/path/to/my-project", nil - } + handler.runtime.ProjectRoot = "/path/to/my-project" handler.shims.FilepathBase = func(path string) string { return "my-project" @@ -1880,12 +1928,12 @@ func TestBaseBlueprintHandler_setRepositoryDefaults(t *testing.T) { t.Run("FallsBackToGitRemoteOrigin", func(t *testing.T) { handler := setup(t) - mockConfigHandler := handler.configHandler.(*config.MockConfigHandler) + mockConfigHandler := handler.runtime.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { return false } - mockShell := handler.shell.(*shell.MockShell) + mockShell := handler.runtime.Shell.(*shell.MockShell) mockShell.ExecSilentFunc = func(command string, args ...string) (string, error) { if command == "git" && len(args) == 3 && args[0] == "config" && args[2] == "remote.origin.url" { return "https://github.com/user/repo.git\n", nil @@ -1907,12 +1955,12 @@ func TestBaseBlueprintHandler_setRepositoryDefaults(t *testing.T) { t.Run("PreservesSSHGitRemoteOrigin", func(t *testing.T) { handler := setup(t) - mockConfigHandler := handler.configHandler.(*config.MockConfigHandler) + mockConfigHandler := handler.runtime.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { return false } - mockShell := handler.shell.(*shell.MockShell) + mockShell := handler.runtime.Shell.(*shell.MockShell) mockShell.ExecSilentFunc = func(command string, args ...string) (string, error) { if command == "git" && len(args) == 3 && args[0] == "config" && args[2] == "remote.origin.url" { return "git@github.com:windsorcli/core.git\n", nil @@ -1934,12 +1982,12 @@ func TestBaseBlueprintHandler_setRepositoryDefaults(t *testing.T) { t.Run("HandlesGitRemoteOriginError", func(t *testing.T) { handler := setup(t) - mockConfigHandler := handler.configHandler.(*config.MockConfigHandler) + mockConfigHandler := handler.runtime.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { return false } - mockShell := handler.shell.(*shell.MockShell) + mockShell := handler.runtime.Shell.(*shell.MockShell) mockShell.ExecSilentFunc = func(command string, args ...string) (string, error) { return "", fmt.Errorf("not a git repository") } @@ -1957,12 +2005,12 @@ func TestBaseBlueprintHandler_setRepositoryDefaults(t *testing.T) { t.Run("HandlesEmptyGitRemoteOriginOutput", func(t *testing.T) { handler := setup(t) - mockConfigHandler := handler.configHandler.(*config.MockConfigHandler) + mockConfigHandler := handler.runtime.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { return false } - mockShell := handler.shell.(*shell.MockShell) + mockShell := handler.runtime.Shell.(*shell.MockShell) mockShell.ExecSilentFunc = func(command string, args ...string) (string, error) { return "", nil } @@ -1980,7 +2028,7 @@ func TestBaseBlueprintHandler_setRepositoryDefaults(t *testing.T) { t.Run("DevModeFallsBackToGitWhenDevelopmentURLFails", func(t *testing.T) { handler := setup(t) - mockConfigHandler := handler.configHandler.(*config.MockConfigHandler) + mockConfigHandler := handler.runtime.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { if key == "dev" { return true @@ -1991,7 +2039,7 @@ func TestBaseBlueprintHandler_setRepositoryDefaults(t *testing.T) { return "" } - mockShell := handler.shell.(*shell.MockShell) + mockShell := handler.runtime.Shell.(*shell.MockShell) mockShell.ExecSilentFunc = func(command string, args ...string) (string, error) { if command == "git" { return "https://github.com/fallback/repo.git", nil @@ -2016,7 +2064,11 @@ func TestBaseBlueprintHandler_normalizeGitURL(t *testing.T) { setup := func(t *testing.T) *BaseBlueprintHandler { t.Helper() mocks := setupMocks(t) - handler := NewBlueprintHandler(mocks.Injector) + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } return handler } @@ -2073,17 +2125,21 @@ func TestBaseBlueprintHandler_getDevelopmentRepositoryURL(t *testing.T) { setup := func(t *testing.T) *BaseBlueprintHandler { t.Helper() mocks := setupMocks(t) - handler := NewBlueprintHandler(mocks.Injector) + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } handler.shims = mocks.Shims - handler.configHandler = mocks.ConfigHandler - handler.shell = mocks.Shell + handler.runtime.ConfigHandler = mocks.ConfigHandler + handler.runtime.Shell = mocks.Shell return handler } t.Run("GeneratesCorrectDevelopmentURL", func(t *testing.T) { handler := setup(t) - mockConfigHandler := handler.configHandler.(*config.MockConfigHandler) + mockConfigHandler := handler.runtime.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { if key == "dns.domain" { return "dev.example.com" @@ -2091,10 +2147,7 @@ func TestBaseBlueprintHandler_getDevelopmentRepositoryURL(t *testing.T) { return "" } - mockShell := handler.shell.(*shell.MockShell) - mockShell.GetProjectRootFunc = func() (string, error) { - return "/home/user/projects/my-awesome-project", nil - } + handler.runtime.ProjectRoot = "/home/user/projects/my-awesome-project" handler.shims.FilepathBase = func(path string) string { return "my-awesome-project" @@ -2111,7 +2164,7 @@ func TestBaseBlueprintHandler_getDevelopmentRepositoryURL(t *testing.T) { t.Run("UsesDefaultDomainWhenNotSet", func(t *testing.T) { handler := setup(t) - mockConfigHandler := handler.configHandler.(*config.MockConfigHandler) + mockConfigHandler := handler.runtime.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { if key == "dns.domain" && len(defaultValue) > 0 { return defaultValue[0] @@ -2119,10 +2172,7 @@ func TestBaseBlueprintHandler_getDevelopmentRepositoryURL(t *testing.T) { return "" } - mockShell := handler.shell.(*shell.MockShell) - mockShell.GetProjectRootFunc = func() (string, error) { - return "/home/user/projects/my-project", nil - } + handler.runtime.ProjectRoot = "/home/user/projects/my-project" handler.shims.FilepathBase = func(path string) string { return "my-project" @@ -2139,7 +2189,7 @@ func TestBaseBlueprintHandler_getDevelopmentRepositoryURL(t *testing.T) { t.Run("ReturnsEmptyWhenProjectRootFails", func(t *testing.T) { handler := setup(t) - mockConfigHandler := handler.configHandler.(*config.MockConfigHandler) + mockConfigHandler := handler.runtime.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { if key == "dns.domain" { return "example.com" @@ -2147,10 +2197,7 @@ func TestBaseBlueprintHandler_getDevelopmentRepositoryURL(t *testing.T) { return "" } - mockShell := handler.shell.(*shell.MockShell) - mockShell.GetProjectRootFunc = func() (string, error) { - return "", fmt.Errorf("project root not found") - } + handler.runtime.ProjectRoot = "" url := handler.getDevelopmentRepositoryURL() @@ -2162,7 +2209,7 @@ func TestBaseBlueprintHandler_getDevelopmentRepositoryURL(t *testing.T) { t.Run("ReturnsEmptyWhenFolderNameEmpty", func(t *testing.T) { handler := setup(t) - mockConfigHandler := handler.configHandler.(*config.MockConfigHandler) + mockConfigHandler := handler.runtime.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { if key == "dns.domain" { return "example.com" @@ -2170,10 +2217,7 @@ func TestBaseBlueprintHandler_getDevelopmentRepositoryURL(t *testing.T) { return "" } - mockShell := handler.shell.(*shell.MockShell) - mockShell.GetProjectRootFunc = func() (string, error) { - return "/home/user/projects/", nil - } + handler.runtime.ProjectRoot = "/home/user/projects/" handler.shims.FilepathBase = func(path string) string { return "" @@ -2189,7 +2233,7 @@ func TestBaseBlueprintHandler_getDevelopmentRepositoryURL(t *testing.T) { t.Run("HandlesComplexProjectPaths", func(t *testing.T) { handler := setup(t) - mockConfigHandler := handler.configHandler.(*config.MockConfigHandler) + mockConfigHandler := handler.runtime.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { if key == "dns.domain" { return "staging.example.io" @@ -2197,10 +2241,7 @@ func TestBaseBlueprintHandler_getDevelopmentRepositoryURL(t *testing.T) { return "" } - mockShell := handler.shell.(*shell.MockShell) - mockShell.GetProjectRootFunc = func() (string, error) { - return "/var/www/projects/nested/deep/project-with-dashes", nil - } + handler.runtime.ProjectRoot = "/var/www/projects/nested/deep/project-with-dashes" handler.shims.FilepathBase = func(path string) string { return "project-with-dashes" @@ -2219,15 +2260,15 @@ func TestBlueprintHandler_getSources(t *testing.T) { setup := func(t *testing.T) (*BaseBlueprintHandler, *Mocks) { t.Helper() mocks := setupMocks(t) - handler := NewBlueprintHandler(mocks.Injector) + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } handler.shims = mocks.Shims handler.blueprint = blueprintv1alpha1.Blueprint{ Sources: []blueprintv1alpha1.Source{}, } - err := handler.Initialize() - if err != nil { - t.Fatalf("Failed to initialize handler: %v", err) - } return handler, mocks } @@ -2269,9 +2310,12 @@ func TestBlueprintHandler_getRepository(t *testing.T) { setup := func(t *testing.T) (*BaseBlueprintHandler, *Mocks) { t.Helper() mocks := setupMocks(t) - handler := NewBlueprintHandler(mocks.Injector) + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } handler.shims = mocks.Shims - err := handler.Initialize() if err != nil { t.Fatalf("Failed to initialize handler: %v", err) } @@ -2319,9 +2363,12 @@ func TestBlueprintHandler_loadConfig(t *testing.T) { setup := func(t *testing.T) (*BaseBlueprintHandler, *Mocks) { t.Helper() mocks := setupMocks(t) - handler := NewBlueprintHandler(mocks.Injector) + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } handler.shims = mocks.Shims - err := handler.Initialize() if err != nil { t.Fatalf("Failed to initialize handler: %v", err) } @@ -2331,6 +2378,7 @@ func TestBlueprintHandler_loadConfig(t *testing.T) { t.Run("Success", func(t *testing.T) { // Given a blueprint handler handler, _ := setup(t) + handler.runtime.ConfigRoot = "/test/config" // When loading the config err := handler.loadConfig() @@ -2350,6 +2398,7 @@ func TestBlueprintHandler_loadConfig(t *testing.T) { t.Run("CustomPathOverride", func(t *testing.T) { // Given a blueprint handler handler, _ := setup(t) + handler.runtime.ConfigRoot = "/test/config" // And a mock file system that tracks checked paths var checkedPaths []string @@ -2399,6 +2448,7 @@ func TestBlueprintHandler_loadConfig(t *testing.T) { t.Run("DefaultBlueprint", func(t *testing.T) { // Given a blueprint handler handler, _ := setup(t) + handler.runtime.ConfigRoot = "/test/config" // And a mock file system that returns no existing files handler.shims.Stat = func(name string) (os.FileInfo, error) { @@ -2450,33 +2500,33 @@ func TestBlueprintHandler_loadConfig(t *testing.T) { t.Run("ErrorGettingConfigRoot", func(t *testing.T) { // Given a mock config handler that returns an error mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetConfigRootFunc = func() (string, error) { - return "", fmt.Errorf("error getting config root") - } opts := &SetupOptions{ ConfigHandler: mockConfigHandler, } mocks := setupMocks(t, opts) + mocks.Runtime.ConfigRoot = "" // And a blueprint handler using that config handler - handler := NewBlueprintHandler(mocks.Injector) - handler.shims = mocks.Shims - if err := handler.Initialize(); err != nil { - t.Fatalf("Failed to initialize handler: %v", err) + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) } + handler.shims = mocks.Shims // When loading the config - err := handler.loadConfig() + err = handler.loadConfig() // Then an error should be returned - if err == nil || !strings.Contains(err.Error(), "error getting config root") { - t.Errorf("Expected error containing 'error getting config root', got: %v", err) + if err == nil || !strings.Contains(err.Error(), "config root is empty") { + t.Errorf("Expected error containing 'config root is empty', got: %v", err) } }) t.Run("ErrorReadingYamlFile", func(t *testing.T) { // Given a blueprint handler handler, _ := setup(t) + handler.runtime.ConfigRoot = "/test/config" // And a mock file system that finds yaml file but fails to read it handler.shims.Stat = func(name string) (os.FileInfo, error) { @@ -2504,6 +2554,7 @@ func TestBlueprintHandler_loadConfig(t *testing.T) { t.Run("ErrorLoadingYamlFile", func(t *testing.T) { // Given a blueprint handler handler, _ := setup(t) + handler.runtime.ConfigRoot = "/test/config" // And a mock file system that returns an error for yaml files handler.shims.Stat = func(name string) (os.FileInfo, error) { @@ -2531,6 +2582,7 @@ func TestBlueprintHandler_loadConfig(t *testing.T) { t.Run("ErrorUnmarshallingYamlBlueprint", func(t *testing.T) { // Given a blueprint handler handler, _ := setup(t) + handler.runtime.ConfigRoot = "/test/config" // And a mock file system with a yaml file handler.shims.Stat = func(name string) (os.FileInfo, error) { @@ -2564,6 +2616,7 @@ func TestBlueprintHandler_loadConfig(t *testing.T) { t.Run("EmptyEvaluatedJsonnet", func(t *testing.T) { // Given a blueprint handler with local context handler, mocks := setup(t) + handler.runtime.ConfigRoot = "/test/config" mocks.ConfigHandler.SetContext("local") // And a mock jsonnet VM that returns empty result @@ -2624,13 +2677,8 @@ func TestBlueprintHandler_loadConfig(t *testing.T) { } return false } - mockConfigHandler.GetConfigRootFunc = func() (string, error) { - return "/tmp/test-config", nil - } - - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "/Users/test/project/cli", nil - } + mocks.Runtime.ConfigRoot = "/tmp/test-config" + mocks.Runtime.ProjectRoot = "/Users/test/project/cli" handler.shims.FilepathBase = func(path string) string { if path == "/Users/test/project/cli" { diff --git a/pkg/composer/blueprint/blueprint_handler_public_test.go b/pkg/composer/blueprint/blueprint_handler_public_test.go index c0b00dccf..f8e80bc39 100644 --- a/pkg/composer/blueprint/blueprint_handler_public_test.go +++ b/pkg/composer/blueprint/blueprint_handler_public_test.go @@ -17,6 +17,7 @@ import ( "github.com/windsorcli/cli/pkg/constants" "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/provisioner/kubernetes" + "github.com/windsorcli/cli/pkg/runtime" "github.com/windsorcli/cli/pkg/runtime/config" "github.com/windsorcli/cli/pkg/runtime/shell" ) @@ -215,6 +216,7 @@ type Mocks struct { ConfigHandler config.ConfigHandler Shims *Shims KubernetesManager *kubernetes.MockKubernetesManager + Runtime *runtime.Runtime } type SetupOptions struct { @@ -348,19 +350,11 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { return nil } - mockConfigHandler.GetConfigRootFunc = func() (string, error) { - return tmpDir, nil - } - configHandler = mockConfigHandler } // Create mock shell and kubernetes manager mockShell := shell.NewMockShell() - // Set default GetProjectRoot implementation to use writable temp directory - mockShell.GetProjectRootFunc = func() (string, error) { - return tmpDir, nil - } mockKubernetesManager := kubernetes.NewMockKubernetesManager(nil) // Initialize safe default implementations for all mock functions @@ -440,6 +434,16 @@ contexts: } } + // Create runtime + rt := &runtime.Runtime{ + ProjectRoot: tmpDir, + ConfigRoot: tmpDir, + TemplateRoot: filepath.Join(tmpDir, "contexts", "_template"), + Injector: injector, + ConfigHandler: configHandler, + Shell: mockShell, + } + // Cleanup function t.Cleanup(func() { os.Unsetenv("WINDSOR_PROJECT_ROOT") @@ -454,6 +458,7 @@ contexts: ConfigHandler: configHandler, Shims: shims, KubernetesManager: mockKubernetesManager, + Runtime: rt, } } @@ -463,11 +468,15 @@ contexts: func TestBlueprintHandler_NewBlueprintHandler(t *testing.T) { t.Run("CreatesHandlerWithMocks", func(t *testing.T) { - // Given an injector with mocks + // Given mocks mocks := setupMocks(t) + mockArtifactBuilder := artifact.NewMockArtifact() // When creating a new blueprint handler - handler := NewBlueprintHandler(mocks.Injector) + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } // Then the handler should be properly initialized if handler == nil { @@ -475,120 +484,53 @@ func TestBlueprintHandler_NewBlueprintHandler(t *testing.T) { } // And basic fields should be set - if handler.injector == nil { - t.Error("Expected injector to be set") - } if handler.shims == nil { t.Error("Expected shims to be set") } - // And dependency fields should be nil until Initialize() is called - if handler.configHandler != nil { - t.Error("Expected configHandler to be nil before Initialize()") + // And dependencies should be set + if handler.runtime.ConfigHandler == nil { + t.Error("Expected configHandler to be set") } - if handler.shell != nil { - t.Error("Expected shell to be nil before Initialize()") + if handler.runtime.Shell == nil { + t.Error("Expected shell to be set") } - // kubernetesManager removed - no longer part of blueprint handler - - // When Initialize is called - err := handler.Initialize() - if err != nil { - t.Fatalf("Initialize() failed: %v", err) - } - - // Then dependencies should be injected - if handler.configHandler == nil { - t.Error("Expected configHandler to be set after Initialize()") + if handler.artifactBuilder == nil { + t.Error("Expected artifactBuilder to be set") } - if handler.shell == nil { - t.Error("Expected shell to be set after Initialize()") - } - // kubernetesManager removed - no longer part of blueprint handler }) } -func TestBlueprintHandler_Initialize(t *testing.T) { - setup := func(t *testing.T) (*BaseBlueprintHandler, *Mocks) { - t.Helper() +func TestBlueprintHandler_NewBlueprintHandlerWithError(t *testing.T) { + t.Run("ErrorGettingProjectRoot", func(t *testing.T) { + // Given a runtime with empty ProjectRoot mocks := setupMocks(t) - handler := NewBlueprintHandler(mocks.Injector) - handler.shims = mocks.Shims - return handler, mocks - } - - t.Run("Success", func(t *testing.T) { - // Given a handler - handler, _ := setup(t) + mocks.Runtime.ProjectRoot = "" - // When calling Initialize - err := handler.Initialize() + // When creating a new blueprint handler + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) - // Then no error should be returned + // Then it should succeed (ProjectRoot is set on runtime, not validated in constructor) if err != nil { - t.Errorf("expected nil error, got %v", err) - } - }) - - t.Run("ErrorGettingProjectRoot", func(t *testing.T) { - // Given a handler - handler, mocks := setup(t) - - // And a shell that returns an error - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "", fmt.Errorf("get project root error") - } - - // When calling Initialize - err := handler.Initialize() - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "get project root error") { - t.Errorf("Expected error about get project root error, got: %v", err) - } - }) - - t.Run("ErrorResolvingConfigHandler", func(t *testing.T) { - // Given an injector with no config handler registered - handler, mocks := setup(t) - - mocks.Injector.Register("configHandler", nil) - - // When calling Initialize - err := handler.Initialize() - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") + t.Errorf("Expected no error, got: %v", err) } - }) - - t.Run("ErrorResolvingShell", func(t *testing.T) { - // Given an injector with no shell registered - handler, mocks := setup(t) - mocks.Injector.Register("shell", nil) - - // When calling Initialize - err := handler.Initialize() - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") + if handler == nil { + t.Error("Expected non-nil handler") } }) - } func TestBlueprintHandler_GetTerraformComponents(t *testing.T) { setup := func(t *testing.T) (*BaseBlueprintHandler, *Mocks) { t.Helper() mocks := setupMocks(t) - handler := NewBlueprintHandler(mocks.Injector) + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } handler.shims = mocks.Shims - err := handler.Initialize() if err != nil { t.Fatalf("Failed to initialize handler: %v", err) } @@ -601,7 +543,7 @@ func TestBlueprintHandler_GetTerraformComponents(t *testing.T) { // And a project root directory projectRoot := "/test/project" - handler.projectRoot = projectRoot + handler.runtime.ProjectRoot = projectRoot // And a set of sources sources := []blueprintv1alpha1.Source{ @@ -675,7 +617,7 @@ func TestBlueprintHandler_GetTerraformComponents(t *testing.T) { // And a project root directory projectRoot := "/test/project" - handler.projectRoot = projectRoot + handler.runtime.ProjectRoot = projectRoot // And a set of sources sources := []blueprintv1alpha1.Source{ @@ -725,7 +667,7 @@ func TestBlueprintHandler_GetTerraformComponents(t *testing.T) { // And a project root directory projectRoot := "/test/project" - handler.projectRoot = projectRoot + handler.runtime.ProjectRoot = projectRoot // And an OCI source sources := []blueprintv1alpha1.Source{ @@ -774,9 +716,12 @@ func TestBlueprintHandler_GetLocalTemplateData(t *testing.T) { setup := func(t *testing.T) (BlueprintHandler, *Mocks) { t.Helper() mocks := setupMocks(t) - handler := NewBlueprintHandler(mocks.Injector) + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } handler.shims = mocks.Shims - err := handler.Initialize() if err != nil { t.Fatalf("Failed to initialize handler: %v", err) } @@ -788,9 +733,7 @@ func TestBlueprintHandler_GetLocalTemplateData(t *testing.T) { handler, mocks := setup(t) // Mock shell to return project root - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return filepath.Join("/mock", "project"), nil - } + mocks.Runtime.ProjectRoot = filepath.Join("/mock", "project") // Mock shims to return error for template directory (doesn't exist) if baseHandler, ok := handler.(*BaseBlueprintHandler); ok { @@ -820,13 +763,15 @@ func TestBlueprintHandler_GetLocalTemplateData(t *testing.T) { // Set up mocks first, before initializing the handler mocks := setupMocks(t) - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return projectRoot, nil - } + mocks.Runtime.ProjectRoot = projectRoot + mocks.Runtime.TemplateRoot = templateDir - handler := NewBlueprintHandler(mocks.Injector) + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } handler.shims = mocks.Shims - err := handler.Initialize() if err != nil { t.Fatalf("Failed to initialize handler: %v", err) } @@ -924,7 +869,7 @@ func TestBlueprintHandler_GetLocalTemplateData(t *testing.T) { // Mock shims to return error when reading template directory if baseHandler, ok := handler.(*BaseBlueprintHandler); ok { baseHandler.shims.Stat = func(path string) (os.FileInfo, error) { - if path == baseHandler.templateRoot { + if path == baseHandler.runtime.TemplateRoot { return nil, fmt.Errorf("failed to read template directory") } return nil, os.ErrNotExist @@ -932,7 +877,7 @@ func TestBlueprintHandler_GetLocalTemplateData(t *testing.T) { // Mock ReadDir to return error when trying to read the template directory baseHandler.shims.ReadDir = func(path string) ([]os.DirEntry, error) { - if path == baseHandler.templateRoot { + if path == baseHandler.runtime.TemplateRoot { return nil, fmt.Errorf("failed to read template directory") } return nil, fmt.Errorf("directory not found") @@ -964,13 +909,15 @@ func TestBlueprintHandler_GetLocalTemplateData(t *testing.T) { // Set up mocks first, before initializing the handler mocks := setupMocks(t) - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return projectRoot, nil - } + mocks.Runtime.ProjectRoot = projectRoot + mocks.Runtime.TemplateRoot = templateDir - handler := NewBlueprintHandler(mocks.Injector) + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } handler.shims = mocks.Shims - err := handler.Initialize() if err != nil { t.Fatalf("Failed to initialize handler: %v", err) } @@ -985,7 +932,10 @@ func TestBlueprintHandler_GetLocalTemplateData(t *testing.T) { } baseHandler.shims.ReadDir = func(path string) ([]os.DirEntry, error) { - return nil, fmt.Errorf("failed to read directory") + if path == templateDir { + return nil, fmt.Errorf("failed to read directory") + } + return nil, os.ErrNotExist } // When getting local template data @@ -1012,14 +962,12 @@ func TestBlueprintHandler_GetLocalTemplateData(t *testing.T) { // Ensure the handler uses the mock shell and config handler baseHandler := handler.(*BaseBlueprintHandler) - baseHandler.shell = mocks.Shell - baseHandler.configHandler = mocks.ConfigHandler + baseHandler.runtime.Shell = mocks.Shell + baseHandler.runtime.ConfigHandler = mocks.ConfigHandler // Mock local context values projectRoot := filepath.Join("tmp", "test") - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return projectRoot, nil - } + mocks.Runtime.ProjectRoot = projectRoot baseHandler.shims.Stat = func(path string) (os.FileInfo, error) { // Normalize path separators for cross-platform compatibility normalizedPath := filepath.ToSlash(path) @@ -1095,9 +1043,7 @@ substitutions: mockConfigHandler.GetContextFunc = func() string { return "test-context" } - mockConfigHandler.GetConfigRootFunc = func() (string, error) { - return filepath.Join(projectRoot, "contexts", "test-context"), nil - } + mocks.Runtime.ConfigRoot = filepath.Join(projectRoot, "contexts", "test-context") mockConfigHandler.GetContextValuesFunc = func() (map[string]any, error) { return map[string]any{ "external_domain": "context.test", @@ -1178,24 +1124,20 @@ substitutions: // Ensure the handler uses the mock shell and config handler baseHandler := handler.(*BaseBlueprintHandler) - baseHandler.shell = mocks.Shell - baseHandler.configHandler = mocks.ConfigHandler + baseHandler.runtime.Shell = mocks.Shell + baseHandler.runtime.ConfigHandler = mocks.ConfigHandler projectRoot := filepath.Join("mock", "project") // Mock shell to return project root - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return projectRoot, nil - } + mocks.Runtime.ProjectRoot = projectRoot // Mock config handler to return context if mockConfigHandler, ok := mocks.ConfigHandler.(*config.MockConfigHandler); ok { mockConfigHandler.GetContextFunc = func() string { return "test-context" } - mockConfigHandler.GetConfigRootFunc = func() (string, error) { - return filepath.Join(projectRoot, "contexts", "test-context"), nil - } + mocks.Runtime.ConfigRoot = filepath.Join(projectRoot, "contexts", "test-context") mockConfigHandler.GetContextValuesFunc = func() (map[string]any, error) { return map[string]any{ "external_domain": "context.test", @@ -1353,30 +1295,30 @@ substitutions: // Set up mocks first, before initializing the handler mocks := setupMocks(t) - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return projectRoot, nil - } + mocks.Runtime.ProjectRoot = projectRoot + mocks.Runtime.TemplateRoot = templateDir - handler := NewBlueprintHandler(mocks.Injector) + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } handler.shims = mocks.Shims - err := handler.Initialize() if err != nil { t.Fatalf("Failed to initialize handler: %v", err) } // Ensure the handler uses the mock shell and config handler baseHandler := handler - baseHandler.shell = mocks.Shell - baseHandler.configHandler = mocks.ConfigHandler + baseHandler.runtime.Shell = mocks.Shell + baseHandler.runtime.ConfigHandler = mocks.ConfigHandler // Mock config handler to return context if mockConfigHandler, ok := mocks.ConfigHandler.(*config.MockConfigHandler); ok { mockConfigHandler.GetContextFunc = func() string { return "test-context" } - mockConfigHandler.GetConfigRootFunc = func() (string, error) { - return filepath.Join(projectRoot, "contexts", "test-context"), nil - } + mocks.Runtime.ConfigRoot = filepath.Join(projectRoot, "contexts", "test-context") mockConfigHandler.GetContextValuesFunc = func() (map[string]any, error) { return map[string]any{ "external_domain": "context.test", @@ -1478,13 +1420,15 @@ substitutions: // Set up mocks first, before initializing the handler mocks := setupMocks(t) - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return projectRoot, nil - } + mocks.Runtime.ProjectRoot = projectRoot + mocks.Runtime.TemplateRoot = templateDir - handler := NewBlueprintHandler(mocks.Injector) + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } handler.shims = mocks.Shims - err := handler.Initialize() if err != nil { t.Fatalf("Failed to initialize handler: %v", err) } @@ -1527,6 +1471,7 @@ substitutions: // Then an error should occur if err == nil { t.Error("Expected error when GetContextValues fails") + return } if !strings.Contains(err.Error(), "failed to load context values") { @@ -1539,9 +1484,12 @@ func TestBlueprintHandler_loadData(t *testing.T) { setup := func(t *testing.T) (*BaseBlueprintHandler, *Mocks) { t.Helper() mocks := setupMocks(t) - handler := NewBlueprintHandler(mocks.Injector) + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } handler.shims = mocks.Shims - err := handler.Initialize() if err != nil { t.Fatalf("Failed to initialize handler: %v", err) } @@ -1749,24 +1697,19 @@ func TestBlueprintHandler_Write(t *testing.T) { setup := func(t *testing.T) (*BaseBlueprintHandler, *Mocks) { t.Helper() mocks := setupMocks(t) - handler := NewBlueprintHandler(mocks.Injector) - handler.shims = mocks.Shims - - // Override GetConfigRoot to return the expected path for Write tests - mocks.ConfigHandler.(*config.MockConfigHandler).GetConfigRootFunc = func() (string, error) { - return "mock-config-root", nil - } - - err := handler.Initialize() + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) if err != nil { - t.Fatalf("Failed to initialize handler: %v", err) + t.Fatalf("NewBlueprintHandler() failed: %v", err) } + handler.shims = mocks.Shims return handler, mocks } t.Run("Success", func(t *testing.T) { // Given a blueprint handler with a loaded blueprint handler, mocks := setup(t) + mocks.Runtime.ConfigRoot = "/test/config" // Set up the blueprint with test data handler.blueprint = blueprintv1alpha1.Blueprint{ @@ -1814,7 +1757,7 @@ func TestBlueprintHandler_Write(t *testing.T) { } // And the file should be written to the correct path - expectedPath := filepath.Join("mock-config-root", "blueprint.yaml") + expectedPath := filepath.Join(mocks.Runtime.ConfigRoot, "blueprint.yaml") if writtenPath != expectedPath { t.Errorf("Expected file path %s, got %s", expectedPath, writtenPath) } @@ -1828,6 +1771,7 @@ func TestBlueprintHandler_Write(t *testing.T) { t.Run("WithOverwriteTrue", func(t *testing.T) { // Given a blueprint handler handler, mocks := setup(t) + mocks.Runtime.ConfigRoot = "/test/config" // Set up the blueprint with test data handler.blueprint = blueprintv1alpha1.Blueprint{ @@ -1866,7 +1810,7 @@ func TestBlueprintHandler_Write(t *testing.T) { } // And the file should be written (overwrite) - expectedPath := filepath.Join("mock-config-root", "blueprint.yaml") + expectedPath := filepath.Join(mocks.Runtime.ConfigRoot, "blueprint.yaml") if writtenPath != expectedPath { t.Errorf("Expected file path %s, got %s", expectedPath, writtenPath) } @@ -1902,28 +1846,22 @@ func TestBlueprintHandler_Write(t *testing.T) { }) t.Run("ErrorGettingConfigRoot", func(t *testing.T) { - // Given a blueprint handler with a mock config handler that returns an error - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetConfigRootFunc = func() (string, error) { - return "", fmt.Errorf("config root error") - } - opts := &SetupOptions{ - ConfigHandler: mockConfigHandler, - } - mocks := setupMocks(t, opts) - handler := NewBlueprintHandler(mocks.Injector) - handler.shims = mocks.Shims - err := handler.Initialize() + // Given a blueprint handler with empty ConfigRoot + mocks := setupMocks(t) + mocks.Runtime.ConfigRoot = "" + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) if err != nil { - t.Fatalf("Failed to initialize handler: %v", err) + t.Fatalf("NewBlueprintHandler() failed: %v", err) } + handler.shims = mocks.Shims // When Write is called err = handler.Write() // Then an error should be returned if err == nil { - t.Errorf("Expected error from GetConfigRoot, got nil") + t.Errorf("Expected error from empty ConfigRoot, got nil") } }) @@ -2087,18 +2025,17 @@ func TestBlueprintHandler_LoadBlueprint(t *testing.T) { t.Run("LoadsBlueprintSuccessfullyWithLocalTemplates", func(t *testing.T) { // Given a blueprint handler with local templates mocks := setupMocks(t) - handler := NewBlueprintHandler(mocks.Injector) - if err := handler.Initialize(); err != nil { - t.Fatalf("Initialize() failed: %v", err) + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) } // Set up shims after initialization handler.shims = mocks.Shims // Set up project root and create template root directory tmpDir := t.TempDir() - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return tmpDir, nil - } + mocks.Runtime.ProjectRoot = tmpDir templateRoot := filepath.Join(tmpDir, "contexts", "_template") if err := os.MkdirAll(templateRoot, 0755); err != nil { t.Fatalf("Failed to create template root: %v", err) @@ -2128,7 +2065,7 @@ kustomizations: []` } // When loading blueprint - err := handler.LoadBlueprint() + err = handler.LoadBlueprint() // Then should succeed if err != nil { @@ -2148,12 +2085,13 @@ func TestBaseBlueprintHandler_GetLocalTemplateData(t *testing.T) { projectRoot := os.Getenv("WINDSOR_PROJECT_ROOT") mocks := setupMocks(t) - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return projectRoot, nil - } + mocks.Runtime.ProjectRoot = projectRoot - handler := NewBlueprintHandler(mocks.Injector) - err := handler.Initialize() + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } if err != nil { t.Fatalf("Failed to initialize handler: %v", err) } @@ -2254,12 +2192,13 @@ metadata: projectRoot := os.Getenv("WINDSOR_PROJECT_ROOT") mocks := setupMocks(t) - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return projectRoot, nil - } + mocks.Runtime.ProjectRoot = projectRoot - handler := NewBlueprintHandler(mocks.Injector) - err := handler.Initialize() + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } if err != nil { t.Fatalf("Failed to initialize handler: %v", err) } @@ -2306,12 +2245,13 @@ metadata: projectRoot := os.Getenv("WINDSOR_PROJECT_ROOT") mocks := setupMocks(t) - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return projectRoot, nil - } + mocks.Runtime.ProjectRoot = projectRoot - handler := NewBlueprintHandler(mocks.Injector) - err := handler.Initialize() + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } if err != nil { t.Fatalf("Failed to initialize handler: %v", err) } @@ -2374,12 +2314,13 @@ metadata: projectRoot := os.Getenv("WINDSOR_PROJECT_ROOT") mocks := setupMocks(t) - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return projectRoot, nil - } + mocks.Runtime.ProjectRoot = projectRoot - handler := NewBlueprintHandler(mocks.Injector) - err := handler.Initialize() + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } if err != nil { t.Fatalf("Failed to initialize handler: %v", err) } @@ -2485,12 +2426,13 @@ terraform: projectRoot := os.Getenv("WINDSOR_PROJECT_ROOT") mocks := setupMocks(t) - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return projectRoot, nil - } + mocks.Runtime.ProjectRoot = projectRoot - handler := NewBlueprintHandler(mocks.Injector) - err := handler.Initialize() + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } if err != nil { t.Fatalf("Failed to initialize handler: %v", err) } @@ -2554,12 +2496,13 @@ terraform: projectRoot := os.Getenv("WINDSOR_PROJECT_ROOT") mocks := setupMocks(t) - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return projectRoot, nil - } + mocks.Runtime.ProjectRoot = projectRoot - handler := NewBlueprintHandler(mocks.Injector) - err := handler.Initialize() + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } if err != nil { t.Fatalf("Failed to initialize handler: %v", err) } @@ -2629,12 +2572,13 @@ terraform: projectRoot := os.Getenv("WINDSOR_PROJECT_ROOT") mocks := setupMocks(t) - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return projectRoot, nil - } + mocks.Runtime.ProjectRoot = projectRoot - handler := NewBlueprintHandler(mocks.Injector) - err := handler.Initialize() + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } if err != nil { t.Fatalf("Failed to initialize handler: %v", err) } @@ -2654,12 +2598,13 @@ terraform: projectRoot := os.Getenv("WINDSOR_PROJECT_ROOT") mocks := setupMocks(t) - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return projectRoot, nil - } + mocks.Runtime.ProjectRoot = projectRoot - handler := NewBlueprintHandler(mocks.Injector) - err := handler.Initialize() + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } if err != nil { t.Fatalf("Failed to initialize handler: %v", err) } @@ -2718,12 +2663,13 @@ terraform: projectRoot := os.Getenv("WINDSOR_PROJECT_ROOT") mocks := setupMocks(t) - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return projectRoot, nil - } + mocks.Runtime.ProjectRoot = projectRoot - handler := NewBlueprintHandler(mocks.Injector) - err := handler.Initialize() + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } if err != nil { t.Fatalf("Failed to initialize handler: %v", err) } @@ -2797,12 +2743,13 @@ kustomize: projectRoot := os.Getenv("WINDSOR_PROJECT_ROOT") mocks := setupMocks(t) - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return projectRoot, nil - } + mocks.Runtime.ProjectRoot = projectRoot - handler := NewBlueprintHandler(mocks.Injector) - err := handler.Initialize() + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } if err != nil { t.Fatalf("Failed to initialize handler: %v", err) } @@ -2853,9 +2800,12 @@ func TestBaseBlueprintHandler_Generate(t *testing.T) { setup := func(t *testing.T) (*BaseBlueprintHandler, *Mocks) { t.Helper() mocks := setupMocks(t) - handler := NewBlueprintHandler(mocks.Injector) + mockArtifactBuilder := artifact.NewMockArtifact() + handler, err := NewBlueprintHandler(mocks.Runtime, mockArtifactBuilder) + if err != nil { + t.Fatalf("NewBlueprintHandler() failed: %v", err) + } handler.shims = mocks.Shims - err := handler.Initialize() if err != nil { t.Fatalf("Failed to initialize handler: %v", err) } diff --git a/pkg/composer/blueprint/feature_evaluator.go b/pkg/composer/blueprint/feature_evaluator.go index c5afa7324..cdf4cf903 100644 --- a/pkg/composer/blueprint/feature_evaluator.go +++ b/pkg/composer/blueprint/feature_evaluator.go @@ -9,10 +9,8 @@ import ( "github.com/expr-lang/expr" "github.com/google/go-jsonnet" "github.com/windsorcli/cli/api/v1alpha1" - "github.com/windsorcli/cli/pkg/runtime/config" "github.com/windsorcli/cli/pkg/constants" - "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/runtime/shell" + "github.com/windsorcli/cli/pkg/runtime" ) // FeatureEvaluator provides lightweight expression evaluation for blueprint feature conditions. @@ -26,39 +24,23 @@ import ( // FeatureEvaluator provides lightweight expression evaluation for feature conditions. type FeatureEvaluator struct { - injector di.Injector - configHandler config.ConfigHandler - shell shell.Shell - shims *Shims + runtime *runtime.Runtime + shims *Shims } // ============================================================================= // Constructor // ============================================================================= -// NewFeatureEvaluator creates a new feature evaluator with the provided dependency injector. -func NewFeatureEvaluator(injector di.Injector) *FeatureEvaluator { - return &FeatureEvaluator{ - injector: injector, - shims: NewShims(), +// NewFeatureEvaluator creates a new feature evaluator with the provided dependencies. +// If overrides are provided, any non-nil component in the override FeatureEvaluator will be used instead of creating a default. +func NewFeatureEvaluator(rt *runtime.Runtime) *FeatureEvaluator { + evaluator := &FeatureEvaluator{ + runtime: rt, + shims: NewShims(), } -} -// ============================================================================= -// Initialization -// ============================================================================= - -// Initialize resolves and assigns dependencies from the injector. -func (e *FeatureEvaluator) Initialize() error { - if e.injector != nil { - if configHandler := e.injector.Resolve("configHandler"); configHandler != nil { - e.configHandler = configHandler.(config.ConfigHandler) - } - if shellService := e.injector.Resolve("shell"); shellService != nil { - e.shell = shellService.(shell.Shell) - } - } - return nil + return evaluator } // ============================================================================= @@ -483,16 +465,13 @@ func (e *FeatureEvaluator) buildContextMap(config map[string]any) map[string]any contextMap := make(map[string]any) maps.Copy(contextMap, config) - if e.configHandler != nil { - contextName := e.configHandler.GetContext() + if e.runtime != nil && e.runtime.ConfigHandler != nil { + contextName := e.runtime.ConfigHandler.GetContext() contextMap["name"] = contextName } - if e.shell != nil { - projectRoot, err := e.shell.GetProjectRoot() - if err == nil { - contextMap["projectName"] = e.shims.FilepathBase(projectRoot) - } + if e.runtime != nil && e.runtime.ProjectRoot != "" { + contextMap["projectName"] = e.shims.FilepathBase(e.runtime.ProjectRoot) } return contextMap @@ -644,10 +623,8 @@ func (e *FeatureEvaluator) resolvePath(path string, featurePath string) string { return filepath.Clean(filepath.Join(featureDir, path)) } - if e.shell != nil { - if projectRoot, err := e.shell.GetProjectRoot(); err == nil && projectRoot != "" { - return filepath.Clean(filepath.Join(projectRoot, path)) - } + if e.runtime != nil && e.runtime.ProjectRoot != "" { + return filepath.Clean(filepath.Join(e.runtime.ProjectRoot, path)) } return filepath.Clean(path) diff --git a/pkg/composer/blueprint/feature_evaluator_test.go b/pkg/composer/blueprint/feature_evaluator_test.go index cd77c506c..913139dcc 100644 --- a/pkg/composer/blueprint/feature_evaluator_test.go +++ b/pkg/composer/blueprint/feature_evaluator_test.go @@ -1,14 +1,13 @@ package blueprint import ( - "fmt" "os" "path/filepath" "strings" "testing" + "github.com/windsorcli/cli/pkg/runtime" "github.com/windsorcli/cli/pkg/runtime/config" - "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/runtime/shell" ) @@ -18,8 +17,15 @@ import ( func TestNewFeatureEvaluator(t *testing.T) { t.Run("CreatesNewFeatureEvaluatorSuccessfully", func(t *testing.T) { - injector := di.NewInjector() - evaluator := NewFeatureEvaluator(injector) + configHandler := config.NewMockConfigHandler() + mockShell := shell.NewMockShell() + rt := &runtime.Runtime{ + ProjectRoot: "/test", + ConfigRoot: "/test", + ConfigHandler: configHandler, + Shell: mockShell, + } + evaluator := NewFeatureEvaluator(rt) if evaluator == nil { t.Fatal("Expected evaluator, got nil") } @@ -31,9 +37,15 @@ func TestNewFeatureEvaluator(t *testing.T) { // ============================================================================= func TestFeatureEvaluator_EvaluateExpression(t *testing.T) { - injector := di.NewInjector() - evaluator := NewFeatureEvaluator(injector) - _ = evaluator.Initialize() + configHandler := config.NewMockConfigHandler() + mockShell := shell.NewMockShell() + rt := &runtime.Runtime{ + ProjectRoot: "/test", + ConfigRoot: "/test", + ConfigHandler: configHandler, + Shell: mockShell, + } + evaluator := NewFeatureEvaluator(rt) tests := []struct { name string @@ -174,9 +186,15 @@ func TestFeatureEvaluator_EvaluateExpression(t *testing.T) { } func TestFeatureEvaluator_EvaluateValue(t *testing.T) { - injector := di.NewInjector() - evaluator := NewFeatureEvaluator(injector) - _ = evaluator.Initialize() + configHandler := config.NewMockConfigHandler() + mockShell := shell.NewMockShell() + rt := &runtime.Runtime{ + ProjectRoot: "/test", + ConfigRoot: "/test", + ConfigHandler: configHandler, + Shell: mockShell, + } + evaluator := NewFeatureEvaluator(rt) tests := []struct { name string @@ -292,9 +310,13 @@ func TestFeatureEvaluator_EvaluateValue(t *testing.T) { } func TestFeatureEvaluator_EvaluateDefaults(t *testing.T) { - injector := di.NewInjector() - evaluator := NewFeatureEvaluator(injector) - _ = evaluator.Initialize() + configHandler := config.NewMockConfigHandler() + mockShell := shell.NewMockShell() + rt := &runtime.Runtime{ + ConfigHandler: configHandler, + Shell: mockShell, + } + evaluator := NewFeatureEvaluator(rt) t.Run("EvaluatesLiteralValues", func(t *testing.T) { defaults := map[string]any{ @@ -636,9 +658,13 @@ func TestFeatureEvaluator_EvaluateDefaults(t *testing.T) { // ============================================================================= func TestFeatureEvaluator_extractExpression(t *testing.T) { - injector := di.NewInjector() - evaluator := NewFeatureEvaluator(injector) - _ = evaluator.Initialize() + configHandler := config.NewMockConfigHandler() + mockShell := shell.NewMockShell() + rt := &runtime.Runtime{ + ConfigHandler: configHandler, + Shell: mockShell, + } + evaluator := NewFeatureEvaluator(rt) tests := []struct { name string @@ -698,9 +724,13 @@ func TestFeatureEvaluator_extractExpression(t *testing.T) { } func TestFeatureEvaluator_evaluateDefaultValue(t *testing.T) { - injector := di.NewInjector() - evaluator := NewFeatureEvaluator(injector) - _ = evaluator.Initialize() + configHandler := config.NewMockConfigHandler() + mockShell := shell.NewMockShell() + rt := &runtime.Runtime{ + ConfigHandler: configHandler, + Shell: mockShell, + } + evaluator := NewFeatureEvaluator(rt) t.Run("LiteralStringPassesThrough", func(t *testing.T) { result, err := evaluator.evaluateDefaultValue("talos", map[string]any{}, "features/test.yaml") @@ -886,16 +916,16 @@ func TestFeatureEvaluator_JsonnetFunction(t *testing.T) { t.Fatalf("Failed to create test file: %v", err) } - injector := di.NewInjector() mockConfig := config.NewMockConfigHandler() - mockConfig.GetConfigRootFunc = func() (string, error) { return tmpDir, nil } mockShell := shell.NewMockShell() - mockShell.GetProjectRootFunc = func() (string, error) { return tmpDir, nil } - injector.Register("configHandler", mockConfig) - injector.Register("shell", mockShell) - evaluator := NewFeatureEvaluator(injector) - _ = evaluator.Initialize() + rt := &runtime.Runtime{ + ProjectRoot: tmpDir, + ConfigRoot: tmpDir, + ConfigHandler: mockConfig, + Shell: mockShell, + } + evaluator := NewFeatureEvaluator(rt) config := map[string]any{} featurePath := filepath.Join(expectedDir, "test.yaml") @@ -939,16 +969,16 @@ func TestFeatureEvaluator_JsonnetFunction(t *testing.T) { t.Fatalf("Failed to create test file: %v", err) } - injector := di.NewInjector() mockConfig := config.NewMockConfigHandler() - mockConfig.GetConfigRootFunc = func() (string, error) { return tmpDir, nil } mockShell := shell.NewMockShell() - mockShell.GetProjectRootFunc = func() (string, error) { return tmpDir, nil } - injector.Register("configHandler", mockConfig) - injector.Register("shell", mockShell) - evaluator := NewFeatureEvaluator(injector) - _ = evaluator.Initialize() + rt := &runtime.Runtime{ + ProjectRoot: tmpDir, + ConfigRoot: tmpDir, + ConfigHandler: mockConfig, + Shell: mockShell, + } + evaluator := NewFeatureEvaluator(rt) config := map[string]any{ "namespace": "production", "region": "us-west-2", @@ -993,16 +1023,16 @@ func TestFeatureEvaluator_JsonnetFunction(t *testing.T) { t.Fatalf("Failed to create test file: %v", err) } - injector := di.NewInjector() mockConfig := config.NewMockConfigHandler() - mockConfig.GetConfigRootFunc = func() (string, error) { return tmpDir, nil } mockShell := shell.NewMockShell() - mockShell.GetProjectRootFunc = func() (string, error) { return projectDir, nil } - injector.Register("configHandler", mockConfig) - injector.Register("shell", mockShell) - evaluator := NewFeatureEvaluator(injector) - _ = evaluator.Initialize() + rt := &runtime.Runtime{ + ProjectRoot: projectDir, + ConfigRoot: tmpDir, + ConfigHandler: mockConfig, + Shell: mockShell, + } + evaluator := NewFeatureEvaluator(rt) config := map[string]any{ "environment": "production", } @@ -1043,15 +1073,15 @@ func TestFeatureEvaluator_JsonnetFunction(t *testing.T) { t.Fatalf("Failed to create test file: %v", err) } - injector := di.NewInjector() mockConfig := config.NewMockConfigHandler() mockConfig.GetConfigRootFunc = func() (string, error) { return tmpDir, nil } mockShell := shell.NewMockShell() mockShell.GetProjectRootFunc = func() (string, error) { return tmpDir, nil } - injector.Register("configHandler", mockConfig) - injector.Register("shell", mockShell) - evaluator := NewFeatureEvaluator(injector) - _ = evaluator.Initialize() + rt := &runtime.Runtime{ + ConfigHandler: mockConfig, + Shell: mockShell, + } + evaluator := NewFeatureEvaluator(rt) config := map[string]any{} featurePath := filepath.Join(tmpDir, "contexts", "_template", "features", "test.yaml") @@ -1072,15 +1102,15 @@ func TestFeatureEvaluator_JsonnetFunction(t *testing.T) { t.Run("JsonnetFileNotFound", func(t *testing.T) { tmpDir := t.TempDir() - injector := di.NewInjector() mockConfig := config.NewMockConfigHandler() mockConfig.GetConfigRootFunc = func() (string, error) { return tmpDir, nil } mockShell := shell.NewMockShell() mockShell.GetProjectRootFunc = func() (string, error) { return tmpDir, nil } - injector.Register("configHandler", mockConfig) - injector.Register("shell", mockShell) - evaluator := NewFeatureEvaluator(injector) - _ = evaluator.Initialize() + rt := &runtime.Runtime{ + ConfigHandler: mockConfig, + Shell: mockShell, + } + evaluator := NewFeatureEvaluator(rt) config := map[string]any{} _, err := evaluator.EvaluateValue(`jsonnet("nonexistent.jsonnet")`, config, "features/test.yaml") @@ -1100,15 +1130,15 @@ func TestFeatureEvaluator_JsonnetFunction(t *testing.T) { t.Fatalf("Failed to create test file: %v", err) } - injector := di.NewInjector() mockConfig := config.NewMockConfigHandler() mockConfig.GetConfigRootFunc = func() (string, error) { return tmpDir, nil } mockShell := shell.NewMockShell() mockShell.GetProjectRootFunc = func() (string, error) { return tmpDir, nil } - injector.Register("configHandler", mockConfig) - injector.Register("shell", mockShell) - evaluator := NewFeatureEvaluator(injector) - _ = evaluator.Initialize() + rt := &runtime.Runtime{ + ConfigHandler: mockConfig, + Shell: mockShell, + } + evaluator := NewFeatureEvaluator(rt) config := map[string]any{} _, err := evaluator.EvaluateValue(`jsonnet("invalid.jsonnet")`, config, "features/test.yaml") @@ -1134,16 +1164,16 @@ func TestFeatureEvaluator_FileFunction(t *testing.T) { t.Fatalf("Failed to create test file: %v", err) } - injector := di.NewInjector() mockConfig := config.NewMockConfigHandler() - mockConfig.GetConfigRootFunc = func() (string, error) { return tmpDir, nil } mockShell := shell.NewMockShell() - mockShell.GetProjectRootFunc = func() (string, error) { return tmpDir, nil } - injector.Register("configHandler", mockConfig) - injector.Register("shell", mockShell) - evaluator := NewFeatureEvaluator(injector) - _ = evaluator.Initialize() + rt := &runtime.Runtime{ + ProjectRoot: tmpDir, + ConfigRoot: tmpDir, + ConfigHandler: mockConfig, + Shell: mockShell, + } + evaluator := NewFeatureEvaluator(rt) featurePath := filepath.Join(expectedDir, "test.yaml") result, err := evaluator.EvaluateValue(`file("test.txt")`, map[string]any{}, featurePath) @@ -1178,15 +1208,15 @@ enabled: true` t.Fatalf("Failed to create test file: %v", err) } - injector := di.NewInjector() mockConfig := config.NewMockConfigHandler() mockConfig.GetConfigRootFunc = func() (string, error) { return tmpDir, nil } mockShell := shell.NewMockShell() mockShell.GetProjectRootFunc = func() (string, error) { return tmpDir, nil } - injector.Register("configHandler", mockConfig) - injector.Register("shell", mockShell) - evaluator := NewFeatureEvaluator(injector) - _ = evaluator.Initialize() + rt := &runtime.Runtime{ + ConfigHandler: mockConfig, + Shell: mockShell, + } + evaluator := NewFeatureEvaluator(rt) featurePath := filepath.Join(expectedDir, "test.yaml") result, err := evaluator.EvaluateValue(`file("config.yaml")`, map[string]any{}, featurePath) @@ -1206,15 +1236,15 @@ enabled: true` t.Run("FileNotFound", func(t *testing.T) { tmpDir := t.TempDir() - injector := di.NewInjector() mockConfig := config.NewMockConfigHandler() mockConfig.GetConfigRootFunc = func() (string, error) { return tmpDir, nil } mockShell := shell.NewMockShell() mockShell.GetProjectRootFunc = func() (string, error) { return tmpDir, nil } - injector.Register("configHandler", mockConfig) - injector.Register("shell", mockShell) - evaluator := NewFeatureEvaluator(injector) - _ = evaluator.Initialize() + rt := &runtime.Runtime{ + ConfigHandler: mockConfig, + Shell: mockShell, + } + evaluator := NewFeatureEvaluator(rt) _, err := evaluator.EvaluateValue(`file("nonexistent.txt")`, map[string]any{}, "features/test.yaml") if err == nil { @@ -1244,15 +1274,15 @@ func TestFeatureEvaluator_FileLoadingInDefaults(t *testing.T) { t.Fatalf("Failed to create test file: %v", err) } - injector := di.NewInjector() mockConfig := config.NewMockConfigHandler() mockConfig.GetConfigRootFunc = func() (string, error) { return tmpDir, nil } mockShell := shell.NewMockShell() mockShell.GetProjectRootFunc = func() (string, error) { return tmpDir, nil } - injector.Register("configHandler", mockConfig) - injector.Register("shell", mockShell) - evaluator := NewFeatureEvaluator(injector) - _ = evaluator.Initialize() + rt := &runtime.Runtime{ + ConfigHandler: mockConfig, + Shell: mockShell, + } + evaluator := NewFeatureEvaluator(rt) defaults := map[string]any{ "db_config": `${jsonnet("db-config.jsonnet")}`, @@ -1300,15 +1330,15 @@ func TestFeatureEvaluator_FileLoadingInDefaults(t *testing.T) { t.Fatalf("Failed to create test file: %v", err) } - injector := di.NewInjector() mockConfig := config.NewMockConfigHandler() mockConfig.GetConfigRootFunc = func() (string, error) { return tmpDir, nil } mockShell := shell.NewMockShell() mockShell.GetProjectRootFunc = func() (string, error) { return tmpDir, nil } - injector.Register("configHandler", mockConfig) - injector.Register("shell", mockShell) - evaluator := NewFeatureEvaluator(injector) - _ = evaluator.Initialize() + rt := &runtime.Runtime{ + ConfigHandler: mockConfig, + Shell: mockShell, + } + evaluator := NewFeatureEvaluator(rt) defaults := map[string]any{ "ssh_key": `${file("key.pub")}`, @@ -1340,9 +1370,13 @@ func TestFeatureEvaluator_AbsolutePaths(t *testing.T) { t.Fatalf("Failed to create test file: %v", err) } - injector := di.NewInjector() - evaluator := NewFeatureEvaluator(injector) - _ = evaluator.Initialize() + configHandler := config.NewMockConfigHandler() + mockShell := shell.NewMockShell() + rt := &runtime.Runtime{ + ConfigHandler: configHandler, + Shell: mockShell, + } + evaluator := NewFeatureEvaluator(rt) result, err := evaluator.EvaluateValue(`jsonnet("`+strings.ReplaceAll(jsonnetPath, "\\", "\\\\")+`")`, map[string]any{}, "features/test.yaml") if err != nil { @@ -1370,19 +1404,15 @@ func TestFeatureEvaluator_PathResolution(t *testing.T) { t.Fatalf("Failed to create test file: %v", err) } - injector := di.NewInjector() mockConfig := config.NewMockConfigHandler() - mockConfig.GetConfigRootFunc = func() (string, error) { - return "", fmt.Errorf("no context root") - } mockShell := shell.NewMockShell() - mockShell.GetProjectRootFunc = func() (string, error) { - return tmpDir, nil + rt := &runtime.Runtime{ + ProjectRoot: tmpDir, + ConfigRoot: "", + ConfigHandler: mockConfig, + Shell: mockShell, } - injector.Register("configHandler", mockConfig) - injector.Register("shell", mockShell) - evaluator := NewFeatureEvaluator(injector) - _ = evaluator.Initialize() + evaluator := NewFeatureEvaluator(rt) result, err := evaluator.EvaluateValue(`jsonnet("config.jsonnet")`, map[string]any{}, "") if err != nil { @@ -1409,15 +1439,15 @@ func TestFeatureEvaluator_PathResolution(t *testing.T) { t.Fatalf("Failed to create test file: %v", err) } - injector := di.NewInjector() - mockConfig := config.NewMockConfigHandler() - mockConfig.GetConfigRootFunc = func() (string, error) { - return "", fmt.Errorf("no context root") + configHandler := config.NewMockConfigHandler() + mockShell := shell.NewMockShell() + rt := &runtime.Runtime{ + ProjectRoot: tmpDir, + ConfigRoot: tmpDir, + ConfigHandler: configHandler, + Shell: mockShell, } - injector.Register("configHandler", mockConfig) - - evaluator := NewFeatureEvaluator(injector) - _ = evaluator.Initialize() + evaluator := NewFeatureEvaluator(rt) result, err := evaluator.EvaluateValue(`jsonnet("`+strings.ReplaceAll(testFile, "\\", "\\\\")+`")`, map[string]any{}, "features/test.yaml") if err != nil { @@ -1460,7 +1490,6 @@ func TestFeatureEvaluator_PathResolution(t *testing.T) { t.Fatalf("Failed to create test file: %v", err) } - injector := di.NewInjector() mockConfig := config.NewMockConfigHandler() mockConfig.GetConfigRootFunc = func() (string, error) { return tmpDir, nil @@ -1469,10 +1498,11 @@ func TestFeatureEvaluator_PathResolution(t *testing.T) { mockShell.GetProjectRootFunc = func() (string, error) { return tmpDir, nil } - injector.Register("configHandler", mockConfig) - injector.Register("shell", mockShell) - evaluator := NewFeatureEvaluator(injector) - _ = evaluator.Initialize() + rt := &runtime.Runtime{ + ConfigHandler: mockConfig, + Shell: mockShell, + } + evaluator := NewFeatureEvaluator(rt) featurePath := filepath.Join(featuresDir, "test.yaml") result, err := evaluator.EvaluateValue(`jsonnet("config.jsonnet")`, map[string]any{}, featurePath) @@ -1510,7 +1540,6 @@ func TestFeatureEvaluator_PathResolution(t *testing.T) { t.Fatalf("Failed to create test file: %v", err) } - injector := di.NewInjector() mockConfig := config.NewMockConfigHandler() mockConfig.GetConfigRootFunc = func() (string, error) { return tmpDir, nil @@ -1519,10 +1548,11 @@ func TestFeatureEvaluator_PathResolution(t *testing.T) { mockShell.GetProjectRootFunc = func() (string, error) { return tmpDir, nil } - injector.Register("configHandler", mockConfig) - injector.Register("shell", mockShell) - evaluator := NewFeatureEvaluator(injector) - _ = evaluator.Initialize() + rt := &runtime.Runtime{ + ConfigHandler: mockConfig, + Shell: mockShell, + } + evaluator := NewFeatureEvaluator(rt) featurePath := filepath.Join(expectedDir, "test.yaml") result, err := evaluator.EvaluateValue(`jsonnet("talos-dev.jsonnet").worker_config_patches`, map[string]any{}, featurePath) @@ -1566,7 +1596,6 @@ func TestFeatureEvaluator_PathResolution(t *testing.T) { t.Fatalf("Failed to create test file: %v", err) } - injector := di.NewInjector() mockConfig := config.NewMockConfigHandler() mockConfig.GetConfigRootFunc = func() (string, error) { return tmpDir, nil @@ -1575,10 +1604,11 @@ func TestFeatureEvaluator_PathResolution(t *testing.T) { mockShell.GetProjectRootFunc = func() (string, error) { return tmpDir, nil } - injector.Register("configHandler", mockConfig) - injector.Register("shell", mockShell) - evaluator := NewFeatureEvaluator(injector) - _ = evaluator.Initialize() + rt := &runtime.Runtime{ + ConfigHandler: mockConfig, + Shell: mockShell, + } + evaluator := NewFeatureEvaluator(rt) featurePath := filepath.Join(expectedDir, "test.yaml") result, err := evaluator.EvaluateValue(`jsonnet("nested.jsonnet").config.nested.deeply.value`, map[string]any{}, featurePath) @@ -1612,14 +1642,15 @@ func TestFeatureEvaluator_PathResolution(t *testing.T) { t.Fatalf("Failed to create test file: %v", err) } - injector := di.NewInjector() + mockConfig := config.NewMockConfigHandler() mockShell := shell.NewMockShell() - mockShell.GetProjectRootFunc = func() (string, error) { - return tmpDir, nil + rt := &runtime.Runtime{ + ProjectRoot: tmpDir, + ConfigRoot: tmpDir, + ConfigHandler: mockConfig, + Shell: mockShell, } - injector.Register("shell", mockShell) - evaluator := NewFeatureEvaluator(injector) - _ = evaluator.Initialize() + evaluator := NewFeatureEvaluator(rt) featurePath := filepath.Join(featuresDir, "test.yaml") result, err := evaluator.EvaluateValue(`jsonnet("../configs/talos-dev.jsonnet").worker_config_patches`, map[string]any{}, featurePath) @@ -1651,19 +1682,15 @@ func TestFeatureEvaluator_PathResolution(t *testing.T) { t.Fatalf("Failed to create test file: %v", err) } - injector := di.NewInjector() mockConfig := config.NewMockConfigHandler() - mockConfig.GetConfigRootFunc = func() (string, error) { - return "", fmt.Errorf("no context root") - } mockShell := shell.NewMockShell() - mockShell.GetProjectRootFunc = func() (string, error) { - return "", fmt.Errorf("no project root") + rt := &runtime.Runtime{ + ProjectRoot: "", + ConfigRoot: "", + ConfigHandler: mockConfig, + Shell: mockShell, } - injector.Register("configHandler", mockConfig) - injector.Register("shell", mockShell) - evaluator := NewFeatureEvaluator(injector) - _ = evaluator.Initialize() + evaluator := NewFeatureEvaluator(rt) result, err := evaluator.EvaluateValue(`jsonnet("`+strings.ReplaceAll(testFile, "\\", "\\\\")+`")`, map[string]any{}, "features/test.yaml") if err != nil { diff --git a/pkg/composer/composer.go b/pkg/composer/composer.go index f7d3a802d..73a5924f1 100644 --- a/pkg/composer/composer.go +++ b/pkg/composer/composer.go @@ -10,7 +10,7 @@ import ( "github.com/windsorcli/cli/pkg/composer/artifact" "github.com/windsorcli/cli/pkg/composer/blueprint" "github.com/windsorcli/cli/pkg/composer/terraform" - context "github.com/windsorcli/cli/pkg/runtime" + "github.com/windsorcli/cli/pkg/runtime" ) // The Composer package provides high-level resource management functionality @@ -22,22 +22,14 @@ import ( // Types // ============================================================================= -// ComposerRuntime holds the execution context for resource operations. -// It embeds the base Runtime and includes all resource-specific dependencies. -type ComposerRuntime struct { - context.Runtime - - // Resource-specific dependencies - ArtifactBuilder artifact.Artifact - BlueprintHandler blueprint.BlueprintHandler - TerraformResolver terraform.ModuleResolver -} - // Composer manages the lifecycle of all resource types (artifact, blueprint, terraform). // It provides a unified interface for creating, initializing, and managing these resources // with proper dependency injection and error handling. type Composer struct { - *ComposerRuntime + Runtime *runtime.Runtime + ArtifactBuilder artifact.Artifact + BlueprintHandler blueprint.BlueprintHandler + TerraformResolver terraform.ModuleResolver } // ============================================================================= @@ -45,24 +37,42 @@ type Composer struct { // ============================================================================= // NewComposer creates and initializes a new Composer instance with the provided execution context. -// It sets up all required resource handlers—artifact builder, blueprint handler, and terraform resolver— -// and registers each handler with the dependency injector for use throughout the resource lifecycle. +// It sets up all required resource handlers—artifact builder, blueprint handler, and terraform resolver. +// If overrides are provided, any non-nil component in the override Composer will be used instead of creating a default. // Returns a pointer to the fully initialized Composer struct. -func NewComposer(ctx *ComposerRuntime) *Composer { +func NewComposer(rt *runtime.Runtime, opts ...*Composer) *Composer { composer := &Composer{ - ComposerRuntime: ctx, + Runtime: rt, + } + + if len(opts) > 0 && opts[0] != nil { + overrides := opts[0] + if overrides.ArtifactBuilder != nil { + composer.ArtifactBuilder = overrides.ArtifactBuilder + } + if overrides.BlueprintHandler != nil { + composer.BlueprintHandler = overrides.BlueprintHandler + } + if overrides.TerraformResolver != nil { + composer.TerraformResolver = overrides.TerraformResolver + } } if composer.ArtifactBuilder == nil { - composer.ArtifactBuilder = artifact.NewArtifactBuilder() + composer.ArtifactBuilder = artifact.NewArtifactBuilder(rt) } - composer.Injector.Register("artifactBuilder", composer.ArtifactBuilder) - composer.BlueprintHandler = blueprint.NewBlueprintHandler(composer.Injector) - composer.Injector.Register("blueprintHandler", composer.BlueprintHandler) + if composer.BlueprintHandler == nil { + var err error + composer.BlueprintHandler, err = blueprint.NewBlueprintHandler(rt, composer.ArtifactBuilder) + if err != nil { + return nil + } + } - composer.TerraformResolver = terraform.NewStandardModuleResolver(composer.Injector) - composer.Injector.Register("terraformResolver", composer.TerraformResolver) + if composer.TerraformResolver == nil { + composer.TerraformResolver = terraform.NewStandardModuleResolver(rt, composer.BlueprintHandler) + } return composer } @@ -72,13 +82,9 @@ func NewComposer(ctx *ComposerRuntime) *Composer { // ============================================================================= // Bundle creates a complete artifact bundle from the project's templates, kustomize, and terraform files. -// It initializes the artifact builder and creates a distributable artifact. +// It creates a distributable artifact. // The outputPath specifies where to save the bundle file. Returns the actual output path or an error. func (r *Composer) Bundle(outputPath, tag string) (string, error) { - if err := r.ArtifactBuilder.Initialize(r.Injector); err != nil { - return "", fmt.Errorf("failed to initialize artifact builder: %w", err) - } - actualOutputPath, err := r.ArtifactBuilder.Write(outputPath, tag) if err != nil { return "", fmt.Errorf("failed to create artifact bundle: %w", err) @@ -97,10 +103,6 @@ func (r *Composer) Push(registryURL string) (string, error) { return "", fmt.Errorf("failed to parse registry URL: %w", err) } - if err := r.ArtifactBuilder.Initialize(r.Injector); err != nil { - return "", fmt.Errorf("failed to initialize artifact builder: %w", err) - } - if err := r.ArtifactBuilder.Bundle(); err != nil { return "", fmt.Errorf("failed to bundle artifacts: %w", err) } @@ -128,17 +130,10 @@ func (r *Composer) Generate(overwrite ...bool) error { shouldOverwrite = overwrite[0] } - if err := r.BlueprintHandler.Initialize(); err != nil { - return fmt.Errorf("failed to initialize blueprint handler: %w", err) - } if err := r.BlueprintHandler.LoadBlueprint(); err != nil { return fmt.Errorf("failed to load blueprint data: %w", err) } - if err := r.TerraformResolver.Initialize(); err != nil { - return fmt.Errorf("failed to initialize terraform resolver: %w", err) - } - if err := r.BlueprintHandler.Write(shouldOverwrite); err != nil { return fmt.Errorf("failed to write blueprint files: %w", err) } @@ -151,7 +146,7 @@ func (r *Composer) Generate(overwrite ...bool) error { return fmt.Errorf("failed to generate .gitignore: %w", err) } - if r.ConfigHandler.GetBool("terraform.enabled", false) { + if r.Runtime.ConfigHandler.GetBool("terraform.enabled", false) { if err := r.TerraformResolver.GenerateTfvars(shouldOverwrite); err != nil { return fmt.Errorf("failed to generate terraform files: %w", err) } @@ -164,9 +159,6 @@ func (r *Composer) Generate(overwrite ...bool) error { // It initializes the blueprint handler if needed and loads the blueprint data before generating. // Returns the generated blueprint or an error if initialization or loading fails. func (r *Composer) GenerateBlueprint() (*blueprintv1alpha1.Blueprint, error) { - if err := r.BlueprintHandler.Initialize(); err != nil { - return nil, fmt.Errorf("failed to initialize blueprint handler: %w", err) - } if err := r.BlueprintHandler.LoadBlueprint(); err != nil { return nil, fmt.Errorf("failed to load blueprint data: %w", err) } @@ -194,10 +186,7 @@ func (r *Composer) generateGitignore() error { "contexts/**/.azure/", } - projectRoot, err := r.Shell.GetProjectRoot() - if err != nil { - return fmt.Errorf("failed to get project root: %w", err) - } + projectRoot := r.Runtime.ProjectRoot gitignorePath := filepath.Join(projectRoot, ".gitignore") diff --git a/pkg/composer/composer_test.go b/pkg/composer/composer_test.go index 68b62f237..72808eac6 100644 --- a/pkg/composer/composer_test.go +++ b/pkg/composer/composer_test.go @@ -1,6 +1,8 @@ package composer import ( + "os" + "path/filepath" "testing" "github.com/windsorcli/cli/pkg/di" @@ -17,40 +19,47 @@ import ( func setupComposerMocks(t *testing.T) *Mocks { t.Helper() + // Create temporary directory for test + tmpDir := t.TempDir() + injector := di.NewInjector() configHandler := config.NewMockConfigHandler() + // Set up GetConfigRoot to return temp directory + configHandler.GetConfigRootFunc = func() (string, error) { + return tmpDir, nil + } + shell := shell.NewMockShell() + // Set up GetProjectRoot to return temp directory + shell.GetProjectRootFunc = func() (string, error) { + return tmpDir, nil + } // Create execution context - execCtx := &runtime.Runtime{ + rt := &runtime.Runtime{ ContextName: "test-context", - ProjectRoot: "/test/project", - ConfigRoot: "/test/project/contexts/test-context", - TemplateRoot: "/test/project/contexts/_template", + ProjectRoot: tmpDir, + ConfigRoot: filepath.Join(tmpDir, "contexts", "test-context"), + TemplateRoot: filepath.Join(tmpDir, "contexts", "_template"), Injector: injector, ConfigHandler: configHandler, Shell: shell, } - // Create composer execution context - composerCtx := &ComposerRuntime{ - Runtime: *execCtx, - } - return &Mocks{ - Injector: injector, - ConfigHandler: configHandler, - Shell: shell, - ComposerRuntime: composerCtx, + Injector: injector, + ConfigHandler: configHandler, + Shell: shell, + Runtime: rt, } } // Mocks contains all the mock dependencies for testing type Mocks struct { - Injector di.Injector - ConfigHandler config.ConfigHandler - Shell shell.Shell - ComposerRuntime *ComposerRuntime + Injector di.Injector + ConfigHandler config.ConfigHandler + Shell shell.Shell + Runtime *runtime.Runtime } // ============================================================================= @@ -61,21 +70,21 @@ func TestNewComposer(t *testing.T) { t.Run("CreatesComposerWithDependencies", func(t *testing.T) { mocks := setupComposerMocks(t) - composer := NewComposer(mocks.ComposerRuntime) + composer := NewComposer(mocks.Runtime) if composer == nil { t.Fatal("Expected Composer to be created") } - if composer.Injector != mocks.Injector { + if composer.Runtime.Injector != mocks.Injector { t.Error("Expected injector to be set") } - if composer.Shell != mocks.Shell { + if composer.Runtime.Shell != mocks.Shell { t.Error("Expected shell to be set") } - if composer.ConfigHandler != mocks.ConfigHandler { + if composer.Runtime.ConfigHandler != mocks.ConfigHandler { t.Error("Expected config handler to be set") } @@ -97,21 +106,21 @@ func TestCreateComposer(t *testing.T) { t.Run("CreatesComposerWithDependencies", func(t *testing.T) { mocks := setupComposerMocks(t) - composer := NewComposer(mocks.ComposerRuntime) + composer := NewComposer(mocks.Runtime) if composer == nil { t.Fatal("Expected Composer to be created") } - if composer.Injector != mocks.Injector { + if composer.Runtime.Injector != mocks.Injector { t.Error("Expected injector to be set") } - if composer.ConfigHandler != mocks.ConfigHandler { + if composer.Runtime.ConfigHandler != mocks.ConfigHandler { t.Error("Expected config handler to be set") } - if composer.Shell != mocks.Shell { + if composer.Runtime.Shell != mocks.Shell { t.Error("Expected shell to be set") } }) @@ -124,7 +133,7 @@ func TestCreateComposer(t *testing.T) { func TestComposer_Push(t *testing.T) { t.Run("HandlesPushSuccessfully", func(t *testing.T) { mocks := setupComposerMocks(t) - composer := NewComposer(mocks.ComposerRuntime) + composer := NewComposer(mocks.Runtime) // This test would need proper mocking of the artifact builder // For now, we'll just test that the method exists and handles errors @@ -139,62 +148,70 @@ func TestComposer_Push(t *testing.T) { func TestComposer_Generate(t *testing.T) { t.Run("HandlesGenerateSuccessfully", func(t *testing.T) { mocks := setupComposerMocks(t) - composer := NewComposer(mocks.ComposerRuntime) + // Create empty TemplateRoot directory so LoadBlueprint tries to load from template + if err := os.MkdirAll(mocks.Runtime.TemplateRoot, 0755); err != nil { + t.Fatalf("Failed to create TemplateRoot: %v", err) + } + composer := NewComposer(mocks.Runtime) + if composer == nil { + t.Fatal("Expected non-nil composer") + } - // This test would need proper mocking of the blueprint handler and terraform resolver - // For now, we'll just test that the method exists and handles errors + // Generate will fail because blueprint.yaml doesn't exist in ConfigRoot and template is empty err := composer.Generate() - // We expect an error here because we don't have proper mocks set up + // We expect an error because blueprint.yaml doesn't exist in the test setup if err == nil { - t.Error("Expected error due to missing mocks, but got nil") + t.Error("Expected error due to missing blueprint.yaml, but got nil") } }) t.Run("HandlesGenerateWithOverwrite", func(t *testing.T) { mocks := setupComposerMocks(t) - composer := NewComposer(mocks.ComposerRuntime) + // Create empty TemplateRoot directory so LoadBlueprint tries to load from template + if err := os.MkdirAll(mocks.Runtime.TemplateRoot, 0755); err != nil { + t.Fatalf("Failed to create TemplateRoot: %v", err) + } + composer := NewComposer(mocks.Runtime) + if composer == nil { + t.Fatal("Expected non-nil composer") + } - // This test would need proper mocking of the blueprint handler and terraform resolver - // For now, we'll just test that the method exists and handles errors + // Generate will fail because blueprint.yaml doesn't exist in ConfigRoot and template is empty err := composer.Generate(true) - // We expect an error here because we don't have proper mocks set up + // We expect an error because blueprint.yaml doesn't exist in the test setup if err == nil { - t.Error("Expected error due to missing mocks, but got nil") + t.Error("Expected error due to missing blueprint.yaml, but got nil") } }) } // ============================================================================= -// Test ComposerRuntime +// Test Runtime // ============================================================================= -func TestComposerRuntime(t *testing.T) { - t.Run("CreatesComposerRuntime", func(t *testing.T) { - execCtx := &runtime.Runtime{ +func TestRuntime(t *testing.T) { + t.Run("CreatesRuntime", func(t *testing.T) { + rt := &runtime.Runtime{ ContextName: "test-context", ProjectRoot: "/test/project", ConfigRoot: "/test/project/contexts/test-context", TemplateRoot: "/test/project/contexts/_template", } - composerCtx := &ComposerRuntime{ - Runtime: *execCtx, - } - - if composerCtx.ContextName != "test-context" { - t.Errorf("Expected context name 'test-context', got: %s", composerCtx.ContextName) + if rt.ContextName != "test-context" { + t.Errorf("Expected context name 'test-context', got: %s", rt.ContextName) } - if composerCtx.ProjectRoot != "/test/project" { - t.Errorf("Expected project root '/test/project', got: %s", composerCtx.ProjectRoot) + if rt.ProjectRoot != "/test/project" { + t.Errorf("Expected project root '/test/project', got: %s", rt.ProjectRoot) } - if composerCtx.ConfigRoot != "/test/project/contexts/test-context" { - t.Errorf("Expected config root '/test/project/contexts/test-context', got: %s", composerCtx.ConfigRoot) + if rt.ConfigRoot != "/test/project/contexts/test-context" { + t.Errorf("Expected config root '/test/project/contexts/test-context', got: %s", rt.ConfigRoot) } - if composerCtx.TemplateRoot != "/test/project/contexts/_template" { - t.Errorf("Expected template root '/test/project/contexts/_template', got: %s", composerCtx.TemplateRoot) + if rt.TemplateRoot != "/test/project/contexts/_template" { + t.Errorf("Expected template root '/test/project/contexts/_template', got: %s", rt.TemplateRoot) } }) } diff --git a/pkg/composer/terraform/module_resolver.go b/pkg/composer/terraform/module_resolver.go index 14dd90c0e..8d94ee611 100644 --- a/pkg/composer/terraform/module_resolver.go +++ b/pkg/composer/terraform/module_resolver.go @@ -12,9 +12,7 @@ import ( "github.com/hashicorp/hcl/v2/hclwrite" blueprintv1alpha1 "github.com/windsorcli/cli/api/v1alpha1" "github.com/windsorcli/cli/pkg/composer/blueprint" - "github.com/windsorcli/cli/pkg/runtime/config" - "github.com/windsorcli/cli/pkg/runtime/shell" - "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/runtime" "github.com/zclconf/go-cty/cty" ) @@ -29,7 +27,6 @@ import ( // ModuleResolver processes terraform module sources and generates appropriate module configurations type ModuleResolver interface { - Initialize() error ProcessModules() error GenerateTfvars(overwrite bool) error } @@ -41,9 +38,7 @@ type ModuleResolver interface { // BaseModuleResolver provides common functionality for all module resolvers type BaseModuleResolver struct { shims *Shims - injector di.Injector - shell shell.Shell - configHandler config.ConfigHandler + runtime *runtime.Runtime blueprintHandler blueprint.BlueprintHandler reset bool } @@ -52,44 +47,23 @@ type BaseModuleResolver struct { // Constructor // ============================================================================= -// NewBaseModuleResolver creates a new base module resolver -func NewBaseModuleResolver(injector di.Injector) *BaseModuleResolver { - return &BaseModuleResolver{ - shims: NewShims(), - injector: injector, - } -} - -// ============================================================================= -// Public Methods -// ============================================================================= - -// Initialize sets up the base module resolver -// Initialize sets up the required handlers and shell for the BaseModuleResolver using the dependency injector. -// It resolves and assigns the shell, config handler, and blueprint handler. -// Returns an error if any required dependency cannot be resolved. -func (h *BaseModuleResolver) Initialize() error { - var ok bool - - shellInterface := h.injector.Resolve("shell") - h.shell, ok = shellInterface.(shell.Shell) - if !ok { - return fmt.Errorf("failed to resolve shell") - } - - configHandlerInterface := h.injector.Resolve("configHandler") - h.configHandler, ok = configHandlerInterface.(config.ConfigHandler) - if !ok { - return fmt.Errorf("failed to resolve config handler") +// NewBaseModuleResolver creates a new base module resolver with the provided dependencies. +// If overrides are provided, any non-nil component in the override BaseModuleResolver will be used instead of creating a default. +func NewBaseModuleResolver(rt *runtime.Runtime, blueprintHandler blueprint.BlueprintHandler, opts ...*BaseModuleResolver) *BaseModuleResolver { + resolver := &BaseModuleResolver{ + shims: NewShims(), + runtime: rt, + blueprintHandler: blueprintHandler, } - blueprintHandlerInterface := h.injector.Resolve("blueprintHandler") - h.blueprintHandler, ok = blueprintHandlerInterface.(blueprint.BlueprintHandler) - if !ok { - return fmt.Errorf("failed to resolve blueprint handler") + if len(opts) > 0 && opts[0] != nil { + overrides := opts[0] + if overrides.shims != nil { + resolver.shims = overrides.shims + } } - return nil + return resolver } // GenerateTfvars creates Terraform configuration files, including tfvars files, for all blueprint components. @@ -102,15 +76,8 @@ func (h *BaseModuleResolver) Initialize() error { func (h *BaseModuleResolver) GenerateTfvars(overwrite bool) error { h.reset = overwrite - contextPath, err := h.configHandler.GetConfigRoot() - if err != nil { - return fmt.Errorf("failed to get config root: %w", err) - } - - projectRoot, err := h.shell.GetProjectRoot() - if err != nil { - return fmt.Errorf("failed to get project root: %w", err) - } + contextPath := h.runtime.ConfigRoot + projectRoot := h.runtime.ProjectRoot components := h.blueprintHandler.GetTerraformComponents() diff --git a/pkg/composer/terraform/module_resolver_test.go b/pkg/composer/terraform/module_resolver_test.go index 96d6ff914..96b1eb7bd 100644 --- a/pkg/composer/terraform/module_resolver_test.go +++ b/pkg/composer/terraform/module_resolver_test.go @@ -14,9 +14,10 @@ import ( blueprintv1alpha1 "github.com/windsorcli/cli/api/v1alpha1" "github.com/windsorcli/cli/pkg/composer/artifact" "github.com/windsorcli/cli/pkg/composer/blueprint" + "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/runtime" "github.com/windsorcli/cli/pkg/runtime/config" "github.com/windsorcli/cli/pkg/runtime/shell" - "github.com/windsorcli/cli/pkg/di" ) // ============================================================================= @@ -29,6 +30,7 @@ type Mocks struct { ConfigHandler config.ConfigHandler BlueprintHandler *blueprint.MockBlueprintHandler Shims *Shims + Runtime *runtime.Runtime } type SetupOptions struct { @@ -135,12 +137,20 @@ contexts: shims := setupShims(t) + // Create runtime + rt := &runtime.Runtime{ + Injector: injector, + ConfigHandler: configHandler, + Shell: mockShell, + } + return &Mocks{ Injector: injector, Shell: mockShell, ConfigHandler: configHandler, BlueprintHandler: mockBlueprintHandler, Shims: shims, + Runtime: rt, } } @@ -237,78 +247,32 @@ func setupShims(t *testing.T) *Shims { // ============================================================================= func TestBaseModuleResolver_NewBaseModuleResolver(t *testing.T) { - t.Run("CreatesResolverWithInjector", func(t *testing.T) { - // Given an injector + t.Run("CreatesResolverWithDependencies", func(t *testing.T) { + // Given mocks mocks := setupMocks(t) // When creating a new base module resolver - resolver := NewBaseModuleResolver(mocks.Injector) + resolver := NewBaseModuleResolver(mocks.Runtime, mocks.BlueprintHandler) // Then the resolver should be properly initialized if resolver == nil { t.Fatal("Expected non-nil resolver") } - // And basic fields should be set - if resolver.injector == nil { - t.Error("Expected injector to be set") - } - // And shims should be initialized if resolver.shims == nil { t.Error("Expected shims to be initialized") } - // And dependency fields should be nil until Initialize() is called - if resolver.shell != nil { - t.Error("Expected shell to be nil before Initialize()") + // And dependency fields should be set + if resolver.runtime.Shell == nil { + t.Error("Expected shell to be set") } - if resolver.blueprintHandler != nil { - t.Error("Expected blueprintHandler to be nil before Initialize()") + if resolver.blueprintHandler == nil { + t.Error("Expected blueprintHandler to be set") } - }) -} - -func TestBaseModuleResolver_Initialize(t *testing.T) { - setup := func(t *testing.T) (*BaseModuleResolver, *Mocks) { - t.Helper() - mocks := setupMocks(t) - resolver := NewBaseModuleResolver(mocks.Injector) - return resolver, mocks - } - - t.Run("Success", func(t *testing.T) { - // Given a resolver - resolver, _ := setup(t) - - // When calling Initialize - err := resolver.Initialize() - - // Then no error should be returned - if err != nil { - t.Errorf("Expected nil error, got %v", err) - } - - // And dependencies should be injected - if resolver.shell == nil { - t.Error("Expected shell to be set after Initialize()") - } - }) - - t.Run("HandlesMissingShellDependency", func(t *testing.T) { - // Given a resolver with injector that doesn't have shell - injector := di.NewInjector() - resolver := NewBaseModuleResolver(injector) - - // When calling Initialize - err := resolver.Initialize() - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to resolve shell") { - t.Errorf("Expected error about shell resolution, got: %v", err) + if resolver.runtime.ConfigHandler == nil { + t.Error("Expected configHandler to be set") } }) } @@ -317,11 +281,7 @@ func TestBaseModuleResolver_writeShimMainTf(t *testing.T) { setup := func(t *testing.T) (*BaseModuleResolver, *Mocks) { t.Helper() mocks := setupMocks(t) - resolver := NewBaseModuleResolver(mocks.Injector) - err := resolver.Initialize() - if err != nil { - t.Fatalf("Failed to initialize resolver: %v", err) - } + resolver := NewBaseModuleResolver(mocks.Runtime, mocks.BlueprintHandler) return resolver, mocks } @@ -403,11 +363,7 @@ func TestBaseModuleResolver_writeShimVariablesTf(t *testing.T) { setup := func(t *testing.T) (*BaseModuleResolver, *Mocks) { t.Helper() mocks := setupMocks(t) - resolver := NewBaseModuleResolver(mocks.Injector) - err := resolver.Initialize() - if err != nil { - t.Fatalf("Failed to initialize resolver: %v", err) - } + resolver := NewBaseModuleResolver(mocks.Runtime, mocks.BlueprintHandler) return resolver, mocks } @@ -561,11 +517,7 @@ func TestBaseModuleResolver_writeShimOutputsTf(t *testing.T) { setup := func(t *testing.T) (*BaseModuleResolver, *Mocks) { t.Helper() mocks := setupMocks(t) - resolver := NewBaseModuleResolver(mocks.Injector) - err := resolver.Initialize() - if err != nil { - t.Fatalf("Failed to initialize resolver: %v", err) - } + resolver := NewBaseModuleResolver(mocks.Runtime, mocks.BlueprintHandler) return resolver, mocks } @@ -874,11 +826,7 @@ func TestShims_NewShims(t *testing.T) { func TestBaseModuleResolver_GenerateTfvars(t *testing.T) { t.Run("Success", func(t *testing.T) { mocks := setupMocks(t) - resolver := NewBaseModuleResolver(mocks.Injector) - resolver.blueprintHandler = mocks.BlueprintHandler - if err := resolver.Initialize(); err != nil { - t.Fatalf("Failed to initialize: %v", err) - } + resolver := NewBaseModuleResolver(mocks.Runtime, mocks.BlueprintHandler) projectRoot, _ := mocks.Shell.GetProjectRootFunc() variablesDir := filepath.Join(projectRoot, ".windsor", ".tf_modules", "test-module") @@ -896,4 +844,3 @@ func TestBaseModuleResolver_GenerateTfvars(t *testing.T) { } }) } - diff --git a/pkg/composer/terraform/oci_module_resolver.go b/pkg/composer/terraform/oci_module_resolver.go index 63b7a765f..0cfd5e567 100644 --- a/pkg/composer/terraform/oci_module_resolver.go +++ b/pkg/composer/terraform/oci_module_resolver.go @@ -9,9 +9,9 @@ import ( "github.com/briandowns/spinner" blueprintv1alpha1 "github.com/windsorcli/cli/api/v1alpha1" - "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/composer/artifact" "github.com/windsorcli/cli/pkg/composer/blueprint" + "github.com/windsorcli/cli/pkg/runtime" ) // The OCIModuleResolver is a terraform module resolver for OCI artifact sources. @@ -33,38 +33,18 @@ type OCIModuleResolver struct { // Constructor // ============================================================================= -// NewOCIModuleResolver creates a new OCI module resolver -func NewOCIModuleResolver(injector di.Injector) *OCIModuleResolver { +// NewOCIModuleResolver creates a new OCI module resolver with the provided dependencies. +func NewOCIModuleResolver(rt *runtime.Runtime, blueprintHandler blueprint.BlueprintHandler, artifactBuilder artifact.Artifact) *OCIModuleResolver { return &OCIModuleResolver{ - BaseModuleResolver: NewBaseModuleResolver(injector), + BaseModuleResolver: NewBaseModuleResolver(rt, blueprintHandler), + artifactBuilder: artifactBuilder, } } -// Initialize sets up the OCI module resolver with required dependencies -func (h *OCIModuleResolver) Initialize() error { - if err := h.BaseModuleResolver.Initialize(); err != nil { - return err - } - - artifactBuilderInterface := h.injector.Resolve("artifactBuilder") - var ok bool - h.artifactBuilder, ok = artifactBuilderInterface.(artifact.Artifact) - if !ok { - return fmt.Errorf("failed to resolve artifact builder") - } - - blueprintHandlerInterface := h.injector.Resolve("blueprintHandler") - h.blueprintHandler, ok = blueprintHandlerInterface.(blueprint.BlueprintHandler) - if !ok { - return fmt.Errorf("failed to resolve blueprint handler") - } - - return nil -} - // ============================================================================= // Public Methods // ============================================================================= + // shouldHandle determines if this resolver should handle the given source by checking // if the source is an OCI artifact URL. Returns true only for sources that begin with // the "oci://" protocol prefix, indicating they are OCI registry artifacts. @@ -195,9 +175,9 @@ func (h *OCIModuleResolver) extractOCIModule(resolvedSource, componentPath strin cacheKey := fmt.Sprintf("%s/%s:%s", registry, repository, tag) - projectRoot, err := h.shell.GetProjectRoot() - if err != nil { - return "", fmt.Errorf("failed to get project root: %w", err) + projectRoot := h.runtime.ProjectRoot + if projectRoot == "" { + return "", fmt.Errorf("failed to get project root: project root is empty") } extractionKey := fmt.Sprintf("%s-%s-%s", registry, repository, tag) @@ -223,9 +203,9 @@ func (h *OCIModuleResolver) extractOCIModule(resolvedSource, componentPath strin // under the project root, preserving file permissions and handling executable scripts. Returns an error if // extraction fails at any step, including directory creation, file writing, or permission setting. func (h *OCIModuleResolver) extractModuleFromArtifact(artifactData []byte, modulePath, extractionKey string) error { - projectRoot, err := h.shell.GetProjectRoot() - if err != nil { - return fmt.Errorf("failed to get project root: %w", err) + projectRoot := h.runtime.ProjectRoot + if projectRoot == "" { + return fmt.Errorf("failed to get project root: project root is empty") } reader := h.shims.NewBytesReader(artifactData) diff --git a/pkg/composer/terraform/oci_module_resolver_test.go b/pkg/composer/terraform/oci_module_resolver_test.go index adbef0573..4a8296a00 100644 --- a/pkg/composer/terraform/oci_module_resolver_test.go +++ b/pkg/composer/terraform/oci_module_resolver_test.go @@ -9,9 +9,7 @@ import ( "testing" blueprintv1alpha1 "github.com/windsorcli/cli/api/v1alpha1" - "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/composer/artifact" - "github.com/windsorcli/cli/pkg/runtime/shell" ) // MockTarReader provides a mock implementation for TarReader interface @@ -40,11 +38,12 @@ func (m *MockTarReader) Read(p []byte) (int, error) { func TestOCIModuleResolver_NewOCIModuleResolver(t *testing.T) { t.Run("Success", func(t *testing.T) { - // Given a dependency injector - injector := di.NewInjector() + // Given dependencies + mocks := setupMocks(t, &SetupOptions{}) + mockArtifactBuilder := artifact.NewMockArtifact() // When creating a new OCI module resolver - resolver := NewOCIModuleResolver(injector) + resolver := NewOCIModuleResolver(mocks.Runtime, mocks.BlueprintHandler, mockArtifactBuilder) // Then it should be created successfully if resolver == nil { @@ -53,87 +52,29 @@ func TestOCIModuleResolver_NewOCIModuleResolver(t *testing.T) { if resolver.BaseModuleResolver == nil { t.Error("Expected BaseModuleResolver to be set") } + if resolver.artifactBuilder == nil { + t.Error("Expected artifactBuilder to be set") + } }) } -func TestOCIModuleResolver_Initialize(t *testing.T) { +func TestOCIModuleResolver_NewOCIModuleResolverWithDependencies(t *testing.T) { t.Run("Success", func(t *testing.T) { // Given a resolver with all required dependencies mocks := setupMocks(t, &SetupOptions{}) - resolver := NewOCIModuleResolver(mocks.Injector) - resolver.shims = mocks.Shims - - // When calling Initialize - err := resolver.Initialize() + mockArtifactBuilder := artifact.NewMockArtifact() + resolver := NewOCIModuleResolver(mocks.Runtime, mocks.BlueprintHandler, mockArtifactBuilder) + resolver.BaseModuleResolver.shims = mocks.Shims - // Then no error should be returned and dependencies should be injected - if err != nil { - t.Errorf("Expected nil error, got %v", err) - } - if resolver.shell == nil { - t.Error("Expected shell to be set after Initialize()") + // Then dependencies should be set + if resolver.BaseModuleResolver.runtime.Shell == nil { + t.Error("Expected shell to be set") } if resolver.artifactBuilder == nil { - t.Error("Expected artifactBuilder to be set after Initialize()") - } - if resolver.blueprintHandler == nil { - t.Error("Expected blueprintHandler to be set after Initialize()") - } - }) - - t.Run("HandlesInitializationErrors", func(t *testing.T) { - // Given a resolver with missing dependencies - injector := di.NewInjector() - resolver := NewOCIModuleResolver(injector) - - // When calling Initialize - err := resolver.Initialize() - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to resolve") { - t.Errorf("Expected dependency resolution error, got: %v", err) + t.Error("Expected artifactBuilder to be set") } - }) - - t.Run("HandlesArtifactBuilderTypeAssertionError", func(t *testing.T) { - // Given a resolver with wrong artifact builder type - injector := di.NewInjector() - injector.Register("shell", "invalid-shell-type") - injector.Register("artifactBuilder", "invalid-artifact-builder-type") - injector.Register("blueprintHandler", "invalid-blueprint-handler-type") - resolver := NewOCIModuleResolver(injector) - - // When calling Initialize - err := resolver.Initialize() - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to resolve") { - t.Errorf("Expected artifact builder type assertion error, got: %v", err) - } - }) - - t.Run("HandlesBlueprintHandlerTypeAssertionError", func(t *testing.T) { - mocks := setupMocks(t, &SetupOptions{}) - injector := di.NewInjector() - injector.Register("shell", mocks.Shell) - injector.Register("configHandler", mocks.ConfigHandler) - injector.Register("artifactBuilder", artifact.NewMockArtifact()) - injector.Register("blueprintHandler", "invalid-blueprint-handler-type") - resolver := NewOCIModuleResolver(injector) - - err := resolver.Initialize() - - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to resolve blueprint handler") { - t.Errorf("Expected blueprint handler type assertion error, got: %v", err) + if resolver.BaseModuleResolver.blueprintHandler == nil { + t.Error("Expected blueprintHandler to be set") } }) } @@ -142,12 +83,9 @@ func TestOCIModuleResolver_shouldHandle(t *testing.T) { t.Run("HandlesOCIAndRejectsNonOCI", func(t *testing.T) { // Given a resolver mocks := setupMocks(t, &SetupOptions{}) - resolver := NewOCIModuleResolver(mocks.Injector) - err := resolver.Initialize() - if err != nil { - t.Fatalf("Failed to initialize resolver: %v", err) - } - resolver.shims = mocks.Shims + mockArtifactBuilder := artifact.NewMockArtifact() + resolver := NewOCIModuleResolver(mocks.Runtime, mocks.BlueprintHandler, mockArtifactBuilder) + resolver.BaseModuleResolver.shims = mocks.Shims // When checking various source types testCases := []struct { @@ -175,12 +113,9 @@ func TestOCIModuleResolver_ProcessModules(t *testing.T) { setup := func(t *testing.T) (*OCIModuleResolver, *Mocks) { t.Helper() mocks := setupMocks(t, &SetupOptions{}) - resolver := NewOCIModuleResolver(mocks.Injector) - err := resolver.Initialize() - if err != nil { - t.Fatalf("Failed to initialize resolver: %v", err) - } - resolver.shims = mocks.Shims + mockArtifactBuilder := artifact.NewMockArtifact() + resolver := NewOCIModuleResolver(mocks.Runtime, mocks.BlueprintHandler, mockArtifactBuilder) + resolver.BaseModuleResolver.shims = mocks.Shims return resolver, mocks } @@ -197,15 +132,33 @@ func TestOCIModuleResolver_ProcessModules(t *testing.T) { } } + // Set up artifact builder to return mock data with correct cache key + mockArtifactBuilder := artifact.NewMockArtifact() + mockArtifactBuilder.PullFunc = func(refs []string) (map[string][]byte, error) { + artifacts := make(map[string][]byte) + for _, ref := range refs { + // Cache key format is registry/repository:tag (without oci:// prefix) + if strings.HasPrefix(ref, "oci://") { + cacheKey := strings.TrimPrefix(ref, "oci://") + artifacts[cacheKey] = []byte("mock artifact data") + } else { + artifacts[ref] = []byte("mock artifact data") + } + } + return artifacts, nil + } + resolver.artifactBuilder = mockArtifactBuilder + // Mock tar reader for successful extraction mockTarReader := &MockTarReader{ NextFunc: func() (*tar.Header, error) { return nil, io.EOF }, } - resolver.shims.NewTarReader = func(r io.Reader) TarReader { + resolver.BaseModuleResolver.shims.NewTarReader = func(r io.Reader) TarReader { return mockTarReader } + resolver.BaseModuleResolver.runtime.ProjectRoot = "/test/project" // When processing modules err := resolver.ProcessModules() @@ -307,7 +260,7 @@ func TestOCIModuleResolver_ProcessModules(t *testing.T) { } // Mock MkdirAll to fail for component processing - resolver.shims.MkdirAll = func(path string, perm os.FileMode) error { + resolver.BaseModuleResolver.shims.MkdirAll = func(path string, perm os.FileMode) error { return errors.New("mkdir error") } @@ -328,12 +281,9 @@ func TestOCIModuleResolver_parseOCIRef(t *testing.T) { t.Run("ParsesValidReferences", func(t *testing.T) { // Given a resolver mocks := setupMocks(t, &SetupOptions{}) - resolver := NewOCIModuleResolver(mocks.Injector) - err := resolver.Initialize() - if err != nil { - t.Fatalf("Failed to initialize resolver: %v", err) - } - resolver.shims = mocks.Shims + mockArtifactBuilder := artifact.NewMockArtifact() + resolver := NewOCIModuleResolver(mocks.Runtime, mocks.BlueprintHandler, mockArtifactBuilder) + resolver.BaseModuleResolver.shims = mocks.Shims // When parsing various valid OCI references testCases := []struct { @@ -367,12 +317,9 @@ func TestOCIModuleResolver_parseOCIRef(t *testing.T) { t.Run("HandlesInvalidReferences", func(t *testing.T) { // Given a resolver mocks := setupMocks(t, &SetupOptions{}) - resolver := NewOCIModuleResolver(mocks.Injector) - err := resolver.Initialize() - if err != nil { - t.Fatalf("Failed to initialize resolver: %v", err) - } - resolver.shims = mocks.Shims + mockArtifactBuilder := artifact.NewMockArtifact() + resolver := NewOCIModuleResolver(mocks.Runtime, mocks.BlueprintHandler, mockArtifactBuilder) + resolver.BaseModuleResolver.shims = mocks.Shims // When parsing invalid OCI references testCases := []string{ @@ -393,12 +340,9 @@ func TestOCIModuleResolver_parseOCIRef(t *testing.T) { t.Run("HandlesEdgeCases", func(t *testing.T) { // Given a resolver mocks := setupMocks(t, &SetupOptions{}) - resolver := NewOCIModuleResolver(mocks.Injector) - err := resolver.Initialize() - if err != nil { - t.Fatalf("Failed to initialize resolver: %v", err) - } - resolver.shims = mocks.Shims + mockArtifactBuilder := artifact.NewMockArtifact() + resolver := NewOCIModuleResolver(mocks.Runtime, mocks.BlueprintHandler, mockArtifactBuilder) + resolver.BaseModuleResolver.shims = mocks.Shims // When parsing edge case OCI references errorCases := []struct { @@ -433,12 +377,9 @@ func TestOCIModuleResolver_parseOCIRef(t *testing.T) { t.Run("HandlesComplexValidReferences", func(t *testing.T) { // Given a resolver mocks := setupMocks(t, &SetupOptions{}) - resolver := NewOCIModuleResolver(mocks.Injector) - err := resolver.Initialize() - if err != nil { - t.Fatalf("Failed to initialize resolver: %v", err) - } - resolver.shims = mocks.Shims + mockArtifactBuilder := artifact.NewMockArtifact() + resolver := NewOCIModuleResolver(mocks.Runtime, mocks.BlueprintHandler, mockArtifactBuilder) + resolver.BaseModuleResolver.shims = mocks.Shims // When parsing complex but valid OCI references testCases := []struct { @@ -476,12 +417,9 @@ func TestOCIModuleResolver_extractOCIModule(t *testing.T) { setup := func(t *testing.T) *OCIModuleResolver { t.Helper() mocks := setupMocks(t, &SetupOptions{}) - resolver := NewOCIModuleResolver(mocks.Injector) - err := resolver.Initialize() - if err != nil { - t.Fatalf("Failed to initialize resolver: %v", err) - } - resolver.shims = mocks.Shims + mockArtifactBuilder := artifact.NewMockArtifact() + resolver := NewOCIModuleResolver(mocks.Runtime, mocks.BlueprintHandler, mockArtifactBuilder) + resolver.BaseModuleResolver.shims = mocks.Shims return resolver } @@ -500,9 +438,10 @@ func TestOCIModuleResolver_extractOCIModule(t *testing.T) { return nil, io.EOF }, } - resolver.shims.NewTarReader = func(r io.Reader) TarReader { + resolver.BaseModuleResolver.shims.NewTarReader = func(r io.Reader) TarReader { return mockTarReader } + resolver.BaseModuleResolver.runtime.ProjectRoot = "/test/project" // When extracting OCI module path, err := resolver.extractOCIModule(resolvedSource, componentPath, ociArtifacts) @@ -526,9 +465,10 @@ func TestOCIModuleResolver_extractOCIModule(t *testing.T) { } // Mock Stat to return success (cache hit) - resolver.shims.Stat = func(path string) (os.FileInfo, error) { + resolver.BaseModuleResolver.shims.Stat = func(path string) (os.FileInfo, error) { return nil, nil } + resolver.BaseModuleResolver.runtime.ProjectRoot = "/test/project" // When extracting OCI module path, err := resolver.extractOCIModule(resolvedSource, componentPath, ociArtifacts) @@ -577,6 +517,10 @@ func TestOCIModuleResolver_extractOCIModule(t *testing.T) { } for _, tc := range errorCases { + // Set ProjectRoot for cases that need it + if tc.name != "InvalidOCISourceFormat" && tc.name != "MissingPathSeparator" { + resolver.BaseModuleResolver.runtime.ProjectRoot = "/test/project" + } // When extracting OCI module with error conditions _, err := resolver.extractOCIModule(tc.resolvedSource, tc.componentPath, tc.ociArtifacts) @@ -599,10 +543,8 @@ func TestOCIModuleResolver_extractOCIModule(t *testing.T) { "registry.example.com/module:latest": []byte("mock artifact data"), } - // Mock shell to return error - resolver.shell.(*shell.MockShell).GetProjectRootFunc = func() (string, error) { - return "", errors.New("project root error") - } + // Set ProjectRoot to empty to trigger error + resolver.BaseModuleResolver.runtime.ProjectRoot = "" // When extracting OCI module _, err := resolver.extractOCIModule(resolvedSource, componentPath, ociArtifacts) @@ -644,10 +586,8 @@ func TestOCIModuleResolver_extractOCIModule(t *testing.T) { "registry.example.com/module:latest": []byte("mock artifact data"), } - // Mock shell to return error during extraction - resolver.shell.(*shell.MockShell).GetProjectRootFunc = func() (string, error) { - return "", errors.New("project root error during extraction") - } + // Set ProjectRoot to empty to trigger error during extraction + resolver.BaseModuleResolver.runtime.ProjectRoot = "" // When extracting OCI module _, err := resolver.extractOCIModule(resolvedSource, componentPath, ociArtifacts) @@ -666,18 +606,16 @@ func TestOCIModuleResolver_extractModuleFromArtifact(t *testing.T) { setup := func(t *testing.T) *OCIModuleResolver { t.Helper() mocks := setupMocks(t, &SetupOptions{}) - resolver := NewOCIModuleResolver(mocks.Injector) - err := resolver.Initialize() - if err != nil { - t.Fatalf("Failed to initialize resolver: %v", err) - } - resolver.shims = mocks.Shims + mockArtifactBuilder := artifact.NewMockArtifact() + resolver := NewOCIModuleResolver(mocks.Runtime, mocks.BlueprintHandler, mockArtifactBuilder) + resolver.BaseModuleResolver.shims = mocks.Shims return resolver } t.Run("Success", func(t *testing.T) { // Given a resolver with valid artifact data resolver := setup(t) + resolver.BaseModuleResolver.runtime.ProjectRoot = "/test/project" artifactData := []byte("mock artifact data") modulePath := "terraform/test-module" extractionKey := "registry-module-latest" @@ -707,7 +645,7 @@ func TestOCIModuleResolver_extractModuleFromArtifact(t *testing.T) { return 0, io.EOF }, } - resolver.shims.NewTarReader = func(r io.Reader) TarReader { + resolver.BaseModuleResolver.shims.NewTarReader = func(r io.Reader) TarReader { return mockTarReader } @@ -769,6 +707,8 @@ func TestOCIModuleResolver_extractModuleFromArtifact(t *testing.T) { } for _, tc := range errorCases { + // Set ProjectRoot for tests that need it + resolver.BaseModuleResolver.runtime.ProjectRoot = "/test/project" // When extracting with error conditions tc.setupMocks(resolver) err := resolver.extractModuleFromArtifact(artifactData, modulePath, extractionKey) @@ -790,10 +730,8 @@ func TestOCIModuleResolver_extractModuleFromArtifact(t *testing.T) { modulePath := "terraform/test-module" extractionKey := "registry-module-latest" - // Mock shell to return error - resolver.shell.(*shell.MockShell).GetProjectRootFunc = func() (string, error) { - return "", errors.New("project root error") - } + // Set ProjectRoot to empty to trigger error + resolver.BaseModuleResolver.runtime.ProjectRoot = "" // When extracting module from artifact err := resolver.extractModuleFromArtifact(artifactData, modulePath, extractionKey) @@ -810,6 +748,7 @@ func TestOCIModuleResolver_extractModuleFromArtifact(t *testing.T) { t.Run("HandlesFileCreationError", func(t *testing.T) { // Given a resolver with file creation error resolver := setup(t) + resolver.BaseModuleResolver.runtime.ProjectRoot = "/test/project" artifactData := []byte("mock artifact data") modulePath := "terraform/test-module" extractionKey := "registry-module-latest" @@ -824,12 +763,12 @@ func TestOCIModuleResolver_extractModuleFromArtifact(t *testing.T) { }, nil }, } - resolver.shims.NewTarReader = func(r io.Reader) TarReader { + resolver.BaseModuleResolver.shims.NewTarReader = func(r io.Reader) TarReader { return mockTarReader } // Mock Create to return error - resolver.shims.Create = func(name string) (*os.File, error) { + resolver.BaseModuleResolver.shims.Create = func(name string) (*os.File, error) { return nil, errors.New("file creation error") } @@ -848,6 +787,7 @@ func TestOCIModuleResolver_extractModuleFromArtifact(t *testing.T) { t.Run("HandlesCopyError", func(t *testing.T) { // Given a resolver with copy error resolver := setup(t) + resolver.BaseModuleResolver.runtime.ProjectRoot = "/test/project" artifactData := []byte("mock artifact data") modulePath := "terraform/test-module" extractionKey := "registry-module-latest" @@ -862,12 +802,12 @@ func TestOCIModuleResolver_extractModuleFromArtifact(t *testing.T) { }, nil }, } - resolver.shims.NewTarReader = func(r io.Reader) TarReader { + resolver.BaseModuleResolver.shims.NewTarReader = func(r io.Reader) TarReader { return mockTarReader } // Mock Copy to return error - resolver.shims.Copy = func(dst io.Writer, src io.Reader) (int64, error) { + resolver.BaseModuleResolver.shims.Copy = func(dst io.Writer, src io.Reader) (int64, error) { return 0, errors.New("copy error") } @@ -886,6 +826,7 @@ func TestOCIModuleResolver_extractModuleFromArtifact(t *testing.T) { t.Run("HandlesChmodError", func(t *testing.T) { // Given a resolver with chmod error resolver := setup(t) + resolver.BaseModuleResolver.runtime.ProjectRoot = "/test/project" artifactData := []byte("mock artifact data") modulePath := "terraform/test-module" extractionKey := "registry-module-latest" @@ -900,12 +841,12 @@ func TestOCIModuleResolver_extractModuleFromArtifact(t *testing.T) { }, nil }, } - resolver.shims.NewTarReader = func(r io.Reader) TarReader { + resolver.BaseModuleResolver.shims.NewTarReader = func(r io.Reader) TarReader { return mockTarReader } // Mock Chmod to return error - resolver.shims.Chmod = func(name string, mode os.FileMode) error { + resolver.BaseModuleResolver.shims.Chmod = func(name string, mode os.FileMode) error { return errors.New("chmod error") } @@ -924,6 +865,7 @@ func TestOCIModuleResolver_extractModuleFromArtifact(t *testing.T) { t.Run("HandlesParentDirectoryCreationError", func(t *testing.T) { // Given a resolver with parent directory creation error resolver := setup(t) + resolver.BaseModuleResolver.runtime.ProjectRoot = "/test/project" artifactData := []byte("mock artifact data") modulePath := "terraform/test-module" extractionKey := "registry-module-latest" @@ -938,13 +880,13 @@ func TestOCIModuleResolver_extractModuleFromArtifact(t *testing.T) { }, nil }, } - resolver.shims.NewTarReader = func(r io.Reader) TarReader { + resolver.BaseModuleResolver.shims.NewTarReader = func(r io.Reader) TarReader { return mockTarReader } // Mock MkdirAll to return error for parent directory creation callCount := 0 - resolver.shims.MkdirAll = func(path string, perm os.FileMode) error { + resolver.BaseModuleResolver.shims.MkdirAll = func(path string, perm os.FileMode) error { callCount++ if callCount > 1 { // First call succeeds, second fails return errors.New("parent directory creation error") @@ -969,12 +911,9 @@ func TestOCIModuleResolver_processComponent(t *testing.T) { setup := func(t *testing.T) *OCIModuleResolver { t.Helper() mocks := setupMocks(t, &SetupOptions{}) - resolver := NewOCIModuleResolver(mocks.Injector) - err := resolver.Initialize() - if err != nil { - t.Fatalf("Failed to initialize resolver: %v", err) - } - resolver.shims = mocks.Shims + mockArtifactBuilder := artifact.NewMockArtifact() + resolver := NewOCIModuleResolver(mocks.Runtime, mocks.BlueprintHandler, mockArtifactBuilder) + resolver.BaseModuleResolver.shims = mocks.Shims return resolver } @@ -996,9 +935,10 @@ func TestOCIModuleResolver_processComponent(t *testing.T) { return nil, io.EOF }, } - resolver.shims.NewTarReader = func(r io.Reader) TarReader { + resolver.BaseModuleResolver.shims.NewTarReader = func(r io.Reader) TarReader { return mockTarReader } + resolver.BaseModuleResolver.runtime.ProjectRoot = "/test/project" // When processing component err := resolver.processComponent(component, ociArtifacts) @@ -1022,7 +962,7 @@ func TestOCIModuleResolver_processComponent(t *testing.T) { } // Mock MkdirAll to return error - resolver.shims.MkdirAll = func(path string, perm os.FileMode) error { + resolver.BaseModuleResolver.shims.MkdirAll = func(path string, perm os.FileMode) error { return errors.New("mkdir error") } @@ -1078,12 +1018,13 @@ func TestOCIModuleResolver_processComponent(t *testing.T) { return nil, io.EOF }, } - resolver.shims.NewTarReader = func(r io.Reader) TarReader { + resolver.BaseModuleResolver.shims.NewTarReader = func(r io.Reader) TarReader { return mockTarReader } + resolver.BaseModuleResolver.runtime.ProjectRoot = "/test/project" // Mock FilepathRel to return error - resolver.shims.FilepathRel = func(basepath, targpath string) (string, error) { + resolver.BaseModuleResolver.shims.FilepathRel = func(basepath, targpath string) (string, error) { return "", errors.New("filepath rel error") } @@ -1117,12 +1058,13 @@ func TestOCIModuleResolver_processComponent(t *testing.T) { return nil, io.EOF }, } - resolver.shims.NewTarReader = func(r io.Reader) TarReader { + resolver.BaseModuleResolver.shims.NewTarReader = func(r io.Reader) TarReader { return mockTarReader } + resolver.BaseModuleResolver.runtime.ProjectRoot = "/test/project" // Mock WriteFile to return error for main.tf - resolver.shims.WriteFile = func(path string, data []byte, perm os.FileMode) error { + resolver.BaseModuleResolver.shims.WriteFile = func(path string, data []byte, perm os.FileMode) error { if strings.HasSuffix(path, "main.tf") { return errors.New("write main.tf error") } @@ -1159,12 +1101,13 @@ func TestOCIModuleResolver_processComponent(t *testing.T) { return nil, io.EOF }, } - resolver.shims.NewTarReader = func(r io.Reader) TarReader { + resolver.BaseModuleResolver.shims.NewTarReader = func(r io.Reader) TarReader { return mockTarReader } + resolver.BaseModuleResolver.runtime.ProjectRoot = "/test/project" // Mock WriteFile to return error for variables.tf - resolver.shims.WriteFile = func(path string, data []byte, perm os.FileMode) error { + resolver.BaseModuleResolver.shims.WriteFile = func(path string, data []byte, perm os.FileMode) error { if strings.HasSuffix(path, "variables.tf") { return errors.New("write variables.tf error") } @@ -1201,12 +1144,13 @@ func TestOCIModuleResolver_processComponent(t *testing.T) { return nil, io.EOF }, } - resolver.shims.NewTarReader = func(r io.Reader) TarReader { + resolver.BaseModuleResolver.shims.NewTarReader = func(r io.Reader) TarReader { return mockTarReader } + resolver.BaseModuleResolver.runtime.ProjectRoot = "/test/project" // Mock WriteFile to return error for outputs.tf - resolver.shims.WriteFile = func(path string, data []byte, perm os.FileMode) error { + resolver.BaseModuleResolver.shims.WriteFile = func(path string, data []byte, perm os.FileMode) error { if strings.HasSuffix(path, "outputs.tf") { return errors.New("write outputs.tf error") } diff --git a/pkg/composer/terraform/standard_module_resolver.go b/pkg/composer/terraform/standard_module_resolver.go index e3c364427..cba9eac90 100644 --- a/pkg/composer/terraform/standard_module_resolver.go +++ b/pkg/composer/terraform/standard_module_resolver.go @@ -5,10 +5,8 @@ import ( "path/filepath" "strings" - "github.com/windsorcli/cli/pkg/runtime/config" - "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/composer/blueprint" - "github.com/windsorcli/cli/pkg/runtime/shell" + "github.com/windsorcli/cli/pkg/runtime" ) // The StandardModuleResolver is a terraform module resolver for standard source types. @@ -29,51 +27,22 @@ type TerraformInitOutput struct { // StandardModuleResolver handles standard terraform module sources (git, local paths, etc.) type StandardModuleResolver struct { *BaseModuleResolver - configHandler config.ConfigHandler - reset bool + reset bool } // ============================================================================= // Constructor // ============================================================================= -// NewStandardModuleResolver creates a new standard module resolver -func NewStandardModuleResolver(injector di.Injector) *StandardModuleResolver { - base := NewBaseModuleResolver(injector) - return &StandardModuleResolver{ - BaseModuleResolver: base, +// NewStandardModuleResolver creates a new standard module resolver with the provided dependencies. +// If overrides are provided, any non-nil component in the override StandardModuleResolver will be used instead of creating a default. +func NewStandardModuleResolver(rt *runtime.Runtime, blueprintHandler blueprint.BlueprintHandler) *StandardModuleResolver { + resolver := &StandardModuleResolver{ + BaseModuleResolver: NewBaseModuleResolver(rt, blueprintHandler), reset: false, } -} - -// Initialize sets up the StandardModuleResolver by resolving and assigning required dependencies. -// It initializes the base resolver, then resolves and assigns the blueprint handler, shell, and config handler -// from the dependency injector. Returns an error if any dependency cannot be resolved. -func (h *StandardModuleResolver) Initialize() error { - if err := h.BaseModuleResolver.Initialize(); err != nil { - return err - } - blueprintHandlerInterface := h.injector.Resolve("blueprintHandler") - var ok bool - h.blueprintHandler, ok = blueprintHandlerInterface.(blueprint.BlueprintHandler) - if !ok { - return fmt.Errorf("failed to resolve blueprint handler") - } - - shellInterface := h.injector.Resolve("shell") - h.shell, ok = shellInterface.(shell.Shell) - if !ok { - return fmt.Errorf("failed to resolve shell") - } - - configHandlerInterface := h.injector.Resolve("configHandler") - h.configHandler, ok = configHandlerInterface.(config.ConfigHandler) - if !ok { - return fmt.Errorf("failed to resolve config handler") - } - - return nil + return resolver } // ============================================================================= @@ -114,9 +83,9 @@ func (h *StandardModuleResolver) ProcessModules() error { return fmt.Errorf("failed to change to module directory for %s: %w", component.Path, err) } - contextPath, err := h.configHandler.GetConfigRoot() - if err != nil { - return fmt.Errorf("failed to get config root for %s: %w", component.Path, err) + contextPath := h.runtime.ConfigRoot + if contextPath == "" { + return fmt.Errorf("failed to get config root: config root is empty") } tfDataDir := filepath.Join(contextPath, ".terraform", component.Path) @@ -124,7 +93,7 @@ func (h *StandardModuleResolver) ProcessModules() error { return fmt.Errorf("failed to set TF_DATA_DIR for %s: %w", component.Path, err) } - output, err := h.shell.ExecProgress( + output, err := h.runtime.Shell.ExecProgress( fmt.Sprintf("📥 Loading component %s", component.Path), "terraform", "init", diff --git a/pkg/composer/terraform/standard_module_resolver_test.go b/pkg/composer/terraform/standard_module_resolver_test.go index d315c19d5..8615ef3e4 100644 --- a/pkg/composer/terraform/standard_module_resolver_test.go +++ b/pkg/composer/terraform/standard_module_resolver_test.go @@ -10,9 +10,6 @@ import ( blueprintv1alpha1 "github.com/windsorcli/cli/api/v1alpha1" "github.com/windsorcli/cli/pkg/runtime/config" - "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/composer/blueprint" - "github.com/windsorcli/cli/pkg/runtime/shell" ) // The StandardModuleResolverTest is a test suite for the StandardModuleResolver implementation @@ -26,8 +23,8 @@ import ( func TestStandardModuleResolver_NewStandardModuleResolver(t *testing.T) { t.Run("CreatesStandardModuleResolver", func(t *testing.T) { - injector := di.NewInjector() - resolver := NewStandardModuleResolver(injector) + mocks := setupMocks(t, &SetupOptions{}) + resolver := NewStandardModuleResolver(mocks.Runtime, mocks.BlueprintHandler) if resolver == nil { t.Fatal("Expected non-nil standard module resolver") } @@ -40,11 +37,11 @@ func TestStandardModuleResolver_NewStandardModuleResolver(t *testing.T) { }) } -func TestStandardModuleResolver_Initialize(t *testing.T) { +func TestStandardModuleResolver_NewStandardModuleResolverWithDependencies(t *testing.T) { setup := func(t *testing.T) (*StandardModuleResolver, *Mocks) { t.Helper() mocks := setupMocks(t, &SetupOptions{}) - resolver := NewStandardModuleResolver(mocks.Injector) + resolver := NewStandardModuleResolver(mocks.Runtime, mocks.BlueprintHandler) return resolver, mocks } @@ -52,174 +49,15 @@ func TestStandardModuleResolver_Initialize(t *testing.T) { // Given a standard module resolver with valid dependencies resolver, _ := setup(t) - // When Initialize is called - err := resolver.Initialize() - - // Then no error is returned and all handlers are set - if err != nil { - t.Errorf("Expected nil error, got %v", err) - } - if resolver.blueprintHandler == nil { - t.Error("Expected blueprintHandler to be set after Initialize()") - } - if resolver.shell == nil { - t.Error("Expected shell to be set after Initialize()") - } - if resolver.configHandler == nil { - t.Error("Expected configHandler to be set after Initialize()") - } - }) - - t.Run("HandlesBaseInitializeError", func(t *testing.T) { - // Given a resolver with an injector missing shell dependency - injector := di.NewInjector() - resolver := NewStandardModuleResolver(injector) - - // When Initialize is called - err := resolver.Initialize() - - // Then an error is returned indicating shell resolution failure - if err == nil { - t.Error("Expected error, got nil") - } - if err == nil || !strings.Contains(err.Error(), "failed to resolve shell") { - t.Errorf("Expected shell resolution error, got: %v", err) - } - }) - - t.Run("HandlesMissingBlueprintHandler", func(t *testing.T) { - // Given a resolver with blueprintHandler explicitly set to nil in injector - resolver, mocks := setup(t) - mocks.Injector.Register("blueprintHandler", nil) - - // When Initialize is called - err := resolver.Initialize() - - // Then an error is returned indicating blueprint handler resolution failure - if err == nil { - t.Error("Expected error, got nil") - } - if err == nil || !strings.Contains(err.Error(), "failed to resolve blueprint handler") { - t.Errorf("Expected blueprint handler resolution error, got: %v", err) - } - }) - - t.Run("HandlesMissingConfigHandler", func(t *testing.T) { - // Given a resolver with configHandler explicitly set to nil in injector - resolver, mocks := setup(t) - mocks.Injector.Register("configHandler", nil) - - // When Initialize is called - err := resolver.Initialize() - - // Then an error is returned indicating config handler resolution failure - if err == nil { - t.Error("Expected error, got nil") - } - if err == nil || !strings.Contains(err.Error(), "failed to resolve config handler") { - t.Errorf("Expected config handler resolution error, got: %v", err) - } - }) - - t.Run("HandlesShellTypeAssertionError", func(t *testing.T) { - // Given a resolver with shell registered as an invalid type - resolver, mocks := setup(t) - mocks.Injector.Register("shell", "invalid-shell-type") - - // When Initialize is called - err := resolver.Initialize() - - // Then an error is returned indicating shell type assertion failure - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to resolve shell") { - t.Errorf("Expected shell type assertion error, got: %v", err) - } - }) - - t.Run("HandlesBlueprintHandlerTypeAssertionError", func(t *testing.T) { - // Given a resolver with blueprintHandler registered as an invalid type - resolver, mocks := setup(t) - mocks.Injector.Register("blueprintHandler", "invalid-blueprint-handler-type") - - // When Initialize is called - err := resolver.Initialize() - - // Then an error is returned indicating blueprint handler type assertion failure - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to resolve blueprint handler") { - t.Errorf("Expected blueprint handler type assertion error, got: %v", err) - } - }) - - t.Run("HandlesConfigHandlerTypeAssertionError", func(t *testing.T) { - // Given a resolver with configHandler registered as an invalid type - resolver, mocks := setup(t) - mocks.Injector.Register("configHandler", "invalid-config-handler-type") - - // When Initialize is called - err := resolver.Initialize() - - // Then an error is returned indicating config handler type assertion failure - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to resolve config handler") { - t.Errorf("Expected config handler type assertion error, got: %v", err) - } - }) - - t.Run("HandlesNilShellResolution", func(t *testing.T) { - // Given a resolver with shell registered as nil interface - resolver, mocks := setup(t) - mocks.Injector.Register("shell", (*shell.Shell)(nil)) - - // When Initialize is called - err := resolver.Initialize() - - // Then an error is returned indicating shell resolution failure - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to resolve shell") { - t.Errorf("Expected shell resolution error, got: %v", err) - } - }) - - t.Run("HandlesNilBlueprintHandlerResolution", func(t *testing.T) { - // Given a resolver with blueprintHandler registered as nil interface - resolver, mocks := setup(t) - mocks.Injector.Register("blueprintHandler", (*blueprint.BlueprintHandler)(nil)) - - // When Initialize is called - err := resolver.Initialize() - - // Then an error is returned indicating blueprint handler resolution failure - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to resolve blueprint handler") { - t.Errorf("Expected blueprint handler resolution error, got: %v", err) + // Then all handlers are set + if resolver.BaseModuleResolver.blueprintHandler == nil { + t.Error("Expected blueprintHandler to be set") } - }) - - t.Run("HandlesNilConfigHandlerResolution", func(t *testing.T) { - // Given a resolver with configHandler registered as nil interface - resolver, mocks := setup(t) - mocks.Injector.Register("configHandler", (*config.ConfigHandler)(nil)) - - // When Initialize is called - err := resolver.Initialize() - - // Then an error is returned indicating config handler resolution failure - if err == nil { - t.Error("Expected error, got nil") + if resolver.BaseModuleResolver.runtime.Shell == nil { + t.Error("Expected shell to be set") } - if !strings.Contains(err.Error(), "failed to resolve config handler") { - t.Errorf("Expected config handler resolution error, got: %v", err) + if resolver.BaseModuleResolver.runtime.ConfigHandler == nil { + t.Error("Expected configHandler to be set") } }) } @@ -228,21 +66,18 @@ func TestStandardModuleResolver_ProcessModules(t *testing.T) { setup := func(t *testing.T) (*StandardModuleResolver, *Mocks) { t.Helper() mocks := setupMocks(t, &SetupOptions{}) - resolver := NewStandardModuleResolver(mocks.Injector) - err := resolver.Initialize() - if err != nil { - t.Fatalf("Failed to initialize resolver: %v", err) - } - resolver.shims = mocks.Shims + resolver := NewStandardModuleResolver(mocks.Runtime, mocks.BlueprintHandler) + resolver.BaseModuleResolver.shims = mocks.Shims return resolver, mocks } t.Run("Success", func(t *testing.T) { // Given a resolver with proper JSON unmarshaling for complete path coverage resolver, mocks := setup(t) + resolver.BaseModuleResolver.runtime.ConfigRoot = "/test/config" // Use real JSON unmarshaling to exercise the parsing logic - resolver.shims.JsonUnmarshal = func(data []byte, v any) error { + resolver.BaseModuleResolver.shims.JsonUnmarshal = func(data []byte, v any) error { return json.Unmarshal(data, v) } @@ -266,7 +101,7 @@ func TestStandardModuleResolver_ProcessModules(t *testing.T) { t.Run("HandlesNilBlueprintHandler", func(t *testing.T) { // Given a resolver with nil blueprint handler resolver, _ := setup(t) - resolver.blueprintHandler = nil + resolver.BaseModuleResolver.blueprintHandler = nil // When ProcessModules is called err := resolver.ProcessModules() @@ -280,7 +115,7 @@ func TestStandardModuleResolver_ProcessModules(t *testing.T) { t.Run("HandlesMkdirAllError", func(t *testing.T) { // Given a resolver with MkdirAll shim returning error resolver, _ := setup(t) - resolver.shims.MkdirAll = func(path string, perm os.FileMode) error { + resolver.BaseModuleResolver.shims.MkdirAll = func(path string, perm os.FileMode) error { return errors.New("mkdir error") } @@ -296,7 +131,7 @@ func TestStandardModuleResolver_ProcessModules(t *testing.T) { t.Run("HandlesChdirError", func(t *testing.T) { // Given a resolver with Chdir shim returning error resolver, _ := setup(t) - resolver.shims.Chdir = func(path string) error { + resolver.BaseModuleResolver.shims.Chdir = func(path string) error { return errors.New("chdir error") } @@ -313,17 +148,15 @@ func TestStandardModuleResolver_ProcessModules(t *testing.T) { // Given a resolver with config handler GetConfigRoot returning error resolver, mocks := setup(t) mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetConfigRootFunc = func() (string, error) { - return "", errors.New("config root error") - } mocks.Injector.Register("configHandler", mockConfigHandler) - resolver.configHandler = mockConfigHandler + resolver.BaseModuleResolver.runtime.ConfigHandler = mockConfigHandler + resolver.BaseModuleResolver.runtime.ConfigRoot = "" // When ProcessModules is called err := resolver.ProcessModules() // Then an error is returned indicating failure to get config root - if err == nil || !strings.Contains(err.Error(), "failed to get config root") { + if err == nil || !strings.Contains(err.Error(), "config root is empty") { t.Errorf("Expected config root error, got: %v", err) } }) @@ -332,12 +165,10 @@ func TestStandardModuleResolver_ProcessModules(t *testing.T) { // Given a resolver with Setenv shim returning error for TF_DATA_DIR resolver, mocks := setup(t) mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.GetConfigRootFunc = func() (string, error) { - return "/mock/config/root", nil - } mocks.Injector.Register("configHandler", mockConfigHandler) - resolver.configHandler = mockConfigHandler - resolver.shims.Setenv = func(key, value string) error { + resolver.BaseModuleResolver.runtime.ConfigHandler = mockConfigHandler + resolver.BaseModuleResolver.runtime.ConfigRoot = "/mock/config/root" + resolver.BaseModuleResolver.shims.Setenv = func(key, value string) error { if key == "TF_DATA_DIR" { return errors.New("setenv error") } @@ -356,6 +187,7 @@ func TestStandardModuleResolver_ProcessModules(t *testing.T) { t.Run("HandlesTerraformInitError", func(t *testing.T) { // Given a resolver with Shell.ExecProgressFunc returning error for terraform init resolver, mocks := setup(t) + resolver.BaseModuleResolver.runtime.ConfigRoot = "/test/config" mocks.Shell.ExecProgressFunc = func(msg, cmd string, args ...string) (string, error) { if cmd == "terraform" && len(args) > 0 && args[0] == "init" { return "", errors.New("terraform init error") @@ -375,7 +207,7 @@ func TestStandardModuleResolver_ProcessModules(t *testing.T) { t.Run("HandlesWriteShimMainTfError", func(t *testing.T) { // Given a resolver with WriteFile shim returning error for main.tf resolver, _ := setup(t) - resolver.shims.WriteFile = func(path string, data []byte, perm os.FileMode) error { + resolver.BaseModuleResolver.shims.WriteFile = func(path string, data []byte, perm os.FileMode) error { if strings.HasSuffix(path, "main.tf") { return errors.New("write main.tf error") } @@ -394,7 +226,8 @@ func TestStandardModuleResolver_ProcessModules(t *testing.T) { t.Run("HandlesWriteShimVariablesTfError", func(t *testing.T) { // Given a resolver with WriteFile shim returning error for variables.tf resolver, _ := setup(t) - resolver.shims.WriteFile = func(path string, data []byte, perm os.FileMode) error { + resolver.BaseModuleResolver.runtime.ConfigRoot = "/test/config" + resolver.BaseModuleResolver.shims.WriteFile = func(path string, data []byte, perm os.FileMode) error { if strings.HasSuffix(path, "variables.tf") { return errors.New("write variables.tf error") } @@ -413,7 +246,8 @@ func TestStandardModuleResolver_ProcessModules(t *testing.T) { t.Run("HandlesWriteShimOutputsTfError", func(t *testing.T) { // Given a resolver with WriteFile shim returning error for outputs.tf resolver, _ := setup(t) - resolver.shims.WriteFile = func(path string, data []byte, perm os.FileMode) error { + resolver.BaseModuleResolver.runtime.ConfigRoot = "/test/config" + resolver.BaseModuleResolver.shims.WriteFile = func(path string, data []byte, perm os.FileMode) error { if strings.HasSuffix(path, "outputs.tf") { return errors.New("write outputs.tf error") } @@ -474,7 +308,8 @@ func TestStandardModuleResolver_ProcessModules(t *testing.T) { t.Run("HandlesTerraformInitOutputParsing", func(t *testing.T) { // Given a resolver with custom JsonUnmarshal and Stat shims for output parsing edge cases resolver, mocks := setup(t) - resolver.shims.JsonUnmarshal = func(data []byte, v any) error { + resolver.BaseModuleResolver.runtime.ConfigRoot = "/test/config" + resolver.BaseModuleResolver.shims.JsonUnmarshal = func(data []byte, v any) error { if initOutput, ok := v.(*TerraformInitOutput); ok { jsonStr := string(data) if strings.Contains(jsonStr, `"empty_line"`) { @@ -517,7 +352,7 @@ func TestStandardModuleResolver_ProcessModules(t *testing.T) { } // And Stat shim only succeeds for /valid/path - resolver.shims.Stat = func(path string) (os.FileInfo, error) { + resolver.BaseModuleResolver.shims.Stat = func(path string) (os.FileInfo, error) { if path == "/valid/path" { return nil, nil } @@ -554,11 +389,7 @@ func TestStandardModuleResolver_shouldHandle(t *testing.T) { setup := func(t *testing.T) *StandardModuleResolver { t.Helper() mocks := setupMocks(t, &SetupOptions{}) - resolver := NewStandardModuleResolver(mocks.Injector) - err := resolver.Initialize() - if err != nil { - t.Fatalf("Failed to initialize resolver: %v", err) - } + resolver := NewStandardModuleResolver(mocks.Runtime, mocks.BlueprintHandler) resolver.shims = mocks.Shims return resolver } @@ -756,11 +587,7 @@ func TestStandardModuleResolver_isTerraformRegistryModule(t *testing.T) { setup := func(t *testing.T) *StandardModuleResolver { t.Helper() mocks := setupMocks(t, &SetupOptions{}) - resolver := NewStandardModuleResolver(mocks.Injector) - err := resolver.Initialize() - if err != nil { - t.Fatalf("Failed to initialize resolver: %v", err) - } + resolver := NewStandardModuleResolver(mocks.Runtime, mocks.BlueprintHandler) resolver.shims = mocks.Shims return resolver } diff --git a/pkg/project/project.go b/pkg/project/project.go index bfecf0d44..a581c5c65 100644 --- a/pkg/project/project.go +++ b/pkg/project/project.go @@ -16,7 +16,7 @@ import ( // It coordinates context, provisioner, composer, and workstation managers // to provide a unified interface for project initialization and management. type Project struct { - Context *runtime.Runtime + Runtime *runtime.Runtime Provisioner *provisioner.Provisioner Composer *composer.Composer Workstation *workstation.Workstation @@ -28,59 +28,56 @@ type Project struct { // is in dev mode. If an existing context is provided, it will be reused; otherwise, // a new context will be created. Returns the initialized Project or an error if any step fails. // After creation, call Configure() to apply flag overrides if needed. -func NewProject(injector di.Injector, contextName string, existingCtx ...*runtime.Runtime) (*Project, error) { - var baseCtx *runtime.Runtime +func NewProject(injector di.Injector, contextName string, existingRuntime ...*runtime.Runtime) (*Project, error) { + var rt *runtime.Runtime var err error - if len(existingCtx) > 0 && existingCtx[0] != nil { - baseCtx = existingCtx[0] + if len(existingRuntime) > 0 && existingRuntime[0] != nil { + rt = existingRuntime[0] } else { - baseCtx = &runtime.Runtime{ + rt = &runtime.Runtime{ Injector: injector, } - baseCtx, err = runtime.NewRuntime(baseCtx) + rt, err = runtime.NewRuntime(rt) if err != nil { return nil, fmt.Errorf("failed to initialize context: %w", err) } } if contextName == "" { - contextName = baseCtx.ConfigHandler.GetContext() + contextName = rt.ConfigHandler.GetContext() if contextName == "" { contextName = "local" } } - baseCtx.ContextName = contextName - baseCtx.ConfigRoot = filepath.Join(baseCtx.ProjectRoot, "contexts", contextName) + rt.ContextName = contextName + rt.ConfigRoot = filepath.Join(rt.ProjectRoot, "contexts", contextName) - if err := baseCtx.ApplyConfigDefaults(); err != nil { + if err := rt.ApplyConfigDefaults(); err != nil { return nil, err } provCtx := &provisioner.ProvisionerRuntime{ - Runtime: *baseCtx, + Runtime: *rt, } prov := provisioner.NewProvisioner(provCtx) - composerCtx := &composer.ComposerRuntime{ - Runtime: *baseCtx, - } - comp := composer.NewComposer(composerCtx) + comp := composer.NewComposer(rt) var ws *workstation.Workstation - if baseCtx.ConfigHandler.IsDevMode(baseCtx.ContextName) { + if rt.ConfigHandler.IsDevMode(rt.ContextName) { workstationCtx := &workstation.WorkstationRuntime{ - Runtime: *baseCtx, + Runtime: *rt, } - ws, err = workstation.NewWorkstation(workstationCtx, baseCtx.Injector) + ws, err = workstation.NewWorkstation(workstationCtx, rt.Injector) if err != nil { return nil, fmt.Errorf("failed to create workstation: %w", err) } } return &Project{ - Context: baseCtx, + Runtime: rt, Provisioner: prov, Composer: comp, Workstation: ws, @@ -91,17 +88,17 @@ func NewProject(injector di.Injector, contextName string, existingCtx ...*runtim // This should be called after NewProject if command flags need to override // configuration values. Returns an error if loading or applying overrides fails. func (p *Project) Configure(flagOverrides map[string]any) error { - contextName := p.Context.ContextName + contextName := p.Runtime.ContextName if contextName == "" { contextName = "local" } - if p.Context.ConfigHandler.IsDevMode(contextName) { + if p.Runtime.ConfigHandler.IsDevMode(contextName) { if flagOverrides == nil { flagOverrides = make(map[string]any) } if _, exists := flagOverrides["provider"]; !exists { - if p.Context.ConfigHandler.GetString("provider") == "" { + if p.Runtime.ConfigHandler.GetString("provider") == "" { flagOverrides["provider"] = "generic" } } @@ -114,16 +111,16 @@ func (p *Project) Configure(flagOverrides map[string]any) error { } } - if err := p.Context.ApplyProviderDefaults(providerOverride); err != nil { + if err := p.Runtime.ApplyProviderDefaults(providerOverride); err != nil { return err } - if err := p.Context.ConfigHandler.LoadConfig(); err != nil { + if err := p.Runtime.ConfigHandler.LoadConfig(); err != nil { return fmt.Errorf("failed to load config: %w", err) } for key, value := range flagOverrides { - if err := p.Context.ConfigHandler.Set(key, value); err != nil { + if err := p.Runtime.ConfigHandler.Set(key, value); err != nil { return fmt.Errorf("failed to set %s: %w", key, err) } } @@ -143,10 +140,10 @@ func (p *Project) Initialize(overwrite bool) error { } } - if err := p.Context.ConfigHandler.GenerateContextID(); err != nil { + if err := p.Runtime.ConfigHandler.GenerateContextID(); err != nil { return fmt.Errorf("failed to generate context ID: %w", err) } - if err := p.Context.ConfigHandler.SaveConfig(overwrite); err != nil { + if err := p.Runtime.ConfigHandler.SaveConfig(overwrite); err != nil { return fmt.Errorf("failed to save config: %w", err) } @@ -154,11 +151,11 @@ func (p *Project) Initialize(overwrite bool) error { return fmt.Errorf("failed to generate infrastructure: %w", err) } - if err := p.Context.PrepareTools(); err != nil { + if err := p.Runtime.PrepareTools(); err != nil { return err } - if err := p.Context.LoadEnvironment(true); err != nil { + if err := p.Runtime.LoadEnvironment(true); err != nil { return fmt.Errorf("failed to load environment: %w", err) } @@ -170,26 +167,26 @@ func (p *Project) Initialize(overwrite bool) error { // saved state, then deletes the .volumes directory, .windsor/.tf_modules directory, // .windsor/Corefile, and .windsor/docker-compose.yaml. Returns an error if any cleanup step fails. func (p *Project) PerformCleanup() error { - if err := p.Context.ConfigHandler.Clean(); err != nil { + if err := p.Runtime.ConfigHandler.Clean(); err != nil { return fmt.Errorf("error cleaning up context specific artifacts: %w", err) } - volumesPath := filepath.Join(p.Context.ProjectRoot, ".volumes") + volumesPath := filepath.Join(p.Runtime.ProjectRoot, ".volumes") if err := os.RemoveAll(volumesPath); err != nil { return fmt.Errorf("error deleting .volumes folder: %w", err) } - tfModulesPath := filepath.Join(p.Context.ProjectRoot, ".windsor", ".tf_modules") + tfModulesPath := filepath.Join(p.Runtime.ProjectRoot, ".windsor", ".tf_modules") if err := os.RemoveAll(tfModulesPath); err != nil { return fmt.Errorf("error deleting .windsor/.tf_modules folder: %w", err) } - corefilePath := filepath.Join(p.Context.ProjectRoot, ".windsor", "Corefile") + corefilePath := filepath.Join(p.Runtime.ProjectRoot, ".windsor", "Corefile") if err := os.RemoveAll(corefilePath); err != nil { return fmt.Errorf("error deleting .windsor/Corefile: %w", err) } - dockerComposePath := filepath.Join(p.Context.ProjectRoot, ".windsor", "docker-compose.yaml") + dockerComposePath := filepath.Join(p.Runtime.ProjectRoot, ".windsor", "docker-compose.yaml") if err := os.RemoveAll(dockerComposePath); err != nil { return fmt.Errorf("error deleting .windsor/docker-compose.yaml: %w", err) } diff --git a/pkg/project/project_test.go b/pkg/project/project_test.go index 81c64d2e8..3c342baf4 100644 --- a/pkg/project/project_test.go +++ b/pkg/project/project_test.go @@ -7,12 +7,12 @@ import ( "testing" "github.com/windsorcli/cli/pkg/composer" + "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/provisioner" "github.com/windsorcli/cli/pkg/runtime" "github.com/windsorcli/cli/pkg/runtime/config" "github.com/windsorcli/cli/pkg/runtime/shell" "github.com/windsorcli/cli/pkg/runtime/tools" - "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/provisioner" "github.com/windsorcli/cli/pkg/workstation" ) @@ -102,11 +102,11 @@ func setupMocks(t *testing.T) *Mocks { injector.Register("configHandler", configHandler) injector.Register("toolsManager", mockToolsManager) - baseCtx := &runtime.Runtime{ + rt := &runtime.Runtime{ Injector: injector, } - ctx, err := runtime.NewRuntime(baseCtx) + ctx, err := runtime.NewRuntime(rt) if err != nil { t.Fatalf("Failed to create context: %v", err) } @@ -116,10 +116,7 @@ func setupMocks(t *testing.T) *Mocks { } prov := provisioner.NewProvisioner(provCtx) - composerCtx := &composer.ComposerRuntime{ - Runtime: *ctx, - } - comp := composer.NewComposer(composerCtx) + comp := composer.NewComposer(ctx) return &Mocks{ Injector: injector, @@ -148,8 +145,8 @@ func TestNewProject(t *testing.T) { t.Fatal("Expected Project to be created") } - if proj.Context == nil { - t.Error("Expected Context to be set") + if proj.Runtime == nil { + t.Error("Expected Runtime to be set") } if proj.Provisioner == nil { @@ -160,8 +157,8 @@ func TestNewProject(t *testing.T) { t.Error("Expected Composer to be set") } - if proj.Context.ContextName != "test-context" { - t.Errorf("Expected ContextName to be 'test-context', got: %s", proj.Context.ContextName) + if proj.Runtime.ContextName != "test-context" { + t.Errorf("Expected ContextName to be 'test-context', got: %s", proj.Runtime.ContextName) } }) @@ -174,8 +171,8 @@ func TestNewProject(t *testing.T) { t.Fatalf("Expected no error, got: %v", err) } - if proj.Context.ContextName != "custom-context" { - t.Errorf("Expected ContextName to be 'custom-context', got: %s", proj.Context.ContextName) + if proj.Runtime.ContextName != "custom-context" { + t.Errorf("Expected ContextName to be 'custom-context', got: %s", proj.Runtime.ContextName) } }) @@ -188,8 +185,8 @@ func TestNewProject(t *testing.T) { t.Fatalf("Expected no error, got: %v", err) } - if proj.Context.ContextName != "test-context" { - t.Errorf("Expected ContextName to be 'test-context', got: %s", proj.Context.ContextName) + if proj.Runtime.ContextName != "test-context" { + t.Errorf("Expected ContextName to be 'test-context', got: %s", proj.Runtime.ContextName) } }) @@ -206,8 +203,8 @@ func TestNewProject(t *testing.T) { t.Fatalf("Expected no error, got: %v", err) } - if proj.Context.ContextName != "local" { - t.Errorf("Expected ContextName to be 'local', got: %s", proj.Context.ContextName) + if proj.Runtime.ContextName != "local" { + t.Errorf("Expected ContextName to be 'local', got: %s", proj.Runtime.ContextName) } }) diff --git a/pkg/provisioner/provisioner_test.go b/pkg/provisioner/provisioner_test.go index 76a324c82..864020ddb 100644 --- a/pkg/provisioner/provisioner_test.go +++ b/pkg/provisioner/provisioner_test.go @@ -92,7 +92,7 @@ func setupProvisionerMocks(t *testing.T) *Mocks { kubernetesClient := k8sclient.NewMockKubernetesClient() clusterClient := cluster.NewMockClusterClient() - execCtx := &runtime.Runtime{ + rt := &runtime.Runtime{ ContextName: "test-context", ProjectRoot: "/test/project", ConfigRoot: "/test/project/contexts/test-context", @@ -103,7 +103,7 @@ func setupProvisionerMocks(t *testing.T) *Mocks { } provisionerCtx := &ProvisionerRuntime{ - Runtime: *execCtx, + Runtime: *rt, TerraformStack: terraformStack, KubernetesManager: kubernetesManager, KubernetesClient: kubernetesClient, @@ -825,7 +825,7 @@ func TestProvisioner_Close(t *testing.T) { func TestProvisionerRuntime(t *testing.T) { t.Run("CreatesProvisionerRuntime", func(t *testing.T) { - execCtx := &runtime.Runtime{ + rt := &runtime.Runtime{ ContextName: "test-context", ProjectRoot: "/test/project", ConfigRoot: "/test/project/contexts/test-context", @@ -833,7 +833,7 @@ func TestProvisionerRuntime(t *testing.T) { } provisionerCtx := &ProvisionerRuntime{ - Runtime: *execCtx, + Runtime: *rt, } if provisionerCtx.ContextName != "test-context" { diff --git a/pkg/runtime/runtime_test.go b/pkg/runtime/runtime_test.go index c485d11c2..ea53c58a0 100644 --- a/pkg/runtime/runtime_test.go +++ b/pkg/runtime/runtime_test.go @@ -9,10 +9,10 @@ import ( "testing" v1alpha1 "github.com/windsorcli/cli/api/v1alpha1" + "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/runtime/config" "github.com/windsorcli/cli/pkg/runtime/secrets" "github.com/windsorcli/cli/pkg/runtime/shell" - "github.com/windsorcli/cli/pkg/di" ) // ============================================================================= @@ -93,29 +93,29 @@ func setupEnvironmentMocks(t *testing.T) *Mocks { injector.Register("contextName", "test-context") // Create execution context - paths will be set automatically by NewRuntime - execCtx := &Runtime{ + rt := &Runtime{ Injector: injector, } - ctx, err := NewRuntime(execCtx) + ctx, err := NewRuntime(rt) if err != nil { t.Fatalf("Failed to create context: %v", err) } return &Mocks{ - Injector: injector, - ConfigHandler: configHandler, - Shell: shell, - Runtime: ctx, + Injector: injector, + ConfigHandler: configHandler, + Shell: shell, + Runtime: ctx, } } // Mocks contains all the mock dependencies for testing type Mocks struct { - Injector di.Injector - ConfigHandler config.ConfigHandler - Shell shell.Shell - Runtime *Runtime + Injector di.Injector + ConfigHandler config.ConfigHandler + Shell shell.Shell + Runtime *Runtime } // ============================================================================= @@ -307,7 +307,6 @@ func TestRuntime_LoadEnvironment(t *testing.T) { }) } - // ============================================================================= // ============================================================================= @@ -581,7 +580,6 @@ func TestRuntime_LoadEnvironment_WithSecrets(t *testing.T) { }) } - func TestRuntime_initializeComponents_EdgeCases(t *testing.T) { t.Run("HandlesToolsManagerInitializationError", func(t *testing.T) { mocks := setupEnvironmentMocks(t)