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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 61 additions & 27 deletions pkg/blueprint/blueprint_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -1323,7 +1323,7 @@ func (b *BaseBlueprintHandler) hasComponentValues(componentName string) bool {
hasUserComponent := false
configRoot, err := b.configHandler.GetConfigRoot()
if err == nil {
valuesPath := filepath.Join(configRoot, "kustomize", "config.yaml")
valuesPath := filepath.Join(configRoot, "kustomize", "values.yaml")
if _, err := b.shims.Stat(valuesPath); err == nil {
if data, err := b.shims.ReadFile(valuesPath); err == nil {
var values map[string]any
Expand Down Expand Up @@ -1357,7 +1357,7 @@ func (b *BaseBlueprintHandler) isOCISource(sourceNameOrURL string) bool {
return false
}

// applyValuesConfigMaps creates ConfigMaps for post-build variable substitution using rendered values data and any existing config.yaml files.
// applyValuesConfigMaps creates ConfigMaps for post-build variable substitution using rendered values data and any existing values.yaml files.
// It generates a ConfigMap for the "common" section and for each component section, merging rendered template values with user-defined values.
// User-defined values take precedence over template values in case of conflicts.
// The resulting ConfigMaps are referenced in PostBuild.SubstituteFrom for variable substitution.
Expand Down Expand Up @@ -1403,7 +1403,7 @@ func (b *BaseBlueprintHandler) applyValuesConfigMaps() error {
}

var userValues map[string]any
valuesPath := filepath.Join(configRoot, "kustomize", "config.yaml")
valuesPath := filepath.Join(configRoot, "kustomize", "values.yaml")
if _, err := b.shims.Stat(valuesPath); err == nil {
data, err := b.shims.ReadFile(valuesPath)
if err == nil {
Expand All @@ -1429,10 +1429,8 @@ func (b *BaseBlueprintHandler) applyValuesConfigMaps() error {
}

allValues := make(map[string]any)
for k, v := range renderedValues { // Template values are base
allValues[k] = v
}
allValues = b.deepMergeMaps(allValues, userValues) // Deep merge user values over template values
maps.Copy(allValues, renderedValues)
allValues = b.deepMergeMaps(allValues, userValues)

if commonValues, exists := allValues["common"]; exists {
if commonMap, ok := commonValues.(map[string]any); ok {
Expand Down Expand Up @@ -1463,47 +1461,83 @@ func (b *BaseBlueprintHandler) applyValuesConfigMaps() error {
}

// validateValuesForSubstitution checks that all values are valid for Flux post-build variable substitution.
// Permitted types are string, numeric, and boolean. Complex types (maps, slices) are rejected.
// Returns an error if any value is not a supported type.
// Permitted types are string, numeric, and boolean. Allows one level of map nesting if all nested values are scalar.
// Slices and nested complex types are not allowed. Returns an error if any value is not a supported type.
func (b *BaseBlueprintHandler) validateValuesForSubstitution(values map[string]any) error {
for key, value := range values {
switch v := value.(type) {
case string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, bool:
continue
case map[string]any, []any:
return fmt.Errorf("values for post-build substitution cannot contain complex types (maps or slices), key '%s' has type %T", key, v)
default:
return fmt.Errorf("values for post-build substitution can only contain strings, numbers, and booleans, key '%s' has unsupported type %T", key, v)
var validate func(map[string]any, string, int) error
validate = func(values map[string]any, parentKey string, depth int) error {
for key, value := range values {
currentKey := key
if parentKey != "" {
currentKey = parentKey + "." + key
}

switch v := value.(type) {
case string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, bool:
continue
case map[string]any:
if depth >= 1 {
return fmt.Errorf("values for post-build substitution cannot contain nested complex types, key '%s' has type %T", currentKey, v)
}
err := validate(v, currentKey, depth+1)
if err != nil {
return err
}
case []any:
return fmt.Errorf("values for post-build substitution cannot contain slices, key '%s' has type %T", currentKey, v)
default:
return fmt.Errorf("values for post-build substitution can only contain strings, numbers, booleans, or maps of scalar types, key '%s' has unsupported type %T", currentKey, v)
}
}
return nil
}
return nil
return validate(values, "", 0)
}

// createConfigMap creates a ConfigMap named configMapName in the "flux-system" namespace for post-build variable substitution.
// Only scalar values (string, int, float, bool) are supported. Complex types are rejected. The resulting ConfigMap data is a map of string keys to string values.
// Supports scalar values and one level of map nesting. The resulting ConfigMap data is a map of string keys to string values.
func (b *BaseBlueprintHandler) createConfigMap(values map[string]any, configMapName string) error {
if err := b.validateValuesForSubstitution(values); err != nil {
return fmt.Errorf("invalid values in %s: %w", configMapName, err)
}

stringValues := make(map[string]string)
if err := b.flattenValuesToConfigMap(values, "", stringValues); err != nil {
return fmt.Errorf("failed to flatten values for %s: %w", configMapName, err)
}

if err := b.kubernetesManager.ApplyConfigMap(configMapName, constants.DEFAULT_FLUX_SYSTEM_NAMESPACE, stringValues); err != nil {
return fmt.Errorf("failed to apply ConfigMap %s: %w", configMapName, err)
}

return nil
}

// flattenValuesToConfigMap recursively flattens nested values into a flat map suitable for ConfigMap data.
// Nested maps are flattened using dot notation (e.g., "ingress.host").
func (b *BaseBlueprintHandler) flattenValuesToConfigMap(values map[string]any, prefix string, result map[string]string) error {
for key, value := range values {
currentKey := key
if prefix != "" {
currentKey = prefix + "." + key
}

switch v := value.(type) {
case string:
stringValues[key] = v
result[currentKey] = v
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
stringValues[key] = fmt.Sprintf("%v", v)
result[currentKey] = fmt.Sprintf("%v", v)
case bool:
stringValues[key] = fmt.Sprintf("%t", v)
result[currentKey] = fmt.Sprintf("%t", v)
case map[string]any:
err := b.flattenValuesToConfigMap(v, currentKey, result)
if err != nil {
return err
}
default:
return fmt.Errorf("unsupported value type for key %s: %T", key, v)
}
}

if err := b.kubernetesManager.ApplyConfigMap(configMapName, constants.DEFAULT_FLUX_SYSTEM_NAMESPACE, stringValues); err != nil {
return fmt.Errorf("failed to apply ConfigMap %s: %w", configMapName, err)
}

return nil
}

Expand Down
24 changes: 12 additions & 12 deletions pkg/blueprint/blueprint_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3672,20 +3672,20 @@ func TestBaseBlueprintHandler_applyValuesConfigMaps(t *testing.T) {
return []string{}
}

// And mock centralized config.yaml with component values
// And mock centralized values.yaml with component values
handler.shims.Stat = func(name string) (os.FileInfo, error) {
if name == filepath.Join("/test/config", "kustomize") {
return &mockFileInfo{name: "kustomize"}, nil
}
if name == filepath.Join("/test/config", "kustomize", "config.yaml") {
return &mockFileInfo{name: "config.yaml"}, nil
if name == filepath.Join("/test/config", "kustomize", "values.yaml") {
return &mockFileInfo{name: "values.yaml"}, nil
}
return nil, os.ErrNotExist
}

// And mock file read for centralized values
handler.shims.ReadFile = func(name string) ([]byte, error) {
if name == filepath.Join("/test/config", "kustomize", "config.yaml") {
if name == filepath.Join("/test/config", "kustomize", "values.yaml") {
return []byte(`common:
domain: example.com
ingress:
Expand Down Expand Up @@ -4058,17 +4058,17 @@ func TestBaseBlueprintHandler_toFluxKustomization_WithValuesConfigMaps(t *testin
return "/test/config", nil
}

// And mock that global config.yaml exists
// And mock that global values.yaml exists
handler.shims.Stat = func(name string) (os.FileInfo, error) {
if name == filepath.Join("/test/config", "kustomize", "config.yaml") {
return &mockFileInfo{name: "config.yaml"}, nil
if name == filepath.Join("/test/config", "kustomize", "values.yaml") {
return &mockFileInfo{name: "values.yaml"}, nil
}
return nil, os.ErrNotExist
}

// And mock the config.yaml content with ingress component
// And mock the values.yaml content with ingress component
handler.shims.ReadFile = func(name string) ([]byte, error) {
if name == filepath.Join("/test/config", "kustomize", "config.yaml") {
if name == filepath.Join("/test/config", "kustomize", "values.yaml") {
return []byte(`ingress:
key: value`), nil
}
Expand Down Expand Up @@ -4142,10 +4142,10 @@ func TestBaseBlueprintHandler_toFluxKustomization_WithValuesConfigMaps(t *testin
return "/test/config", nil
}

// And mock that global config.yaml exists
// And mock that global values.yaml exists
handler.shims.Stat = func(name string) (os.FileInfo, error) {
if name == filepath.Join("/test/config", "kustomize", "config.yaml") {
return &mockFileInfo{name: "config.yaml"}, nil
if name == filepath.Join("/test/config", "kustomize", "values.yaml") {
return &mockFileInfo{name: "values.yaml"}, nil
}
return nil, os.ErrNotExist
}
Expand Down
8 changes: 0 additions & 8 deletions pkg/pipelines/env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,6 @@ type EnvMocks struct {
Shims *Shims
}

func setupEnvShims(t *testing.T) *Shims {
t.Helper()
shims := setupShims(t)

// Add any env-specific shim overrides here if needed
return shims
}

func setupEnvMocks(t *testing.T, opts ...*SetupOptions) *EnvMocks {
t.Helper()

Expand Down
5 changes: 0 additions & 5 deletions pkg/pipelines/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,6 @@ type ExecMocks struct {
*Mocks
}

func setupExecShims(t *testing.T) *Shims {
t.Helper()
return setupShims(t)
}

func setupExecMocks(t *testing.T, opts ...*SetupOptions) *ExecMocks {
t.Helper()

Expand Down
6 changes: 0 additions & 6 deletions pkg/pipelines/hook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,6 @@ type HookMocks struct {
*Mocks
}

// setupHookShims creates shims for hook pipeline tests
func setupHookShims(t *testing.T) *Shims {
t.Helper()
return setupShims(t)
}

// setupHookMocks creates mocks for hook pipeline tests
func setupHookMocks(t *testing.T, opts ...*SetupOptions) *HookMocks {
t.Helper()
Expand Down
81 changes: 0 additions & 81 deletions pkg/pipelines/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"github.com/windsorcli/cli/pkg/artifact"
"github.com/windsorcli/cli/pkg/blueprint"
"github.com/windsorcli/cli/pkg/config"
"github.com/windsorcli/cli/pkg/constants"
"github.com/windsorcli/cli/pkg/di"
"github.com/windsorcli/cli/pkg/env"
"github.com/windsorcli/cli/pkg/generators"
Expand Down Expand Up @@ -307,20 +306,6 @@ func (p *InitPipeline) Execute(ctx context.Context) error {
// Private Methods
// =============================================================================

// determineContextName selects the context name from ctx, config, or defaults to "local" if unset or "local".
func (p *InitPipeline) determineContextName(ctx context.Context) string {
if contextName := ctx.Value("contextName"); contextName != nil {
if name, ok := contextName.(string); ok {
return name
}
}
currentContext := p.configHandler.GetContext()
if currentContext != "" && currentContext != "local" {
return currentContext
}
return "local"
}

// setDefaultConfiguration sets default config values based on provider and VM driver detection.
// For local providers, uses config.DefaultConfig_Localhost if VM driver is "docker-desktop",
// else uses config.DefaultConfig_Full. For non-local, uses config.DefaultConfig.
Expand Down Expand Up @@ -419,72 +404,6 @@ func (p *InitPipeline) processPlatformConfiguration(_ context.Context) error {
return nil
}

// prepareTemplateData determines and loads template data for initialization based on blueprint context, artifact builder, and blueprint handler state.
// It prioritizes blueprint context value, then local blueprint handler data, then the default blueprint artifact, and finally the default template data for the current context.
// Returns a map of template file names to their byte content, or an error if any retrieval or parsing operation fails.
func (p *InitPipeline) prepareTemplateData(ctx context.Context) (map[string][]byte, error) {
var blueprintValue string
if blueprintCtx := ctx.Value("blueprint"); blueprintCtx != nil {
if blueprint, ok := blueprintCtx.(string); ok {
blueprintValue = blueprint
}
}

if blueprintValue != "" {
if p.artifactBuilder != nil {
ociInfo, err := artifact.ParseOCIReference(blueprintValue)
if err != nil {
return nil, fmt.Errorf("failed to parse blueprint reference: %w", err)
}
if ociInfo == nil {
return nil, fmt.Errorf("invalid blueprint reference: %s", blueprintValue)
}
templateData, err := p.artifactBuilder.GetTemplateData(ociInfo.URL)
if err != nil {
return nil, fmt.Errorf("failed to get template data from blueprint: %w", err)
}
return templateData, nil
}
}

if p.blueprintHandler != nil {
// Load all template data
blueprintTemplateData, err := p.blueprintHandler.GetLocalTemplateData()
if err != nil {
return nil, fmt.Errorf("failed to get local template data: %w", err)
}

if len(blueprintTemplateData) > 0 {
return blueprintTemplateData, nil
}
}

if p.artifactBuilder != nil {
effectiveBlueprintURL := constants.GetEffectiveBlueprintURL()
ociInfo, err := artifact.ParseOCIReference(effectiveBlueprintURL)
if err != nil {
return nil, fmt.Errorf("failed to parse default blueprint reference: %w", err)
}
templateData, err := p.artifactBuilder.GetTemplateData(ociInfo.URL)
if err != nil {
return nil, fmt.Errorf("failed to get template data from default blueprint: %w", err)
}
p.fallbackBlueprintURL = effectiveBlueprintURL
return templateData, nil
}

if p.blueprintHandler != nil {
contextName := p.determineContextName(ctx)
defaultTemplateData, err := p.blueprintHandler.GetDefaultTemplateData(contextName)
if err != nil {
return nil, fmt.Errorf("failed to get default template data: %w", err)
}
return defaultTemplateData, nil
}

return make(map[string][]byte), nil
}

// processTemplateData renders and processes template data for the InitPipeline.
// Renders all templates using the template renderer, and loads blueprint data from the rendered output if present.
// Returns the rendered template data map or an error if rendering or blueprint loading fails.
Expand Down
Loading
Loading