Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions pkg/artifact/artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
74 changes: 71 additions & 3 deletions pkg/pipelines/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
}

Expand Down
24 changes: 18 additions & 6 deletions pkg/pipelines/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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())
Expand Down Expand Up @@ -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)
Expand All @@ -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())
Expand Down
34 changes: 14 additions & 20 deletions pkg/pipelines/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ type BasePipeline struct {
injector di.Injector
templateRenderer template.Template
artifactBuilder bundler.Artifact
blueprintHandler blueprint.BlueprintHandler
}

// =============================================================================
Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down
84 changes: 79 additions & 5 deletions pkg/pipelines/pipeline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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())
Expand Down Expand Up @@ -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)
Expand All @@ -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())
Expand Down
3 changes: 2 additions & 1 deletion pkg/template/jsonnet_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading