From 9203d1074a0d405c2abbfd5dcf563a44eeb83300 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Mon, 20 Oct 2025 14:20:54 -0400 Subject: [PATCH] refactor(template): Remove template package The template processing package is no longer necessary. We now leverage `Features` for composition, with the `expr` function `jsonnet()` for loading jsonnet data in place if necessary. Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/generators/kustomize_generator.go | 188 -- pkg/generators/kustomize_generator_test.go | 589 ------ pkg/pipelines/init.go | 104 +- pkg/pipelines/init_test.go | 11 +- pkg/pipelines/install.go | 23 +- pkg/pipelines/install_test.go | 179 +- pkg/pipelines/pipeline.go | 89 +- pkg/pipelines/pipeline_test.go | 318 --- pkg/template/jsonnet_template.go | 432 ---- pkg/template/jsonnet_template_test.go | 2230 -------------------- pkg/template/mock_template.go | 45 - pkg/template/mock_template_test.go | 157 -- pkg/template/shims.go | 80 - 13 files changed, 68 insertions(+), 4377 deletions(-) delete mode 100644 pkg/generators/kustomize_generator.go delete mode 100644 pkg/generators/kustomize_generator_test.go delete mode 100644 pkg/template/jsonnet_template.go delete mode 100644 pkg/template/jsonnet_template_test.go delete mode 100644 pkg/template/mock_template.go delete mode 100644 pkg/template/mock_template_test.go delete mode 100644 pkg/template/shims.go diff --git a/pkg/generators/kustomize_generator.go b/pkg/generators/kustomize_generator.go deleted file mode 100644 index 1a8edf0f4..000000000 --- a/pkg/generators/kustomize_generator.go +++ /dev/null @@ -1,188 +0,0 @@ -package generators - -import ( - "fmt" - "strings" - - "github.com/windsorcli/cli/pkg/blueprint" - "github.com/windsorcli/cli/pkg/di" -) - -// ============================================================================= -// Types -// ============================================================================= - -// KustomizeGenerator is a generator that processes and generates kustomize files -type KustomizeGenerator struct { - BaseGenerator - blueprintHandler blueprint.BlueprintHandler -} - -// ============================================================================= -// Constructor -// ============================================================================= - -// NewKustomizeGenerator creates a new KustomizeGenerator with the provided dependency injector. -// It initializes the base generator and prepares it for kustomize file generation. -func NewKustomizeGenerator(injector di.Injector) *KustomizeGenerator { - return &KustomizeGenerator{ - BaseGenerator: *NewGenerator(injector), - } -} - -// ============================================================================= -// Public Methods -// ============================================================================= - -// Initialize sets up the KustomizeGenerator dependencies including the blueprint handler. -// Calls the base generator's Initialize method and then resolves the blueprint handler -// for kustomize-specific operations. -func (g *KustomizeGenerator) Initialize() error { - if err := g.BaseGenerator.Initialize(); err != nil { - return fmt.Errorf("failed to initialize base generator: %w", err) - } - - blueprintHandler := g.injector.Resolve("blueprintHandler") - if blueprintHandler == nil { - return fmt.Errorf("blueprint handler not found in dependency injector") - } - - handler, ok := blueprintHandler.(blueprint.BlueprintHandler) - if !ok { - return fmt.Errorf("resolved blueprint handler is not of expected type") - } - - g.blueprintHandler = handler - return nil -} - -// Generate processes kustomize template data and stores it in-memory for use during install. -// Filters data for kustomize-related keys and stores them in the blueprint handler -// instead of writing files to disk. This allows values and patches to be composed -// with user-defined files at install time. -// Returns an error if data is nil or if storing the data fails. -func (g *KustomizeGenerator) Generate(data map[string]any, overwrite ...bool) error { - if data == nil { - return fmt.Errorf("data cannot be nil") - } - - kustomizeData := make(map[string]any) - for key, values := range data { - if strings.HasPrefix(key, "patches/") || key == "substitution" { - if err := g.validateKustomizeData(key, values); err != nil { - return fmt.Errorf("invalid kustomize data for key %s: %w", key, err) - } - kustomizeData[key] = values - } - } - - if len(kustomizeData) > 0 { - g.blueprintHandler.SetRenderedKustomizeData(kustomizeData) - } - - return nil -} - -// ============================================================================= -// Private Methods -// ============================================================================= - -// validateKustomizeData validates kustomize template data for supported kustomize keys. -// For patch keys, validates the value as a Kubernetes manifest. For values keys, accepts map[string]any or -// YAML bytes and validates for post-build substitution compatibility. Returns an error if the data is invalid -// or of unsupported type. -func (g *KustomizeGenerator) validateKustomizeData(key string, values any) error { - if strings.HasPrefix(key, "patches/") { - valuesMap, ok := values.(map[string]any) - if !ok { - return fmt.Errorf("patch values must be a map, got %T", values) - } - return g.validateKubernetesManifest(valuesMap) - } - - if key == "substitution" { - var valuesMap map[string]any - switch v := values.(type) { - case map[string]any: - valuesMap = v - case []byte: - if err := g.shims.YamlUnmarshal(v, &valuesMap); err != nil { - return fmt.Errorf("failed to unmarshal values YAML: %w", err) - } - default: - return fmt.Errorf("values must be a map or YAML bytes, got %T", values) - } - return g.validatePostBuildValues(valuesMap, "", 0) - } - - return nil -} - -// validatePostBuildValues checks if the values map is valid for Flux post-build substitution. -// Permitted types: string, numeric, boolean. Allows one map nesting if all nested values are scalar. -// Slices and nested complex types are not allowed. parentKey is for error reporting (e.g. "ingress.ip"). -// depth tracks nesting (0 = top, 1 = one level deep). Returns error if unsupported type or excess nesting. -func (g *KustomizeGenerator) validatePostBuildValues(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) - } - if err := g.validatePostBuildValues(v, currentKey, depth+1); 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 -} - -// validateKubernetesManifest validates that the content represents a valid Kubernetes manifest. -// Checks for required fields like apiVersion, kind, and metadata.name. -// Returns an error if the manifest is invalid. -func (g *KustomizeGenerator) validateKubernetesManifest(content any) error { - contentMap, ok := content.(map[string]any) - if !ok { - return fmt.Errorf("content must be a map, got %T", content) - } - - apiVersion, ok := contentMap["apiVersion"].(string) - if !ok || apiVersion == "" { - return fmt.Errorf("manifest missing or invalid 'apiVersion' field") - } - - kind, ok := contentMap["kind"].(string) - if !ok || kind == "" { - return fmt.Errorf("manifest missing or invalid 'kind' field") - } - - metadata, ok := contentMap["metadata"].(map[string]any) - if !ok { - return fmt.Errorf("manifest missing 'metadata' field") - } - - name, ok := metadata["name"].(string) - if !ok || name == "" { - return fmt.Errorf("manifest missing or invalid 'metadata.name' field") - } - - return nil -} - -// ============================================================================= -// Interface Compliance -// ============================================================================= - -// Ensure KustomizeGenerator implements Generator -var _ Generator = (*KustomizeGenerator)(nil) diff --git a/pkg/generators/kustomize_generator_test.go b/pkg/generators/kustomize_generator_test.go deleted file mode 100644 index 07c6fdc0e..000000000 --- a/pkg/generators/kustomize_generator_test.go +++ /dev/null @@ -1,589 +0,0 @@ -package generators - -import ( - "strings" - "testing" - - bundler "github.com/windsorcli/cli/pkg/artifact" - "github.com/windsorcli/cli/pkg/blueprint" - "github.com/windsorcli/cli/pkg/config" - "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/shell" -) - -func TestKustomizeGenerator_Generate_InMemory(t *testing.T) { - t.Run("FiltersAndStoresKustomizeData", func(t *testing.T) { - injector := di.NewInjector() - generator := NewKustomizeGenerator(injector) - - // Mock blueprint handler - mockBlueprintHandler := &blueprint.MockBlueprintHandler{} - var setData map[string]any - mockBlueprintHandler.SetRenderedKustomizeDataFunc = func(data map[string]any) { - setData = data - } - - // Initialize with mock - generator.blueprintHandler = mockBlueprintHandler - - data := map[string]any{ - "patches/test/configmap": map[string]any{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]any{ - "name": "test-config", - }, - }, - "substitution": map[string]any{ - "environment": "test", - }, - "other/file": "should be ignored", - "terraform/main.tf": "terraform content", - } - - err := generator.Generate(data, false) - if err != nil { - t.Fatalf("expected Generate to succeed, got: %v", err) - } - - // Verify only kustomize data was stored - if len(setData) != 2 { - t.Errorf("expected 2 kustomize items, got %d", len(setData)) - } - if _, exists := setData["patches/test/configmap"]; !exists { - t.Error("expected patches/test/configmap to be stored") - } - if _, exists := setData["substitution"]; !exists { - t.Error("expected substitution to be stored") - } - if _, exists := setData["other/file"]; exists { - t.Error("expected non-kustomize data to be filtered out") - } - }) - - t.Run("NoKustomizeData", func(t *testing.T) { - injector := di.NewInjector() - generator := NewKustomizeGenerator(injector) - - mockBlueprintHandler := &blueprint.MockBlueprintHandler{} - called := false - mockBlueprintHandler.SetRenderedKustomizeDataFunc = func(data map[string]any) { - called = true - } - - generator.blueprintHandler = mockBlueprintHandler - - data := map[string]any{ - "other/file": "should be ignored", - "terraform/main.tf": "terraform content", - } - - err := generator.Generate(data, false) - if err != nil { - t.Fatalf("expected Generate to succeed with no kustomize data, got: %v", err) - } - - if called { - t.Error("expected SetRenderedKustomizeData not to be called when no kustomize data present") - } - }) - - t.Run("ValidationError", func(t *testing.T) { - injector := di.NewInjector() - generator := NewKustomizeGenerator(injector) - - mockBlueprintHandler := &blueprint.MockBlueprintHandler{} - generator.blueprintHandler = mockBlueprintHandler - - data := map[string]any{ - "patches/test/configmap": "invalid data - should be map", - } - - err := generator.Generate(data, false) - if err == nil { - t.Fatal("expected Generate to fail with validation error") - } - if !strings.Contains(err.Error(), "invalid kustomize data") { - t.Errorf("expected validation error, got: %v", err) - } - }) - - t.Run("NilData", func(t *testing.T) { - injector := di.NewInjector() - generator := NewKustomizeGenerator(injector) - - err := generator.Generate(nil, false) - if err == nil { - t.Fatal("expected Generate to fail with nil data") - } - if !strings.Contains(err.Error(), "data cannot be nil") { - t.Errorf("expected nil data error, got: %v", err) - } - }) -} - -func TestKustomizeGenerator_validateKustomizeData(t *testing.T) { - injector := di.NewInjector() - generator := NewKustomizeGenerator(injector) - - t.Run("ValidPatch", func(t *testing.T) { - data := map[string]any{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]any{ - "name": "test-config", - }, - } - - err := generator.validateKustomizeData("patches/test/configmap", data) - if err != nil { - t.Errorf("expected valid patch to pass validation, got: %v", err) - } - }) - - t.Run("InvalidPatch", func(t *testing.T) { - data := map[string]any{ - "kind": "ConfigMap", - // Missing apiVersion and metadata.name - } - - err := generator.validateKustomizeData("patches/test/configmap", data) - if err == nil { - t.Error("expected invalid patch to fail validation") - } - }) - - t.Run("ValidValues", func(t *testing.T) { - data := map[string]any{ - "environment": "test", - "port": 80, - "enabled": true, - } - - err := generator.validateKustomizeData("substitution", data) - if err != nil { - t.Errorf("expected valid values to pass validation, got: %v", err) - } - }) - - t.Run("InvalidValues", func(t *testing.T) { - data := map[string]any{ - "invalid": []string{"slice", "not", "allowed"}, - } - - err := generator.validateKustomizeData("substitution", data) - if err == nil { - t.Error("expected invalid values to fail validation") - } - }) - - t.Run("NonMapData", func(t *testing.T) { - err := generator.validateKustomizeData("patches/test/configmap", "not a map") - if err == nil { - t.Error("expected non-map data to fail validation") - } - if !strings.Contains(err.Error(), "patch values must be a map") { - t.Errorf("expected map type error, got: %v", err) - } - }) - - t.Run("UnknownKey", func(t *testing.T) { - data := map[string]any{"test": "value"} - err := generator.validateKustomizeData("unknown/key", data) - if err != nil { - t.Errorf("expected unknown key to pass (no validation), got: %v", err) - } - }) -} - -func TestKustomizeGenerator_Initialize(t *testing.T) { - setup := func(t *testing.T) *KustomizeGenerator { - t.Helper() - injector := di.NewInjector() - generator := NewKustomizeGenerator(injector) - return generator - } - - setupWithBaseDependencies := func(t *testing.T) *KustomizeGenerator { - t.Helper() - injector := di.NewInjector() - generator := NewKustomizeGenerator(injector) - - // Register required base dependencies - mockConfigHandler := &config.MockConfigHandler{} - mockShell := &shell.MockShell{} - mockArtifactBuilder := &bundler.MockArtifact{} - - generator.injector.Register("configHandler", mockConfigHandler) - generator.injector.Register("shell", mockShell) - generator.injector.Register("artifactBuilder", mockArtifactBuilder) - - return generator - } - - t.Run("Success", func(t *testing.T) { - // Given a generator with all required dependencies - generator := setupWithBaseDependencies(t) - mockBlueprintHandler := &blueprint.MockBlueprintHandler{} - generator.injector.Register("blueprintHandler", mockBlueprintHandler) - // When initializing - err := generator.Initialize() - // Then no error should be returned - if err != nil { - t.Errorf("Expected error = %v, got = %v", nil, err) - } - if generator.blueprintHandler != mockBlueprintHandler { - t.Error("Expected blueprint handler to be set") - } - }) - - t.Run("BaseGeneratorInitializationFailure", func(t *testing.T) { - // Given a generator with missing config handler - generator := setup(t) - // When initializing - err := generator.Initialize() - // Then error should be returned - if err == nil { - t.Error("Expected error for base generator initialization failure") - } - if !strings.Contains(err.Error(), "failed to initialize base generator") { - t.Errorf("Expected base generator error, got: %v", err) - } - }) - - t.Run("BlueprintHandlerNotFound", func(t *testing.T) { - // Given a generator with base dependencies but no blueprint handler - generator := setupWithBaseDependencies(t) - // When initializing - err := generator.Initialize() - // Then error should be returned from base generator - if err == nil { - t.Error("Expected error for missing blueprint handler") - } - if !strings.Contains(err.Error(), "failed to initialize base generator") { - t.Errorf("Expected base generator error, got: %v", err) - } - }) - - t.Run("BlueprintHandlerWrongType", func(t *testing.T) { - // Given a generator with wrong type in injector - generator := setupWithBaseDependencies(t) - generator.injector.Register("blueprintHandler", "not a blueprint handler") - // When initializing - err := generator.Initialize() - // Then error should be returned from base generator - if err == nil { - t.Error("Expected error for wrong blueprint handler type") - } - if !strings.Contains(err.Error(), "failed to initialize base generator") { - t.Errorf("Expected base generator error, got: %v", err) - } - }) - - t.Run("BlueprintHandlerNil", func(t *testing.T) { - // Given a generator with nil blueprint handler - generator := setupWithBaseDependencies(t) - generator.injector.Register("blueprintHandler", nil) - // When initializing - err := generator.Initialize() - // Then error should be returned from base generator - if err == nil { - t.Error("Expected error for nil blueprint handler") - } - if !strings.Contains(err.Error(), "failed to initialize base generator") { - t.Errorf("Expected base generator error, got: %v", err) - } - }) -} - -func TestKustomizeGenerator_validatePostBuildValues(t *testing.T) { - setup := func(t *testing.T) *KustomizeGenerator { - t.Helper() - injector := di.NewInjector() - generator := NewKustomizeGenerator(injector) - return generator - } - - t.Run("ValidScalarTypes", func(t *testing.T) { - // Given a generator and valid scalar values - generator := setup(t) - values := map[string]any{ - "string": "test", - "int": 42, - "int8": int8(8), - "int16": int16(16), - "int32": int32(32), - "int64": int64(64), - "uint": uint(100), - "uint8": uint8(8), - "uint16": uint16(16), - "uint32": uint32(32), - "uint64": uint64(64), - "float32": float32(3.14), - "float64": float64(3.14159), - "bool": true, - } - // When validating post-build values - err := generator.validatePostBuildValues(values, "", 0) - // Then no error should be returned - if err != nil { - t.Errorf("Expected error = %v, got = %v", nil, err) - } - }) - - t.Run("ValidNestedMap", func(t *testing.T) { - // Given a generator and valid nested map - generator := setup(t) - values := map[string]any{ - "nested": map[string]any{ - "string": "test", - "int": 42, - "bool": true, - }, - } - // When validating post-build values - err := generator.validatePostBuildValues(values, "", 0) - // Then no error should be returned - if err != nil { - t.Errorf("Expected error = %v, got = %v", nil, err) - } - }) - - t.Run("InvalidSlice", func(t *testing.T) { - // Given a generator and values containing a slice - generator := setup(t) - values := map[string]any{ - "invalid": []any{"slice", "not", "allowed"}, - } - // When validating post-build values - err := generator.validatePostBuildValues(values, "", 0) - // Then error should be returned - if err == nil { - t.Error("Expected error for slice values") - } - if !strings.Contains(err.Error(), "cannot contain slices") { - t.Errorf("Expected slice error, got: %v", err) - } - }) - - t.Run("InvalidNestedComplexType", func(t *testing.T) { - // Given a generator and values with nested complex types - generator := setup(t) - values := map[string]any{ - "level1": map[string]any{ - "level2": map[string]any{ - "level3": "too deep", - }, - }, - } - // When validating post-build values - err := generator.validatePostBuildValues(values, "", 0) - // Then error should be returned - if err == nil { - t.Error("Expected error for nested complex types") - } - if !strings.Contains(err.Error(), "cannot contain nested complex types") { - t.Errorf("Expected nested complex type error, got: %v", err) - } - }) - - t.Run("InvalidUnsupportedType", func(t *testing.T) { - // Given a generator and values with unsupported type - generator := setup(t) - values := map[string]any{ - "unsupported": struct{}{}, - } - // When validating post-build values - err := generator.validatePostBuildValues(values, "", 0) - // Then error should be returned - if err == nil { - t.Error("Expected error for unsupported type") - } - if !strings.Contains(err.Error(), "unsupported type") { - t.Errorf("Expected unsupported type error, got: %v", err) - } - }) - - t.Run("EmptyMap", func(t *testing.T) { - // Given a generator and empty values map - generator := setup(t) - values := map[string]any{} - // When validating post-build values - err := generator.validatePostBuildValues(values, "", 0) - // Then no error should be returned - if err != nil { - t.Errorf("Expected error = %v, got = %v", nil, err) - } - }) - - t.Run("ParentKeyReporting", func(t *testing.T) { - // Given a generator and values with parent key context - generator := setup(t) - values := map[string]any{ - "invalid": []any{"slice", "not", "allowed"}, - } - // When validating post-build values with parent key - err := generator.validatePostBuildValues(values, "parent", 0) - // Then error should include parent key in message - if err == nil { - t.Error("Expected error for slice values") - } - if !strings.Contains(err.Error(), "parent.invalid") { - t.Errorf("Expected parent key in error message, got: %v", err) - } - }) - - t.Run("NestedSliceInMap", func(t *testing.T) { - // Given a generator and nested map containing slice - generator := setup(t) - values := map[string]any{ - "nested": map[string]any{ - "invalid": []any{"slice", "in", "nested"}, - }, - } - // When validating post-build values - err := generator.validatePostBuildValues(values, "", 0) - // Then error should be returned - if err == nil { - t.Error("Expected error for slice in nested map") - } - if !strings.Contains(err.Error(), "cannot contain slices") { - t.Errorf("Expected slice error, got: %v", err) - } - if !strings.Contains(err.Error(), "nested.invalid") { - t.Errorf("Expected nested key in error message, got: %v", err) - } - }) - - t.Run("MixedValidAndInvalid", func(t *testing.T) { - // Given a generator and values with mix of valid and invalid types - generator := setup(t) - values := map[string]any{ - "valid": "string", - "invalid": []any{"slice", "not", "allowed"}, - } - // When validating post-build values - err := generator.validatePostBuildValues(values, "", 0) - // Then error should be returned for the invalid type - if err == nil { - t.Error("Expected error for invalid type") - } - if !strings.Contains(err.Error(), "cannot contain slices") { - t.Errorf("Expected slice error, got: %v", err) - } - }) - - t.Run("NilValue", func(t *testing.T) { - // Given a generator and values with nil value - generator := setup(t) - values := map[string]any{ - "nil": nil, - } - // When validating post-build values - err := generator.validatePostBuildValues(values, "", 0) - // Then error should be returned for unsupported type - if err == nil { - t.Error("Expected error for nil value") - } - if !strings.Contains(err.Error(), "unsupported type") { - t.Errorf("Expected unsupported type error, got: %v", err) - } - }) -} - -func TestKustomizeGenerator_Generate_ValuesHandling(t *testing.T) { - t.Run("HandlesKustomizeValuesAsYAMLBytes", func(t *testing.T) { - // Given a kustomize generator with YAML values data - injector := di.NewInjector() - generator := NewKustomizeGenerator(injector) - - // Mock blueprint handler - mockBlueprintHandler := &blueprint.MockBlueprintHandler{} - var setData map[string]any - mockBlueprintHandler.SetRenderedKustomizeDataFunc = func(data map[string]any) { - setData = data - } - - // Initialize with mock - generator.blueprintHandler = mockBlueprintHandler - - // Create YAML values data - yamlData := []byte(`common: - external_domain: test.example.com - registry_url: registry.test.com -logging: - enabled: true -monitoring: - enabled: false`) - - data := map[string]any{ - "substitution": yamlData, - } - - // When Generate is called - err := generator.Generate(data, false) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // Verify the data was stored - if len(setData) != 1 { - t.Errorf("Expected 1 substitution item, got %d", len(setData)) - } - if _, exists := setData["substitution"]; !exists { - t.Error("Expected substitution to be stored") - } - }) - - t.Run("HandlesKustomizeValuesAsMap", func(t *testing.T) { - // Given a kustomize generator with map values data - injector := di.NewInjector() - generator := NewKustomizeGenerator(injector) - - // Mock blueprint handler - mockBlueprintHandler := &blueprint.MockBlueprintHandler{} - var setData map[string]any - mockBlueprintHandler.SetRenderedKustomizeDataFunc = func(data map[string]any) { - setData = data - } - - // Initialize with mock - generator.blueprintHandler = mockBlueprintHandler - - // Create map values data - mapData := map[string]any{ - "common": map[string]any{ - "external_domain": "test.example.com", - "registry_url": "registry.test.com", - }, - "logging": map[string]any{ - "enabled": true, - }, - "monitoring": map[string]any{ - "enabled": false, - }, - } - - data := map[string]any{ - "substitution": mapData, - } - - // When Generate is called - err := generator.Generate(data, false) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - // Verify the data was stored - if len(setData) != 1 { - t.Errorf("Expected 1 substitution item, got %d", len(setData)) - } - if _, exists := setData["substitution"]; !exists { - t.Error("Expected substitution to be stored") - } - }) -} diff --git a/pkg/pipelines/init.go b/pkg/pipelines/init.go index 2d844e07b..71f52ba57 100644 --- a/pkg/pipelines/init.go +++ b/pkg/pipelines/init.go @@ -17,7 +17,6 @@ import ( "github.com/windsorcli/cli/pkg/generators" "github.com/windsorcli/cli/pkg/shell" "github.com/windsorcli/cli/pkg/stack" - "github.com/windsorcli/cli/pkg/template" "github.com/windsorcli/cli/pkg/terraform" "github.com/windsorcli/cli/pkg/tools" "github.com/windsorcli/cli/pkg/workstation/network" @@ -38,7 +37,6 @@ import ( // InitPipeline handles the initialization of a Windsor project type InitPipeline struct { BasePipeline - templateRenderer template.Template blueprintHandler blueprint.BlueprintHandler toolsManager tools.ToolsManager stack stack.Stack @@ -152,7 +150,6 @@ func (p *InitPipeline) Initialize(injector di.Injector, ctx context.Context) err } p.terraformResolvers = terraformResolvers - p.templateRenderer = p.withTemplateRenderer() p.networkManager = p.withNetworking() p.virtualMachine = p.withVirtualMachine() p.containerRuntime = p.withContainerRuntime() @@ -213,12 +210,6 @@ func (p *InitPipeline) Initialize(injector di.Injector, ctx context.Context) err } } - if p.templateRenderer != nil { - if err := p.templateRenderer.Initialize(); err != nil { - return fmt.Errorf("failed to initialize template renderer: %w", err) - } - } - if p.networkManager != nil { if err := p.networkManager.Initialize(); err != nil { return fmt.Errorf("failed to initialize network manager: %w", err) @@ -257,14 +248,12 @@ func (p *InitPipeline) Execute(ctx context.Context) error { return fmt.Errorf("Error writing reset token: %w", err) } - // Phase 2: Template processing - templateData, err := p.prepareTemplateData(ctx) - if err != nil { - return fmt.Errorf("failed to prepare template data: %w", err) - } - renderedData, err := p.processTemplateData(templateData) - if err != nil { - return err + // Phase 2: Blueprint loading + if ctx.Value("blueprint") == nil && p.artifactBuilder != nil { + hasLocalTemplates := p.hasLocalTemplates() + if !hasLocalTemplates { + p.fallbackBlueprintURL = constants.GetEffectiveBlueprintURL() + } } // Phase 3: Blueprint handling @@ -272,7 +261,7 @@ func (p *InitPipeline) Execute(ctx context.Context) error { if resetValue := ctx.Value("reset"); resetValue != nil { reset = resetValue.(bool) } - if err := p.handleBlueprintLoading(ctx, renderedData, reset); err != nil { + if err := p.handleBlueprintLoading(ctx, reset); err != nil { return err } if err := p.blueprintHandler.Write(reset); err != nil { @@ -288,7 +277,7 @@ func (p *InitPipeline) Execute(ctx context.Context) error { // Phase 5: Final file generation for _, generator := range p.generators { - if err := generator.Generate(renderedData, reset); err != nil { + if err := generator.Generate(map[string]any{}, reset); err != nil { return fmt.Errorf("failed to generate from template data: %w", err) } } @@ -311,30 +300,6 @@ func (p *InitPipeline) Execute(ctx context.Context) error { return nil } -// prepareTemplateData sets the fallbackBlueprintURL if the default blueprint URL is used. -// It calls the base pipeline's prepareTemplateData, checks for explicit blueprint context and local templates, -// and assigns the fallback URL for blueprint processing if necessary. -// Returns the prepared template data or an error. -func (p *InitPipeline) prepareTemplateData(ctx context.Context) (map[string][]byte, error) { - templateData, err := p.BasePipeline.prepareTemplateData(ctx) - if err != nil { - return nil, err - } - if ctx.Value("blueprint") == nil && p.artifactBuilder != nil { - blueprintHandler := p.withBlueprintHandler() - hasLocalTemplates := false - if blueprintHandler != nil { - if localTemplateData, err := blueprintHandler.GetLocalTemplateData(); err == nil && len(localTemplateData) > 0 { - hasLocalTemplates = true - } - } - if !hasLocalTemplates { - p.fallbackBlueprintURL = constants.GetEffectiveBlueprintURL() - } - } - return templateData, nil -} - // ============================================================================= // Private Methods // ============================================================================= @@ -468,7 +433,7 @@ func (p *InitPipeline) writeConfigurationFiles() error { // 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 { +func (p *InitPipeline) handleBlueprintLoading(ctx context.Context, reset bool) error { shouldLoadFromTemplate := false usingLocalTemplates := p.hasLocalTemplates() @@ -485,28 +450,22 @@ func (p *InitPipeline) handleBlueprintLoading(ctx context.Context, renderedData } } - if shouldLoadFromTemplate && len(renderedData) > 0 && renderedData["blueprint"] != nil { + if shouldLoadFromTemplate { if p.fallbackBlueprintURL != "" { ctx = context.WithValue(ctx, "blueprint", p.fallbackBlueprintURL) } - if err := p.loadBlueprintFromTemplate(ctx, renderedData); err != nil { - return err - } - 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) - } - } - } - } + + _, err := p.blueprintHandler.GetLocalTemplateData() + if err != nil { + return fmt.Errorf("failed to get template data: %w", err) } - } else if !usingLocalTemplates { + } else { if err := p.blueprintHandler.LoadConfig(); err != nil { - return fmt.Errorf("error loading blueprint config: %w", err) + return fmt.Errorf("failed to load blueprint config: %w", err) } + } + + if !usingLocalTemplates { sources := p.blueprintHandler.GetSources() if len(sources) > 0 && p.artifactBuilder != nil { var ociURLs []string @@ -543,31 +502,6 @@ func (p *InitPipeline) hasLocalTemplates() bool { 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) - } - } - - return nil -} - // ============================================================================= // Interface Compliance // ============================================================================= diff --git a/pkg/pipelines/init_test.go b/pkg/pipelines/init_test.go index 890ed5676..70fc3017f 100644 --- a/pkg/pipelines/init_test.go +++ b/pkg/pipelines/init_test.go @@ -399,12 +399,12 @@ func TestInitPipeline_Execute(t *testing.T) { } }) - t.Run("ReturnsErrorWhenPrepareTemplateDataFails", func(t *testing.T) { + t.Run("ReturnsErrorWhenBlueprintLoadConfigFails", func(t *testing.T) { // Given successful reset token mocks.Shell.WriteResetTokenFunc = func() (string, error) { return "token", nil } - // And blueprint handler returns error + // And blueprint handler returns error on GetLocalTemplateData (template loading path) mocks.BlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { return nil, fmt.Errorf("template data error") } @@ -421,18 +421,19 @@ func TestInitPipeline_Execute(t *testing.T) { if err == nil { t.Error("Expected error, got nil") } - if !strings.Contains(err.Error(), "failed to prepare template data") { + if !strings.Contains(err.Error(), "failed to get template data") { t.Errorf("Expected template data error, got %v", err) } }) t.Run("ReturnsErrorWhenBlueprintWriteFails", func(t *testing.T) { - // Given successful template processing + // Given successful reset token mocks.Shell.WriteResetTokenFunc = func() (string, error) { return "token", nil } + // And successful template data loading mocks.BlueprintHandler.GetLocalTemplateDataFunc = func() (map[string][]byte, error) { - return map[string][]byte{"test.jsonnet": []byte("test")}, nil + return map[string][]byte{"blueprint": []byte("test")}, nil } // And blueprint write fails mocks.BlueprintHandler.WriteFunc = func(overwrite ...bool) error { diff --git a/pkg/pipelines/install.go b/pkg/pipelines/install.go index 87decd843..fc7ad37e5 100644 --- a/pkg/pipelines/install.go +++ b/pkg/pipelines/install.go @@ -8,7 +8,6 @@ import ( "github.com/windsorcli/cli/pkg/blueprint" "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/generators" - "github.com/windsorcli/cli/pkg/template" ) // The InstallPipeline is a specialized component that manages blueprint installation functionality. @@ -24,7 +23,6 @@ import ( type InstallPipeline struct { BasePipeline blueprintHandler blueprint.BlueprintHandler - templateRenderer template.Template generators []generators.Generator artifactBuilder artifact.Artifact } @@ -57,7 +55,6 @@ func (p *InstallPipeline) Initialize(injector di.Injector, ctx context.Context) kubernetesManager := p.withKubernetesManager() _ = p.withKubernetesClient() p.blueprintHandler = p.withBlueprintHandler() - p.templateRenderer = p.withTemplateRenderer() p.artifactBuilder = p.withArtifactBuilder() generators, err := p.withGenerators() if err != nil { @@ -100,34 +97,24 @@ func (p *InstallPipeline) Execute(ctx context.Context) error { return fmt.Errorf("No blueprint handler found") } - // Phase 1: Load blueprint config (cached if already loaded) + // Phase 1: Load blueprint config if err := p.blueprintHandler.LoadConfig(); err != nil { return fmt.Errorf("Error loading blueprint config: %w", err) } - // Phase 2: Process templates for kustomize data - templateData, err := p.prepareTemplateData(ctx) - if err != nil { - return fmt.Errorf("failed to prepare template data: %w", err) - } - renderedData, err := p.processTemplateData(templateData) - if err != nil { - return fmt.Errorf("failed to process template data: %w", err) - } - - // Phase 3: Generate kustomize data using generators + // Phase 2: Generate files using generators for _, generator := range p.generators { - if err := generator.Generate(renderedData, false); err != nil { + if err := generator.Generate(map[string]any{}, false); err != nil { return fmt.Errorf("failed to generate from template data: %w", err) } } - // Phase 4: Install blueprint + // Phase 3: Install blueprint if err := p.blueprintHandler.Install(); err != nil { return fmt.Errorf("Error installing blueprint: %w", err) } - // Phase 5: Wait for kustomizations if requested + // Phase 4: Wait for kustomizations if requested waitFlag := ctx.Value("wait") if waitFlag != nil { if wait, ok := waitFlag.(bool); ok && wait { diff --git a/pkg/pipelines/install_test.go b/pkg/pipelines/install_test.go index 52f1485b5..aedee7e8a 100644 --- a/pkg/pipelines/install_test.go +++ b/pkg/pipelines/install_test.go @@ -3,6 +3,7 @@ package pipelines import ( "context" "fmt" + "strings" "testing" "github.com/windsorcli/cli/pkg/artifact" @@ -354,14 +355,28 @@ func TestInstallPipeline_Execute(t *testing.T) { } // And LoadConfig should be called before Install - if len(callOrder) != 2 { - t.Errorf("Expected 2 method calls, got %d", len(callOrder)) + if len(callOrder) < 2 { + t.Errorf("Expected at least 2 method calls, got %d", len(callOrder)) + } + // Find the first LoadConfig call + loadConfigIndex := -1 + installIndex := -1 + for i, call := range callOrder { + if call == "LoadConfig" && loadConfigIndex == -1 { + loadConfigIndex = i + } + if call == "Install" { + installIndex = i + } } - if callOrder[0] != "LoadConfig" { - t.Errorf("Expected LoadConfig to be called first, got %s", callOrder[0]) + if loadConfigIndex == -1 { + t.Error("Expected LoadConfig to be called") } - if callOrder[1] != "Install" { - t.Errorf("Expected Install to be called second, got %s", callOrder[1]) + if installIndex == -1 { + t.Error("Expected Install to be called") + } + if loadConfigIndex >= installIndex { + t.Error("Expected LoadConfig to be called before Install") } }) @@ -386,37 +401,15 @@ func TestInstallPipeline_Execute(t *testing.T) { } }) - t.Run("ProcessesTemplateDataSuccessfully", func(t *testing.T) { - // Given a pipeline with template data + t.Run("LoadsBlueprintConfigSuccessfully", func(t *testing.T) { + // Given a pipeline pipeline, mocks := setup(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", - }, - } - return nil - } - // 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{ - "blueprint.jsonnet": []byte(`{"kustomize": [{"name": "test"}]}`), - }, nil - } - // When Execute is called err := pipeline.Execute(context.Background()) @@ -424,37 +417,22 @@ func TestInstallPipeline_Execute(t *testing.T) { if err != nil { t.Errorf("Expected no error, got %v", err) } - - // And template processing should be called - if !mockTemplateRenderer.ProcessCalled { - t.Errorf("Expected template processing to be called. Config loaded: %v, Blueprint handler: %v", pipeline.configHandler.IsLoaded(), pipeline.blueprintHandler != nil) - } }) - t.Run("ReturnsErrorWhenTemplateProcessingFails", func(t *testing.T) { - // Given a pipeline with failing template processing + t.Run("ReturnsErrorWhenBlueprintLoadConfigFails", func(t *testing.T) { + // Given a pipeline with failing blueprint loading pipeline, mocks := setup(t) - // Mock template renderer to return error - mockTemplateRenderer := &MockTemplate{} - mockTemplateRenderer.ProcessFunc = func(templateData map[string][]byte, renderedData map[string]any) error { - return fmt.Errorf("template processing failed") + // Mock blueprint handler to return error on LoadConfig + mocks.BlueprintHandler.LoadConfigFunc = func() error { + return fmt.Errorf("blueprint load config failed") } - // 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) { - return map[string][]byte{ - "blueprint.jsonnet": []byte(`{"kustomize": [{"name": "test"}]}`), - }, nil - } - // When Execute is called err := pipeline.Execute(context.Background()) @@ -462,62 +440,10 @@ func TestInstallPipeline_Execute(t *testing.T) { if err == nil { t.Fatal("Expected error, got nil") } - if err.Error() != "failed to process template data: failed to process template data: template processing failed" { - t.Errorf("Expected template processing error, got %q", err.Error()) - } - }) - - t.Run("GeneratesKustomizeDataSuccessfully", func(t *testing.T) { - // Given a pipeline with rendered data - pipeline, mocks := setup(t) - - // Mock template renderer to return test data - mockTemplateRenderer := &MockTemplate{} - mockTemplateRenderer.ProcessFunc = func(templateData map[string][]byte, renderedData map[string]any) error { - renderedData["kustomize/values"] = map[string]any{ - "common": map[string]any{ - "domain": "test.com", - }, - } - return nil - } - // 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) { - return map[string][]byte{ - "blueprint.jsonnet": []byte(`{"kustomize": [{"name": "test"}]}`), - }, nil - } - - // Track generator calls - generatorCalled := false - for i := range pipeline.generators { - mockGenerator := &MockGenerator{} - mockGenerator.GenerateFunc = func(data map[string]any, overwrite ...bool) error { - generatorCalled = true - return nil - } - pipeline.generators[i] = mockGenerator - } - - // When Execute is called - err := pipeline.Execute(context.Background()) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - // And generators should be called - if !generatorCalled { - t.Error("Expected generators to be called") + // And the error should be about blueprint loading + if !strings.Contains(err.Error(), "failed to load blueprint config") && !strings.Contains(err.Error(), "Error loading blueprint config") { + t.Errorf("Expected blueprint loading error, got %q", err.Error()) } }) @@ -621,39 +547,15 @@ func TestInstallPipeline_Execute(t *testing.T) { } }) - t.Run("PassesCorrectDataToGenerators", func(t *testing.T) { - // Given a pipeline with specific rendered data + t.Run("CallsGeneratorsWithNilData", func(t *testing.T) { + // Given a pipeline pipeline, mocks := setup(t) - expectedData := map[string]any{ - "kustomize/values": map[string]any{ - "common": map[string]any{ - "domain": "test.com", - }, - }, - } - - // Mock template renderer to return specific data - mockTemplateRenderer := &MockTemplate{} - mockTemplateRenderer.ProcessFunc = func(templateData map[string][]byte, renderedData map[string]any) error { - renderedData["kustomize/values"] = expectedData["kustomize/values"] - return nil - } - // 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) { - return map[string][]byte{ - "blueprint.jsonnet": []byte(`{"kustomize": [{"name": "test"}]}`), - }, nil - } - // Track data passed to generators var receivedData map[string]any for i := range pipeline.generators { @@ -673,18 +575,9 @@ func TestInstallPipeline_Execute(t *testing.T) { t.Errorf("Expected no error, got %v", err) } - // And correct data should be passed to generators - if receivedData == nil { - t.Fatal("Expected data to be passed to generators") - } - - // Check that the expected data structure is passed - if kustomizeValues, exists := receivedData["kustomize/values"]; !exists { - t.Error("Expected kustomize/values to be in passed data") - } else if commonValues, exists := kustomizeValues.(map[string]any)["common"]; !exists { - t.Error("Expected common values to be in kustomize/values") - } else if domain, exists := commonValues.(map[string]any)["domain"]; !exists || domain != "test.com" { - t.Errorf("Expected domain to be 'test.com', got %v", domain) + // And empty data should be passed to generators (since we no longer use template processing) + if receivedData == nil || len(receivedData) != 0 { + t.Errorf("Expected empty data to be passed to generators, got %v", receivedData) } }) diff --git a/pkg/pipelines/pipeline.go b/pkg/pipelines/pipeline.go index bcdc9e1df..118227ccf 100644 --- a/pkg/pipelines/pipeline.go +++ b/pkg/pipelines/pipeline.go @@ -19,7 +19,6 @@ import ( "github.com/windsorcli/cli/pkg/secrets" "github.com/windsorcli/cli/pkg/shell" "github.com/windsorcli/cli/pkg/stack" - "github.com/windsorcli/cli/pkg/template" "github.com/windsorcli/cli/pkg/terraform" "github.com/windsorcli/cli/pkg/tools" "github.com/windsorcli/cli/pkg/workstation/network" @@ -100,7 +99,6 @@ type BasePipeline struct { configHandler config.ConfigHandler shims *Shims injector di.Injector - templateRenderer template.Template artifactBuilder bundler.Artifact blueprintHandler blueprint.BlueprintHandler } @@ -127,7 +125,6 @@ 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() p.blueprintHandler = p.withBlueprintHandler() @@ -279,9 +276,9 @@ func (p *BasePipeline) withStack() stack.Stack { return stack } -// withGenerators creates and registers generator instances for git, terraform, and kustomize based on configuration. +// withGenerators creates and registers generator instances for git and terraform based on configuration. // It always registers the git generator. The terraform generator is registered if "terraform.enabled" is true. -// The kustomize generator is registered if "cluster.enabled" is true. Returns a slice of initialized generators or an error. +// Returns a slice of initialized generators or an error. func (p *BasePipeline) withGenerators() ([]generators.Generator, error) { var generatorList []generators.Generator @@ -295,12 +292,6 @@ func (p *BasePipeline) withGenerators() ([]generators.Generator, error) { generatorList = append(generatorList, terraformGenerator) } - if p.configHandler.GetBool("cluster.enabled", false) { - kustomizeGenerator := generators.NewKustomizeGenerator(p.injector) - p.injector.Register("kustomizeGenerator", kustomizeGenerator) - generatorList = append(generatorList, kustomizeGenerator) - } - return generatorList, nil } @@ -712,22 +703,6 @@ func (p *BasePipeline) withTerraformResolvers() ([]terraform.ModuleResolver, err return resolvers, nil } -// withTemplateRenderer resolves or creates a jsonnet template renderer from DI container -func (p *BasePipeline) withTemplateRenderer() template.Template { - if existing := p.injector.Resolve("templateRenderer"); existing != nil { - if templateRenderer, ok := existing.(template.Template); ok { - return templateRenderer - } - } - - templateRenderer := template.NewJsonnetTemplate(p.injector) - if err := templateRenderer.Initialize(); err != nil { - return nil - } - p.injector.Register("templateRenderer", templateRenderer) - return templateRenderer -} - // 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 @@ -793,66 +768,6 @@ func (p *BasePipeline) prepareTemplateData(ctx context.Context) (map[string][]by return make(map[string][]byte), nil } -// 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 - } - - 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 err := p.loadBlueprintFromTemplate(context.Background(), renderedData); 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 or falls back to the default blueprint URL if artifactBuilder is set. -// 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 { diff --git a/pkg/pipelines/pipeline_test.go b/pkg/pipelines/pipeline_test.go index ebc53f5ba..2f77746b6 100644 --- a/pkg/pipelines/pipeline_test.go +++ b/pkg/pipelines/pipeline_test.go @@ -21,7 +21,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/workstation/virt" ) @@ -2342,57 +2341,6 @@ contexts: }) } -func TestBasePipeline_withTemplateRenderer(t *testing.T) { - setup := func(t *testing.T) (*BasePipeline, *Mocks) { - pipeline := NewBasePipeline() - mocks := setupMocks(t) - return pipeline, mocks - } - - t.Run("CreatesNewTemplateRendererWhenNotRegistered", func(t *testing.T) { - // Given a pipeline without template renderer registered - pipeline, mocks := setup(t) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Initialize failed: %v", err) - } - - // When getting template renderer - renderer := pipeline.withTemplateRenderer() - - // Then a new template renderer should be created - if renderer == nil { - t.Error("Expected template renderer to not be nil") - } - - // And it should be registered in the injector - registered := mocks.Injector.Resolve("templateRenderer") - if registered == nil { - t.Error("Expected template renderer to be registered") - } - }) - - t.Run("ReusesExistingTemplateRendererWhenRegistered", func(t *testing.T) { - // Given a pipeline with template renderer already registered - pipeline, mocks := setup(t) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Initialize failed: %v", err) - } - - // And an existing template renderer - existingRenderer := pipeline.withTemplateRenderer() - - // When getting template renderer again - renderer := pipeline.withTemplateRenderer() - - // Then the same template renderer should be returned - if renderer != existingRenderer { - t.Error("Expected to reuse existing template renderer") - } - }) -} - func TestBasePipeline_withGenerators(t *testing.T) { setup := func(t *testing.T) (*BasePipeline, *Mocks) { pipeline := NewBasePipeline() @@ -2953,272 +2901,6 @@ func TestBasePipeline_prepareTemplateData(t *testing.T) { }) } -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 diff --git a/pkg/template/jsonnet_template.go b/pkg/template/jsonnet_template.go deleted file mode 100644 index a49c4a54c..000000000 --- a/pkg/template/jsonnet_template.go +++ /dev/null @@ -1,432 +0,0 @@ -package template - -import ( - "fmt" - "strings" - - "github.com/windsorcli/cli/pkg/config" - "github.com/windsorcli/cli/pkg/constants" - "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/shell" -) - -// ============================================================================= -// Interfaces -// ============================================================================= - -// Template defines the interface for template processors -type Template interface { - Initialize() error - Process(templateData map[string][]byte, renderedData map[string]any) error -} - -// ============================================================================= -// Types -// ============================================================================= - -// JsonnetTemplate provides processing for Jsonnet templates for blueprint, terraform, and kustomize files. -// It applies path-based logic to determine how each template file is processed and keyed in the output. -type JsonnetTemplate struct { - injector di.Injector - configHandler config.ConfigHandler - shell shell.Shell - shims *Shims -} - -// ============================================================================= -// Constructor -// ============================================================================= - -// NewJsonnetTemplate constructs a JsonnetTemplate with the provided dependency injector. -func NewJsonnetTemplate(injector di.Injector) *JsonnetTemplate { - return &JsonnetTemplate{ - injector: injector, - shims: NewShims(), - } -} - -// ============================================================================= -// Public Methods -// ============================================================================= - -// Initialize sets up the JsonnetTemplate dependencies by resolving them from the injector. -// Returns an error if initialization fails. -func (t *JsonnetTemplate) Initialize() error { - if t.injector != nil { - if configHandler := t.injector.Resolve("configHandler"); configHandler != nil { - t.configHandler = configHandler.(config.ConfigHandler) - } - if shellService := t.injector.Resolve("shell"); shellService != nil { - t.shell = shellService.(shell.Shell) - } - } - return nil -} - -// Process performs two-phase Jsonnet template processing for blueprint and related files. -// Phase 1: Processes "blueprint.jsonnet" to extract patch and values references. -// Phase 2: Processes only referenced patch and values templates, omitting unreferenced files, and removes the patches field from the blueprint in renderedData. -// Returns an error if any processing step fails. -func (t *JsonnetTemplate) Process(templateData map[string][]byte, renderedData map[string]any) error { - if err := t.processTemplate("blueprint.jsonnet", templateData, renderedData); err != nil { - return err - } - patchRefs := t.extractPatchReferences(renderedData) - patchSet := make(map[string]bool) - for _, ref := range patchRefs { - patchSet[ref] = true - } - for templatePath := range templateData { - if templatePath == "blueprint.jsonnet" { - continue - } - if strings.HasPrefix(templatePath, "patches/") { - if !patchSet[templatePath] { - continue - } - } - if err := t.processTemplate(templatePath, templateData, renderedData); err != nil { - return err - } - } - t.cleanupBlueprint(renderedData) - return nil -} - -// processJsonnetTemplate evaluates a Jsonnet template string using the Windsor context. -// 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 for templates to reference if needed. -// 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. -func (t *JsonnetTemplate) processJsonnetTemplate(templateContent string) (map[string]any, error) { - config := t.configHandler.GetConfig() - contextYAML, err := t.shims.YamlMarshal(config) - if err != nil { - return nil, fmt.Errorf("failed to marshal context to YAML: %w", err) - } - projectRoot, err := t.shell.GetProjectRoot() - if err != nil { - return nil, fmt.Errorf("failed to get project root: %w", err) - } - var contextMap map[string]any = make(map[string]any) - if err := t.shims.YamlUnmarshal(contextYAML, &contextMap); err != nil { - return nil, fmt.Errorf("failed to unmarshal context YAML: %w", err) - } - contextName := t.configHandler.GetContext() - contextMap["name"] = contextName - contextMap["projectName"] = t.shims.FilepathBase(projectRoot) - - contextJSON, err := t.shims.JsonMarshal(contextMap) - if err != nil { - return nil, fmt.Errorf("failed to marshal context map to JSON: %w", err) - } - vm := t.shims.NewJsonnetVM() - helpersLibrary := t.buildHelperLibrary() - vm.ExtCode("helpers", helpersLibrary) - vm.ExtCode("context", string(contextJSON)) - vm.ExtCode("ociUrl", fmt.Sprintf("%q", constants.GetEffectiveBlueprintURL())) - result, err := vm.EvaluateAnonymousSnippet("template.jsonnet", templateContent) - if err != nil { - return nil, fmt.Errorf("failed to evaluate jsonnet template: %w", err) - } - var values map[string]any - if err := t.shims.JsonUnmarshal([]byte(result), &values); err != nil { - return nil, fmt.Errorf("jsonnet template must output valid JSON: %w", err) - } - cleanedValues := t.removeEmptyKeysFromOutput(values) - if cleanedMap, ok := cleanedValues.(map[string]any); ok { - return cleanedMap, nil - } - return values, nil -} - -// removeEmptyKeysFromOutput recursively removes empty keys from the output data. -// This method implements the same logic as the Jsonnet removeEmptyKeys helper function -// but operates on Go data structures after template processing. -func (t *JsonnetTemplate) removeEmptyKeysFromOutput(data any) any { - switch v := data.(type) { - case map[string]any: - cleaned := make(map[string]any) - for key, value := range v { - cleanedValue := t.removeEmptyKeysFromOutput(value) - if !t.isEmptyValue(cleanedValue) { - cleaned[key] = cleanedValue - } - } - return cleaned - case []any: - cleaned := make([]any, 0, len(v)) - for _, item := range v { - cleanedItem := t.removeEmptyKeysFromOutput(item) - if !t.isEmptyValue(cleanedItem) { - cleaned = append(cleaned, cleanedItem) - } - } - return cleaned - default: - return data - } -} - -// isEmptyValue determines if a value should be considered empty and removed. -// Returns true for null, empty maps, and empty slices. -// Empty strings are preserved as they may be valid function results. -func (t *JsonnetTemplate) isEmptyValue(value any) bool { - if value == nil { - return true - } - - switch v := value.(type) { - case map[string]any: - return len(v) == 0 - case []any: - return len(v) == 0 - default: - return false - } -} - -// processTemplate processes a single template file and stores the result in renderedData under a key determined by the template path. -// Recognized mappings: -// - "blueprint.jsonnet" → "blueprint" -// - "terraform/*.jsonnet" → "terraform/*" (without .jsonnet extension) -// - "patches//*.jsonnet" → "patches//*" (without .jsonnet extension) -// -// Templates exclusively contain: -// - blueprint.jsonnet -// - terraform/.jsonnet -// - patches//*.jsonnet -// - values.yaml (processed separately, not here) -// -// If the template does not exist in templateData, no action is performed. Returns an error if processing fails. Unrecognized template types are ignored. -func (t *JsonnetTemplate) processTemplate(templatePath string, templateData map[string][]byte, renderedData map[string]any) error { - content, exists := templateData[templatePath] - if !exists { - return nil - } - - var outputKey string - switch { - case templatePath == "blueprint.jsonnet": - outputKey = "blueprint" - case templatePath == "substitution.jsonnet": - outputKey = "substitution" - case strings.HasPrefix(templatePath, "terraform/") && strings.HasSuffix(templatePath, ".jsonnet"): - outputKey = strings.TrimSuffix(templatePath, ".jsonnet") - case strings.HasPrefix(templatePath, "patches/") && strings.HasSuffix(templatePath, ".jsonnet"): - outputKey = strings.TrimSuffix(templatePath, ".jsonnet") - default: - return nil - } - - values, err := t.processJsonnetTemplate(string(content)) - if err != nil { - return fmt.Errorf("failed to process template %s: %w", templatePath, err) - } - - if t.isEmptyValue(values) { - return nil - } - - renderedData[outputKey] = values - return nil -} - -// extractPatchReferences returns a slice of template file paths for patch references found in the rendered blueprint within renderedData. -// The function inspects the "blueprint" key in renderedData, extracts the "kustomize" array, and collects patch paths from each kustomization's "patches" field. -// Patch paths are normalized to "patches//.jsonnet" format. Returns an empty slice if the blueprint or kustomize section is missing or malformed. -func (t *JsonnetTemplate) extractPatchReferences(renderedData map[string]any) []string { - var templatePaths []string - blueprintData, ok := renderedData["blueprint"] - if !ok { - return templatePaths - } - blueprintMap, ok := blueprintData.(map[string]any) - if !ok { - return templatePaths - } - kustomizeArr, ok := blueprintMap["kustomize"].([]any) - if !ok { - return templatePaths - } - for _, k := range kustomizeArr { - kMap, ok := k.(map[string]any) - if !ok { - continue - } - kustomizationName, ok := kMap["name"].(string) - if !ok { - continue - } - patches, ok := kMap["patches"].([]any) - if !ok { - continue - } - for _, p := range patches { - pMap, ok := p.(map[string]any) - if !ok { - continue - } - if path, ok := pMap["path"].(string); ok && path != "" { - templatePath := "patches/" + kustomizationName + "/" + path + ".jsonnet" - templatePaths = append(templatePaths, templatePath) - } - } - } - return templatePaths -} - -// cleanupBlueprint removes the patches field from each kustomization in the blueprint within renderedData. -// This is used to clean up the output after all referenced patches have been processed. -func (t *JsonnetTemplate) cleanupBlueprint(renderedData map[string]any) { - blueprintData, ok := renderedData["blueprint"] - if !ok { - return - } - blueprintMap, ok := blueprintData.(map[string]any) - if !ok { - return - } - kustomizeArr, ok := blueprintMap["kustomize"].([]any) - if !ok { - return - } - for i, k := range kustomizeArr { - if kMap, ok := k.(map[string]any); ok { - delete(kMap, "patches") - kustomizeArr[i] = kMap - } - } - blueprintMap["kustomize"] = kustomizeArr - renderedData["blueprint"] = blueprintMap -} - -// buildHelperLibrary returns a Jsonnet library string containing helper functions for safe context access and data manipulation. -// Helpers provided: -// - get: Retrieve value by path from object, with default fallback. -// - getString, getInt, getNumber, getBool, getObject, getArray: Typed retrieval with type assertion and default fallback. -// - has: Check if a value exists at a given path. -// - baseUrl: Extract base URL from an endpoint string, removing protocol and port. -// - removeEmptyKeys: Recursively remove empty keys from objects, preserving non-empty values. -func (jt *JsonnetTemplate) buildHelperLibrary() string { - return `{ - get(obj, path, default=null): - if std.findSubstr(".", path) == [] then - if std.type(obj) == "object" && path in obj then obj[path] else default - else - local parts = std.split(path, "."); - local getValue(o, pathParts) = - if std.length(pathParts) == 0 then o - else if std.type(o) != "object" then null - else if !(pathParts[0] in o) then null - else getValue(o[pathParts[0]], pathParts[1:]); - local result = getValue(obj, parts); - if result == null then default else result, - - getString(obj, path, default=""): - local val = self.get(obj, path, null); - if val == null then default - else if std.type(val) == "string" then val - else error "Expected string for '" + path + "' but got " + std.type(val) + ": " + std.toString(val), - - getInt(obj, path, default=0): - local val = self.get(obj, path, null); - if val == null then default - else if std.type(val) == "number" then std.floor(val) - else error "Expected number for '" + path + "' but got " + std.type(val) + ": " + std.toString(val), - - getNumber(obj, path, default=0): - local val = self.get(obj, path, null); - if val == null then default - else if std.type(val) == "number" then val - else error "Expected number for '" + path + "' but got " + std.type(val) + ": " + std.toString(val), - - getBool(obj, path, default=false): - local val = self.get(obj, path, null); - if val == null then default - else if std.type(val) == "boolean" then val - else error "Expected boolean for '" + path + "' but got " + std.type(val) + ": " + std.toString(val), - - getObject(obj, path, default={}): - local val = self.get(obj, path, null); - if val == null then default - else if std.type(val) == "object" then val - else error "Expected object for '" + path + "' but got " + std.type(val) + ": " + std.toString(val), - - getArray(obj, path, default=[]): - local val = self.get(obj, path, null); - if val == null then default - else if std.type(val) == "array" then val - else error "Expected array for '" + path + "' but got " + std.type(val) + ": " + std.toString(val), - - has(obj, path): - self.get(obj, path, null) != null, - - baseUrl(endpoint): - if endpoint == "" then - "" - else - local withoutProtocol = if std.startsWith(endpoint, "https://") then - std.substr(endpoint, 8, std.length(endpoint) - 8) - else if std.startsWith(endpoint, "http://") then - std.substr(endpoint, 7, std.length(endpoint) - 7) - else - endpoint; - local colonPos = std.findSubstr(":", withoutProtocol); - if std.length(colonPos) > 0 then - std.substr(withoutProtocol, 0, colonPos[0]) - else - withoutProtocol, - - removeEmptyKeys(obj): - local _removeEmptyKeys(obj) = - if std.type(obj) == "object" then - local filteredFields = std.filter( - function(key) - local value = obj[key]; - if std.type(value) == "object" || std.type(value) == "array" then - local cleaned = _removeEmptyKeys(value); - if std.type(cleaned) == "object" then - std.length(std.objectFields(cleaned)) > 0 - else - std.length(cleaned) > 0 - else - value != null && (std.type(value) != "string" || value != "") - , - std.objectFields(obj) - ); - { - [key]: if std.type(obj[key]) == "object" || std.type(obj[key]) == "array" then _removeEmptyKeys(obj[key]) else obj[key] - for key in filteredFields - } - else if std.type(obj) == "array" then - local filteredElements = std.filter( - function(element) - if std.type(element) == "object" || std.type(element) == "array" then - local cleaned = _removeEmptyKeys(element); - if std.type(cleaned) == "object" then - std.length(std.objectFields(cleaned)) > 0 - else - std.length(cleaned) > 0 - else - element != null && (std.type(element) != "string" || element != "") - , - obj - ); - [ - if std.type(element) == "object" || std.type(element) == "array" then _removeEmptyKeys(element) else element - for element in filteredElements - ] - else - obj; - _removeEmptyKeys(obj), -}` -} - -// ============================================================================= -// Interface Compliance -// ============================================================================= - -// JsonnetTemplate implements the Template interface. -var _ Template = (*JsonnetTemplate)(nil) diff --git a/pkg/template/jsonnet_template_test.go b/pkg/template/jsonnet_template_test.go deleted file mode 100644 index afdd039f7..000000000 --- a/pkg/template/jsonnet_template_test.go +++ /dev/null @@ -1,2230 +0,0 @@ -package template - -import ( - "fmt" - "os" - "strings" - "testing" - "time" - - "github.com/windsorcli/cli/pkg/config" - "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/shell" -) - -// ============================================================================= -// Test Types -// ============================================================================= - -// SetupOptions provides configuration for test setup -type SetupOptions struct { - // Add any specific setup options if needed -} - -// Mocks contains all mock implementations needed for testing -type Mocks struct { - Injector di.Injector - ConfigHandler *config.MockConfigHandler - Shell *shell.MockShell -} - -// ============================================================================= -// Test Helpers -// ============================================================================= - -// setupMocks creates and configures mock dependencies for testing -func setupMocks(t *testing.T, _ ...*SetupOptions) *Mocks { - t.Helper() - - configHandler := &config.MockConfigHandler{} - shellService := &shell.MockShell{} - - injector := di.NewMockInjector() - injector.Register("configHandler", configHandler) - injector.Register("shell", shellService) - - return &Mocks{ - Injector: injector, - ConfigHandler: configHandler, - Shell: shellService, - } -} - -// setupJsonnetTemplateMocks creates mocks and a JsonnetTemplate instance -func setupJsonnetTemplateMocks(t *testing.T, opts ...*SetupOptions) (*Mocks, *JsonnetTemplate) { - t.Helper() - mocks := setupMocks(t, opts...) - template := NewJsonnetTemplate(mocks.Injector) - return mocks, template -} - -// ============================================================================= -// Test Public Methods -// ============================================================================= - -func TestJsonnetTemplate_NewJsonnetTemplate(t *testing.T) { - t.Run("CreatesTemplateWithDependencies", func(t *testing.T) { - // Given an injector - mocks := setupMocks(t) - - // When creating a new jsonnet template - template := NewJsonnetTemplate(mocks.Injector) - - // Then the template should be properly initialized - if template == nil { - t.Fatal("Expected non-nil template") - } - - // And injector should be set - if template.injector == nil { - t.Error("Expected injector to be set") - } - - // And shims should be set - if template.shims == nil { - t.Error("Expected shims to be set") - } - }) -} - -func TestJsonnetTemplate_Initialize(t *testing.T) { - setup := func(t *testing.T) (*JsonnetTemplate, *Mocks) { - t.Helper() - mocks, template := setupJsonnetTemplateMocks(t) - return template, mocks - } - - t.Run("Success", func(t *testing.T) { - // Given a jsonnet template - template, _ := setup(t) - - // When calling Initialize - err := template.Initialize() - - // Then no error should be returned - if err != nil { - t.Errorf("Expected nil error, got %v", err) - } - - // And dependencies should be injected - if template.configHandler == nil { - t.Error("Expected configHandler to be set after Initialize()") - } - if template.shell == nil { - t.Error("Expected shell to be set after Initialize()") - } - }) - - t.Run("HandlesNilInjector", func(t *testing.T) { - // Given a jsonnet template with nil injector - template := NewJsonnetTemplate(nil) - - // When calling Initialize - err := template.Initialize() - - // Then no error should be returned (handles nil gracefully) - if err != nil { - t.Errorf("Expected nil error, got %v", err) - } - }) -} - -func TestJsonnetTemplate_Process(t *testing.T) { - setup := func(t *testing.T) (*JsonnetTemplate, *Mocks) { - t.Helper() - mocks, template := setupJsonnetTemplateMocks(t) - err := template.Initialize() - if err != nil { - t.Fatalf("Failed to initialize template: %v", err) - } - return template, mocks - } - - t.Run("ProcessesBlueprintJsonnetTemplate", func(t *testing.T) { - // Given a jsonnet template - template, _ := setup(t) - - // And template data containing a blueprint.jsonnet file - templateData := map[string][]byte{ - "blueprint.jsonnet": []byte(`local context = std.extVar("context"); { kind: "Blueprint", metadata: { name: context.name } }`), - } - renderedData := make(map[string]any) - - // And a mock jsonnet VM that returns valid blueprint JSON - template.shims.NewJsonnetVM = func() JsonnetVM { - mockVM := &mockJsonnetVM{ - EvaluateFunc: func(filename, snippet string) (string, error) { - return `{"kind": "Blueprint", "metadata": {"name": "test-context"}}`, nil - }, - } - return mockVM - } - - // And mock config handler returns valid YAML - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("contexts:\n mock-context:\n dns:\n domain: mock.domain.com"), nil - } - - // When processing the template data - err := template.Process(templateData, renderedData) - - // Then no error should be returned - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - // And the rendered data should contain the blueprint content - if len(renderedData) != 1 { - t.Errorf("Expected 1 rendered item, got %d", len(renderedData)) - } - - if _, exists := renderedData["blueprint"]; !exists { - t.Error("Expected blueprint to be rendered") - } - }) - - t.Run("ProcessesTerraformJsonnetTemplates", func(t *testing.T) { - // Given a jsonnet template - template, _ := setup(t) - - // And template data containing terraform/ .jsonnet files - templateData := map[string][]byte{ - "terraform/main.jsonnet": []byte(`local context = std.extVar("context"); { cluster_name: context.name }`), - "terraform/cluster.jsonnet": []byte(`local context = std.extVar("context"); { instance_type: "t3.micro" }`), - } - renderedData := make(map[string]any) - - // And a mock jsonnet VM that returns valid terraform vars - template.shims.NewJsonnetVM = func() JsonnetVM { - mockVM := &mockJsonnetVM{ - EvaluateFunc: func(filename, snippet string) (string, error) { - if strings.Contains(filename, "main") { - return `{"cluster_name": "test-cluster"}`, nil - } - return `{"instance_type": "t3.micro"}`, nil - }, - } - return mockVM - } - - // And mock config handler returns valid YAML - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("contexts:\n mock-context:\n dns:\n domain: mock.domain.com"), nil - } - - // When processing the template data - err := template.Process(templateData, renderedData) - - // Then no error should be returned - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - // And the rendered data should contain the terraform variables - if len(renderedData) != 2 { - t.Errorf("Expected 2 rendered items, got %d", len(renderedData)) - } - - if _, exists := renderedData["terraform/main"]; !exists { - t.Error("Expected terraform/main to be rendered") - } - - if _, exists := renderedData["terraform/cluster"]; !exists { - t.Error("Expected terraform/cluster to be rendered") - } - }) - - t.Run("ProcessesPatchesJsonnetTemplates", func(t *testing.T) { - // Given a jsonnet template - template, _ := setup(t) - - // And template data containing blueprint and patches/ .jsonnet files with subdirectory structure - templateData := map[string][]byte{ - "blueprint.jsonnet": []byte(`{ kustomize: [{ name: "ingress", patches: [{ path: "nginx" }] }, { name: "dns", patches: [{ path: "coredns" }] }] }`), - "patches/ingress/nginx.jsonnet": []byte(`local context = std.extVar("context"); { apiVersion: "v1", kind: "ConfigMap", metadata: { name: "nginx-config" } }`), - "patches/dns/coredns.jsonnet": []byte(`local context = std.extVar("context"); { apiVersion: "v1", kind: "ConfigMap", metadata: { name: "coredns-config" } }`), - } - renderedData := make(map[string]any) - - // And a mock jsonnet VM that returns valid manifests - template.shims.NewJsonnetVM = func() JsonnetVM { - mockVM := &mockJsonnetVM{ - EvaluateFunc: func(filename, snippet string) (string, error) { - if strings.Contains(snippet, `kustomize:`) && strings.Contains(snippet, `patches:`) { - // This is the blueprint template - return `{"kustomize": [{"name": "ingress", "patches": [{"path": "nginx"}]}, {"name": "dns", "patches": [{"path": "coredns"}]}]}`, nil - } - if strings.Contains(snippet, `nginx-config`) { - return `{"apiVersion": "v1", "kind": "ConfigMap", "metadata": {"name": "nginx-config"}}`, nil - } - return `{"apiVersion": "v1", "kind": "ConfigMap", "metadata": {"name": "coredns-config"}}`, nil - }, - } - return mockVM - } - - // And mock config handler returns valid YAML - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("contexts:\n mock-context:\n dns:\n domain: mock.domain.com"), nil - } - - // When processing the template data - err := template.Process(templateData, renderedData) - - // Then no error should be returned - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - // And the rendered data should contain the blueprint and patch manifests - if len(renderedData) != 3 { - t.Errorf("Expected 3 rendered items (blueprint + 2 patches), got %d", len(renderedData)) - } - - // Verify that the blueprint is processed and patches field is cleaned up - if _, exists := renderedData["blueprint"]; !exists { - t.Error("Expected blueprint to be rendered") - } - - // Verify that patches are rendered with correct paths - if _, exists := renderedData["patches/ingress/nginx"]; !exists { - t.Error("Expected patches/ingress/nginx to be rendered") - } - - if _, exists := renderedData["patches/dns/coredns"]; !exists { - t.Error("Expected patches/dns/coredns to be rendered") - } - - // Verify the content is correctly processed - nginxPatch, ok := renderedData["patches/ingress/nginx"].(map[string]any) - if !ok { - t.Error("Expected nginx patch to be a map") - } else { - if nginxPatch["apiVersion"] != "v1" { - t.Errorf("Expected apiVersion to be 'v1', got %v", nginxPatch["apiVersion"]) - } - if nginxPatch["kind"] != "ConfigMap" { - t.Errorf("Expected kind to be 'ConfigMap', got %v", nginxPatch["kind"]) - } - } - - corednsPatch, ok := renderedData["patches/dns/coredns"].(map[string]any) - if !ok { - t.Error("Expected coredns patch to be a map") - } else { - if corednsPatch["apiVersion"] != "v1" { - t.Errorf("Expected apiVersion to be 'v1', got %v", corednsPatch["apiVersion"]) - } - if corednsPatch["kind"] != "ConfigMap" { - t.Errorf("Expected kind to be 'ConfigMap', got %v", corednsPatch["kind"]) - } - } - }) - - t.Run("ProcessesBothBlueprintAndTerraformTemplates", func(t *testing.T) { - // Given a jsonnet template - template, _ := setup(t) - - // And template data containing both blueprint and terraform files - templateData := map[string][]byte{ - "blueprint.jsonnet": []byte(`{ kind: "Blueprint", metadata: { name: "test" } }`), - "terraform/main.jsonnet": []byte(`{ cluster_name: "test-cluster" }`), - "other.jsonnet": []byte(`{ ignored: true }`), - } - renderedData := make(map[string]any) - - // And a mock jsonnet VM that returns valid JSON - template.shims.NewJsonnetVM = func() JsonnetVM { - mockVM := &mockJsonnetVM{ - EvaluateFunc: func(filename, snippet string) (string, error) { - if strings.Contains(filename, "blueprint") { - return `{"kind": "Blueprint", "metadata": {"name": "test"}}`, nil - } - return `{"cluster_name": "test-cluster"}`, nil - }, - } - return mockVM - } - - // And mock config handler returns valid YAML - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("contexts:\n mock-context:\n dns:\n domain: mock.domain.com"), nil - } - - // When processing the template data - err := template.Process(templateData, renderedData) - - // Then no error should be returned - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - // And both blueprint and terraform should be processed, but not other.jsonnet - if len(renderedData) != 2 { - t.Errorf("Expected 2 rendered items, got %d", len(renderedData)) - } - - if _, exists := renderedData["blueprint"]; !exists { - t.Error("Expected blueprint to be rendered") - } - if _, exists := renderedData["terraform/main"]; !exists { - t.Error("Expected terraform/main to be rendered") - } - if _, exists := renderedData["other"]; exists { - t.Error("Expected other.jsonnet to be ignored") - } - }) - - t.Run("IgnoresNonMatchingTemplates", func(t *testing.T) { - // Given a jsonnet template - template, _ := setup(t) - - // And template data containing files that don't match known template patterns - templateData := map[string][]byte{ - "other.jsonnet": []byte(`{"name": "test"}`), - "config.yaml": []byte(`key: value`), - "script.js": []byte(`console.log("hello")`), - } - renderedData := make(map[string]any) - - // When processing the template data - err := template.Process(templateData, renderedData) - - // Then no error should be returned - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - // And no data should be processed - if len(renderedData) != 0 { - t.Errorf("Expected no data to be processed for non-matching templates, got %d items", len(renderedData)) - } - }) - - t.Run("HandlesJsonnetProcessingError", func(t *testing.T) { - // Given a jsonnet template - template, _ := setup(t) - - // And template data containing a blueprint.jsonnet file - templateData := map[string][]byte{ - "blueprint.jsonnet": []byte(`invalid jsonnet`), - } - renderedData := make(map[string]any) - - // And a mock jsonnet VM that returns an error - template.shims.NewJsonnetVM = func() JsonnetVM { - mockVM := &mockJsonnetVM{ - EvaluateFunc: func(filename, snippet string) (string, error) { - return "", fmt.Errorf("jsonnet evaluation error") - }, - } - return mockVM - } - - // And mock config handler returns valid YAML - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("contexts:\n mock-context:\n dns:\n domain: mock.domain.com"), nil - } - - // When processing the template data - err := template.Process(templateData, renderedData) - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to process template") { - t.Errorf("Expected error about template processing, got: %v", err) - } - }) - - t.Run("ProcessesOnlyFirstMatchingRule", func(t *testing.T) { - // Given a jsonnet template - template, _ := setup(t) - - // And template data containing a blueprint.jsonnet file (which matches the first rule) - templateData := map[string][]byte{ - "blueprint.jsonnet": []byte(`{ kind: "Blueprint" }`), - } - renderedData := make(map[string]any) - - // And a mock jsonnet VM - template.shims.NewJsonnetVM = func() JsonnetVM { - mockVM := &mockJsonnetVM{ - EvaluateFunc: func(filename, snippet string) (string, error) { - return `{"kind": "Blueprint"}`, nil - }, - } - return mockVM - } - - // And mock config handler returns valid YAML - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("contexts:\n mock-context:\n dns:\n domain: mock.domain.com"), nil - } - - // When processing the template data - err := template.Process(templateData, renderedData) - - // Then no error should be returned - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - // And the blueprint should be processed by the first matching rule - if len(renderedData) != 1 { - t.Errorf("Expected 1 rendered item, got %d", len(renderedData)) - } - if _, exists := renderedData["blueprint"]; !exists { - t.Error("Expected blueprint to be rendered by blueprint rule") - } - }) -} - -func TestJsonnetTemplate_processJsonnetTemplate(t *testing.T) { - setup := func(t *testing.T) (*JsonnetTemplate, *Mocks) { - t.Helper() - mocks, template := setupJsonnetTemplateMocks(t) - err := template.Initialize() - if err != nil { - t.Fatalf("Failed to initialize template: %v", err) - } - return template, mocks - } - - t.Run("ProcessesValidJsonnetTemplate", func(t *testing.T) { - // Given a jsonnet template - template, _ := setup(t) - - // And a mock jsonnet VM that returns valid JSON - template.shims.NewJsonnetVM = func() JsonnetVM { - mockVM := &mockJsonnetVM{ - EvaluateFunc: func(filename, snippet string) (string, error) { - return `{"key": "value", "number": 42}`, nil - }, - } - return mockVM - } - - // And mock config handler returns valid YAML - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("contexts:\n mock-context:\n dns:\n domain: mock.domain.com"), nil - } - - templateContent := `local context = std.extVar("context"); { key: "value", number: 42 }` - - // When processing the jsonnet template - result, err := template.processJsonnetTemplate(templateContent) - - // Then no error should be returned - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - // And the result should contain the expected values - if result["key"] != "value" { - t.Errorf("Expected key 'value', got: %v", result["key"]) - } - if result["number"] != float64(42) { - t.Errorf("Expected number 42, got: %v", result["number"]) - } - }) - - t.Run("HandlesYamlMarshalError", func(t *testing.T) { - // Given a jsonnet template - template, _ := setup(t) - - // And a shims that returns an error when marshaling - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return nil, fmt.Errorf("yaml marshal error") - } - - templateContent := `local context = std.extVar("context"); { key: "value" }` - - // When processing the jsonnet template - _, err := template.processJsonnetTemplate(templateContent) - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to marshal context to YAML") { - t.Errorf("Expected error about marshal failure, got: %v", err) - } - }) - - t.Run("HandlesProjectRootError", func(t *testing.T) { - // Given a jsonnet template - template, mocks := setup(t) - - // And a shell that returns an error for project root - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "", fmt.Errorf("project root error") - } - - // And mock shims returns valid YAML - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("contexts:\n mock-context:\n dns:\n domain: mock.domain.com"), nil - } - - templateContent := `local context = std.extVar("context"); { key: "value" }` - - // When processing the jsonnet template - _, err := template.processJsonnetTemplate(templateContent) - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to get project root") { - t.Errorf("Expected error about project root failure, got: %v", err) - } - }) - - t.Run("HandlesYamlUnmarshalError", func(t *testing.T) { - // Given a jsonnet template - template, _ := setup(t) - - // And mock shims returns invalid YAML - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("invalid yaml: ["), nil - } - - // And a mock YamlUnmarshal that returns an error - template.shims.YamlUnmarshal = func(data []byte, v any) error { - return fmt.Errorf("yaml unmarshal error") - } - - templateContent := `local context = std.extVar("context"); { key: "value" }` - - // When processing the jsonnet template - _, err := template.processJsonnetTemplate(templateContent) - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to unmarshal context YAML") { - t.Errorf("Expected error about YAML unmarshal failure, got: %v", err) - } - }) - - t.Run("HandlesJsonMarshalError", func(t *testing.T) { - // Given a jsonnet template - template, _ := setup(t) - - // And mock config handler returns valid YAML - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("contexts:\n mock-context:\n dns:\n domain: mock.domain.com"), nil - } - - // And a mock JsonMarshal that returns an error - template.shims.JsonMarshal = func(v any) ([]byte, error) { - return nil, fmt.Errorf("json marshal error") - } - - templateContent := `local context = std.extVar("context"); { key: "value" }` - - // When processing the jsonnet template - _, err := template.processJsonnetTemplate(templateContent) - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to marshal context map to JSON") { - t.Errorf("Expected error about JSON marshal failure, got: %v", err) - } - }) - - t.Run("HandlesJsonnetEvaluationError", func(t *testing.T) { - // Given a jsonnet template - template, _ := setup(t) - - // And mock config handler returns valid YAML - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("contexts:\n mock-context:\n dns:\n domain: mock.domain.com"), nil - } - - // And a mock jsonnet VM that returns an error - template.shims.NewJsonnetVM = func() JsonnetVM { - mockVM := &mockJsonnetVM{ - EvaluateFunc: func(filename, snippet string) (string, error) { - return "", fmt.Errorf("jsonnet evaluation error") - }, - } - return mockVM - } - - templateContent := `invalid jsonnet syntax` - - // When processing the jsonnet template - _, err := template.processJsonnetTemplate(templateContent) - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to evaluate jsonnet template") { - t.Errorf("Expected error about jsonnet evaluation, got: %v", err) - } - }) - - t.Run("HandlesInvalidJsonResult", func(t *testing.T) { - // Given a jsonnet template - template, _ := setup(t) - - // And mock config handler returns valid YAML - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("contexts:\n mock-context:\n dns:\n domain: mock.domain.com"), nil - } - - // And a mock jsonnet VM that returns invalid JSON - template.shims.NewJsonnetVM = func() JsonnetVM { - mockVM := &mockJsonnetVM{ - EvaluateFunc: func(filename, snippet string) (string, error) { - return `invalid json response`, nil - }, - } - return mockVM - } - - templateContent := `local context = std.extVar("context"); "not an object"` - - // When processing the jsonnet template - _, err := template.processJsonnetTemplate(templateContent) - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "jsonnet template must output valid JSON") { - t.Errorf("Expected error about invalid JSON, got: %v", err) - } - }) - - t.Run("HandlesContextDataInjection", func(t *testing.T) { - // Given a jsonnet template - template, mocks := setup(t) - - // And mock config handler returns complex YAML with nested data - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte(` -contexts: - mock-context: - dns: - domain: example.com - cluster: - name: test-cluster - region: us-west-2 -`), nil - } - - // And mock shell returns a specific project root - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "/path/to/test-project", nil - } - - // And a mock jsonnet VM that captures the context data - var capturedContext string - template.shims.NewJsonnetVM = func() JsonnetVM { - mockVM := &mockJsonnetVM{ - EvaluateFunc: func(filename, snippet string) (string, error) { - // Verify the template content references context - if !strings.Contains(snippet, "std.extVar(\"context\")") { - t.Errorf("Expected template to reference context variable") - } - return `{"processed": true, "contextReceived": true}`, nil - }, - } - // Create a custom ExtCode function to capture context - mockVM.ExtCodeFunc = func(key, val string) { - if key == "context" { - capturedContext = val - } - // Store the call as usual - mockVM.ExtCalls = append(mockVM.ExtCalls, struct{ Key, Val string }{key, val}) - } - return mockVM - } - - templateContent := `local context = std.extVar("context"); { processed: true, name: context.name, projectName: context.projectName }` - - // When processing the jsonnet template - result, err := template.processJsonnetTemplate(templateContent) - - // Then no error should be returned - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - // And the result should be processed - if result["processed"] != true { - t.Error("Expected template to be processed") - } - - // And context should have been injected - if capturedContext == "" { - t.Error("Expected context to be captured") - } - - // And context should contain expected fields - if !strings.Contains(capturedContext, "mock-context") { - t.Error("Expected context to contain context name") - } - if !strings.Contains(capturedContext, "test-project") { - t.Error("Expected context to contain project name") - } - }) - - t.Run("HandlesEmptyContextMap", func(t *testing.T) { - // Given a jsonnet template - template, mocks := setup(t) - - // And mock config handler returns minimal YAML - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("{}"), nil - } - - // And mock config handler returns empty context - mocks.ConfigHandler.GetContextFunc = func() string { - return "" - } - - // And a mock jsonnet VM - template.shims.NewJsonnetVM = func() JsonnetVM { - mockVM := &mockJsonnetVM{ - EvaluateFunc: func(filename, snippet string) (string, error) { - return `{"minimal": true}`, nil - }, - } - return mockVM - } - - templateContent := `local context = std.extVar("context"); { minimal: true }` - - // When processing the jsonnet template - result, err := template.processJsonnetTemplate(templateContent) - - // Then no error should be returned - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - // And the result should be processed - if result["minimal"] != true { - t.Error("Expected minimal template to be processed") - } - }) - - t.Run("RemoveEmptyKeysFunction", func(t *testing.T) { - // Given a jsonnet template - template, _ := setup(t) - - // And mock shims returns valid YAML - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("contexts:\n mock-context:\n dns:\n domain: mock.domain.com"), nil - } - - // And a mock jsonnet VM that tests removeEmptyKeys - template.shims.NewJsonnetVM = func() JsonnetVM { - mockVM := &mockJsonnetVM{ - EvaluateFunc: func(filename, snippet string) (string, error) { - // Return what the template would actually produce - return `{"test": "value"}`, nil - }, - } - return mockVM - } - - templateContent := `local context = std.extVar("context"); local helpers = std.extVar("helpers"); { test: "value" }` - - // When processing the jsonnet template - result, err := template.processJsonnetTemplate(templateContent) - - // Then no error should be returned - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - // And the result should contain the expected value - if result["test"] != "value" { - t.Errorf("Expected test 'value', got: %v", result["test"]) - } - }) - - t.Run("AutomaticallyRemovesEmptyKeysFromOutput", func(t *testing.T) { - // Given a jsonnet template that produces output with empty values - template, _ := setup(t) - - // And mock shims returns valid YAML - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("contexts:\n mock-context:\n dns:\n domain: mock.domain.com"), nil - } - - // And a mock jsonnet VM that returns output with empty values - template.shims.NewJsonnetVM = func() JsonnetVM { - mockVM := &mockJsonnetVM{ - EvaluateFunc: func(filename, snippet string) (string, error) { - // Return output with empty values that should be removed - return `{ - "valid_key": "value", - "empty_string": "", - "null_value": null, - "empty_object": {}, - "empty_array": [], - "nested": { - "valid": "nested_value", - "empty": "", - "deep": { - "empty": "" - } - }, - "zero": 0, - "false": false - }`, nil - }, - } - return mockVM - } - - templateContent := `local context = std.extVar("context"); { test: "value" }` - - // When processing the jsonnet template - result, err := template.processJsonnetTemplate(templateContent) - - // Then no error should be returned - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - // And null values and empty objects/arrays should be removed - if _, hasNullValue := result["null_value"]; hasNullValue { - t.Error("Expected 'null_value' key to be removed") - } - if _, hasEmptyObject := result["empty_object"]; hasEmptyObject { - t.Error("Expected 'empty_object' key to be removed") - } - if _, hasEmptyArray := result["empty_array"]; hasEmptyArray { - t.Error("Expected 'empty_array' key to be removed") - } - - // And valid keys should be preserved - if _, hasValidKey := result["valid_key"]; !hasValidKey { - t.Error("Expected 'valid_key' to be preserved") - } - if _, hasZero := result["zero"]; !hasZero { - t.Error("Expected 'zero' to be preserved") - } - if _, hasFalse := result["false"]; !hasFalse { - t.Error("Expected 'false' to be preserved") - } - - // And nested objects should be cleaned - if nested, hasNested := result["nested"].(map[string]any); hasNested { - if _, hasNestedValid := nested["valid"]; !hasNestedValid { - t.Error("Expected nested 'valid' key to be preserved") - } - // Note: 'deep' object is preserved because empty strings are now preserved - } else { - t.Error("Expected 'nested' key to be preserved") - } - }) - - t.Run("RemoveEmptyKeysHelperIsAvailable", func(t *testing.T) { - // Given a jsonnet template that uses removeEmptyKeys helper - template, _ := setup(t) - - // And mock shims returns valid YAML - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("contexts:\n mock-context:\n dns:\n domain: mock.domain.com"), nil - } - - // And a mock jsonnet VM that returns the expected result - template.shims.NewJsonnetVM = func() JsonnetVM { - mockVM := &mockJsonnetVM{ - EvaluateFunc: func(filename, snippet string) (string, error) { - // Return what the template would actually produce - return `{"test": "value"}`, nil - }, - } - return mockVM - } - - templateContent := `local context = std.extVar("context"); local helpers = std.extVar("helpers"); helpers.removeEmptyKeys({})` - - // When processing the jsonnet template - result, err := template.processJsonnetTemplate(templateContent) - - // Then no error should be returned - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - // And the result should be processed - if result == nil { - t.Error("Expected result to be processed") - } - }) - - t.Run("ExactTemplateThatFails", func(t *testing.T) { - // Given the exact template that's failing - template, _ := setup(t) - - // And mock shims returns valid YAML - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("contexts:\n mock-context:\n dns:\n domain: mock.domain.com"), nil - } - - // And a mock jsonnet VM that returns the expected result - template.shims.NewJsonnetVM = func() JsonnetVM { - mockVM := &mockJsonnetVM{ - EvaluateFunc: func(filename, snippet string) (string, error) { - // Return what the template would actually produce - return `{"common": {"external_domain": "test"}, "csi": {"local_volume_path": ""}, "ingress": {"loadbalancer_ip": ""}}`, nil - }, - } - return mockVM - } - - templateContent := `local context = std.extVar("context"); -local hlp = std.extVar("helpers"); - -// Extracts local volume path from cluster.workers.volumes[0] -local volumes = hlp.getArray(context, "cluster.workers.volumes", []); -local raw_volume = if std.length(volumes) > 0 then volumes[0] else ""; -local local_volume_path = - if std.type(raw_volume) == "string" then - ( - local split = std.split(raw_volume, ":"); - if std.length(split) > 1 then split[1] else split[0] - ) - else - ""; - -hlp.removeEmptyKeys({ - common: { - external_domain: hlp.getString(context, "dns.domain", "test"), - }, - csi: { - local_volume_path: local_volume_path, - }, - ingress: { - loadbalancer_ip: hlp.getString(context, "network.loadbalancer_ips.start", ""), - }, -})` - - // When processing the jsonnet template - result, err := template.processJsonnetTemplate(templateContent) - - // Then no error should be returned - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - // And the result should be processed - if result == nil { - t.Error("Expected result to be processed") - } - }) - - t.Run("CheckRemoveEmptyKeysHelperExists", func(t *testing.T) { - // Given a jsonnet template that checks if removeEmptyKeys exists - template, _ := setup(t) - - // And mock shims returns valid YAML - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("contexts:\n mock-context:\n dns:\n domain: mock.domain.com"), nil - } - - // And a mock jsonnet VM that returns the expected result - template.shims.NewJsonnetVM = func() JsonnetVM { - mockVM := &mockJsonnetVM{ - EvaluateFunc: func(filename, snippet string) (string, error) { - // Return what the template would actually produce - return `{"hasRemoveEmptyKeys": true}`, nil - }, - } - return mockVM - } - - templateContent := `local context = std.extVar("context"); local helpers = std.extVar("helpers"); {"hasRemoveEmptyKeys": "removeEmptyKeys" in helpers}` - - // When processing the jsonnet template - result, err := template.processJsonnetTemplate(templateContent) - - // Then no error should be returned - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - // And the result should indicate that removeEmptyKeys exists - if hasRemoveEmptyKeys, ok := result["hasRemoveEmptyKeys"].(bool); ok { - if !hasRemoveEmptyKeys { - t.Error("Expected removeEmptyKeys to be available in helpers") - } - } else { - t.Error("Expected hasRemoveEmptyKeys to be a boolean") - } - }) - - t.Run("RemoveEmptyKeysWorksWithArrays", func(t *testing.T) { - // Given a jsonnet template that uses removeEmptyKeys with arrays - template, _ := setup(t) - - // And mock shims returns valid YAML - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("contexts:\n mock-context:\n dns:\n domain: mock.domain.com"), nil - } - - // And a mock jsonnet VM that returns the expected result - template.shims.NewJsonnetVM = func() JsonnetVM { - mockVM := &mockJsonnetVM{ - EvaluateFunc: func(filename, snippet string) (string, error) { - // Return what the template would actually produce after removeEmptyKeys processing - return `{"test": "value", "array": ["valid", "another"]}`, nil - }, - } - return mockVM - } - - templateContent := `local context = std.extVar("context"); local helpers = std.extVar("helpers"); helpers.removeEmptyKeys({test: "value", array: ["valid", "", null, "another"]})` - - // When processing the jsonnet template - result, err := template.processJsonnetTemplate(templateContent) - - // Then no error should be returned - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - // And the result should contain the cleaned array - if array, ok := result["array"].([]any); ok { - // Should only contain non-empty elements - if len(array) != 2 { - t.Errorf("Expected 2 elements in array, got: %v", array) - } - // Check that empty elements were removed - for _, element := range array { - if element == "" || element == nil { - t.Errorf("Expected empty elements to be removed, got: %v", element) - } - } - } else { - t.Error("Expected 'array' key to be present and be an array") - } - }) -} - -func TestJsonnetTemplate_RealShimsIntegration(t *testing.T) { - t.Run("UsesRealShimsForJsonnetVM", func(t *testing.T) { - // Given a jsonnet template that uses real shims (not mocked) - mocks := setupMocks(t) - template := NewJsonnetTemplate(mocks.Injector) - err := template.Initialize() - if err != nil { - t.Fatalf("Failed to initialize template: %v", err) - } - - // And mock config handler returns valid YAML - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("contexts:\n test-context:\n value: test"), nil - } - - // When processing a simple jsonnet template using real shims - templateContent := `local context = std.extVar("context"); { result: "success", hasContext: std.objectHas(context, "name") }` - result, err := template.processJsonnetTemplate(templateContent) - - // Then no error should be returned - if err != nil { - t.Fatalf("Expected no error with real shims, got: %v", err) - } - - // And the result should be processed correctly - if result["result"] != "success" { - t.Errorf("Expected result 'success', got: %v", result["result"]) - } - - // And context should be available - if hasContext, ok := result["hasContext"].(bool); !ok || !hasContext { - t.Error("Expected context to be available in jsonnet template") - } - }) - - t.Run("ShimsProvideBasicFunctionality", func(t *testing.T) { - // Given real shims - shims := NewShims() - - // When testing basic functionality - // Then all function fields should be set - if shims.ReadFile == nil { - t.Error("Expected ReadFile to be set") - } - if shims.JsonMarshal == nil { - t.Error("Expected JsonMarshal to be set") - } - if shims.JsonUnmarshal == nil { - t.Error("Expected JsonUnmarshal to be set") - } - if shims.YamlMarshal == nil { - t.Error("Expected YamlMarshal to be set") - } - if shims.YamlUnmarshal == nil { - t.Error("Expected YamlUnmarshal to be set") - } - if shims.NewJsonnetVM == nil { - t.Error("Expected NewJsonnetVM to be set") - } - if shims.FilepathBase == nil { - t.Error("Expected FilepathBase to be set") - } - - // And JsonnetVM should be creatable - vm := shims.NewJsonnetVM() - if vm == nil { - t.Error("Expected NewJsonnetVM to create a VM") - } - - // And basic shim functions should work - testData := map[string]interface{}{"test": "value"} - jsonBytes, err := shims.JsonMarshal(testData) - if err != nil { - t.Errorf("Expected JsonMarshal to work, got error: %v", err) - } - - var unmarshaledData map[string]interface{} - err = shims.JsonUnmarshal(jsonBytes, &unmarshaledData) - if err != nil { - t.Errorf("Expected JsonUnmarshal to work, got error: %v", err) - } - if unmarshaledData["test"] != "value" { - t.Errorf("Expected unmarshaled data to match, got: %v", unmarshaledData) - } - - // And FilepathBase should work - baseName := shims.FilepathBase("/path/to/file.txt") - if baseName != "file.txt" { - t.Errorf("Expected base name 'file.txt', got: %v", baseName) - } - }) -} - -func TestJsonnetTemplate_buildHelperLibrary(t *testing.T) { - setup := func(t *testing.T) (*JsonnetTemplate, *Mocks) { - t.Helper() - mocks, template := setupJsonnetTemplateMocks(t) - err := template.Initialize() - if err != nil { - t.Fatalf("Failed to initialize template: %v", err) - } - return template, mocks - } - - t.Run("GeneratesValidJsonnetLibrary", func(t *testing.T) { - // Given a jsonnet template - template, _ := setup(t) - - // When building the helper library - helperLib := template.buildHelperLibrary() - - // Then it should be a valid Jsonnet object - if !strings.HasPrefix(helperLib, "{") { - t.Error("Expected helper library to start with '{'") - } - if !strings.HasSuffix(helperLib, "}") { - t.Error("Expected helper library to end with '}'") - } - - // And it should contain the expected helper functions - expectedFunctions := []string{ - // Smart helpers (handle both path-based and key-based access) - "get(obj, path, default=null):", - "getString(obj, path, default=\"\"):", - "getInt(obj, path, default=0):", - "getNumber(obj, path, default=0):", - "getBool(obj, path, default=false):", - "getObject(obj, path, default={}):", - "getArray(obj, path, default=[]):", - "has(obj, path):", - - // URL helpers - "baseUrl(endpoint):", - } - - for _, expectedFunc := range expectedFunctions { - if !strings.Contains(helperLib, expectedFunc) { - t.Errorf("Expected helper library to contain '%s'", expectedFunc) - } - } - }) -} - -func TestJsonnetTemplate_processJsonnetTemplateWithHelpers(t *testing.T) { - setup := func(t *testing.T) (*JsonnetTemplate, *Mocks) { - t.Helper() - mocks, template := setupJsonnetTemplateMocks(t) - err := template.Initialize() - if err != nil { - t.Fatalf("Failed to initialize template: %v", err) - } - return template, mocks - } - - t.Run("InjectsHelpersAsLibrary", func(t *testing.T) { - // Given a jsonnet template - template, _ := setup(t) - - // And a mock jsonnet VM that captures all ExtCode calls - var extCalls []struct{ Key, Val string } - template.shims.NewJsonnetVM = func() JsonnetVM { - mockVM := &mockJsonnetVM{ - EvaluateFunc: func(filename, snippet string) (string, error) { - return `{"success": true}`, nil - }, - } - mockVM.ExtCodeFunc = func(key, val string) { - extCalls = append(extCalls, struct{ Key, Val string }{key, val}) - } - return mockVM - } - - // And mock config handler returns valid YAML - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("contexts:\n mock-context:\n dns:\n domain: mock.domain.com"), nil - } - - templateContent := `local helpers = std.extVar("helpers"); local context = std.extVar("context"); { result: helpers.getString(context, "dns.domain", "default") }` - - // When processing the jsonnet template - _, err := template.processJsonnetTemplate(templateContent) - - // Then no error should be returned - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - // And windsor helpers should be injected as a library - var foundHelpers, foundContext bool - for _, call := range extCalls { - if call.Key == "helpers" { - foundHelpers = true - // Verify the helpers library contains expected functions - if !strings.Contains(call.Val, "getString") { - t.Error("Expected helpers library to contain getString function") - } - if !strings.Contains(call.Val, "baseUrl") { - t.Error("Expected helpers library to contain baseUrl function") - } - } - if call.Key == "context" { - foundContext = true - } - } - - if !foundHelpers { - t.Error("Expected helpers to be injected as ExtCode") - } - if !foundContext { - t.Error("Expected context to be injected as ExtCode") - } - }) - - t.Run("test helper functions with real Jsonnet VM", func(t *testing.T) { - templateContent := ` -local helpers = std.extVar("helpers"); -local context = std.extVar("context"); - -{ - // Test primary helpers (path-based access) - vmDriver: helpers.getString(context, "vm.driver", "default-driver"), - vmCores: helpers.getInt(context, "vm.cores", 2), - haEnabled: helpers.getBool(context, "cluster.ha.enabled", false), - - // Test nested path access - nodeIp: helpers.getString(context, "cluster.nodes.master.ip", "192.168.1.1"), - - // Test object and array access - cluster: helpers.getObject(context, "cluster", {}), - tags: helpers.getArray(context, "tags", ["default"]), - - // Test key-based helpers (same function, different usage) - localValue: helpers.getString({test: "value"}, "test", "fallback"), - localInt: helpers.getInt({number: 42}, "number", 0), - - // Test path access with primary helpers - pathValue: helpers.get({nested: {value: "found"}}, "nested.value", "not found"), - - // Test existence checking - hasVm: helpers.has(context, "vm.driver"), - hasNonexistent: helpers.has(context, "does.not.exist"), -}` - - // Given a jsonnet template using real shims - template, _ := setup(t) - - // Set up mock config for the context - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte(` -vm: - driver: colima - cores: 4 -cluster: - ha: - enabled: true - nodes: - master: - ip: 10.0.1.100 -tags: - - production - - k8s -`), nil - } - - // When processing a template that uses helper functions - result, err := template.processJsonnetTemplate(templateContent) - - // Then no error should be returned - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - // And the helper functions should work correctly - if result["vmDriver"] != "colima" { - t.Errorf("Expected vmDriver 'colima', got: %v", result["vmDriver"]) - } - if result["vmCores"] != float64(4) { // JSON unmarshaling converts to float64 - t.Errorf("Expected vmCores 4, got: %v", result["vmCores"]) - } - if result["haEnabled"] != true { - t.Errorf("Expected haEnabled true, got: %v", result["haEnabled"]) - } - if result["nodeIp"] != "10.0.1.100" { - t.Errorf("Expected nodeIp '10.0.1.100', got: %v", result["nodeIp"]) - } - - // Verify cluster object - cluster, ok := result["cluster"].(map[string]any) - if !ok { - t.Errorf("Expected cluster to be object, got: %T", result["cluster"]) - } else { - if cluster["ha"] == nil { - t.Error("Expected cluster to contain ha config") - } - } - - // Verify tags array - tags, ok := result["tags"].([]any) - if !ok { - t.Errorf("Expected tags to be array, got: %T", result["tags"]) - } else { - if len(tags) != 2 { - t.Errorf("Expected tags array length 2, got: %d", len(tags)) - } - } - - // Verify generic helpers work - if result["localValue"] != "value" { - t.Errorf("Expected localValue 'value', got: %v", result["localValue"]) - } - if result["localInt"] != float64(42) { - t.Errorf("Expected localInt 42, got: %v", result["localInt"]) - } - - // Verify path access works - if result["pathValue"] != "found" { - t.Errorf("Expected pathValue 'found', got: %v", result["pathValue"]) - } - - // Verify existence checking - if result["hasVm"] != true { - t.Errorf("Expected hasVm true, got: %v", result["hasVm"]) - } - if result["hasNonexistent"] != false { - t.Errorf("Expected hasNonexistent false, got: %v", result["hasNonexistent"]) - } - }) - - t.Run("HelpersHandleNestedPathsCorrectly", func(t *testing.T) { - // Given a jsonnet template - template, _ := setup(t) - - // And mock config handler returns nested test data - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte(` -deeply: - nested: - object: - value: "found" - number: 123 - enabled: true -partial: - path: "exists" -`), nil - } - - // When processing a template that tests nested path navigation - templateContent := ` -local helpers = std.extVar("helpers"); -local context = std.extVar("context"); -{ - deepValue: helpers.getString(context, "deeply.nested.object.value", "not-found"), - deepNumber: helpers.getInt(context, "deeply.nested.object.number", 0), - deepBool: helpers.getBool(context, "deeply.nested.object.enabled", false), - partialPath: helpers.getString(context, "partial.path", "missing"), - missingDeepPath: helpers.getString(context, "deeply.nested.missing.value", "default"), - totallyMissing: helpers.getString(context, "not.there.at.all", "default"), -}` - - result, err := template.processJsonnetTemplate(templateContent) - - // Then no error should be returned - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - // And nested paths should be resolved correctly - if result["deepValue"] != "found" { - t.Errorf("Expected deepValue 'found', got: %v", result["deepValue"]) - } - if result["deepNumber"] != float64(123) { - t.Errorf("Expected deepNumber 123, got: %v", result["deepNumber"]) - } - if result["deepBool"] != true { - t.Errorf("Expected deepBool true, got: %v", result["deepBool"]) - } - if result["partialPath"] != "exists" { - t.Errorf("Expected partialPath 'exists', got: %v", result["partialPath"]) - } - if result["missingDeepPath"] != "default" { - t.Errorf("Expected missingDeepPath 'default', got: %v", result["missingDeepPath"]) - } - if result["totallyMissing"] != "default" { - t.Errorf("Expected totallyMissing 'default', got: %v", result["totallyMissing"]) - } - }) -} - -// ============================================================================= -// Test Helpers -// ============================================================================= - -type mockJsonnetVM struct { - EvaluateFunc func(filename, snippet string) (string, error) - ExtCodeFunc func(key, val string) - ExtCalls []struct{ Key, Val string } -} - -func (m *mockJsonnetVM) ExtCode(key, val string) { - if m.ExtCodeFunc != nil { - m.ExtCodeFunc(key, val) - } else { - m.ExtCalls = append(m.ExtCalls, struct{ Key, Val string }{key, val}) - } -} - -func (m *mockJsonnetVM) EvaluateAnonymousSnippet(filename, snippet string) (string, error) { - if m.EvaluateFunc != nil { - return m.EvaluateFunc(filename, snippet) - } - return "", nil -} - -func TestJsonnetTemplate_urlHelpers(t *testing.T) { - setup := func(t *testing.T) (*JsonnetTemplate, *Mocks) { - t.Helper() - mocks, template := setupJsonnetTemplateMocks(t) - err := template.Initialize() - if err != nil { - t.Fatalf("Failed to initialize template: %v", err) - } - return template, mocks - } - - t.Run("ExtractBaseUrlFromHttpsEndpoint", func(t *testing.T) { - templateContent := ` -local helpers = std.extVar("helpers"); -{ - baseUrl: helpers.baseUrl("https://api.example.com:6443") -}` - - template, _ := setup(t) - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("contexts:\n mock-context: {}"), nil - } - - result, err := template.processJsonnetTemplate(templateContent) - - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - if result["baseUrl"] != "api.example.com" { - t.Errorf("Expected baseUrl 'api.example.com', got: %v", result["baseUrl"]) - } - }) - - t.Run("ExtractBaseUrlFromHttpEndpoint", func(t *testing.T) { - templateContent := ` -local helpers = std.extVar("helpers"); -{ - baseUrl: helpers.baseUrl("http://localhost:8080") -}` - - template, _ := setup(t) - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("contexts:\n mock-context: {}"), nil - } - - result, err := template.processJsonnetTemplate(templateContent) - - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - if result["baseUrl"] != "localhost" { - t.Errorf("Expected baseUrl 'localhost', got: %v", result["baseUrl"]) - } - }) - - t.Run("ExtractBaseUrlFromPlainHost", func(t *testing.T) { - templateContent := ` -local helpers = std.extVar("helpers"); -{ - baseUrl: helpers.baseUrl("example.com:9000") -}` - - template, _ := setup(t) - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("contexts:\n mock-context: {}"), nil - } - - result, err := template.processJsonnetTemplate(templateContent) - - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - if result["baseUrl"] != "example.com" { - t.Errorf("Expected baseUrl 'example.com', got: %v", result["baseUrl"]) - } - }) - - t.Run("ExtractBaseUrlFromHostWithoutPort", func(t *testing.T) { - templateContent := ` -local helpers = std.extVar("helpers"); -{ - baseUrl: helpers.baseUrl("example.com") -}` - - template, _ := setup(t) - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("contexts:\n mock-context: {}"), nil - } - - result, err := template.processJsonnetTemplate(templateContent) - - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - if result["baseUrl"] != "example.com" { - t.Errorf("Expected baseUrl 'example.com', got: %v", result["baseUrl"]) - } - }) - - t.Run("ExtractBaseUrlFromEmptyString", func(t *testing.T) { - templateContent := ` -local helpers = std.extVar("helpers"); -{ - baseUrl: helpers.baseUrl("") -}` - - template, _ := setup(t) - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("contexts:\n mock-context: {}"), nil - } - - // And a mock jsonnet VM that returns the expected result - template.shims.NewJsonnetVM = func() JsonnetVM { - mockVM := &mockJsonnetVM{ - EvaluateFunc: func(filename, snippet string) (string, error) { - return `{"baseUrl": ""}`, nil - }, - } - return mockVM - } - - result, err := template.processJsonnetTemplate(templateContent) - - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - if result["baseUrl"] != "" { - t.Errorf("Expected baseUrl '', got: %v", result["baseUrl"]) - } - }) -} - -func TestJsonnetTemplate_typeValidation(t *testing.T) { - setup := func(t *testing.T) (*JsonnetTemplate, *Mocks) { - t.Helper() - mocks, template := setupJsonnetTemplateMocks(t) - err := template.Initialize() - if err != nil { - t.Fatalf("Failed to initialize template: %v", err) - } - return template, mocks - } - - t.Run("GetStringSuccess", func(t *testing.T) { - templateContent := ` -local helpers = std.extVar("helpers"); -local context = std.extVar("context"); -{ - provider: helpers.getString(context, "provider", "default") -}` - - template, _ := setup(t) - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("provider: aws"), nil - } - - result, err := template.processJsonnetTemplate(templateContent) - - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - if result["provider"] != "aws" { - t.Errorf("Expected provider 'aws', got: %v", result["provider"]) - } - }) - - t.Run("GetStringMissingUsesDefault", func(t *testing.T) { - templateContent := ` -local helpers = std.extVar("helpers"); -local context = std.extVar("context"); -{ - provider: helpers.getString(context, "provider", "default") -}` - - template, _ := setup(t) - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("{}"), nil - } - - result, err := template.processJsonnetTemplate(templateContent) - - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - if result["provider"] != "default" { - t.Errorf("Expected provider 'default', got: %v", result["provider"]) - } - }) - - t.Run("GetStringWrongTypeThrowsError", func(t *testing.T) { - templateContent := ` -local helpers = std.extVar("helpers"); -local context = std.extVar("context"); -{ - provider: helpers.getString(context, "provider", "default") -}` - - template, _ := setup(t) - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("provider: 123"), nil - } - - _, err := template.processJsonnetTemplate(templateContent) - - if err == nil { - t.Error("Expected error for wrong type, got none") - } - - if !strings.Contains(err.Error(), "Expected string for 'provider' but got number") { - t.Errorf("Expected type error message, got: %v", err) - } - }) - - t.Run("GetIntWrongTypeThrowsError", func(t *testing.T) { - templateContent := ` -local helpers = std.extVar("helpers"); -local context = std.extVar("context"); -{ - cores: helpers.getInt(context, "vm.cores", 2) -}` - - template, _ := setup(t) - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("vm:\n cores: \"not-a-number\""), nil - } - - _, err := template.processJsonnetTemplate(templateContent) - - if err == nil { - t.Error("Expected error for wrong type, got none") - } - - if !strings.Contains(err.Error(), "Expected number for 'vm.cores' but got string") { - t.Errorf("Expected type error message, got: %v", err) - } - }) - - t.Run("GetBoolWrongTypeThrowsError", func(t *testing.T) { - templateContent := ` -local helpers = std.extVar("helpers"); -local context = std.extVar("context"); -{ - enabled: helpers.getBool(context, "feature.enabled", false) -}` - - template, _ := setup(t) - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("feature:\n enabled: \"yes\""), nil - } - - _, err := template.processJsonnetTemplate(templateContent) - - if err == nil { - t.Error("Expected error for wrong type, got none") - } - - if !strings.Contains(err.Error(), "Expected boolean for 'feature.enabled' but got string") { - t.Errorf("Expected type error message, got: %v", err) - } - }) - - t.Run("GetObjectWrongTypeThrowsError", func(t *testing.T) { - templateContent := ` -local helpers = std.extVar("helpers"); -local context = std.extVar("context"); -{ - cluster: helpers.getObject(context, "cluster", {}) -}` - - template, _ := setup(t) - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("cluster: \"not-an-object\""), nil - } - - _, err := template.processJsonnetTemplate(templateContent) - - if err == nil { - t.Error("Expected error for wrong type, got none") - } - - if !strings.Contains(err.Error(), "Expected object for 'cluster' but got string") { - t.Errorf("Expected type error message, got: %v", err) - } - }) - - t.Run("GetArrayWrongTypeThrowsError", func(t *testing.T) { - templateContent := ` -local helpers = std.extVar("helpers"); -local context = std.extVar("context"); -{ - tags: helpers.getArray(context, "tags", []) -}` - - template, _ := setup(t) - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte("tags: \"not-an-array\""), nil - } - - _, err := template.processJsonnetTemplate(templateContent) - - if err == nil { - t.Error("Expected error for wrong type, got none") - } - - if !strings.Contains(err.Error(), "Expected array for 'tags' but got string") { - t.Errorf("Expected type error message, got: %v", err) - } - }) -} - -// ============================================================================= -// Test Helpers -// ============================================================================= - -type mockDirEntry struct { - name string - isDir bool -} - -func (m *mockDirEntry) Name() string { - return m.name -} - -func (m *mockDirEntry) IsDir() bool { - return m.isDir -} - -func (m *mockDirEntry) Type() os.FileMode { - if m.isDir { - return os.ModeDir - } - return 0 -} - -func (m *mockDirEntry) Info() (os.FileInfo, error) { - return &mockFileInfo{name: m.name, isDir: m.isDir}, nil -} - -type mockFileInfo struct { - name string - isDir bool -} - -func (m *mockFileInfo) Name() string { - return m.name -} - -func (m *mockFileInfo) Size() int64 { - return 0 -} - -func (m *mockFileInfo) Mode() os.FileMode { - if m.isDir { - return os.ModeDir - } - return 0 -} - -func (m *mockFileInfo) ModTime() time.Time { - return time.Now() -} - -func (m *mockFileInfo) IsDir() bool { - return m.isDir -} - -func (m *mockFileInfo) Sys() interface{} { - return nil -} - -func TestJsonnetTemplate_processTemplate(t *testing.T) { - setup := func(t *testing.T) (*JsonnetTemplate, *Mocks) { - t.Helper() - mocks, template := setupJsonnetTemplateMocks(t) - _ = template.Initialize() - return template, mocks - } - - t.Run("BlueprintJsonnet", func(t *testing.T) { - // Given a template and blueprint.jsonnet content - template, _ := setup(t) - templateData := map[string][]byte{ - "blueprint.jsonnet": []byte(`{ kustomize: [{ name: "test" }] }`), - } - renderedData := map[string]any{} - - // Mock jsonnet processing - template.shims.NewJsonnetVM = func() JsonnetVM { - return &mockJsonnetVM{ - EvaluateFunc: func(filename, snippet string) (string, error) { - return `{"kustomize":[{"name":"test"}]}`, nil - }, - } - } - - // When processing blueprint.jsonnet - err := template.processTemplate("blueprint.jsonnet", templateData, renderedData) - - // Then should succeed and add blueprint data - if err != nil { - t.Fatalf("expected no error, got: %v", err) - } - if renderedData["blueprint"] == nil { - t.Error("expected blueprint data to be added") - } - }) - - t.Run("TerraformJsonnet", func(t *testing.T) { - // Given a template and terraform jsonnet content - template, _ := setup(t) - templateData := map[string][]byte{ - "terraform/main.jsonnet": []byte(`{ region: "us-west-2" }`), - } - renderedData := map[string]any{} - - // Mock jsonnet processing - template.shims.NewJsonnetVM = func() JsonnetVM { - return &mockJsonnetVM{ - EvaluateFunc: func(filename, snippet string) (string, error) { - return `{"region":"us-west-2"}`, nil - }, - } - } - - // When processing terraform/main.jsonnet - err := template.processTemplate("terraform/main.jsonnet", templateData, renderedData) - - // Then should succeed and add terraform data - if err != nil { - t.Fatalf("expected no error, got: %v", err) - } - if renderedData["terraform/main"] == nil { - t.Error("expected terraform data to be added") - } - }) - - t.Run("PatchJsonnet", func(t *testing.T) { - // Given a template and patch content - template, _ := setup(t) - templateData := map[string][]byte{ - "patches/ingress/patch.jsonnet": []byte(`{ apiVersion: "v1", kind: "ConfigMap" }`), - } - renderedData := map[string]any{} - - // Mock jsonnet processing - template.shims.NewJsonnetVM = func() JsonnetVM { - return &mockJsonnetVM{ - EvaluateFunc: func(filename, snippet string) (string, error) { - return `{"apiVersion":"v1","kind":"ConfigMap"}`, nil - }, - } - } - - // When processing patch - err := template.processTemplate("patches/ingress/patch.jsonnet", templateData, renderedData) - - // Then should succeed and add patch data - if err != nil { - t.Fatalf("expected no error, got: %v", err) - } - if renderedData["patches/ingress/patch"] == nil { - t.Error("expected patch data to be added") - } - }) - - t.Run("KustomizeValuesGlobal", func(t *testing.T) { - // Given a template and global values content - template, _ := setup(t) - templateData := map[string][]byte{ - "kustomize/values.jsonnet": []byte(`{ domain: "example.com" }`), - } - renderedData := map[string]any{} - - // When processing global values (should be skipped) - err := template.processTemplate("kustomize/values.jsonnet", templateData, renderedData) - - // Then should succeed but not add any data (skipped) - if err != nil { - t.Fatalf("expected no error, got: %v", err) - } - if renderedData["kustomize/values"] != nil { - t.Error("expected kustomize/values to be skipped, but data was added") - } - }) - - t.Run("KustomizeValuesComponent", func(t *testing.T) { - // Given a template and component values content - template, _ := setup(t) - templateData := map[string][]byte{ - "kustomize/ingress/values.jsonnet": []byte(`{ host: "example.com" }`), - } - renderedData := map[string]any{} - - // When processing component values (should be skipped) - err := template.processTemplate("kustomize/ingress/values.jsonnet", templateData, renderedData) - - // Then should succeed but not add any data (skipped) - if err != nil { - t.Fatalf("expected no error, got: %v", err) - } - if renderedData["kustomize/values"] != nil { - t.Error("expected kustomize/values to be skipped, but data was added") - } - }) - - t.Run("KustomizeValuesComponentWithValuesSubdirectory", func(t *testing.T) { - // Given a template and component values in a "values" subdirectory - template, _ := setup(t) - templateData := map[string][]byte{ - "kustomize/values/values.jsonnet": []byte(`{ global: "config" }`), - } - renderedData := map[string]any{} - - // When processing values subdirectory (should be skipped) - err := template.processTemplate("kustomize/values/values.jsonnet", templateData, renderedData) - - // Then should succeed but not add any data (skipped) - if err != nil { - t.Fatalf("expected no error, got: %v", err) - } - if renderedData["kustomize/values"] != nil { - t.Error("expected kustomize/values to be skipped, but data was added") - } - }) - - t.Run("UnsupportedPath", func(t *testing.T) { - // Given a template and unsupported path - template, _ := setup(t) - templateData := map[string][]byte{ - "unsupported/path.jsonnet": []byte(`{ data: "value" }`), - } - renderedData := map[string]any{} - - // When processing unsupported path - err := template.processTemplate("unsupported/path.jsonnet", templateData, renderedData) - - // Then should return nil (no error, just ignored) - if err != nil { - t.Fatalf("expected no error, got: %v", err) - } - if len(renderedData) != 0 { - t.Error("expected no data to be added for unsupported path") - } - }) - - t.Run("MissingTemplateData", func(t *testing.T) { - // Given a template and missing template data - template, _ := setup(t) - templateData := map[string][]byte{} - renderedData := map[string]any{} - - // When processing missing template - err := template.processTemplate("blueprint.jsonnet", templateData, renderedData) - - // Then should return nil (no error, just ignored) - if err != nil { - t.Fatalf("expected no error, got: %v", err) - } - if len(renderedData) != 0 { - t.Error("expected no data to be added for missing template") - } - }) - - t.Run("JsonnetProcessingError", func(t *testing.T) { - // Given a template and blueprint content - template, _ := setup(t) - templateData := map[string][]byte{ - "blueprint.jsonnet": []byte(`{ invalid: jsonnet }`), - } - renderedData := map[string]any{} - - // Mock jsonnet processing to fail - template.shims.NewJsonnetVM = func() JsonnetVM { - return &mockJsonnetVM{ - EvaluateFunc: func(filename, snippet string) (string, error) { - return "", fmt.Errorf("jsonnet processing error") - }, - } - } - - // When processing blueprint with jsonnet error - err := template.processTemplate("blueprint.jsonnet", templateData, renderedData) - - // Then should return error - if err == nil { - t.Fatal("expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to process template") { - t.Errorf("expected error about template processing, got: %v", err) - } - }) - - t.Run("KustomizeValuesComplexPath", func(t *testing.T) { - // Given a template and complex kustomize values path - template, _ := setup(t) - templateData := map[string][]byte{ - "kustomize/ingress/nginx/values.jsonnet": []byte(`{ port: 80 }`), - } - renderedData := map[string]any{} - - // When processing complex kustomize values path (should be skipped) - err := template.processTemplate("kustomize/ingress/nginx/values.jsonnet", templateData, renderedData) - - // Then should succeed but not add any data (skipped) - if err != nil { - t.Fatalf("expected no error, got: %v", err) - } - if renderedData["kustomize/values"] != nil { - t.Error("expected kustomize/values to be skipped, but data was added") - } - }) - - t.Run("ProcessesSubstitutionJsonnetTemplate", func(t *testing.T) { - // Given a template with substitution.jsonnet - template, mocks := setup(t) - - // Mock config handler returns basic config - template.shims.YamlMarshal = func(v any) ([]byte, error) { - return []byte(`contexts: - test: - name: test-context`), nil - } - - // Mock shell returns project root - mocks.Shell.GetProjectRootFunc = func() (string, error) { - return "/test/project", nil - } - - // Mock jsonnet VM to return substitution values - template.shims.NewJsonnetVM = func() JsonnetVM { - return &mockJsonnetVM{ - EvaluateFunc: func(filename, snippet string) (string, error) { - return `{"common": {"external_domain": "test.example.com", "registry_url": "registry.test.com"}, "app_config": {"replicas": 3}}`, nil - }, - } - } - - // Set up template data with substitution.jsonnet - templateData := map[string][]byte{ - "substitution.jsonnet": []byte(`local context = std.extVar("context"); -{ - common: { - external_domain: context.external_domain, - registry_url: context.registry_url - }, - app_config: { - replicas: context.replicas || 3 - } -}`), - } - - renderedData := make(map[string]any) - - // When processing templates - err := template.Process(templateData, renderedData) - - // Then no error should occur - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } - - // And substitution values should be captured in substitution - if substitutionValues, exists := renderedData["substitution"]; exists { - if substitutionMap, ok := substitutionValues.(map[string]any); ok { - // Verify common section - if common, exists := substitutionMap["common"].(map[string]any); exists { - if common["external_domain"] != "test.example.com" { - t.Errorf("Expected external_domain to be 'test.example.com', got %v", common["external_domain"]) - } - if common["registry_url"] != "registry.test.com" { - t.Errorf("Expected registry_url to be 'registry.test.com', got %v", common["registry_url"]) - } - } else { - t.Error("Expected 'common' section to exist in substitution values") - } - - // Verify app_config section - if appConfig, exists := substitutionMap["app_config"].(map[string]any); exists { - if appConfig["replicas"] != float64(3) { - t.Errorf("Expected replicas to be 3, got %v", appConfig["replicas"]) - } - } else { - t.Error("Expected 'app_config' section to exist in substitution values") - } - } else { - t.Error("Expected substitution values to be a map") - } - } else { - t.Error("Expected 'substitution' to exist in rendered data") - } - }) -} diff --git a/pkg/template/mock_template.go b/pkg/template/mock_template.go deleted file mode 100644 index 1b5258fdf..000000000 --- a/pkg/template/mock_template.go +++ /dev/null @@ -1,45 +0,0 @@ -package template - -import "github.com/windsorcli/cli/pkg/di" - -// MockTemplate is a mock implementation of the Template interface for testing -type MockTemplate struct { - InitializeFunc func() error - ProcessFunc func(templateData map[string][]byte, renderedData map[string]any) error -} - -// ============================================================================= -// Constructor -// ============================================================================= - -// NewMockTemplate creates a new MockTemplate instance -func NewMockTemplate(injector di.Injector) *MockTemplate { - return &MockTemplate{} -} - -// ============================================================================= -// Public Methods -// ============================================================================= - -// Initialize calls the mock InitializeFunc if set, otherwise returns nil -func (m *MockTemplate) Initialize() error { - if m.InitializeFunc != nil { - return m.InitializeFunc() - } - return nil -} - -// Process calls the mock ProcessFunc if set, otherwise returns nil -func (m *MockTemplate) Process(templateData map[string][]byte, renderedData map[string]any) error { - if m.ProcessFunc != nil { - return m.ProcessFunc(templateData, renderedData) - } - return nil -} - -// ============================================================================= -// Interface Compliance -// ============================================================================= - -// Ensure MockTemplate implements Template interface -var _ Template = (*MockTemplate)(nil) diff --git a/pkg/template/mock_template_test.go b/pkg/template/mock_template_test.go deleted file mode 100644 index be759e550..000000000 --- a/pkg/template/mock_template_test.go +++ /dev/null @@ -1,157 +0,0 @@ -package template - -import ( - "fmt" - "testing" - - "github.com/windsorcli/cli/pkg/di" -) - -// ============================================================================= -// Test Public Methods -// ============================================================================= - -func TestMockTemplate_NewMockTemplate(t *testing.T) { - t.Run("CreatesTemplateWithInjector", func(t *testing.T) { - // Given an injector - injector := di.NewInjector() - - // When creating a new mock template - template := NewMockTemplate(injector) - - // Then the template should be properly initialized - if template == nil { - t.Fatal("Expected non-nil template") - } - }) -} - -func TestMockTemplate_Initialize(t *testing.T) { - setup := func(t *testing.T) *MockTemplate { - t.Helper() - injector := di.NewInjector() - return NewMockTemplate(injector) - } - - t.Run("DefaultBehavior", func(t *testing.T) { - // Given a mock template without initialize function - template := setup(t) - - // When calling Initialize - err := template.Initialize() - - // Then no error should be returned - if err != nil { - t.Errorf("Expected nil error, got %v", err) - } - }) - - t.Run("CustomInitializeFunc", func(t *testing.T) { - // Given a mock template with custom initialize function - template := setup(t) - template.InitializeFunc = func() error { - return fmt.Errorf("custom error") - } - - // When calling Initialize - err := template.Initialize() - - // Then the custom error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if err.Error() != "custom error" { - t.Errorf("Expected 'custom error', got: %v", err) - } - }) -} - -func TestMockTemplate_Process(t *testing.T) { - setup := func(t *testing.T) *MockTemplate { - t.Helper() - injector := di.NewInjector() - return NewMockTemplate(injector) - } - - t.Run("DefaultBehavior", func(t *testing.T) { - // Given a mock template without process function - template := setup(t) - - // And template data - templateData := map[string][]byte{ - "test.jsonnet": []byte(`{"key": "value"}`), - } - renderedData := make(map[string]any) - - // When calling Process - err := template.Process(templateData, renderedData) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected nil error, got %v", err) - } - - // And rendered data should remain empty - if len(renderedData) != 0 { - t.Errorf("Expected empty rendered data, got %d items", len(renderedData)) - } - }) - - t.Run("CustomProcessFunc", func(t *testing.T) { - // Given a mock template with custom process function - template := setup(t) - template.ProcessFunc = func(templateData map[string][]byte, renderedData map[string]any) error { - for key := range templateData { - renderedData[key] = "processed" - } - return nil - } - - // And template data - templateData := map[string][]byte{ - "test.jsonnet": []byte(`{"key": "value"}`), - } - renderedData := make(map[string]any) - - // When calling Process - err := template.Process(templateData, renderedData) - - // Then no error should be returned - if err != nil { - t.Errorf("Expected nil error, got %v", err) - } - - // And rendered data should contain the processed result - if len(renderedData) != 1 { - t.Errorf("Expected 1 rendered item, got %d", len(renderedData)) - } - if renderedData["test.jsonnet"] != "processed" { - t.Errorf("Expected 'processed', got: %v", renderedData["test.jsonnet"]) - } - }) - - t.Run("ProcessFuncReturnsError", func(t *testing.T) { - // Given a mock template with error-returning process function - template := setup(t) - template.ProcessFunc = func(templateData map[string][]byte, renderedData map[string]any) error { - return fmt.Errorf("process error") - } - - // And template data - templateData := map[string][]byte{ - "test.jsonnet": []byte(`{"key": "value"}`), - } - renderedData := make(map[string]any) - - // When calling Process - err := template.Process(templateData, renderedData) - - // Then an error should be returned - if err == nil { - t.Error("Expected error, got nil") - } - if err.Error() != "process error" { - t.Errorf("Expected 'process error', got: %v", err) - } - }) -} diff --git a/pkg/template/shims.go b/pkg/template/shims.go deleted file mode 100644 index 14ae176bb..000000000 --- a/pkg/template/shims.go +++ /dev/null @@ -1,80 +0,0 @@ -package template - -import ( - "encoding/json" - "os" - "path/filepath" - - "github.com/goccy/go-yaml" - "github.com/google/go-jsonnet" -) - -// The shims package is a system call abstraction layer for the template package -// It provides mockable wrappers around system and runtime functions -// It serves as a testing aid by allowing system calls to be intercepted -// It enables dependency injection and test isolation for system-level operations - -// ============================================================================= -// Types -// ============================================================================= - -// JsonnetVM provides an interface for Jsonnet virtual machine operations -type JsonnetVM interface { - ExtCode(key, val string) - EvaluateAnonymousSnippet(filename, snippet string) (string, error) -} - -// RealJsonnetVM is the real implementation of JsonnetVM -type RealJsonnetVM struct { - vm *jsonnet.VM -} - -// ExtCode sets external code for the Jsonnet VM -func (j *RealJsonnetVM) ExtCode(key, val string) { - j.vm.ExtCode(key, val) -} - -// EvaluateAnonymousSnippet evaluates a Jsonnet snippet -func (j *RealJsonnetVM) EvaluateAnonymousSnippet(filename, snippet string) (string, error) { - return j.vm.EvaluateAnonymousSnippet(filename, snippet) -} - -// Shims provides mockable wrappers around system and runtime functions -type Shims struct { - ReadFile func(name string) ([]byte, error) - ReadDir func(name string) ([]os.DirEntry, error) - Stat func(name string) (os.FileInfo, error) - WriteFile func(name string, data []byte, perm os.FileMode) error - MkdirAll func(path string, perm os.FileMode) error - Getenv func(key string) string - YamlUnmarshal func(data []byte, v any) error - YamlMarshal func(v any) ([]byte, error) - JsonMarshal func(v any) ([]byte, error) - JsonUnmarshal func(data []byte, v any) error - NewJsonnetVM func() JsonnetVM - FilepathBase func(path string) string -} - -// ============================================================================= -// Constructor -// ============================================================================= - -// NewShims creates a new Shims instance with default implementations -func NewShims() *Shims { - return &Shims{ - ReadFile: os.ReadFile, - ReadDir: os.ReadDir, - Stat: os.Stat, - WriteFile: os.WriteFile, - MkdirAll: os.MkdirAll, - Getenv: os.Getenv, - YamlUnmarshal: yaml.Unmarshal, - YamlMarshal: yaml.Marshal, - JsonMarshal: json.Marshal, - JsonUnmarshal: json.Unmarshal, - NewJsonnetVM: func() JsonnetVM { - return &RealJsonnetVM{vm: jsonnet.MakeVM()} - }, - FilepathBase: filepath.Base, - } -}