From 21577f4c72c1617e0aac00bd6abf3e661cb649aa Mon Sep 17 00:00:00 2001 From: Ryan VanGundy Date: Tue, 1 Jul 2025 19:11:44 -0400 Subject: [PATCH] feat(cmd): Add windsor bundle command --- cmd/bundle.go | 74 ++++ cmd/bundle_test.go | 427 +++++++++++++++++++++ pkg/controller/controller.go | 114 ++++++ pkg/controller/controller_test.go | 608 ++++++++++++++++++++++++++++++ 4 files changed, 1223 insertions(+) create mode 100644 cmd/bundle.go create mode 100644 cmd/bundle_test.go diff --git a/cmd/bundle.go b/cmd/bundle.go new file mode 100644 index 000000000..423ecbdb7 --- /dev/null +++ b/cmd/bundle.go @@ -0,0 +1,74 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + ctrl "github.com/windsorcli/cli/pkg/controller" +) + +// bundleCmd represents the bundle command +var bundleCmd = &cobra.Command{ + Use: "bundle", + Short: "Bundle blueprints into distributable artifacts", + Long: `Bundle blueprints into distributable artifacts for deployment. + +This command packages your Windsor blueprints into compressed archives that can be +distributed and deployed to target environments. The bundling process includes: + +- Template bundling: Packages Jsonnet templates from contexts/_template/ +- Kustomize bundling: Packages Kustomize configurations +- Metadata generation: Creates deployment metadata with build information +- Archive creation: Compresses everything into .tar.gz format + +The resulting artifacts are compatible with FluxCD OCI registries and other +deployment systems that support compressed archives. + +Tag format is required as "name:version". If metadata.yaml +exists, tag values override metadata values. Tag is required if no metadata.yaml +exists or if metadata.yaml lacks name/version fields.`, + RunE: func(cmd *cobra.Command, args []string) error { + controller := cmd.Context().Value(controllerKey).(ctrl.Controller) + + // Initialize with requirements including bundler functionality + if err := controller.InitializeWithRequirements(ctrl.Requirements{ + CommandName: cmd.Name(), + Bundler: true, + }); err != nil { + return fmt.Errorf("failed to initialize controller: %w", err) + } + + // Resolve artifact builder from controller + artifact := controller.ResolveArtifactBuilder() + if artifact == nil { + return fmt.Errorf("artifact builder not available") + } + + // Resolve all bundlers and run them + bundlers := controller.ResolveAllBundlers() + for _, bundler := range bundlers { + if err := bundler.Bundle(artifact); err != nil { + return fmt.Errorf("bundling failed: %w", err) + } + } + + // Get tag and output path from flags + tag, _ := cmd.Flags().GetString("tag") + outputPath, _ := cmd.Flags().GetString("output") + + // Create the final artifact + actualOutputPath, err := artifact.Create(outputPath, tag) + if err != nil { + return fmt.Errorf("failed to create artifact: %w", err) + } + + fmt.Printf("Blueprint bundled successfully: %s\n", actualOutputPath) + return nil + }, +} + +func init() { + rootCmd.AddCommand(bundleCmd) + bundleCmd.Flags().StringP("output", "o", ".", "Output path for bundle archive (file or directory)") + bundleCmd.Flags().StringP("tag", "t", "", "Tag in 'name:version' format (required if no metadata.yaml or missing name/version)") +} diff --git a/cmd/bundle_test.go b/cmd/bundle_test.go new file mode 100644 index 000000000..82f62f11d --- /dev/null +++ b/cmd/bundle_test.go @@ -0,0 +1,427 @@ +package cmd + +import ( + "fmt" + "testing" + + "github.com/windsorcli/cli/pkg/bundler" + "github.com/windsorcli/cli/pkg/controller" + "github.com/windsorcli/cli/pkg/di" +) + +// ============================================================================= +// Types +// ============================================================================= + +// Extend Mocks with additional fields needed for bundle command tests +type BundleMocks struct { + *Mocks + ArtifactBuilder *bundler.MockArtifact + TemplateBundler *bundler.MockBundler + KustomizeBundler *bundler.MockBundler +} + +// ============================================================================= +// Helpers +// ============================================================================= + +func setupBundleMocks(t *testing.T) *BundleMocks { + setup := func(t *testing.T) *BundleMocks { + t.Helper() + opts := &SetupOptions{ + ConfigStr: ` +contexts: + default: + bundler: + enabled: true`, + } + mocks := setupMocks(t, opts) + + // Create mock artifact builder + artifactBuilder := bundler.NewMockArtifact() + artifactBuilder.InitializeFunc = func(injector di.Injector) error { return nil } + artifactBuilder.AddFileFunc = func(path string, content []byte) error { return nil } + artifactBuilder.CreateFunc = func(outputPath string, tag string) (string, error) { + if tag != "" { + return "test-v1.0.0.tar.gz", nil + } + return "blueprint-v1.0.0.tar.gz", nil + } + + // Create mock template bundler + templateBundler := bundler.NewMockBundler() + templateBundler.InitializeFunc = func(injector di.Injector) error { return nil } + templateBundler.BundleFunc = func(artifact bundler.Artifact) error { return nil } + + // Create mock kustomize bundler + kustomizeBundler := bundler.NewMockBundler() + kustomizeBundler.InitializeFunc = func(injector di.Injector) error { return nil } + kustomizeBundler.BundleFunc = func(artifact bundler.Artifact) error { return nil } + + // Set up controller mocks + mocks.Controller.InitializeWithRequirementsFunc = func(req controller.Requirements) error { + if req.Bundler { + return nil + } + return fmt.Errorf("bundler requirement not met") + } + + // Register bundler components in the injector + mocks.Injector.Register("artifactBuilder", artifactBuilder) + mocks.Injector.Register("templateBundler", templateBundler) + mocks.Injector.Register("kustomizeBundler", kustomizeBundler) + + return &BundleMocks{ + Mocks: mocks, + ArtifactBuilder: artifactBuilder, + TemplateBundler: templateBundler, + KustomizeBundler: kustomizeBundler, + } + } + + return setup(t) +} + +// ============================================================================= +// Tests +// ============================================================================= + +func TestBundleCmd(t *testing.T) { + t.Run("SuccessWithDefaults", func(t *testing.T) { + // Given a properly configured bundle environment + mocks := setupBundleMocks(t) + + // When executing the bundle command with defaults + rootCmd.SetArgs([]string{"bundle"}) + err := Execute(mocks.Controller) + + // Then no error should be returned + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + t.Run("SuccessWithTag", func(t *testing.T) { + // Given a properly configured bundle environment + mocks := setupBundleMocks(t) + + // When executing the bundle command with tag + rootCmd.SetArgs([]string{"bundle", "--tag", "myproject:v2.0.0"}) + err := Execute(mocks.Controller) + + // Then no error should be returned + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + t.Run("SuccessWithCustomOutput", func(t *testing.T) { + // Given a properly configured bundle environment + mocks := setupBundleMocks(t) + + // When executing the bundle command with custom output path + rootCmd.SetArgs([]string{"bundle", "--output", "/tmp/my-bundle.tar.gz", "--tag", "test:v1.0.0"}) + err := Execute(mocks.Controller) + + // Then no error should be returned + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + t.Run("SuccessWithShortFlags", func(t *testing.T) { + // Given a properly configured bundle environment + mocks := setupBundleMocks(t) + + // When executing the bundle command with short flags + rootCmd.SetArgs([]string{"bundle", "-o", "output/", "-t", "myapp:v1.2.3"}) + err := Execute(mocks.Controller) + + // Then no error should be returned + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + t.Run("ErrorInitializingController", func(t *testing.T) { + // Given a bundle environment with failing controller initialization + mocks := setupBundleMocks(t) + mocks.Controller.InitializeWithRequirementsFunc = func(req controller.Requirements) error { + return fmt.Errorf("failed to initialize controller") + } + + // When executing the bundle command + rootCmd.SetArgs([]string{"bundle"}) + err := Execute(mocks.Controller) + + // Then an error should be returned + if err == nil { + t.Error("Expected error, got nil") + } + expectedError := "failed to initialize controller: failed to initialize controller" + if err.Error() != expectedError { + t.Errorf("Expected error %q, got %q", expectedError, err.Error()) + } + }) + + t.Run("ErrorArtifactBuilderNotAvailable", func(t *testing.T) { + // Given a bundle environment with no artifact builder + mocks := setupBundleMocks(t) + // Don't register the artifact builder to simulate it being unavailable + mocks.Injector.Register("artifactBuilder", nil) + + // When executing the bundle command + rootCmd.SetArgs([]string{"bundle"}) + err := Execute(mocks.Controller) + + // Then an error should be returned + if err == nil { + t.Error("Expected error, got nil") + } + expectedError := "artifact builder not available" + if err.Error() != expectedError { + t.Errorf("Expected error %q, got %q", expectedError, err.Error()) + } + }) + + t.Run("ErrorTemplateBundlerFails", func(t *testing.T) { + // Given a bundle environment with failing template bundler + mocks := setupBundleMocks(t) + mocks.TemplateBundler.BundleFunc = func(artifact bundler.Artifact) error { + return fmt.Errorf("template bundling failed") + } + + // When executing the bundle command + rootCmd.SetArgs([]string{"bundle"}) + err := Execute(mocks.Controller) + + // Then an error should be returned + if err == nil { + t.Error("Expected error, got nil") + } + expectedError := "bundling failed: template bundling failed" + if err.Error() != expectedError { + t.Errorf("Expected error %q, got %q", expectedError, err.Error()) + } + }) + + t.Run("ErrorKustomizeBundlerFails", func(t *testing.T) { + // Given a bundle environment with failing kustomize bundler + mocks := setupBundleMocks(t) + mocks.KustomizeBundler.BundleFunc = func(artifact bundler.Artifact) error { + return fmt.Errorf("kustomize bundling failed") + } + + // When executing the bundle command + rootCmd.SetArgs([]string{"bundle"}) + err := Execute(mocks.Controller) + + // Then an error should be returned + if err == nil { + t.Error("Expected error, got nil") + } + expectedError := "bundling failed: kustomize bundling failed" + if err.Error() != expectedError { + t.Errorf("Expected error %q, got %q", expectedError, err.Error()) + } + }) + + t.Run("ErrorArtifactCreationFails", func(t *testing.T) { + // Given a bundle environment with failing artifact creation + mocks := setupBundleMocks(t) + mocks.ArtifactBuilder.CreateFunc = func(outputPath string, tag string) (string, error) { + return "", fmt.Errorf("artifact creation failed") + } + + // When executing the bundle command + rootCmd.SetArgs([]string{"bundle"}) + err := Execute(mocks.Controller) + + // Then an error should be returned + if err == nil { + t.Error("Expected error, got nil") + } + expectedError := "failed to create artifact: artifact creation failed" + if err.Error() != expectedError { + t.Errorf("Expected error %q, got %q", expectedError, err.Error()) + } + }) + + t.Run("VerifyBundlerRequirementPassed", func(t *testing.T) { + // Given a bundle environment that validates requirements + mocks := setupBundleMocks(t) + var receivedRequirements controller.Requirements + mocks.Controller.InitializeWithRequirementsFunc = func(req controller.Requirements) error { + receivedRequirements = req + return nil + } + + // When executing the bundle command + rootCmd.SetArgs([]string{"bundle"}) + err := Execute(mocks.Controller) + + // Then no error should be returned + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + // And the bundler requirement should be set + if !receivedRequirements.Bundler { + t.Error("Expected bundler requirement to be true") + } + + // And the command name should be correct + if receivedRequirements.CommandName != "bundle" { + t.Errorf("Expected command name to be 'bundle', got %s", receivedRequirements.CommandName) + } + }) + + t.Run("VerifyAllBundlersAreCalled", func(t *testing.T) { + // Given a bundle environment that tracks bundler calls + mocks := setupBundleMocks(t) + templateBundlerCalled := false + kustomizeBundlerCalled := false + + mocks.TemplateBundler.BundleFunc = func(artifact bundler.Artifact) error { + templateBundlerCalled = true + return nil + } + mocks.KustomizeBundler.BundleFunc = func(artifact bundler.Artifact) error { + kustomizeBundlerCalled = true + return nil + } + + // When executing the bundle command + rootCmd.SetArgs([]string{"bundle"}) + err := Execute(mocks.Controller) + + // Then no error should be returned + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + // And both bundlers should be called + if !templateBundlerCalled { + t.Error("Expected template bundler to be called") + } + if !kustomizeBundlerCalled { + t.Error("Expected kustomize bundler to be called") + } + }) + + t.Run("VerifyCorrectParametersPassedToArtifact", func(t *testing.T) { + // Given a bundle environment that tracks artifact parameters + mocks := setupBundleMocks(t) + var receivedOutputPath, receivedTag string + + mocks.ArtifactBuilder.CreateFunc = func(outputPath string, tag string) (string, error) { + receivedOutputPath = outputPath + receivedTag = tag + return "test-result.tar.gz", nil + } + + // When executing the bundle command with specific parameters + rootCmd.SetArgs([]string{"bundle", "--output", "/custom/path", "--tag", "myapp:v2.1.0"}) + err := Execute(mocks.Controller) + + // Then no error should be returned + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + // And the correct parameters should be passed + if receivedOutputPath != "/custom/path" { + t.Errorf("Expected output path '/custom/path', got %s", receivedOutputPath) + } + if receivedTag != "myapp:v2.1.0" { + t.Errorf("Expected tag 'myapp:v2.1.0', got %s", receivedTag) + } + }) + + t.Run("VerifyDefaultOutputPath", func(t *testing.T) { + // Given a fresh bundle environment that tracks artifact parameters + mocks := setupBundleMocks(t) + var receivedOutputPath string + + // Reset command flags to avoid state contamination + bundleCmd.ResetFlags() + bundleCmd.Flags().StringP("output", "o", ".", "Output path for bundle archive (file or directory)") + bundleCmd.Flags().StringP("tag", "t", "", "Tag in 'name:version' format (required if no metadata.yaml or missing name/version)") + + // Create a fresh artifact builder to avoid state contamination + freshArtifactBuilder := bundler.NewMockArtifact() + freshArtifactBuilder.InitializeFunc = func(injector di.Injector) error { return nil } + freshArtifactBuilder.AddFileFunc = func(path string, content []byte) error { return nil } + freshArtifactBuilder.CreateFunc = func(outputPath string, tag string) (string, error) { + receivedOutputPath = outputPath + return "test-result.tar.gz", nil + } + mocks.Injector.Register("artifactBuilder", freshArtifactBuilder) + + // When executing the bundle command without output flag + rootCmd.SetArgs([]string{"bundle", "--tag", "test:v1.0.0"}) + err := Execute(mocks.Controller) + + // Then no error should be returned + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + // And the default output path should be used + if receivedOutputPath != "." { + t.Errorf("Expected default output path '.', got %s", receivedOutputPath) + } + }) + + t.Run("VerifyEmptyTagHandling", func(t *testing.T) { + // Given a fresh bundle environment that tracks artifact parameters + mocks := setupBundleMocks(t) + var receivedTag string + + // Reset command flags to avoid state contamination + bundleCmd.ResetFlags() + bundleCmd.Flags().StringP("output", "o", ".", "Output path for bundle archive (file or directory)") + bundleCmd.Flags().StringP("tag", "t", "", "Tag in 'name:version' format (required if no metadata.yaml or missing name/version)") + + // Create a fresh artifact builder to avoid state contamination + freshArtifactBuilder := bundler.NewMockArtifact() + freshArtifactBuilder.InitializeFunc = func(injector di.Injector) error { return nil } + freshArtifactBuilder.AddFileFunc = func(path string, content []byte) error { return nil } + freshArtifactBuilder.CreateFunc = func(outputPath string, tag string) (string, error) { + receivedTag = tag + return "test-result.tar.gz", nil + } + mocks.Injector.Register("artifactBuilder", freshArtifactBuilder) + + // When executing the bundle command without tag flag + rootCmd.SetArgs([]string{"bundle"}) + err := Execute(mocks.Controller) + + // Then no error should be returned + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + // And an empty tag should be passed + if receivedTag != "" { + t.Errorf("Expected empty tag, got %s", receivedTag) + } + }) + + t.Run("VerifyNoBundlersHandling", func(t *testing.T) { + // Given a bundle environment with no bundlers + mocks := setupBundleMocks(t) + // Don't register any bundlers to simulate empty list + mocks.Injector.Register("templateBundler", nil) + mocks.Injector.Register("kustomizeBundler", nil) + + // When executing the bundle command + rootCmd.SetArgs([]string{"bundle"}) + err := Execute(mocks.Controller) + + // Then no error should be returned (empty bundlers list is valid) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) +} diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index eee7d9572..0756a24f2 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -8,6 +8,7 @@ import ( secretsConfigType "github.com/windsorcli/cli/api/v1alpha1/secrets" "github.com/windsorcli/cli/pkg/blueprint" + "github.com/windsorcli/cli/pkg/bundler" "github.com/windsorcli/cli/pkg/cluster" "github.com/windsorcli/cli/pkg/config" "github.com/windsorcli/cli/pkg/di" @@ -57,6 +58,8 @@ type Controller interface { ResolveContainerRuntime() virt.ContainerRuntime ResolveStack() stack.Stack ResolveAllGenerators() []generators.Generator + ResolveArtifactBuilder() bundler.Artifact + ResolveAllBundlers() []bundler.Bundler WriteConfigurationFiles() error SetEnvironmentVariables() error ResolveKubernetesManager() kubernetes.KubernetesManager @@ -115,6 +118,10 @@ type ComponentConstructors struct { NewWindsorStack func(di.Injector) stack.Stack NewTalosClusterClient func(di.Injector) *cluster.TalosClusterClient + + NewArtifactBuilder func(di.Injector) bundler.Artifact + NewTemplateBundler func(di.Injector) bundler.Bundler + NewKustomizeBundler func(di.Injector) bundler.Bundler } // Requirements defines the operational requirements for the controller @@ -144,6 +151,7 @@ type Requirements struct { Blueprint bool // Needs blueprint handling Generators bool // Needs code generation Stack bool // Needs stack components + Bundler bool // Needs bundler and artifact functionality // Command info for context-specific decisions CommandName string // Name of the command @@ -276,6 +284,16 @@ func NewDefaultConstructors() ComponentConstructors { NewTalosClusterClient: func(injector di.Injector) *cluster.TalosClusterClient { return cluster.NewTalosClusterClient(injector) }, + + NewArtifactBuilder: func(injector di.Injector) bundler.Artifact { + return bundler.NewArtifactBuilder() + }, + NewTemplateBundler: func(injector di.Injector) bundler.Bundler { + return bundler.NewTemplateBundler() + }, + NewKustomizeBundler: func(injector di.Injector) bundler.Bundler { + return bundler.NewKustomizeBundler() + }, } } @@ -316,6 +334,7 @@ func (c *BaseController) CreateComponents() error { {"network", c.createNetworkComponents}, {"stack", c.createStackComponent}, {"blueprint", c.createBlueprintComponent}, + {"bundler", c.createBundlerComponents}, } for _, cc := range componentCreators { @@ -418,6 +437,22 @@ func (c *BaseController) InitializeComponents() error { } } + artifactBuilder := c.ResolveArtifactBuilder() + if artifactBuilder != nil { + if err := artifactBuilder.Initialize(c.injector); err != nil { + return fmt.Errorf("error initializing artifact builder: %w", err) + } + } + + bundlers := c.ResolveAllBundlers() + if len(bundlers) > 0 { + for _, bundler := range bundlers { + if err := bundler.Initialize(c.injector); err != nil { + return fmt.Errorf("error initializing bundler: %w", err) + } + } + } + stack := c.ResolveStack() if stack != nil { if err := stack.Initialize(); err != nil { @@ -658,6 +693,26 @@ func (c *BaseController) ResolveAllGenerators() []generators.Generator { return generatorsInstances } +// ResolveArtifactBuilder returns the artifact builder component +// It retrieves the artifact builder from the dependency injection container +func (c *BaseController) ResolveArtifactBuilder() bundler.Artifact { + instance := c.injector.Resolve("artifactBuilder") + artifactBuilder, _ := instance.(bundler.Artifact) + return artifactBuilder +} + +// ResolveAllBundlers returns all configured bundlers +// It retrieves all bundlers from the dependency injection container +func (c *BaseController) ResolveAllBundlers() []bundler.Bundler { + instances, _ := c.injector.ResolveAll((*bundler.Bundler)(nil)) + bundlerInstances := make([]bundler.Bundler, 0, len(instances)) + for _, instance := range instances { + bundlerInstance, _ := instance.(bundler.Bundler) + bundlerInstances = append(bundlerInstances, bundlerInstance) + } + return bundlerInstances +} + // SetEnvironmentVariables configures the environment for all components // It sets environment variables from all configured environment printers func (c *BaseController) SetEnvironmentVariables() error { @@ -1211,6 +1266,65 @@ func (c *BaseController) createClusterComponents(req Requirements) error { return nil } +// createBundlerComponents creates bundler components if required +// It sets up the artifact builder and all bundlers for blueprint packaging +func (c *BaseController) createBundlerComponents(req Requirements) error { + if !req.Bundler { + return nil + } + + // Create artifact builder if not already exists + if existingArtifactBuilder := c.ResolveArtifactBuilder(); existingArtifactBuilder == nil { + if c.constructors.NewArtifactBuilder == nil { + return fmt.Errorf("NewArtifactBuilder constructor is nil") + } + artifactBuilder := c.constructors.NewArtifactBuilder(c.injector) + if artifactBuilder == nil { + return fmt.Errorf("NewArtifactBuilder returned nil") + } + c.injector.Register("artifactBuilder", artifactBuilder) + } + + // Create bundlers if not already exist + existingBundlers := c.ResolveAllBundlers() + existingBundlerNames := make(map[string]bool) + + // Track existing bundlers + for _, bundler := range existingBundlers { + if c.injector.Resolve("templateBundler") == bundler { + existingBundlerNames["templateBundler"] = true + } else if c.injector.Resolve("kustomizeBundler") == bundler { + existingBundlerNames["kustomizeBundler"] = true + } + } + + // Create template bundler if not exists + if !existingBundlerNames["templateBundler"] { + if c.constructors.NewTemplateBundler == nil { + return fmt.Errorf("NewTemplateBundler constructor is nil") + } + templateBundler := c.constructors.NewTemplateBundler(c.injector) + if templateBundler == nil { + return fmt.Errorf("NewTemplateBundler returned nil") + } + c.injector.Register("templateBundler", templateBundler) + } + + // Create kustomize bundler if not exists + if !existingBundlerNames["kustomizeBundler"] { + if c.constructors.NewKustomizeBundler == nil { + return fmt.Errorf("NewKustomizeBundler constructor is nil") + } + kustomizeBundler := c.constructors.NewKustomizeBundler(c.injector) + if kustomizeBundler == nil { + return fmt.Errorf("NewKustomizeBundler returned nil") + } + c.injector.Register("kustomizeBundler", kustomizeBundler) + } + + return nil +} + // ============================================================================= // Interface compliance // ============================================================================= diff --git a/pkg/controller/controller_test.go b/pkg/controller/controller_test.go index 5fc585f34..608b488df 100644 --- a/pkg/controller/controller_test.go +++ b/pkg/controller/controller_test.go @@ -11,6 +11,7 @@ import ( "github.com/windsorcli/cli/api/v1alpha1/docker" secretsConfigType "github.com/windsorcli/cli/api/v1alpha1/secrets" "github.com/windsorcli/cli/pkg/blueprint" + "github.com/windsorcli/cli/pkg/bundler" "github.com/windsorcli/cli/pkg/config" "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/env" @@ -340,6 +341,27 @@ func TestNewController(t *testing.T) { } return nil }, + "NewArtifactBuilder": func() error { + builder := controller.constructors.NewArtifactBuilder(mocks.Injector) + if builder == nil { + return fmt.Errorf("NewArtifactBuilder returned nil") + } + return nil + }, + "NewTemplateBundler": func() error { + bundler := controller.constructors.NewTemplateBundler(mocks.Injector) + if bundler == nil { + return fmt.Errorf("NewTemplateBundler returned nil") + } + return nil + }, + "NewKustomizeBundler": func() error { + bundler := controller.constructors.NewKustomizeBundler(mocks.Injector) + if bundler == nil { + return fmt.Errorf("NewKustomizeBundler returned nil") + } + return nil + }, } // When each constructor is tested for name, test := range constructorTests { @@ -381,6 +403,7 @@ func TestBaseController_SetRequirements(t *testing.T) { Blueprint: true, Generators: true, Stack: true, + Bundler: true, CommandName: "test-command", Flags: map[string]bool{"verbose": true}, } @@ -4060,3 +4083,588 @@ func TestBaseController_createKubernetesComponents(t *testing.T) { } }) } + +// ============================================================================= +// Test Bundler Components +// ============================================================================= + +func TestBaseController_ResolveArtifactBuilder(t *testing.T) { + setup := func(t *testing.T) (*BaseController, *Mocks) { + t.Helper() + mocks := setupMocks(t) + controller := NewController(mocks.Injector) + return controller, mocks + } + + t.Run("ReturnsArtifactBuilderWhenRegistered", func(t *testing.T) { + // Given a controller with a registered artifact builder + controller, mocks := setup(t) + artifactBuilder := &bundler.MockArtifact{} + mocks.Injector.Register("artifactBuilder", artifactBuilder) + + // When resolving the artifact builder + resolvedBuilder := controller.ResolveArtifactBuilder() + + // Then the registered artifact builder should be returned + if resolvedBuilder != artifactBuilder { + t.Errorf("Expected artifact builder to be %v, got %v", artifactBuilder, resolvedBuilder) + } + }) + + t.Run("ReturnsNilWhenArtifactBuilderNotRegistered", func(t *testing.T) { + // Given a controller with no artifact builder registered + controller, mocks := setup(t) + mocks.Injector.Register("artifactBuilder", nil) + + // When resolving the artifact builder + builder := controller.ResolveArtifactBuilder() + + // Then nil should be returned + if builder != nil { + t.Errorf("Expected artifact builder to be nil, got %v", builder) + } + }) +} + +func TestBaseController_ResolveAllBundlers(t *testing.T) { + setup := func(t *testing.T) (*BaseController, *Mocks) { + t.Helper() + mocks := setupMocks(t) + controller := NewController(mocks.Injector) + return controller, mocks + } + + t.Run("ReturnsAllRegisteredBundlers", func(t *testing.T) { + // Given a controller with multiple bundlers registered + controller, mocks := setup(t) + templateBundler := &bundler.MockBundler{} + kustomizeBundler := &bundler.MockBundler{} + mocks.Injector.Register("templateBundler", templateBundler) + mocks.Injector.Register("kustomizeBundler", kustomizeBundler) + + // When resolving all bundlers + bundlers := controller.ResolveAllBundlers() + + // Then all registered bundlers should be returned + if len(bundlers) != 2 { + t.Errorf("Expected 2 bundlers, got %d", len(bundlers)) + } + + // And the bundlers should include both registered ones + foundTemplate := false + foundKustomize := false + for _, b := range bundlers { + if b == templateBundler { + foundTemplate = true + } + if b == kustomizeBundler { + foundKustomize = true + } + } + + if !foundTemplate { + t.Error("Expected template bundler to be in returned bundlers") + } + if !foundKustomize { + t.Error("Expected kustomize bundler to be in returned bundlers") + } + }) + + t.Run("ReturnsEmptySliceWhenNoBundlersRegistered", func(t *testing.T) { + // Given a controller with no bundlers registered + controller, mocks := setup(t) + mocks.Injector.Register("templateBundler", nil) + mocks.Injector.Register("kustomizeBundler", nil) + + // When resolving all bundlers + bundlers := controller.ResolveAllBundlers() + + // Then an empty slice should be returned + if len(bundlers) != 0 { + t.Errorf("Expected 0 bundlers, got %d", len(bundlers)) + } + }) + + t.Run("HandlesPartialBundlerRegistration", func(t *testing.T) { + // Given a controller with only one bundler registered + controller, mocks := setup(t) + templateBundler := &bundler.MockBundler{} + mocks.Injector.Register("templateBundler", templateBundler) + mocks.Injector.Register("kustomizeBundler", nil) + + // When resolving all bundlers + bundlers := controller.ResolveAllBundlers() + + // Then only the registered bundler should be returned + if len(bundlers) != 1 { + t.Errorf("Expected 1 bundler, got %d", len(bundlers)) + } + + if bundlers[0] != templateBundler { + t.Error("Expected template bundler to be the only returned bundler") + } + }) +} + +func TestBaseController_createBundlerComponents(t *testing.T) { + setup := func(t *testing.T) (*BaseController, *Mocks) { + t.Helper() + mocks := setupMocks(t) + controller := NewController(mocks.Injector) + return controller, mocks + } + + t.Run("ReturnsEarlyWhenBundlerNotRequired", func(t *testing.T) { + // Given bundler is not required + controller, _ := setup(t) + + // When creating bundler components with no bundler requirement + err := controller.createBundlerComponents(Requirements{ + Bundler: false, + }) + + // Then no error should be returned + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + t.Run("CreatesArtifactBuilderWhenRequired", func(t *testing.T) { + // Given bundler is required and no existing artifact builder + controller, mocks := setup(t) + mockArtifactBuilder := &bundler.MockArtifact{} + controller.constructors.NewArtifactBuilder = func(di.Injector) bundler.Artifact { + return mockArtifactBuilder + } + + // When creating bundler components with bundler requirement + err := controller.createBundlerComponents(Requirements{ + Bundler: true, + }) + + // Then artifact builder should be created and registered + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + resolvedBuilder := mocks.Injector.Resolve("artifactBuilder") + if resolvedBuilder != mockArtifactBuilder { + t.Error("Expected artifact builder to be registered") + } + }) + + t.Run("DoesNotCreateArtifactBuilderWhenAlreadyExists", func(t *testing.T) { + // Given bundler is required and artifact builder already exists + controller, mocks := setup(t) + existingBuilder := &bundler.MockArtifact{} + mocks.Injector.Register("artifactBuilder", existingBuilder) + + // Track if constructor is called + constructorCalled := false + controller.constructors.NewArtifactBuilder = func(di.Injector) bundler.Artifact { + constructorCalled = true + return &bundler.MockArtifact{} + } + + // When creating bundler components + err := controller.createBundlerComponents(Requirements{ + Bundler: true, + }) + + // Then constructor should not be called + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if constructorCalled { + t.Error("Expected artifact builder constructor not to be called when builder already exists") + } + + // And existing builder should remain + resolvedBuilder := mocks.Injector.Resolve("artifactBuilder") + if resolvedBuilder != existingBuilder { + t.Error("Expected existing artifact builder to remain") + } + }) + + t.Run("CreatesTemplateBundlerWhenRequired", func(t *testing.T) { + // Given bundler is required and no existing template bundler + controller, mocks := setup(t) + mockTemplateBundler := &bundler.MockBundler{} + controller.constructors.NewTemplateBundler = func(di.Injector) bundler.Bundler { + return mockTemplateBundler + } + + // When creating bundler components + err := controller.createBundlerComponents(Requirements{ + Bundler: true, + }) + + // Then template bundler should be created and registered + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + resolvedBundler := mocks.Injector.Resolve("templateBundler") + if resolvedBundler != mockTemplateBundler { + t.Error("Expected template bundler to be registered") + } + }) + + t.Run("DoesNotCreateTemplateBundlerWhenAlreadyExists", func(t *testing.T) { + // Given bundler is required and template bundler already exists + controller, mocks := setup(t) + existingBundler := &bundler.MockBundler{} + mocks.Injector.Register("templateBundler", existingBundler) + + // Track if constructor is called + constructorCalled := false + controller.constructors.NewTemplateBundler = func(di.Injector) bundler.Bundler { + constructorCalled = true + return &bundler.MockBundler{} + } + + // When creating bundler components + err := controller.createBundlerComponents(Requirements{ + Bundler: true, + }) + + // Then constructor should not be called + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if constructorCalled { + t.Error("Expected template bundler constructor not to be called when bundler already exists") + } + + // And existing bundler should remain + resolvedBundler := mocks.Injector.Resolve("templateBundler") + if resolvedBundler != existingBundler { + t.Error("Expected existing template bundler to remain") + } + }) + + t.Run("CreatesKustomizeBundlerWhenRequired", func(t *testing.T) { + // Given bundler is required and no existing kustomize bundler + controller, mocks := setup(t) + mockKustomizeBundler := &bundler.MockBundler{} + controller.constructors.NewKustomizeBundler = func(di.Injector) bundler.Bundler { + return mockKustomizeBundler + } + + // When creating bundler components + err := controller.createBundlerComponents(Requirements{ + Bundler: true, + }) + + // Then kustomize bundler should be created and registered + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + resolvedBundler := mocks.Injector.Resolve("kustomizeBundler") + if resolvedBundler != mockKustomizeBundler { + t.Error("Expected kustomize bundler to be registered") + } + }) + + t.Run("DoesNotCreateKustomizeBundlerWhenAlreadyExists", func(t *testing.T) { + // Given bundler is required and kustomize bundler already exists + controller, mocks := setup(t) + existingBundler := &bundler.MockBundler{} + mocks.Injector.Register("kustomizeBundler", existingBundler) + + // Track if constructor is called + constructorCalled := false + controller.constructors.NewKustomizeBundler = func(di.Injector) bundler.Bundler { + constructorCalled = true + return &bundler.MockBundler{} + } + + // When creating bundler components + err := controller.createBundlerComponents(Requirements{ + Bundler: true, + }) + + // Then constructor should not be called + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if constructorCalled { + t.Error("Expected kustomize bundler constructor not to be called when bundler already exists") + } + + // And existing bundler should remain + resolvedBundler := mocks.Injector.Resolve("kustomizeBundler") + if resolvedBundler != existingBundler { + t.Error("Expected existing kustomize bundler to remain") + } + }) + + t.Run("HandlesNilArtifactBuilderConstructor", func(t *testing.T) { + // Given bundler is required but artifact builder constructor is nil + controller, _ := setup(t) + controller.constructors.NewArtifactBuilder = nil + + // When creating bundler components + err := controller.createBundlerComponents(Requirements{ + Bundler: true, + }) + + // Then an error should be returned + if err == nil { + t.Error("Expected error when artifact builder constructor is nil") + } + if !strings.Contains(err.Error(), "NewArtifactBuilder constructor is nil") { + t.Errorf("Expected error about nil constructor, got %v", err) + } + }) + + t.Run("HandlesNilArtifactBuilderFromConstructor", func(t *testing.T) { + // Given artifact builder constructor returns nil + controller, _ := setup(t) + controller.constructors.NewArtifactBuilder = func(di.Injector) bundler.Artifact { + return nil + } + + // When creating bundler components + err := controller.createBundlerComponents(Requirements{ + Bundler: true, + }) + + // Then an error should be returned + if err == nil { + t.Error("Expected error when artifact builder constructor returns nil") + } + if !strings.Contains(err.Error(), "NewArtifactBuilder returned nil") { + t.Errorf("Expected error about nil artifact builder, got %v", err) + } + }) + + t.Run("HandlesNilTemplateBundlerConstructor", func(t *testing.T) { + // Given bundler is required but template bundler constructor is nil + controller, _ := setup(t) + controller.constructors.NewTemplateBundler = nil + + // When creating bundler components + err := controller.createBundlerComponents(Requirements{ + Bundler: true, + }) + + // Then an error should be returned + if err == nil { + t.Error("Expected error when template bundler constructor is nil") + } + if !strings.Contains(err.Error(), "NewTemplateBundler constructor is nil") { + t.Errorf("Expected error about nil constructor, got %v", err) + } + }) + + t.Run("HandlesNilTemplateBundlerFromConstructor", func(t *testing.T) { + // Given template bundler constructor returns nil + controller, _ := setup(t) + controller.constructors.NewTemplateBundler = func(di.Injector) bundler.Bundler { + return nil + } + + // When creating bundler components + err := controller.createBundlerComponents(Requirements{ + Bundler: true, + }) + + // Then an error should be returned + if err == nil { + t.Error("Expected error when template bundler constructor returns nil") + } + if !strings.Contains(err.Error(), "NewTemplateBundler returned nil") { + t.Errorf("Expected error about nil template bundler, got %v", err) + } + }) + + t.Run("HandlesNilKustomizeBundlerConstructor", func(t *testing.T) { + // Given bundler is required but kustomize bundler constructor is nil + controller, _ := setup(t) + controller.constructors.NewKustomizeBundler = nil + + // When creating bundler components + err := controller.createBundlerComponents(Requirements{ + Bundler: true, + }) + + // Then an error should be returned + if err == nil { + t.Error("Expected error when kustomize bundler constructor is nil") + } + if !strings.Contains(err.Error(), "NewKustomizeBundler constructor is nil") { + t.Errorf("Expected error about nil constructor, got %v", err) + } + }) + + t.Run("HandlesNilKustomizeBundlerFromConstructor", func(t *testing.T) { + // Given kustomize bundler constructor returns nil + controller, _ := setup(t) + controller.constructors.NewKustomizeBundler = func(di.Injector) bundler.Bundler { + return nil + } + + // When creating bundler components + err := controller.createBundlerComponents(Requirements{ + Bundler: true, + }) + + // Then an error should be returned + if err == nil { + t.Error("Expected error when kustomize bundler constructor returns nil") + } + if !strings.Contains(err.Error(), "NewKustomizeBundler returned nil") { + t.Errorf("Expected error about nil kustomize bundler, got %v", err) + } + }) + + t.Run("CreatesAllBundlerComponentsSuccessfully", func(t *testing.T) { + // Given bundler is required and all constructors are valid + controller, mocks := setup(t) + mockArtifactBuilder := &bundler.MockArtifact{} + mockTemplateBundler := &bundler.MockBundler{} + mockKustomizeBundler := &bundler.MockBundler{} + + controller.constructors.NewArtifactBuilder = func(di.Injector) bundler.Artifact { + return mockArtifactBuilder + } + controller.constructors.NewTemplateBundler = func(di.Injector) bundler.Bundler { + return mockTemplateBundler + } + controller.constructors.NewKustomizeBundler = func(di.Injector) bundler.Bundler { + return mockKustomizeBundler + } + + // When creating bundler components + err := controller.createBundlerComponents(Requirements{ + Bundler: true, + }) + + // Then no error should be returned + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + // And all components should be registered + if builder := mocks.Injector.Resolve("artifactBuilder"); builder != mockArtifactBuilder { + t.Error("Expected artifact builder to be registered") + } + if bundler := mocks.Injector.Resolve("templateBundler"); bundler != mockTemplateBundler { + t.Error("Expected template bundler to be registered") + } + if bundler := mocks.Injector.Resolve("kustomizeBundler"); bundler != mockKustomizeBundler { + t.Error("Expected kustomize bundler to be registered") + } + }) +} + +func TestBaseController_BundlerRequirementIntegration(t *testing.T) { + setup := func(t *testing.T) (*BaseController, *Mocks) { + t.Helper() + mocks := setupMocks(t) + controller := NewController(mocks.Injector) + return controller, mocks + } + + t.Run("BundlerRequirementIncludedInSetRequirements", func(t *testing.T) { + // Given a controller + controller, _ := setup(t) + + // When setting requirements with bundler enabled + requirements := Requirements{ + Bundler: true, + CommandName: "bundle", + } + controller.SetRequirements(requirements) + + // Then the bundler requirement should be set + if !controller.requirements.Bundler { + t.Error("Expected bundler requirement to be true") + } + }) + + t.Run("CreateComponentsIncludesBundlerComponents", func(t *testing.T) { + // Given a controller with bundler requirements + controller, mocks := setup(t) + + // And valid bundler constructors + mockArtifactBuilder := &bundler.MockArtifact{} + mockTemplateBundler := &bundler.MockBundler{} + mockKustomizeBundler := &bundler.MockBundler{} + + controller.constructors.NewArtifactBuilder = func(di.Injector) bundler.Artifact { + return mockArtifactBuilder + } + controller.constructors.NewTemplateBundler = func(di.Injector) bundler.Bundler { + return mockTemplateBundler + } + controller.constructors.NewKustomizeBundler = func(di.Injector) bundler.Bundler { + return mockKustomizeBundler + } + + // Set requirements before creating components + controller.SetRequirements(Requirements{ + Bundler: true, + CommandName: "test", + }) + + // When creating components + err := controller.CreateComponents() + + // Then no error should be returned + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + // And bundler components should be created + if builder := mocks.Injector.Resolve("artifactBuilder"); builder == nil { + t.Error("Expected artifact builder to be created during CreateComponents") + } + if bundler := mocks.Injector.Resolve("templateBundler"); bundler == nil { + t.Error("Expected template bundler to be created during CreateComponents") + } + if bundler := mocks.Injector.Resolve("kustomizeBundler"); bundler == nil { + t.Error("Expected kustomize bundler to be created during CreateComponents") + } + }) + + t.Run("InitializeWithRequirementsHandlesBundlerRequirement", func(t *testing.T) { + // Given a controller with valid bundler constructors + controller, _ := setup(t) + mockArtifactBuilder := &bundler.MockArtifact{} + mockTemplateBundler := &bundler.MockBundler{} + mockKustomizeBundler := &bundler.MockBundler{} + + controller.constructors.NewArtifactBuilder = func(di.Injector) bundler.Artifact { + return mockArtifactBuilder + } + controller.constructors.NewTemplateBundler = func(di.Injector) bundler.Bundler { + return mockTemplateBundler + } + controller.constructors.NewKustomizeBundler = func(di.Injector) bundler.Bundler { + return mockKustomizeBundler + } + + // When initializing with bundler requirements + err := controller.InitializeWithRequirements(Requirements{ + Bundler: true, + CommandName: "bundle", + }) + + // Then no error should be returned + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + // And bundler components should be available + if builder := controller.ResolveArtifactBuilder(); builder == nil { + t.Error("Expected artifact builder to be available after InitializeWithRequirements") + } + bundlers := controller.ResolveAllBundlers() + if len(bundlers) == 0 { + t.Error("Expected bundlers to be available after InitializeWithRequirements") + } + }) +}