diff --git a/pkg/artifact/artifact.go b/pkg/artifact/artifact.go index da59e61ad..b9fb81239 100644 --- a/pkg/artifact/artifact.go +++ b/pkg/artifact/artifact.go @@ -354,16 +354,15 @@ func (a *ArtifactBuilder) GetTemplateData(ociRef string) (map[string][]byte, err return nil, fmt.Errorf("failed to parse OCI reference %s: %w", ociRef, err) } + artifacts, err := a.Pull([]string{ociRef}) + if err != nil { + return nil, fmt.Errorf("failed to pull OCI artifact %s: %w", ociRef, err) + } + cacheKey := fmt.Sprintf("%s/%s:%s", registry, repository, tag) - var artifactData []byte - if cached, ok := a.ociCache[cacheKey]; ok { - artifactData = cached - } else { - artifactData, err = a.downloadOCIArtifact(registry, repository, tag) - if err != nil { - return nil, fmt.Errorf("failed to download OCI artifact %s: %w", ociRef, err) - } - a.ociCache[cacheKey] = artifactData + artifactData, exists := artifacts[cacheKey] + if !exists { + return nil, fmt.Errorf("failed to retrieve artifact data for %s", ociRef) } templateData := make(map[string][]byte) diff --git a/pkg/pipelines/init.go b/pkg/pipelines/init.go index f2af3ce2f..a09636700 100644 --- a/pkg/pipelines/init.go +++ b/pkg/pipelines/init.go @@ -460,11 +460,12 @@ func (p *InitPipeline) writeConfigurationFiles() error { return nil } -// handleBlueprintLoading loads blueprint data based on the reset flag and blueprint file presence. +// handleBlueprintLoading loads blueprint data for the InitPipeline based on the reset flag and blueprint file presence. // If reset is true, loads blueprint from template data if available. If reset is false, prefers an existing blueprint.yaml file over template data. // If no template blueprint data exists, loads from existing config. Returns an error if loading fails. func (p *InitPipeline) handleBlueprintLoading(ctx context.Context, renderedData map[string]any, reset bool) error { shouldLoadFromTemplate := false + usingLocalTemplates := p.hasLocalTemplates() if reset { shouldLoadFromTemplate = true @@ -486,9 +487,76 @@ func (p *InitPipeline) handleBlueprintLoading(ctx context.Context, renderedData if err := p.loadBlueprintFromTemplate(ctx, renderedData); err != nil { return err } - } else { + if usingLocalTemplates { + if blueprintData, exists := renderedData["blueprint"]; exists { + if blueprintMap, ok := blueprintData.(map[string]any); ok { + if sources, ok := blueprintMap["sources"].([]any); ok && len(sources) > 0 { + if err := p.loadExplicitSources(sources); err != nil { + return fmt.Errorf("failed to load explicit sources: %w", err) + } + } + } + } + } + } else if !usingLocalTemplates { if err := p.blueprintHandler.LoadConfig(); err != nil { - return fmt.Errorf("Error loading blueprint config: %w", err) + return fmt.Errorf("error loading blueprint config: %w", err) + } + sources := p.blueprintHandler.GetSources() + if len(sources) > 0 && p.artifactBuilder != nil { + var ociURLs []string + for _, source := range sources { + if strings.HasPrefix(source.Url, "oci://") { + ociURLs = append(ociURLs, source.Url) + } + } + if len(ociURLs) > 0 { + _, err := p.artifactBuilder.Pull(ociURLs) + if err != nil { + return fmt.Errorf("failed to load OCI sources: %w", err) + } + } + } + } + + return nil +} + +// hasLocalTemplates checks if the contexts/_template directory exists in the project. +func (p *InitPipeline) hasLocalTemplates() bool { + if p.shell == nil || p.shims == nil { + return false + } + + projectRoot, err := p.shell.GetProjectRoot() + if err != nil { + return false + } + + templateDir := filepath.Join(projectRoot, "contexts", "_template") + _, err = p.shims.Stat(templateDir) + return err == nil +} + +// loadExplicitSources loads OCI sources that are explicitly defined in blueprint templates. +func (p *InitPipeline) loadExplicitSources(sources []any) error { + if p.artifactBuilder == nil { + return nil + } + + var ociURLs []string + for _, source := range sources { + if sourceMap, ok := source.(map[string]any); ok { + if url, ok := sourceMap["url"].(string); ok && strings.HasPrefix(url, "oci://") { + ociURLs = append(ociURLs, url) + } + } + } + + if len(ociURLs) > 0 { + _, err := p.artifactBuilder.Pull(ociURLs) + if err != nil { + return fmt.Errorf("failed to load explicit OCI sources: %w", err) } } diff --git a/pkg/pipelines/init_test.go b/pkg/pipelines/init_test.go index cfeb72fad..d22048203 100644 --- a/pkg/pipelines/init_test.go +++ b/pkg/pipelines/init_test.go @@ -917,7 +917,7 @@ func TestInitPipeline_prepareTemplateData(t *testing.T) { // Set up BasePipeline properly pipeline.BasePipeline = *NewBasePipeline() - pipeline.BasePipeline.injector = di.NewInjector() + injector := di.NewInjector() mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) expectedLocalData := map[string][]byte{ @@ -926,7 +926,12 @@ func TestInitPipeline_prepareTemplateData(t *testing.T) { mockBlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { return expectedLocalData, nil } - pipeline.BasePipeline.injector.Register("blueprintHandler", mockBlueprintHandler) + injector.Register("blueprintHandler", mockBlueprintHandler) + + // Initialize the pipeline to set up all components + if err := pipeline.BasePipeline.Initialize(injector, context.Background()); err != nil { + t.Fatalf("Failed to initialize pipeline: %v", err) + } // When prepareTemplateData is called with no blueprint context templateData, err := pipeline.BasePipeline.prepareTemplateData(context.Background()) @@ -995,15 +1000,14 @@ func TestInitPipeline_prepareTemplateData(t *testing.T) { // Set up BasePipeline properly pipeline.BasePipeline = *NewBasePipeline() - pipeline.BasePipeline.injector = di.NewInjector() - pipeline.BasePipeline.artifactBuilder = nil + injector := di.NewInjector() // Mock config handler (needed for determineContextName) mockConfigHandler := config.NewMockConfigHandler() mockConfigHandler.GetContextFunc = func() string { return "local" } - pipeline.BasePipeline.configHandler = mockConfigHandler + injector.Register("configHandler", mockConfigHandler) // Mock blueprint handler with no local templates but default template mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) @@ -1016,7 +1020,15 @@ func TestInitPipeline_prepareTemplateData(t *testing.T) { mockBlueprintHandler.GetDefaultTemplateDataFunc = func(contextName string) (map[string][]byte, error) { return expectedDefaultData, nil } - pipeline.BasePipeline.injector.Register("blueprintHandler", mockBlueprintHandler) + injector.Register("blueprintHandler", mockBlueprintHandler) + + // Initialize the pipeline to set up all components + if err := pipeline.BasePipeline.Initialize(injector, context.Background()); err != nil { + t.Fatalf("Failed to initialize pipeline: %v", err) + } + + // Set artifact builder to nil to test the "no artifact builder" scenario + pipeline.BasePipeline.artifactBuilder = nil // When prepareTemplateData is called templateData, err := pipeline.BasePipeline.prepareTemplateData(context.Background()) diff --git a/pkg/pipelines/pipeline.go b/pkg/pipelines/pipeline.go index 0b3521df5..ac3897c4d 100644 --- a/pkg/pipelines/pipeline.go +++ b/pkg/pipelines/pipeline.go @@ -102,6 +102,7 @@ type BasePipeline struct { injector di.Injector templateRenderer template.Template artifactBuilder bundler.Artifact + blueprintHandler blueprint.BlueprintHandler } // ============================================================================= @@ -128,6 +129,7 @@ func (p *BasePipeline) Initialize(injector di.Injector, ctx context.Context) err p.shims = p.withShims() p.templateRenderer = p.withTemplateRenderer() p.artifactBuilder = p.withArtifactBuilder() + p.blueprintHandler = p.withBlueprintHandler() if err := p.shell.Initialize(); err != nil { return fmt.Errorf("failed to initialize shell: %w", err) @@ -781,8 +783,10 @@ func (p *BasePipeline) withTemplateRenderer() template.Template { 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. +// prepareTemplateData loads template data for pipeline execution. +// Source priority: blueprint context, local handler data, default artifact, +// then default template for current context. Returns a map of template file +// names to byte content, or error if loading fails. func (p *BasePipeline) prepareTemplateData(ctx context.Context) (map[string][]byte, error) { var blueprintValue string if blueprintCtx := ctx.Value("blueprint"); blueprintCtx != nil { @@ -808,8 +812,8 @@ func (p *BasePipeline) prepareTemplateData(ctx context.Context) (map[string][]by } } - if blueprintHandler := p.withBlueprintHandler(); blueprintHandler != nil { - blueprintTemplateData, err := blueprintHandler.GetLocalTemplateData() + if p.blueprintHandler != nil { + blueprintTemplateData, err := p.blueprintHandler.GetLocalTemplateData() if err != nil { return nil, fmt.Errorf("failed to get local template data: %w", err) } @@ -832,9 +836,9 @@ func (p *BasePipeline) prepareTemplateData(ctx context.Context) (map[string][]by return templateData, nil } - if blueprintHandler := p.withBlueprintHandler(); blueprintHandler != nil { + if p.blueprintHandler != nil { contextName := p.determineContextName(ctx) - defaultTemplateData, err := blueprintHandler.GetDefaultTemplateData(contextName) + defaultTemplateData, err := p.blueprintHandler.GetDefaultTemplateData(contextName) if err != nil { return nil, fmt.Errorf("failed to get default template data: %w", err) } @@ -844,9 +848,8 @@ func (p *BasePipeline) prepareTemplateData(ctx context.Context) (map[string][]by 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. +// processTemplateData renders template data using the pipeline's template renderer. +// Returns a map of rendered template data or an error if processing fails. func (p *BasePipeline) processTemplateData(templateData map[string][]byte) (map[string]any, error) { if p.templateRenderer == nil || len(templateData) == 0 { return nil, nil @@ -857,10 +860,8 @@ func (p *BasePipeline) processTemplateData(templateData map[string][]byte) (map[ 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) - } + if err := p.loadBlueprintFromTemplate(context.Background(), renderedData); err != nil { + return nil, fmt.Errorf("failed to load blueprint from template: %w", err) } return renderedData, nil @@ -894,13 +895,6 @@ func (p *BasePipeline) loadBlueprintFromTemplate(ctx context.Context, renderedDa return err } } - } else if p.artifactBuilder != nil { - effectiveBlueprintURL := constants.GetEffectiveBlueprintURL() - var err error - ociInfo, err = bundler.ParseOCIReference(effectiveBlueprintURL) - if err != nil { - return fmt.Errorf("failed to parse default blueprint reference: %w", err) - } } blueprintHandler := p.withBlueprintHandler() diff --git a/pkg/pipelines/pipeline_test.go b/pkg/pipelines/pipeline_test.go index cc948d907..564eab2cc 100644 --- a/pkg/pipelines/pipeline_test.go +++ b/pkg/pipelines/pipeline_test.go @@ -2923,7 +2923,7 @@ func TestBasePipeline_prepareTemplateData(t *testing.T) { t.Run("Priority2_LocalTemplatesWhenNoExplicitBlueprint", func(t *testing.T) { // Given a pipeline with local templates but no explicit blueprint pipeline := NewBasePipeline() - pipeline.injector = di.NewInjector() + injector := di.NewInjector() mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) expectedLocalData := map[string][]byte{ @@ -2932,7 +2932,12 @@ func TestBasePipeline_prepareTemplateData(t *testing.T) { mockBlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { return expectedLocalData, nil } - pipeline.injector.Register("blueprintHandler", mockBlueprintHandler) + injector.Register("blueprintHandler", mockBlueprintHandler) + + // Initialize the pipeline to set up all components + if err := pipeline.Initialize(injector, context.Background()); err != nil { + t.Fatalf("Failed to initialize pipeline: %v", err) + } // When prepareTemplateData is called with no blueprint context templateData, err := pipeline.prepareTemplateData(context.Background()) @@ -2992,17 +2997,78 @@ func TestBasePipeline_prepareTemplateData(t *testing.T) { } }) + t.Run("Priority3_LocalTemplateDirectoryExistsUsesLocalEvenIfEmpty", func(t *testing.T) { + // Given a pipeline with contexts/_template directory that exists but has no .jsonnet files + pipeline := NewBasePipeline() + injector := di.NewInjector() + + // Mock shell to return project root + mockShell := shell.NewMockShell(nil) + mockShell.GetProjectRootFunc = func() (string, error) { + return "/test/project", nil + } + injector.Register("shell", mockShell) + + // Mock shims to simulate contexts/_template directory exists + shims := &Shims{ + Stat: func(path string) (os.FileInfo, error) { + if path == "/test/project/contexts/_template" { + return &mockInitFileInfo{name: "_template", isDir: true}, nil + } + return nil, os.ErrNotExist + }, + } + injector.Register("shims", shims) + + // Mock blueprint handler with empty local templates (no .jsonnet files) + mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) + mockBlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { + // Return empty map but with values.yaml data merged in + return map[string][]byte{ + "values": []byte("external_domain: local.test"), + }, nil + } + injector.Register("blueprintHandler", mockBlueprintHandler) + + // Mock artifact builder (should NOT be called) + mockArtifactBuilder := artifact.NewMockArtifact() + mockArtifactBuilder.GetTemplateDataFunc = func(ociRef string) (map[string][]byte, error) { + t.Error("Artifact builder should not be called when local template directory exists") + return nil, fmt.Errorf("should not be called") + } + injector.Register("artifactBuilder", mockArtifactBuilder) + + // Initialize the pipeline to set up all components + if err := pipeline.Initialize(injector, context.Background()); err != nil { + t.Fatalf("Failed to initialize pipeline: %v", err) + } + + // When prepareTemplateData is called with no blueprint context + templateData, err := pipeline.prepareTemplateData(context.Background()) + + // Then should use local template data even if it only contains values.yaml + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if len(templateData) != 1 { + t.Errorf("Expected 1 template file (values), got %d", len(templateData)) + } + if string(templateData["values"]) != "external_domain: local.test" { + t.Error("Expected local values data") + } + }) + t.Run("Priority4_EmbeddedDefaultWhenNoArtifactBuilder", func(t *testing.T) { // Given a pipeline with no artifact builder pipeline := NewBasePipeline() - pipeline.injector = di.NewInjector() + injector := di.NewInjector() // Mock config handler (needed for determineContextName) mockConfigHandler := config.NewMockConfigHandler() mockConfigHandler.GetContextFunc = func() string { return "local" } - pipeline.configHandler = mockConfigHandler + injector.Register("configHandler", mockConfigHandler) // Mock blueprint handler with no local templates but default template mockBlueprintHandler := blueprint.NewMockBlueprintHandler(nil) @@ -3015,7 +3081,15 @@ func TestBasePipeline_prepareTemplateData(t *testing.T) { mockBlueprintHandler.GetDefaultTemplateDataFunc = func(contextName string) (map[string][]byte, error) { return expectedDefaultData, nil } - pipeline.injector.Register("blueprintHandler", mockBlueprintHandler) + injector.Register("blueprintHandler", mockBlueprintHandler) + + // Initialize the pipeline to set up all components + if err := pipeline.Initialize(injector, context.Background()); err != nil { + t.Fatalf("Failed to initialize pipeline: %v", err) + } + + // Set artifact builder to nil to test the "no artifact builder" scenario + pipeline.artifactBuilder = nil // When prepareTemplateData is called templateData, err := pipeline.prepareTemplateData(context.Background()) diff --git a/pkg/template/jsonnet_template.go b/pkg/template/jsonnet_template.go index 7951ea48a..b4842855c 100644 --- a/pkg/template/jsonnet_template.go +++ b/pkg/template/jsonnet_template.go @@ -96,7 +96,8 @@ func (t *JsonnetTemplate) Process(templateData map[string][]byte, renderedData m // processJsonnetTemplate evaluates a Jsonnet template string using the Windsor context and values data. // The Windsor configuration is marshaled to YAML, converted to a map, and augmented with context and project name metadata. -// The context is serialized to JSON and injected into the Jsonnet VM as an external variable, along with helper functions and the effective blueprint URL. +// The context is serialized to JSON and injected into the Jsonnet VM as an external variable, along with helper functions +// and the effective blueprint URL for templates to reference if needed. // If values data is provided, it is merged into the context map before serialization. // The template is evaluated, and the output is unmarshaled from JSON into a map. // Returns the resulting map or an error if any step fails.