diff --git a/pkg/blueprint/blueprint_handler.go b/pkg/blueprint/blueprint_handler.go index c8978ffd5..277f87b9b 100644 --- a/pkg/blueprint/blueprint_handler.go +++ b/pkg/blueprint/blueprint_handler.go @@ -1323,7 +1323,7 @@ func (b *BaseBlueprintHandler) hasComponentValues(componentName string) bool { hasUserComponent := false configRoot, err := b.configHandler.GetConfigRoot() if err == nil { - valuesPath := filepath.Join(configRoot, "kustomize", "config.yaml") + valuesPath := filepath.Join(configRoot, "kustomize", "values.yaml") if _, err := b.shims.Stat(valuesPath); err == nil { if data, err := b.shims.ReadFile(valuesPath); err == nil { var values map[string]any @@ -1357,7 +1357,7 @@ func (b *BaseBlueprintHandler) isOCISource(sourceNameOrURL string) bool { return false } -// applyValuesConfigMaps creates ConfigMaps for post-build variable substitution using rendered values data and any existing config.yaml files. +// applyValuesConfigMaps creates ConfigMaps for post-build variable substitution using rendered values data and any existing values.yaml files. // It generates a ConfigMap for the "common" section and for each component section, merging rendered template values with user-defined values. // User-defined values take precedence over template values in case of conflicts. // The resulting ConfigMaps are referenced in PostBuild.SubstituteFrom for variable substitution. @@ -1403,7 +1403,7 @@ func (b *BaseBlueprintHandler) applyValuesConfigMaps() error { } var userValues map[string]any - valuesPath := filepath.Join(configRoot, "kustomize", "config.yaml") + valuesPath := filepath.Join(configRoot, "kustomize", "values.yaml") if _, err := b.shims.Stat(valuesPath); err == nil { data, err := b.shims.ReadFile(valuesPath) if err == nil { @@ -1429,10 +1429,8 @@ func (b *BaseBlueprintHandler) applyValuesConfigMaps() error { } allValues := make(map[string]any) - for k, v := range renderedValues { // Template values are base - allValues[k] = v - } - allValues = b.deepMergeMaps(allValues, userValues) // Deep merge user values over template values + maps.Copy(allValues, renderedValues) + allValues = b.deepMergeMaps(allValues, userValues) if commonValues, exists := allValues["common"]; exists { if commonMap, ok := commonValues.(map[string]any); ok { @@ -1463,47 +1461,83 @@ func (b *BaseBlueprintHandler) applyValuesConfigMaps() error { } // validateValuesForSubstitution checks that all values are valid for Flux post-build variable substitution. -// Permitted types are string, numeric, and boolean. Complex types (maps, slices) are rejected. -// Returns an error if any value is not a supported type. +// Permitted types are string, numeric, and boolean. Allows one level of map nesting if all nested values are scalar. +// Slices and nested complex types are not allowed. Returns an error if any value is not a supported type. func (b *BaseBlueprintHandler) validateValuesForSubstitution(values map[string]any) error { - for key, value := range values { - switch v := value.(type) { - case string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, bool: - continue - case map[string]any, []any: - return fmt.Errorf("values for post-build substitution cannot contain complex types (maps or slices), key '%s' has type %T", key, v) - default: - return fmt.Errorf("values for post-build substitution can only contain strings, numbers, and booleans, key '%s' has unsupported type %T", key, v) + var validate func(map[string]any, string, int) error + validate = func(values map[string]any, parentKey string, depth int) error { + for key, value := range values { + currentKey := key + if parentKey != "" { + currentKey = parentKey + "." + key + } + + switch v := value.(type) { + case string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, bool: + continue + case map[string]any: + if depth >= 1 { + return fmt.Errorf("values for post-build substitution cannot contain nested complex types, key '%s' has type %T", currentKey, v) + } + err := validate(v, currentKey, depth+1) + if err != nil { + return err + } + case []any: + return fmt.Errorf("values for post-build substitution cannot contain slices, key '%s' has type %T", currentKey, v) + default: + return fmt.Errorf("values for post-build substitution can only contain strings, numbers, booleans, or maps of scalar types, key '%s' has unsupported type %T", currentKey, v) + } } + return nil } - return nil + return validate(values, "", 0) } // createConfigMap creates a ConfigMap named configMapName in the "flux-system" namespace for post-build variable substitution. -// Only scalar values (string, int, float, bool) are supported. Complex types are rejected. The resulting ConfigMap data is a map of string keys to string values. +// Supports scalar values and one level of map nesting. The resulting ConfigMap data is a map of string keys to string values. func (b *BaseBlueprintHandler) createConfigMap(values map[string]any, configMapName string) error { if err := b.validateValuesForSubstitution(values); err != nil { return fmt.Errorf("invalid values in %s: %w", configMapName, err) } stringValues := make(map[string]string) + if err := b.flattenValuesToConfigMap(values, "", stringValues); err != nil { + return fmt.Errorf("failed to flatten values for %s: %w", configMapName, err) + } + + if err := b.kubernetesManager.ApplyConfigMap(configMapName, constants.DEFAULT_FLUX_SYSTEM_NAMESPACE, stringValues); err != nil { + return fmt.Errorf("failed to apply ConfigMap %s: %w", configMapName, err) + } + + return nil +} + +// flattenValuesToConfigMap recursively flattens nested values into a flat map suitable for ConfigMap data. +// Nested maps are flattened using dot notation (e.g., "ingress.host"). +func (b *BaseBlueprintHandler) flattenValuesToConfigMap(values map[string]any, prefix string, result map[string]string) error { for key, value := range values { + currentKey := key + if prefix != "" { + currentKey = prefix + "." + key + } + switch v := value.(type) { case string: - stringValues[key] = v + result[currentKey] = v case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64: - stringValues[key] = fmt.Sprintf("%v", v) + result[currentKey] = fmt.Sprintf("%v", v) case bool: - stringValues[key] = fmt.Sprintf("%t", v) + result[currentKey] = fmt.Sprintf("%t", v) + case map[string]any: + err := b.flattenValuesToConfigMap(v, currentKey, result) + if err != nil { + return err + } default: return fmt.Errorf("unsupported value type for key %s: %T", key, v) } } - - if err := b.kubernetesManager.ApplyConfigMap(configMapName, constants.DEFAULT_FLUX_SYSTEM_NAMESPACE, stringValues); err != nil { - return fmt.Errorf("failed to apply ConfigMap %s: %w", configMapName, err) - } - return nil } diff --git a/pkg/blueprint/blueprint_handler_test.go b/pkg/blueprint/blueprint_handler_test.go index 03841da4d..87dc1f2bc 100644 --- a/pkg/blueprint/blueprint_handler_test.go +++ b/pkg/blueprint/blueprint_handler_test.go @@ -3672,20 +3672,20 @@ func TestBaseBlueprintHandler_applyValuesConfigMaps(t *testing.T) { return []string{} } - // And mock centralized config.yaml with component values + // And mock centralized values.yaml with component values handler.shims.Stat = func(name string) (os.FileInfo, error) { if name == filepath.Join("/test/config", "kustomize") { return &mockFileInfo{name: "kustomize"}, nil } - if name == filepath.Join("/test/config", "kustomize", "config.yaml") { - return &mockFileInfo{name: "config.yaml"}, nil + if name == filepath.Join("/test/config", "kustomize", "values.yaml") { + return &mockFileInfo{name: "values.yaml"}, nil } return nil, os.ErrNotExist } // And mock file read for centralized values handler.shims.ReadFile = func(name string) ([]byte, error) { - if name == filepath.Join("/test/config", "kustomize", "config.yaml") { + if name == filepath.Join("/test/config", "kustomize", "values.yaml") { return []byte(`common: domain: example.com ingress: @@ -4058,17 +4058,17 @@ func TestBaseBlueprintHandler_toFluxKustomization_WithValuesConfigMaps(t *testin return "/test/config", nil } - // And mock that global config.yaml exists + // And mock that global values.yaml exists handler.shims.Stat = func(name string) (os.FileInfo, error) { - if name == filepath.Join("/test/config", "kustomize", "config.yaml") { - return &mockFileInfo{name: "config.yaml"}, nil + if name == filepath.Join("/test/config", "kustomize", "values.yaml") { + return &mockFileInfo{name: "values.yaml"}, nil } return nil, os.ErrNotExist } - // And mock the config.yaml content with ingress component + // And mock the values.yaml content with ingress component handler.shims.ReadFile = func(name string) ([]byte, error) { - if name == filepath.Join("/test/config", "kustomize", "config.yaml") { + if name == filepath.Join("/test/config", "kustomize", "values.yaml") { return []byte(`ingress: key: value`), nil } @@ -4142,10 +4142,10 @@ func TestBaseBlueprintHandler_toFluxKustomization_WithValuesConfigMaps(t *testin return "/test/config", nil } - // And mock that global config.yaml exists + // And mock that global values.yaml exists handler.shims.Stat = func(name string) (os.FileInfo, error) { - if name == filepath.Join("/test/config", "kustomize", "config.yaml") { - return &mockFileInfo{name: "config.yaml"}, nil + if name == filepath.Join("/test/config", "kustomize", "values.yaml") { + return &mockFileInfo{name: "values.yaml"}, nil } return nil, os.ErrNotExist } diff --git a/pkg/pipelines/env_test.go b/pkg/pipelines/env_test.go index 7fe0d1294..316091205 100644 --- a/pkg/pipelines/env_test.go +++ b/pkg/pipelines/env_test.go @@ -24,14 +24,6 @@ type EnvMocks struct { Shims *Shims } -func setupEnvShims(t *testing.T) *Shims { - t.Helper() - shims := setupShims(t) - - // Add any env-specific shim overrides here if needed - return shims -} - func setupEnvMocks(t *testing.T, opts ...*SetupOptions) *EnvMocks { t.Helper() diff --git a/pkg/pipelines/exec_test.go b/pkg/pipelines/exec_test.go index cccbfe206..ab2d57e1c 100644 --- a/pkg/pipelines/exec_test.go +++ b/pkg/pipelines/exec_test.go @@ -16,11 +16,6 @@ type ExecMocks struct { *Mocks } -func setupExecShims(t *testing.T) *Shims { - t.Helper() - return setupShims(t) -} - func setupExecMocks(t *testing.T, opts ...*SetupOptions) *ExecMocks { t.Helper() diff --git a/pkg/pipelines/hook_test.go b/pkg/pipelines/hook_test.go index cc656601d..9bb5c3bae 100644 --- a/pkg/pipelines/hook_test.go +++ b/pkg/pipelines/hook_test.go @@ -15,12 +15,6 @@ type HookMocks struct { *Mocks } -// setupHookShims creates shims for hook pipeline tests -func setupHookShims(t *testing.T) *Shims { - t.Helper() - return setupShims(t) -} - // setupHookMocks creates mocks for hook pipeline tests func setupHookMocks(t *testing.T, opts ...*SetupOptions) *HookMocks { t.Helper() diff --git a/pkg/pipelines/init.go b/pkg/pipelines/init.go index 1088df646..bac01cf43 100644 --- a/pkg/pipelines/init.go +++ b/pkg/pipelines/init.go @@ -11,7 +11,6 @@ import ( "github.com/windsorcli/cli/pkg/artifact" "github.com/windsorcli/cli/pkg/blueprint" "github.com/windsorcli/cli/pkg/config" - "github.com/windsorcli/cli/pkg/constants" "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/env" "github.com/windsorcli/cli/pkg/generators" @@ -307,20 +306,6 @@ func (p *InitPipeline) Execute(ctx context.Context) error { // Private Methods // ============================================================================= -// determineContextName selects the context name from ctx, config, or defaults to "local" if unset or "local". -func (p *InitPipeline) determineContextName(ctx context.Context) string { - if contextName := ctx.Value("contextName"); contextName != nil { - if name, ok := contextName.(string); ok { - return name - } - } - currentContext := p.configHandler.GetContext() - if currentContext != "" && currentContext != "local" { - return currentContext - } - return "local" -} - // setDefaultConfiguration sets default config values based on provider and VM driver detection. // For local providers, uses config.DefaultConfig_Localhost if VM driver is "docker-desktop", // else uses config.DefaultConfig_Full. For non-local, uses config.DefaultConfig. @@ -419,72 +404,6 @@ func (p *InitPipeline) processPlatformConfiguration(_ context.Context) error { return nil } -// prepareTemplateData determines and loads template data for initialization based on blueprint context, artifact builder, and blueprint handler state. -// It prioritizes blueprint context value, then local blueprint handler data, then the default blueprint artifact, and finally the default template data for the current context. -// Returns a map of template file names to their byte content, or an error if any retrieval or parsing operation fails. -func (p *InitPipeline) prepareTemplateData(ctx context.Context) (map[string][]byte, error) { - var blueprintValue string - if blueprintCtx := ctx.Value("blueprint"); blueprintCtx != nil { - if blueprint, ok := blueprintCtx.(string); ok { - blueprintValue = blueprint - } - } - - if blueprintValue != "" { - if p.artifactBuilder != nil { - ociInfo, err := artifact.ParseOCIReference(blueprintValue) - if err != nil { - return nil, fmt.Errorf("failed to parse blueprint reference: %w", err) - } - if ociInfo == nil { - return nil, fmt.Errorf("invalid blueprint reference: %s", blueprintValue) - } - templateData, err := p.artifactBuilder.GetTemplateData(ociInfo.URL) - if err != nil { - return nil, fmt.Errorf("failed to get template data from blueprint: %w", err) - } - return templateData, nil - } - } - - if p.blueprintHandler != nil { - // Load all template data - blueprintTemplateData, err := p.blueprintHandler.GetLocalTemplateData() - if err != nil { - return nil, fmt.Errorf("failed to get local template data: %w", err) - } - - if len(blueprintTemplateData) > 0 { - return blueprintTemplateData, nil - } - } - - if p.artifactBuilder != nil { - effectiveBlueprintURL := constants.GetEffectiveBlueprintURL() - ociInfo, err := artifact.ParseOCIReference(effectiveBlueprintURL) - if err != nil { - return nil, fmt.Errorf("failed to parse default blueprint reference: %w", err) - } - templateData, err := p.artifactBuilder.GetTemplateData(ociInfo.URL) - if err != nil { - return nil, fmt.Errorf("failed to get template data from default blueprint: %w", err) - } - p.fallbackBlueprintURL = effectiveBlueprintURL - return templateData, nil - } - - if p.blueprintHandler != nil { - contextName := p.determineContextName(ctx) - defaultTemplateData, err := p.blueprintHandler.GetDefaultTemplateData(contextName) - if err != nil { - return nil, fmt.Errorf("failed to get default template data: %w", err) - } - return defaultTemplateData, nil - } - - return make(map[string][]byte), nil -} - // processTemplateData renders and processes template data for the InitPipeline. // Renders all templates using the template renderer, and loads blueprint data from the rendered output if present. // Returns the rendered template data map or an error if rendering or blueprint loading fails. diff --git a/pkg/pipelines/init_test.go b/pkg/pipelines/init_test.go index c11548400..cfeb72fad 100644 --- a/pkg/pipelines/init_test.go +++ b/pkg/pipelines/init_test.go @@ -16,7 +16,6 @@ import ( "github.com/windsorcli/cli/pkg/kubernetes" "github.com/windsorcli/cli/pkg/shell" "github.com/windsorcli/cli/pkg/stack" - "github.com/windsorcli/cli/pkg/template" "github.com/windsorcli/cli/pkg/tools" "github.com/windsorcli/cli/pkg/virt" ) @@ -840,6 +839,10 @@ func TestInitPipeline_prepareTemplateData(t *testing.T) { // Given a pipeline with both explicit blueprint and local templates pipeline := &InitPipeline{} + // Set up BasePipeline properly + pipeline.BasePipeline = *NewBasePipeline() + pipeline.BasePipeline.injector = di.NewInjector() + // Mock artifact builder that succeeds mockArtifactBuilder := artifact.NewMockArtifact() expectedOCIData := map[string][]byte{ @@ -848,7 +851,7 @@ func TestInitPipeline_prepareTemplateData(t *testing.T) { mockArtifactBuilder.GetTemplateDataFunc = func(ociRef string) (map[string][]byte, error) { return expectedOCIData, nil } - pipeline.artifactBuilder = mockArtifactBuilder + pipeline.BasePipeline.artifactBuilder = mockArtifactBuilder // Mock blueprint handler with local templates mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) @@ -857,13 +860,13 @@ func TestInitPipeline_prepareTemplateData(t *testing.T) { "blueprint.jsonnet": []byte("{ local: 'template-data' }"), }, nil } - pipeline.blueprintHandler = mockBlueprintHandler + pipeline.BasePipeline.injector.Register("blueprintHandler", mockBlueprintHandler) // Create context with explicit blueprint value ctx := context.WithValue(context.Background(), "blueprint", "oci://registry.example.com/blueprint:latest") // When prepareTemplateData is called - templateData, err := pipeline.prepareTemplateData(ctx) + templateData, err := pipeline.BasePipeline.prepareTemplateData(ctx) // Then should use explicit blueprint, not local templates if err != nil { @@ -881,16 +884,20 @@ func TestInitPipeline_prepareTemplateData(t *testing.T) { // Given a pipeline with explicit blueprint that fails pipeline := &InitPipeline{} + // Set up BasePipeline properly + pipeline.BasePipeline = *NewBasePipeline() + pipeline.BasePipeline.injector = di.NewInjector() + mockArtifactBuilder := artifact.NewMockArtifact() mockArtifactBuilder.GetTemplateDataFunc = func(ociRef string) (map[string][]byte, error) { return nil, fmt.Errorf("OCI pull failed") } - pipeline.artifactBuilder = mockArtifactBuilder + pipeline.BasePipeline.artifactBuilder = mockArtifactBuilder ctx := context.WithValue(context.Background(), "blueprint", "oci://registry.example.com/blueprint:latest") // When prepareTemplateData is called - templateData, err := pipeline.prepareTemplateData(ctx) + templateData, err := pipeline.BasePipeline.prepareTemplateData(ctx) // Then should return error if err == nil { @@ -908,6 +915,10 @@ func TestInitPipeline_prepareTemplateData(t *testing.T) { // Given a pipeline with local templates but no explicit blueprint pipeline := &InitPipeline{} + // Set up BasePipeline properly + pipeline.BasePipeline = *NewBasePipeline() + pipeline.BasePipeline.injector = di.NewInjector() + mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) expectedLocalData := map[string][]byte{ "blueprint.jsonnet": []byte("{ local: 'template-data' }"), @@ -915,10 +926,10 @@ func TestInitPipeline_prepareTemplateData(t *testing.T) { mockBlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { return expectedLocalData, nil } - pipeline.blueprintHandler = mockBlueprintHandler + pipeline.BasePipeline.injector.Register("blueprintHandler", mockBlueprintHandler) // When prepareTemplateData is called with no blueprint context - templateData, err := pipeline.prepareTemplateData(context.Background()) + templateData, err := pipeline.BasePipeline.prepareTemplateData(context.Background()) // Then should use local template data if err != nil { @@ -936,6 +947,10 @@ func TestInitPipeline_prepareTemplateData(t *testing.T) { // Given a pipeline with no local templates and artifact builder pipeline := &InitPipeline{} + // Set up BasePipeline properly + pipeline.BasePipeline = *NewBasePipeline() + pipeline.BasePipeline.injector = di.NewInjector() + // Mock artifact builder for default OCI URL mockArtifactBuilder := artifact.NewMockArtifact() expectedDefaultOCIData := map[string][]byte{ @@ -946,17 +961,17 @@ func TestInitPipeline_prepareTemplateData(t *testing.T) { receivedOCIRef = ociRef return expectedDefaultOCIData, nil } - pipeline.artifactBuilder = mockArtifactBuilder + pipeline.BasePipeline.artifactBuilder = mockArtifactBuilder // Mock blueprint handler with no local templates mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) mockBlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { return make(map[string][]byte), nil // Empty local templates } - pipeline.blueprintHandler = mockBlueprintHandler + pipeline.BasePipeline.injector.Register("blueprintHandler", mockBlueprintHandler) // When prepareTemplateData is called with no blueprint context - templateData, err := pipeline.prepareTemplateData(context.Background()) + templateData, err := pipeline.BasePipeline.prepareTemplateData(context.Background()) // Then should use default OCI URL and set fallback URL if err != nil { @@ -972,23 +987,23 @@ func TestInitPipeline_prepareTemplateData(t *testing.T) { if !strings.Contains(receivedOCIRef, "ghcr.io/windsorcli/core") { t.Errorf("Expected default OCI URL to be used, got %s", receivedOCIRef) } - // Verify fallback URL is set - if pipeline.fallbackBlueprintURL == "" { - t.Error("Expected fallbackBlueprintURL to be set") - } }) t.Run("Priority4_EmbeddedDefaultWhenNoArtifactBuilder", func(t *testing.T) { // Given a pipeline with no artifact builder pipeline := &InitPipeline{} - pipeline.artifactBuilder = nil + + // Set up BasePipeline properly + pipeline.BasePipeline = *NewBasePipeline() + pipeline.BasePipeline.injector = di.NewInjector() + pipeline.BasePipeline.artifactBuilder = nil // Mock config handler (needed for determineContextName) mockConfigHandler := config.NewMockConfigHandler() mockConfigHandler.GetContextFunc = func() string { return "local" } - pipeline.configHandler = mockConfigHandler + pipeline.BasePipeline.configHandler = mockConfigHandler // Mock blueprint handler with no local templates but default template mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) @@ -1001,10 +1016,10 @@ func TestInitPipeline_prepareTemplateData(t *testing.T) { mockBlueprintHandler.GetDefaultTemplateDataFunc = func(contextName string) (map[string][]byte, error) { return expectedDefaultData, nil } - pipeline.blueprintHandler = mockBlueprintHandler + pipeline.BasePipeline.injector.Register("blueprintHandler", mockBlueprintHandler) // When prepareTemplateData is called - templateData, err := pipeline.prepareTemplateData(context.Background()) + templateData, err := pipeline.BasePipeline.prepareTemplateData(context.Background()) // Then should use embedded default template if err != nil { @@ -1021,11 +1036,31 @@ func TestInitPipeline_prepareTemplateData(t *testing.T) { t.Run("ReturnsEmptyMapWhenNothingAvailable", func(t *testing.T) { // Given a pipeline with no blueprint handler and no artifact builder pipeline := &InitPipeline{} - pipeline.blueprintHandler = nil - pipeline.artifactBuilder = nil + + // Set up BasePipeline properly + pipeline.BasePipeline = *NewBasePipeline() + pipeline.BasePipeline.injector = di.NewInjector() + pipeline.BasePipeline.artifactBuilder = nil + + // Mock config handler (needed for determineContextName) + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.GetContextFunc = func() string { + return "local" + } + pipeline.BasePipeline.configHandler = mockConfigHandler + + // Mock blueprint handler that returns empty data + mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) + mockBlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { + return make(map[string][]byte), nil // Empty local templates + } + mockBlueprintHandler.GetDefaultTemplateDataFunc = func(contextName string) (map[string][]byte, error) { + return make(map[string][]byte), nil // Empty default templates + } + pipeline.BasePipeline.injector.Register("blueprintHandler", mockBlueprintHandler) // When prepareTemplateData is called - templateData, err := pipeline.prepareTemplateData(context.Background()) + templateData, err := pipeline.BasePipeline.prepareTemplateData(context.Background()) // Then should return empty map if err != nil { @@ -1043,14 +1078,18 @@ func TestInitPipeline_prepareTemplateData(t *testing.T) { // Given a pipeline with invalid OCI reference pipeline := &InitPipeline{} + // Set up BasePipeline properly + pipeline.BasePipeline = *NewBasePipeline() + pipeline.BasePipeline.injector = di.NewInjector() + mockArtifactBuilder := artifact.NewMockArtifact() - pipeline.artifactBuilder = mockArtifactBuilder + pipeline.BasePipeline.artifactBuilder = mockArtifactBuilder // Create context with invalid blueprint value ctx := context.WithValue(context.Background(), "blueprint", "invalid-oci-reference") // When prepareTemplateData is called - templateData, err := pipeline.prepareTemplateData(ctx) + templateData, err := pipeline.BasePipeline.prepareTemplateData(ctx) // Then should return error for invalid reference if err == nil { @@ -1202,354 +1241,3 @@ func TestInitPipeline_setDefaultConfiguration_HostPortsValidation(t *testing.T) } }) } - -// ============================================================================= -// Additional Coverage Tests -// ============================================================================= - -func TestInitPipeline_loadBlueprintFromTemplate(t *testing.T) { - // Given a pipeline with mocks - mocks := setupInitMocks(t) - pipeline := NewInitPipeline() - pipeline.blueprintHandler = mocks.BlueprintHandler - - t.Run("NoBlueprintData_ReturnsNil", func(t *testing.T) { - // Given rendered data without blueprint - renderedData := map[string]any{ - "other": "data", - } - - // When loading blueprint from template - err := pipeline.loadBlueprintFromTemplate(context.Background(), renderedData) - - // Then no error should occur - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("BlueprintDataNotMap_ReturnsNil", func(t *testing.T) { - // Given rendered data with non-map blueprint - renderedData := map[string]any{ - "blueprint": "not-a-map", - } - - // When loading blueprint from template - err := pipeline.loadBlueprintFromTemplate(context.Background(), renderedData) - - // Then no error should occur - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("ValidBlueprintData_LoadsSuccessfully", func(t *testing.T) { - // Given valid blueprint data - renderedData := map[string]any{ - "blueprint": map[string]any{ - "name": "test-blueprint", - "kustomize": []any{ - map[string]any{ - "name": "dns", - "patches": []any{ - map[string]any{"path": "patches/dns/coredns.yaml"}, - }, - }, - }, - }, - } - - // And mock blueprint handler - var loadedData map[string]any - mocks.BlueprintHandler.LoadDataFunc = func(data map[string]any, ociInfo ...*artifact.OCIArtifactInfo) error { - loadedData = data - return nil - } - - // When loading blueprint from template - err := pipeline.loadBlueprintFromTemplate(context.Background(), renderedData) - - // Then no error should occur - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And blueprint data should be loaded - if loadedData == nil { - t.Error("Expected blueprint data to be loaded") - } - - if blueprintName, ok := loadedData["name"].(string); !ok || blueprintName != "test-blueprint" { - t.Error("Expected blueprint name to be loaded correctly") - } - }) - - t.Run("BlueprintHandlerError_ReturnsError", func(t *testing.T) { - // Given valid blueprint data - renderedData := map[string]any{ - "blueprint": map[string]any{ - "name": "test-blueprint", - }, - } - - // And mock blueprint handler that returns error - mocks.BlueprintHandler.LoadDataFunc = func(data map[string]any, ociInfo ...*artifact.OCIArtifactInfo) error { - return fmt.Errorf("blueprint handler error") - } - - // When loading blueprint from template - err := pipeline.loadBlueprintFromTemplate(context.Background(), renderedData) - - // Then error should be returned - if err == nil { - t.Error("Expected error from blueprint handler") - } - - if !strings.Contains(err.Error(), "failed to load blueprint data") { - t.Errorf("Expected error message to contain 'failed to load blueprint data', got %v", err) - } - }) -} - -func TestInitPipeline_processTemplateData(t *testing.T) { - // Given a pipeline with mocks - mocks := setupInitMocks(t) - pipeline := NewInitPipeline() - pipeline.blueprintHandler = mocks.BlueprintHandler - - t.Run("NoTemplateRenderer_ReturnsEmptyData", func(t *testing.T) { - // Given no template renderer - pipeline.templateRenderer = nil - - // And template data - templateData := map[string][]byte{ - "blueprint.jsonnet": []byte("blueprint content"), - } - - // When processing template data - result, err := pipeline.processTemplateData(templateData) - - // Then no error should occur - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And result should be empty - if len(result) != 0 { - t.Errorf("Expected empty result, got %d items", len(result)) - } - }) - - t.Run("EmptyTemplateData_ReturnsEmptyData", func(t *testing.T) { - // Given template renderer - mockTemplate := template.NewMockTemplate(mocks.Injector) - pipeline.templateRenderer = mockTemplate - - // And empty template data - templateData := map[string][]byte{} - - // When processing template data - result, err := pipeline.processTemplateData(templateData) - - // Then no error should occur - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And result should be empty - if len(result) != 0 { - t.Errorf("Expected empty result, got %d items", len(result)) - } - }) - - t.Run("SuccessfulTemplateProcessing_ReturnsRenderedData", func(t *testing.T) { - // Given template renderer - mockTemplate := template.NewMockTemplate(mocks.Injector) - mockTemplate.ProcessFunc = func(templateData map[string][]byte, renderedData map[string]any) error { - // Simulate successful template processing - renderedData["terraform"] = map[string]any{"region": "us-west-2"} - renderedData["patches/namespace"] = map[string]any{"namespace": "test"} - return nil - } - pipeline.templateRenderer = mockTemplate - - // And template data - templateData := map[string][]byte{ - "terraform/region.jsonnet": []byte("terraform content"), - "patches/namespace.jsonnet": []byte("patch content"), - } - - // When processing template data - result, err := pipeline.processTemplateData(templateData) - - // Then no error should occur - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And result should contain rendered data - if len(result) != 2 { - t.Errorf("Expected 2 rendered items, got %d", len(result)) - } - - if _, exists := result["terraform"]; !exists { - t.Error("Expected terraform data to be rendered") - } - - if _, exists := result["patches/namespace"]; !exists { - t.Error("Expected patch data to be rendered") - } - }) - - t.Run("TemplateProcessingError_ReturnsError", func(t *testing.T) { - // Given template renderer that returns error - mockTemplate := template.NewMockTemplate(mocks.Injector) - mockTemplate.ProcessFunc = func(templateData map[string][]byte, renderedData map[string]any) error { - return fmt.Errorf("template processing failed") - } - pipeline.templateRenderer = mockTemplate - - // And template data - templateData := map[string][]byte{ - "test.jsonnet": []byte("test content"), - } - - // When processing template data - result, err := pipeline.processTemplateData(templateData) - - // Then error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - - if !strings.Contains(err.Error(), "failed to process template data") { - t.Errorf("Expected error message to contain 'failed to process template data', got %v", err) - } - - // And result should be nil - if result != nil { - t.Errorf("Expected nil result, got %v", result) - } - }) - - t.Run("BlueprintDataExtraction_LoadsBlueprintSuccessfully", func(t *testing.T) { - // Given template renderer that returns blueprint data - mockTemplate := template.NewMockTemplate(mocks.Injector) - mockTemplate.ProcessFunc = func(templateData map[string][]byte, renderedData map[string]any) error { - renderedData["blueprint"] = map[string]any{ - "name": "test-blueprint", - "kustomize": []any{ - map[string]any{ - "patches": []any{"patch1.yaml"}, - }, - }, - } - return nil - } - pipeline.templateRenderer = mockTemplate - - // And mock blueprint handler - var loadedData map[string]any - mocks.BlueprintHandler.LoadDataFunc = func(data map[string]any, ociInfo ...*artifact.OCIArtifactInfo) error { - loadedData = data - return nil - } - - // And template data - templateData := map[string][]byte{ - "blueprint.jsonnet": []byte("blueprint content"), - } - - // When processing template data - result, err := pipeline.processTemplateData(templateData) - - // Then no error should occur - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And blueprint should be loaded - if loadedData == nil { - t.Error("Expected blueprint to be loaded") - } - - // And result should contain blueprint data - if _, exists := result["blueprint"]; !exists { - t.Error("Expected blueprint data in result") - } - }) - - t.Run("BlueprintDataExtractionError_ReturnsError", func(t *testing.T) { - // Given template renderer that returns blueprint data - mockTemplate := template.NewMockTemplate(mocks.Injector) - mockTemplate.ProcessFunc = func(templateData map[string][]byte, renderedData map[string]any) error { - renderedData["blueprint"] = map[string]any{ - "name": "test-blueprint", - } - return nil - } - pipeline.templateRenderer = mockTemplate - - // And mock blueprint handler that returns error - mocks.BlueprintHandler.LoadDataFunc = func(data map[string]any, ociInfo ...*artifact.OCIArtifactInfo) error { - return fmt.Errorf("blueprint loading failed") - } - - // And template data - templateData := map[string][]byte{ - "blueprint.jsonnet": []byte("blueprint content"), - } - - // When processing template data - result, err := pipeline.processTemplateData(templateData) - - // Then error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - - if !strings.Contains(err.Error(), "failed to load blueprint from template") { - t.Errorf("Expected error message to contain 'failed to load blueprint from template', got %v", err) - } - - // And result should be nil - if result != nil { - t.Errorf("Expected nil result, got %v", result) - } - }) - - t.Run("NonMapBlueprintData_ContinuesWithoutError", func(t *testing.T) { - // Given template renderer that returns non-map blueprint data - mockTemplate := template.NewMockTemplate(mocks.Injector) - mockTemplate.ProcessFunc = func(templateData map[string][]byte, renderedData map[string]any) error { - renderedData["blueprint"] = "not-a-map" - renderedData["terraform"] = map[string]any{"region": "us-west-2"} - return nil - } - pipeline.templateRenderer = mockTemplate - - // And template data - templateData := map[string][]byte{ - "blueprint.jsonnet": []byte("blueprint content"), - "terraform/region.jsonnet": []byte("terraform content"), - } - - // When processing template data - result, err := pipeline.processTemplateData(templateData) - - // Then no error should occur - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // And result should contain rendered data - if len(result) != 2 { - t.Errorf("Expected 2 rendered items, got %d", len(result)) - } - - if _, exists := result["terraform"]; !exists { - t.Error("Expected terraform data to be rendered") - } - }) - -} diff --git a/pkg/pipelines/install.go b/pkg/pipelines/install.go index cc36dde8b..260c17af0 100644 --- a/pkg/pipelines/install.go +++ b/pkg/pipelines/install.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - bundler "github.com/windsorcli/cli/pkg/artifact" + "github.com/windsorcli/cli/pkg/artifact" "github.com/windsorcli/cli/pkg/blueprint" "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/generators" @@ -26,7 +26,7 @@ type InstallPipeline struct { blueprintHandler blueprint.BlueprintHandler templateRenderer template.Template generators []generators.Generator - artifactBuilder bundler.Artifact + artifactBuilder artifact.Artifact } // ============================================================================= @@ -119,11 +119,10 @@ func (p *InstallPipeline) Execute(ctx context.Context) error { } } - // Phase 3: Install blueprint + // Phase 3: Load blueprint config and install if err := p.blueprintHandler.LoadConfig(); err != nil { return fmt.Errorf("Error loading blueprint config: %w", err) } - if err := p.blueprintHandler.Install(); err != nil { return fmt.Errorf("Error installing blueprint: %w", err) } @@ -141,37 +140,17 @@ func (p *InstallPipeline) Execute(ctx context.Context) error { return nil } -// ============================================================================= -// Private Methods -// ============================================================================= - -// prepareTemplateData prepares template data for processing in the InstallPipeline. -// It loads template data from the blueprint handler if available. -func (p *InstallPipeline) prepareTemplateData(_ context.Context) (map[string][]byte, error) { - if p.blueprintHandler != nil { - // Load all template data - blueprintTemplateData, err := p.blueprintHandler.GetLocalTemplateData() - if err != nil { - return nil, fmt.Errorf("failed to get local template data: %w", err) - } - - if len(blueprintTemplateData) > 0 { - return blueprintTemplateData, nil - } - } - - return make(map[string][]byte), nil -} - // processTemplateData renders and processes template data for the InstallPipeline. -// Renders all templates using the template renderer and returns the rendered template data map. +// Unlike the base pipeline, this method does not handle blueprint data extraction +// as blueprint loading is handled separately in the Execute method. func (p *InstallPipeline) processTemplateData(templateData map[string][]byte) (map[string]any, error) { - var renderedData map[string]any - if p.templateRenderer != nil && len(templateData) > 0 { - renderedData = make(map[string]any) - if err := p.templateRenderer.Process(templateData, renderedData); err != nil { - return nil, fmt.Errorf("failed to process template data: %w", err) - } + if p.templateRenderer == nil || len(templateData) == 0 { + return nil, nil + } + + renderedData := make(map[string]any) + if err := p.templateRenderer.Process(templateData, renderedData); err != nil { + return nil, fmt.Errorf("failed to process template data: %w", err) } return renderedData, nil } diff --git a/pkg/pipelines/install_test.go b/pkg/pipelines/install_test.go index 01b493110..5f69d8b6c 100644 --- a/pkg/pipelines/install_test.go +++ b/pkg/pipelines/install_test.go @@ -393,6 +393,7 @@ func TestInstallPipeline_Execute(t *testing.T) { // Mock template renderer to return test data mockTemplateRenderer := &MockTemplate{} mockTemplateRenderer.ProcessFunc = func(templateData map[string][]byte, renderedData map[string]any) error { + t.Log("Template renderer Process called") renderedData["kustomize/values"] = map[string]any{ "common": map[string]any{ "domain": "test.com", @@ -400,10 +401,17 @@ func TestInstallPipeline_Execute(t *testing.T) { } return nil } - pipeline.templateRenderer = mockTemplateRenderer + // Register the mock template renderer in the injector BEFORE initialization + mocks.Injector.Register("templateRenderer", mockTemplateRenderer) + + // Initialize the pipeline to set up generators + if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { + t.Fatalf("Failed to initialize pipeline: %v", err) + } // Mock blueprint handler to return template data mocks.BlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { + t.Log("GetLocalTemplateData called") return map[string][]byte{ "kustomize/values.jsonnet": []byte(`{"common": {"domain": "test.com"}}`), }, nil @@ -419,7 +427,7 @@ func TestInstallPipeline_Execute(t *testing.T) { // And template processing should be called if !mockTemplateRenderer.ProcessCalled { - t.Error("Expected template processing to be called") + t.Errorf("Expected template processing to be called. Config loaded: %v, Blueprint handler: %v", pipeline.configHandler.IsLoaded(), pipeline.blueprintHandler != nil) } }) @@ -432,7 +440,13 @@ func TestInstallPipeline_Execute(t *testing.T) { mockTemplateRenderer.ProcessFunc = func(templateData map[string][]byte, renderedData map[string]any) error { return fmt.Errorf("template processing failed") } - pipeline.templateRenderer = mockTemplateRenderer + // Register the mock template renderer in the injector BEFORE initialization + mocks.Injector.Register("templateRenderer", mockTemplateRenderer) + + // Initialize the pipeline to set up generators + if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { + t.Fatalf("Failed to initialize pipeline: %v", err) + } // Mock blueprint handler to return template data mocks.BlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { @@ -467,7 +481,13 @@ func TestInstallPipeline_Execute(t *testing.T) { } return nil } - pipeline.templateRenderer = mockTemplateRenderer + // Register the mock template renderer in the injector BEFORE initialization + mocks.Injector.Register("templateRenderer", mockTemplateRenderer) + + // Initialize the pipeline to set up generators + if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { + t.Fatalf("Failed to initialize pipeline: %v", err) + } // Mock blueprint handler to return template data mocks.BlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { @@ -515,7 +535,13 @@ func TestInstallPipeline_Execute(t *testing.T) { } return nil } - pipeline.templateRenderer = mockTemplateRenderer + // Register the mock template renderer in the injector BEFORE initialization + mocks.Injector.Register("templateRenderer", mockTemplateRenderer) + + // Initialize the pipeline to set up generators + if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { + t.Fatalf("Failed to initialize pipeline: %v", err) + } // Mock blueprint handler to return template data mocks.BlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { @@ -555,7 +581,13 @@ func TestInstallPipeline_Execute(t *testing.T) { // Don't add any data to renderedData return nil } - pipeline.templateRenderer = mockTemplateRenderer + // Register the mock template renderer in the injector BEFORE initialization + mocks.Injector.Register("templateRenderer", mockTemplateRenderer) + + // Initialize the pipeline to set up generators + if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { + t.Fatalf("Failed to initialize pipeline: %v", err) + } // Mock blueprint handler to return template data mocks.BlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { @@ -607,7 +639,13 @@ func TestInstallPipeline_Execute(t *testing.T) { renderedData["kustomize/values"] = expectedData["kustomize/values"] return nil } - pipeline.templateRenderer = mockTemplateRenderer + // Register the mock template renderer in the injector BEFORE initialization + mocks.Injector.Register("templateRenderer", mockTemplateRenderer) + + // Initialize the pipeline to set up generators + if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { + t.Fatalf("Failed to initialize pipeline: %v", err) + } // Mock blueprint handler to return template data mocks.BlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { @@ -664,7 +702,13 @@ func TestInstallPipeline_Execute(t *testing.T) { } return nil } - pipeline.templateRenderer = mockTemplateRenderer + // Register the mock template renderer in the injector BEFORE initialization + mocks.Injector.Register("templateRenderer", mockTemplateRenderer) + + // Initialize the pipeline to set up generators + if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { + t.Fatalf("Failed to initialize pipeline: %v", err) + } // Mock blueprint handler to return template data mocks.BlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { diff --git a/pkg/pipelines/pipeline.go b/pkg/pipelines/pipeline.go index 41a1ffc54..c11219695 100644 --- a/pkg/pipelines/pipeline.go +++ b/pkg/pipelines/pipeline.go @@ -11,6 +11,7 @@ import ( "github.com/windsorcli/cli/pkg/blueprint" "github.com/windsorcli/cli/pkg/cluster" "github.com/windsorcli/cli/pkg/config" + "github.com/windsorcli/cli/pkg/constants" "github.com/windsorcli/cli/pkg/di" envpkg "github.com/windsorcli/cli/pkg/env" "github.com/windsorcli/cli/pkg/generators" @@ -92,13 +93,15 @@ func WithPipeline(injector di.Injector, ctx context.Context, pipelineName string return pipeline, nil } -// BasePipeline provides common pipeline functionality including config loading +// BasePipeline provides common pipeline functionality including config loading and template processing // Specific pipelines should embed this and add their own dependencies type BasePipeline struct { - shell shell.Shell - configHandler config.ConfigHandler - shims *Shims - injector di.Injector + shell shell.Shell + configHandler config.ConfigHandler + shims *Shims + injector di.Injector + templateRenderer template.Template + artifactBuilder bundler.Artifact } // ============================================================================= @@ -123,6 +126,8 @@ func (p *BasePipeline) Initialize(injector di.Injector, ctx context.Context) err p.shell = p.withShell() p.configHandler = p.withConfigHandler() p.shims = p.withShims() + p.templateRenderer = p.withTemplateRenderer() + p.artifactBuilder = p.withArtifactBuilder() if err := p.shell.Initialize(); err != nil { return fmt.Errorf("failed to initialize shell: %w", err) @@ -769,10 +774,154 @@ func (p *BasePipeline) withTemplateRenderer() template.Template { } templateRenderer := template.NewJsonnetTemplate(p.injector) + if err := templateRenderer.Initialize(); err != nil { + return nil + } p.injector.Register("templateRenderer", templateRenderer) return templateRenderer } +// prepareTemplateData loads template data using blueprint context, artifact builder, and blueprint handler state. +// Priority: blueprint context, then local handler data, then default artifact, then default template for context. +func (p *BasePipeline) prepareTemplateData(ctx context.Context) (map[string][]byte, error) { + var blueprintValue string + if blueprintCtx := ctx.Value("blueprint"); blueprintCtx != nil { + if blueprint, ok := blueprintCtx.(string); ok { + blueprintValue = blueprint + } + } + + if blueprintValue != "" { + if p.artifactBuilder != nil { + ociInfo, err := bundler.ParseOCIReference(blueprintValue) + if err != nil { + return nil, fmt.Errorf("failed to parse blueprint reference: %w", err) + } + if ociInfo == nil { + return nil, fmt.Errorf("invalid blueprint reference: %s", blueprintValue) + } + templateData, err := p.artifactBuilder.GetTemplateData(ociInfo.URL) + if err != nil { + return nil, fmt.Errorf("failed to get template data from blueprint: %w", err) + } + return templateData, nil + } + } + + if blueprintHandler := p.withBlueprintHandler(); blueprintHandler != nil { + blueprintTemplateData, err := blueprintHandler.GetLocalTemplateData() + if err != nil { + return nil, fmt.Errorf("failed to get local template data: %w", err) + } + + if len(blueprintTemplateData) > 0 { + return blueprintTemplateData, nil + } + } + + if p.artifactBuilder != nil { + effectiveBlueprintURL := constants.GetEffectiveBlueprintURL() + ociInfo, err := bundler.ParseOCIReference(effectiveBlueprintURL) + if err != nil { + return nil, fmt.Errorf("failed to parse default blueprint reference: %w", err) + } + templateData, err := p.artifactBuilder.GetTemplateData(ociInfo.URL) + if err != nil { + return nil, fmt.Errorf("failed to get template data from default blueprint: %w", err) + } + return templateData, nil + } + + if blueprintHandler := p.withBlueprintHandler(); blueprintHandler != nil { + contextName := p.determineContextName(ctx) + defaultTemplateData, err := blueprintHandler.GetDefaultTemplateData(contextName) + if err != nil { + return nil, fmt.Errorf("failed to get default template data: %w", err) + } + return defaultTemplateData, nil + } + + return make(map[string][]byte), nil +} + +// processTemplateData renders and processes template data. +// Renders all templates using the template renderer and returns the rendered template data map. +// If blueprint data is present in the rendered data, it will be loaded using the blueprint handler. +func (p *BasePipeline) processTemplateData(templateData map[string][]byte) (map[string]any, error) { + if p.templateRenderer == nil || len(templateData) == 0 { + return nil, nil + } + + renderedData := make(map[string]any) + if err := p.templateRenderer.Process(templateData, renderedData); err != nil { + return nil, fmt.Errorf("failed to process template data: %w", err) + } + + if blueprintData, exists := renderedData["blueprint"]; exists { + if err := p.loadBlueprintFromTemplate(context.Background(), map[string]any{"blueprint": blueprintData}); err != nil { + return nil, fmt.Errorf("failed to load blueprint from template: %w", err) + } + } + + return renderedData, nil +} + +// loadBlueprintFromTemplate loads blueprint data from rendered template data. If the "blueprint" key exists +// in renderedData and is a map, attempts to parse OCI artifact info from the context's "blueprint" value. +// Delegates loading to blueprintHandler.LoadData with the parsed blueprint map and optional OCI info. +func (p *BasePipeline) loadBlueprintFromTemplate(ctx context.Context, renderedData map[string]any) error { + if blueprintData, exists := renderedData["blueprint"]; exists { + if blueprintMap, ok := blueprintData.(map[string]any); ok { + if kustomizeData, exists := blueprintMap["kustomize"]; exists { + if kustomizeList, ok := kustomizeData.([]any); ok { + for _, k := range kustomizeList { + if kustomizeMap, ok := k.(map[string]any); ok { + if _, exists := kustomizeMap["patches"]; exists { + // Patches exist in this kustomization + } + } + } + } + } + + var ociInfo *bundler.OCIArtifactInfo + if blueprintCtx := ctx.Value("blueprint"); blueprintCtx != nil { + if blueprintValue, ok := blueprintCtx.(string); ok { + var err error + ociInfo, err = bundler.ParseOCIReference(blueprintValue) + if err != nil { + return err + } + } + } + + blueprintHandler := p.withBlueprintHandler() + if blueprintHandler != nil { + if err := blueprintHandler.LoadData(blueprintMap, ociInfo); err != nil { + return fmt.Errorf("failed to load blueprint data: %w", err) + } + } + } + } + return nil +} + +// determineContextName selects the context name from ctx, config, or defaults to "local" if unset or "local". +func (p *BasePipeline) determineContextName(ctx context.Context) string { + if contextName := ctx.Value("contextName"); contextName != nil { + if name, ok := contextName.(string); ok { + return name + } + } + if p.configHandler != nil { + currentContext := p.configHandler.GetContext() + if currentContext != "" && currentContext != "local" { + return currentContext + } + } + return "local" +} + // ============================================================================= // Interface Compliance // ============================================================================= diff --git a/pkg/pipelines/pipeline_test.go b/pkg/pipelines/pipeline_test.go index c1025af94..cc948d907 100644 --- a/pkg/pipelines/pipeline_test.go +++ b/pkg/pipelines/pipeline_test.go @@ -21,6 +21,7 @@ import ( "github.com/windsorcli/cli/pkg/kubernetes" "github.com/windsorcli/cli/pkg/shell" "github.com/windsorcli/cli/pkg/stack" + "github.com/windsorcli/cli/pkg/template" "github.com/windsorcli/cli/pkg/tools" "github.com/windsorcli/cli/pkg/virt" ) @@ -2843,3 +2844,584 @@ func TestBasePipeline_withClusterClient(t *testing.T) { } }) } + +// ============================================================================= +// Template Processing Tests +// ============================================================================= + +func TestBasePipeline_prepareTemplateData(t *testing.T) { + t.Run("Priority1_ExplicitBlueprintOverridesLocalTemplates", func(t *testing.T) { + // Given a pipeline with both explicit blueprint and local templates + pipeline := NewBasePipeline() + pipeline.injector = di.NewInjector() + + // Mock artifact builder that succeeds + mockArtifactBuilder := artifact.NewMockArtifact() + expectedOCIData := map[string][]byte{ + "blueprint.jsonnet": []byte("{ explicit: 'oci-data' }"), + } + mockArtifactBuilder.GetTemplateDataFunc = func(ociRef string) (map[string][]byte, error) { + return expectedOCIData, nil + } + pipeline.artifactBuilder = mockArtifactBuilder + + // Mock blueprint handler with local templates + mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) + mockBlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { + return map[string][]byte{ + "blueprint.jsonnet": []byte("{ local: 'template-data' }"), + }, nil + } + pipeline.injector.Register("blueprintHandler", mockBlueprintHandler) + + // Create context with explicit blueprint value + ctx := context.WithValue(context.Background(), "blueprint", "oci://registry.example.com/blueprint:latest") + + // When prepareTemplateData is called + templateData, err := pipeline.prepareTemplateData(ctx) + + // Then should use explicit blueprint, not local templates + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if len(templateData) != 1 { + t.Errorf("Expected 1 template file, got %d", len(templateData)) + } + if string(templateData["blueprint.jsonnet"]) != "{ explicit: 'oci-data' }" { + t.Error("Expected explicit blueprint data to override local templates") + } + }) + + t.Run("Priority1_ExplicitBlueprintFailsWithError", func(t *testing.T) { + // Given a pipeline with explicit blueprint that fails + pipeline := NewBasePipeline() + pipeline.injector = di.NewInjector() + + mockArtifactBuilder := artifact.NewMockArtifact() + mockArtifactBuilder.GetTemplateDataFunc = func(ociRef string) (map[string][]byte, error) { + return nil, fmt.Errorf("OCI pull failed") + } + pipeline.artifactBuilder = mockArtifactBuilder + + ctx := context.WithValue(context.Background(), "blueprint", "oci://registry.example.com/blueprint:latest") + + // When prepareTemplateData is called + templateData, err := pipeline.prepareTemplateData(ctx) + + // Then should return error + if err == nil { + t.Fatal("Expected error, got nil") + } + if !strings.Contains(err.Error(), "failed to get template data from blueprint") { + t.Errorf("Expected error to contain 'failed to get template data from blueprint', got %v", err) + } + if templateData != nil { + t.Error("Expected nil template data on error") + } + }) + + t.Run("Priority2_LocalTemplatesWhenNoExplicitBlueprint", func(t *testing.T) { + // Given a pipeline with local templates but no explicit blueprint + pipeline := NewBasePipeline() + pipeline.injector = di.NewInjector() + + mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) + expectedLocalData := map[string][]byte{ + "blueprint.jsonnet": []byte("{ local: 'template-data' }"), + } + mockBlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { + return expectedLocalData, nil + } + pipeline.injector.Register("blueprintHandler", mockBlueprintHandler) + + // When prepareTemplateData is called with no blueprint context + templateData, err := pipeline.prepareTemplateData(context.Background()) + + // Then should use local template data + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if len(templateData) != 1 { + t.Errorf("Expected 1 template file, got %d", len(templateData)) + } + if string(templateData["blueprint.jsonnet"]) != "{ local: 'template-data' }" { + t.Error("Expected local template data") + } + }) + + t.Run("Priority3_DefaultOCIURLWhenNoLocalTemplates", func(t *testing.T) { + // Given a pipeline with no local templates and artifact builder + pipeline := NewBasePipeline() + pipeline.injector = di.NewInjector() + + // Mock artifact builder for default OCI URL + mockArtifactBuilder := artifact.NewMockArtifact() + expectedDefaultOCIData := map[string][]byte{ + "blueprint.jsonnet": []byte("{ default: 'oci-data' }"), + } + var receivedOCIRef string + mockArtifactBuilder.GetTemplateDataFunc = func(ociRef string) (map[string][]byte, error) { + receivedOCIRef = ociRef + return expectedDefaultOCIData, nil + } + pipeline.artifactBuilder = mockArtifactBuilder + + // Mock blueprint handler with no local templates + mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) + mockBlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { + return make(map[string][]byte), nil // Empty local templates + } + pipeline.injector.Register("blueprintHandler", mockBlueprintHandler) + + // When prepareTemplateData is called with no blueprint context + templateData, err := pipeline.prepareTemplateData(context.Background()) + + // Then should use default OCI URL + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if len(templateData) != 1 { + t.Errorf("Expected 1 template file, got %d", len(templateData)) + } + if string(templateData["blueprint.jsonnet"]) != "{ default: 'oci-data' }" { + t.Error("Expected default OCI blueprint data") + } + // Verify the correct default OCI URL was used + if !strings.Contains(receivedOCIRef, "ghcr.io/windsorcli/core") { + t.Errorf("Expected default OCI URL to be used, got %s", receivedOCIRef) + } + }) + + t.Run("Priority4_EmbeddedDefaultWhenNoArtifactBuilder", func(t *testing.T) { + // Given a pipeline with no artifact builder + pipeline := NewBasePipeline() + pipeline.injector = di.NewInjector() + + // Mock config handler (needed for determineContextName) + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.GetContextFunc = func() string { + return "local" + } + pipeline.configHandler = mockConfigHandler + + // Mock blueprint handler with no local templates but default template + mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) + mockBlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { + return make(map[string][]byte), nil // Empty local templates + } + expectedDefaultData := map[string][]byte{ + "blueprint.jsonnet": []byte("{ embedded: 'default-template' }"), + } + mockBlueprintHandler.GetDefaultTemplateDataFunc = func(contextName string) (map[string][]byte, error) { + return expectedDefaultData, nil + } + pipeline.injector.Register("blueprintHandler", mockBlueprintHandler) + + // When prepareTemplateData is called + templateData, err := pipeline.prepareTemplateData(context.Background()) + + // Then should use embedded default template + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if len(templateData) != 1 { + t.Errorf("Expected 1 template file, got %d", len(templateData)) + } + if string(templateData["blueprint.jsonnet"]) != "{ embedded: 'default-template' }" { + t.Error("Expected embedded default template data") + } + }) + + t.Run("ReturnsEmptyMapWhenNothingAvailable", func(t *testing.T) { + // Given a pipeline with no blueprint handler and no artifact builder + pipeline := NewBasePipeline() + pipeline.injector = di.NewInjector() + + // Set up config handler + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.GetContextFunc = func() string { + return "local" + } + pipeline.configHandler = mockConfigHandler + + // Register a mock blueprint handler that returns empty data + mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) + mockBlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { + return make(map[string][]byte), nil + } + pipeline.injector.Register("blueprintHandler", mockBlueprintHandler) + + // When prepareTemplateData is called + templateData, err := pipeline.prepareTemplateData(context.Background()) + + // Then should return empty map + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if templateData == nil { + t.Error("Expected non-nil template data") + } + if len(templateData) != 0 { + t.Error("Expected empty template data") + } + }) + + t.Run("HandlesInvalidOCIReference", func(t *testing.T) { + // Given a pipeline with invalid OCI reference + pipeline := NewBasePipeline() + pipeline.injector = di.NewInjector() + + mockArtifactBuilder := artifact.NewMockArtifact() + pipeline.artifactBuilder = mockArtifactBuilder + + // Create context with invalid blueprint value + ctx := context.WithValue(context.Background(), "blueprint", "invalid-oci-reference") + + // When prepareTemplateData is called + templateData, err := pipeline.prepareTemplateData(ctx) + + // Then should return error for invalid reference + if err == nil { + t.Fatal("Expected error for invalid OCI reference, got nil") + } + if !strings.Contains(err.Error(), "failed to parse blueprint reference") { + t.Errorf("Expected error to contain 'failed to parse blueprint reference', got %v", err) + } + if templateData != nil { + t.Error("Expected nil template data on error") + } + }) +} + +func TestBasePipeline_processTemplateData(t *testing.T) { + setup := func(t *testing.T) (*BasePipeline, *Mocks) { + pipeline := NewBasePipeline() + mocks := setupMocks(t) + return pipeline, mocks + } + + t.Run("NoTemplateRenderer_ReturnsEmptyData", func(t *testing.T) { + // Given a pipeline with no template renderer + pipeline, _ := setup(t) + pipeline.templateRenderer = nil + + // And template data + templateData := map[string][]byte{ + "blueprint.jsonnet": []byte("blueprint content"), + } + + // When processing template data + result, err := pipeline.processTemplateData(templateData) + + // Then no error should occur + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + // And result should be empty + if len(result) != 0 { + t.Errorf("Expected empty result, got %d items", len(result)) + } + }) + + t.Run("EmptyTemplateData_ReturnsEmptyData", func(t *testing.T) { + // Given a pipeline with template renderer + pipeline, mocks := setup(t) + mockTemplate := template.NewMockTemplate(mocks.Injector) + pipeline.templateRenderer = mockTemplate + + // And empty template data + templateData := map[string][]byte{} + + // When processing template data + result, err := pipeline.processTemplateData(templateData) + + // Then no error should occur + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + // And result should be empty + if len(result) != 0 { + t.Errorf("Expected empty result, got %d items", len(result)) + } + }) + + t.Run("SuccessfulTemplateProcessing_ReturnsRenderedData", func(t *testing.T) { + // Given a pipeline with template renderer + pipeline, mocks := setup(t) + mockTemplate := template.NewMockTemplate(mocks.Injector) + mockTemplate.ProcessFunc = func(templateData map[string][]byte, renderedData map[string]any) error { + // Simulate successful template processing + renderedData["terraform"] = map[string]any{"region": "us-west-2"} + renderedData["patches/namespace"] = map[string]any{"namespace": "test"} + return nil + } + pipeline.templateRenderer = mockTemplate + + // And template data + templateData := map[string][]byte{ + "terraform/region.jsonnet": []byte("terraform content"), + "patches/namespace.jsonnet": []byte("patch content"), + } + + // When processing template data + result, err := pipeline.processTemplateData(templateData) + + // Then no error should occur + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + // And result should contain rendered data + if len(result) != 2 { + t.Errorf("Expected 2 rendered items, got %d", len(result)) + } + + if _, exists := result["terraform"]; !exists { + t.Error("Expected terraform data to be rendered") + } + + if _, exists := result["patches/namespace"]; !exists { + t.Error("Expected patch data to be rendered") + } + }) + + t.Run("TemplateProcessingError_ReturnsError", func(t *testing.T) { + // Given a pipeline with template renderer that returns error + pipeline, mocks := setup(t) + mockTemplate := template.NewMockTemplate(mocks.Injector) + mockTemplate.ProcessFunc = func(templateData map[string][]byte, renderedData map[string]any) error { + return fmt.Errorf("template processing failed") + } + pipeline.templateRenderer = mockTemplate + + // And template data + templateData := map[string][]byte{ + "test.jsonnet": []byte("test content"), + } + + // When processing template data + result, err := pipeline.processTemplateData(templateData) + + // Then error should be returned + if err == nil { + t.Error("Expected error, got nil") + } + + if !strings.Contains(err.Error(), "failed to process template data") { + t.Errorf("Expected error message to contain 'failed to process template data', got %v", err) + } + + // And result should be nil + if result != nil { + t.Errorf("Expected nil result, got %v", result) + } + }) + + t.Run("BlueprintDataExtraction_LoadsBlueprintSuccessfully", func(t *testing.T) { + // Given a pipeline with template renderer that returns blueprint data + pipeline, mocks := setup(t) + + // Initialize the pipeline to set up injector + if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { + t.Fatalf("Failed to initialize pipeline: %v", err) + } + + mockTemplate := template.NewMockTemplate(mocks.Injector) + mockTemplate.ProcessFunc = func(templateData map[string][]byte, renderedData map[string]any) error { + renderedData["blueprint"] = map[string]any{ + "name": "test-blueprint", + "kustomize": []any{ + map[string]any{ + "patches": []any{"patch1.yaml"}, + }, + }, + } + return nil + } + pipeline.templateRenderer = mockTemplate + + // And mock blueprint handler + var loadedData map[string]any + mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) + mockBlueprintHandler.LoadDataFunc = func(data map[string]any, ociInfo ...*artifact.OCIArtifactInfo) error { + loadedData = data + return nil + } + pipeline.injector.Register("blueprintHandler", mockBlueprintHandler) + + // And template data + templateData := map[string][]byte{ + "blueprint.jsonnet": []byte("blueprint content"), + } + + // When processing template data + result, err := pipeline.processTemplateData(templateData) + + // Then no error should occur + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + // And blueprint should be loaded + if loadedData == nil { + t.Error("Expected blueprint to be loaded") + } + + // And result should contain blueprint data + if _, exists := result["blueprint"]; !exists { + t.Error("Expected blueprint data in result") + } + }) + + t.Run("BlueprintDataExtractionError_ReturnsError", func(t *testing.T) { + // Given a pipeline with template renderer that returns blueprint data + pipeline, mocks := setup(t) + + // Initialize the pipeline to set up injector + if err := pipeline.Initialize(mocks.Injector, context.Background()); err != nil { + t.Fatalf("Failed to initialize pipeline: %v", err) + } + + mockTemplate := template.NewMockTemplate(mocks.Injector) + mockTemplate.ProcessFunc = func(templateData map[string][]byte, renderedData map[string]any) error { + renderedData["blueprint"] = map[string]any{ + "name": "test-blueprint", + } + return nil + } + pipeline.templateRenderer = mockTemplate + + // And mock blueprint handler that returns error + mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) + mockBlueprintHandler.LoadDataFunc = func(data map[string]any, ociInfo ...*artifact.OCIArtifactInfo) error { + return fmt.Errorf("blueprint loading failed") + } + pipeline.injector.Register("blueprintHandler", mockBlueprintHandler) + + // And template data + templateData := map[string][]byte{ + "blueprint.jsonnet": []byte("blueprint content"), + } + + // When processing template data + result, err := pipeline.processTemplateData(templateData) + + // Then error should be returned + if err == nil { + t.Error("Expected error, got nil") + } + + if !strings.Contains(err.Error(), "failed to load blueprint from template") { + t.Errorf("Expected error message to contain 'failed to load blueprint from template', got %v", err) + } + + // And result should be nil + if result != nil { + t.Errorf("Expected nil result, got %v", result) + } + }) + + t.Run("NonMapBlueprintData_ContinuesWithoutError", func(t *testing.T) { + // Given a pipeline with template renderer that returns non-map blueprint data + pipeline, mocks := setup(t) + mockTemplate := template.NewMockTemplate(mocks.Injector) + mockTemplate.ProcessFunc = func(templateData map[string][]byte, renderedData map[string]any) error { + renderedData["blueprint"] = "not-a-map" + renderedData["terraform"] = map[string]any{"region": "us-west-2"} + return nil + } + pipeline.templateRenderer = mockTemplate + + // And template data + templateData := map[string][]byte{ + "blueprint.jsonnet": []byte("blueprint content"), + "terraform/region.jsonnet": []byte("terraform content"), + } + + // When processing template data + result, err := pipeline.processTemplateData(templateData) + + // Then no error should occur + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + // And result should contain rendered data + if len(result) != 2 { + t.Errorf("Expected 2 rendered items, got %d", len(result)) + } + + if _, exists := result["terraform"]; !exists { + t.Error("Expected terraform data to be rendered") + } + }) +} + +func TestBasePipeline_determineContextName(t *testing.T) { + t.Run("ReturnsContextNameFromContext", func(t *testing.T) { + // Given a pipeline + pipeline := NewBasePipeline() + + // And context with contextName + ctx := context.WithValue(context.Background(), "contextName", "test-context") + + // When determineContextName is called + result := pipeline.determineContextName(ctx) + + // Then should return context name from context + if result != "test-context" { + t.Errorf("Expected 'test-context', got %s", result) + } + }) + + t.Run("ReturnsContextFromConfigHandler", func(t *testing.T) { + // Given a pipeline with config handler + pipeline := NewBasePipeline() + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.GetContextFunc = func() string { + return "config-context" + } + pipeline.configHandler = mockConfigHandler + + // When determineContextName is called + result := pipeline.determineContextName(context.Background()) + + // Then should return context from config handler + if result != "config-context" { + t.Errorf("Expected 'config-context', got %s", result) + } + }) + + t.Run("ReturnsLocalWhenNoContextSet", func(t *testing.T) { + // Given a pipeline with no context set + pipeline := NewBasePipeline() + + // When determineContextName is called + result := pipeline.determineContextName(context.Background()) + + // Then should return "local" + if result != "local" { + t.Errorf("Expected 'local', got %s", result) + } + }) + + t.Run("ReturnsLocalWhenContextIsLocal", func(t *testing.T) { + // Given a pipeline with config handler returning "local" + pipeline := NewBasePipeline() + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.GetContextFunc = func() string { + return "local" + } + pipeline.configHandler = mockConfigHandler + + // When determineContextName is called + result := pipeline.determineContextName(context.Background()) + + // Then should return "local" + if result != "local" { + t.Errorf("Expected 'local', got %s", result) + } + }) +}