# Configuration Loader for Provisioning System # Implements hierarchical configuration loading with variable interpolation use std log # Main configuration loader - loads and merges all config sources export def load-provisioning-config [ --debug = false # Enable debug logging --validate = true # Validate configuration --environment: string # Override environment (dev/prod/test) --skip-env-detection = false # Skip automatic environment detection ] { if $debug { # log debug "Loading provisioning configuration..." } # Detect current environment if not specified let current_environment = if ($environment | is-not-empty) { $environment } else if not $skip_env_detection { detect-current-environment } else { "" } if $debug and ($current_environment | is-not-empty) { # log debug $"Using environment: ($current_environment)" } # Define configuration sources in precedence order (lowest to highest) let config_sources = [ # 1. System defaults (lowest precedence) { name: "defaults" path: (get-defaults-config-path) required: true } # 2. User configuration { name: "user" path: ($env.HOME | path join ".config" | path join "provisioning" | path join "config.toml") required: false } # 3. Environment-specific user config { name: "user-env" path: ($env.HOME | path join ".config" | path join "provisioning" | path join $"config.($current_environment).toml") required: false } # 4. Project configuration { name: "project" path: ($env.PWD | path join "provisioning.toml") required: false } # 5. Environment-specific project config { name: "project-env" path: ($env.PWD | path join $"config.($current_environment).toml") required: false } # 6. Infrastructure-specific configuration (highest precedence) { name: "infra" path: ($env.PWD | path join ".provisioning.toml") required: false } ] mut final_config = {} # Load and merge configurations for source in $config_sources { let config_data = (load-config-file $source.path $source.required $debug) if ($config_data | is-not-empty) { if $debug { # log debug $"Loaded ($source.name) config from ($source.path)" } $final_config = (deep-merge $final_config $config_data) } } # Apply environment-specific overrides from environments section if ($current_environment | is-not-empty) { let env_config = ($final_config | get -o $"environments.($current_environment)" | default {}) if ($env_config | is-not-empty) { if $debug { # log debug $"Applying environment overrides for: ($current_environment)" } $final_config = (deep-merge $final_config $env_config) } } # Apply environment variables as final overrides $final_config = (apply-environment-variable-overrides $final_config $debug) # Store current environment in config for reference if ($current_environment | is-not-empty) { $final_config = ($final_config | upsert "current_environment" $current_environment) } # Interpolate variables in the final configuration $final_config = (interpolate-config $final_config) # Validate configuration if requested if $validate { let validation_result = (validate-config $final_config --detailed false --strict false) # The validate-config function will throw an error if validation fails when not in detailed mode } if $debug { # log debug "Configuration loading completed" } $final_config } # Load a single configuration file export def load-config-file [ file_path: string required = false debug = false ] { if not ($file_path | path exists) { if $required { error make { msg: $"Required configuration file not found: ($file_path)" } } else { if $debug { # log debug $"Optional config file not found: ($file_path)" } return {} } } if $debug { # log debug $"Loading config file: ($file_path)" } # Load the file - use error handling without complex try-catch if ($file_path | path exists) { open $file_path } else { if $required { error make { msg: $"Configuration file not found: ($file_path)" } } else { {} } } } # Deep merge two configuration records (right takes precedence) export def deep-merge [ base: record override: record ] { mut result = $base for key in ($override | columns) { let override_value = ($override | get $key) let base_value = ($base | get -o $key) if ($base_value | is-empty) { # Key doesn't exist in base, add it $result = ($result | insert $key $override_value) } else if (($base_value | describe) == "record") and (($override_value | describe) == "record") { # Both are records, merge recursively $result = ($result | upsert $key (deep-merge $base_value $override_value)) } else { # Override the value $result = ($result | upsert $key $override_value) } } $result } # Interpolate variables in configuration values export def interpolate-config [ config: record ] { mut result = $config # Get base path for interpolation let base_path = ($config | get -o paths.base | default "") if ($base_path | is-not-empty) { # Interpolate the entire config structure $result = (interpolate-all-paths $result $base_path) } $result } # Interpolate variables in a string using ${path.to.value} syntax export def interpolate-string [ text: string config: record ] { mut result = $text # Simple interpolation for {{paths.base}} pattern if ($result | str contains "{{paths.base}}") { let base_path = (get-config-value $config "paths.base" "") $result = ($result | str replace --all "{{paths.base}}" $base_path) } # Add more interpolation patterns as needed # This is a basic implementation - a full template engine would be more robust $result } # Get a nested configuration value using dot notation export def get-config-value [ config: record path: string default_value: any = null ] { let path_parts = ($path | split row ".") mut current = $config for part in $path_parts { let next_value = ($current | get -o $part) if ($next_value | is-empty) { return $default_value } $current = $next_value } $current } # Validate configuration structure - checks required sections exist export def validate-config-structure [ config: record ] { let required_sections = ["core", "paths", "debug", "sops"] mut errors = [] mut warnings = [] for section in $required_sections { if ($config | get -o $section | is-empty) { $errors = ($errors | append { type: "missing_section", severity: "error", section: $section, message: $"Missing required configuration section: ($section)" }) } } { valid: (($errors | length) == 0), errors: $errors, warnings: $warnings } } # Validate path values - checks paths exist and are absolute export def validate-path-values [ config: record ] { let required_paths = ["base", "providers", "taskservs", "clusters"] mut errors = [] mut warnings = [] let paths = ($config | get -o paths | default {}) for path_name in $required_paths { let path_value = ($paths | get -o $path_name) if ($path_value | is-empty) { $errors = ($errors | append { type: "missing_path", severity: "error", path: $path_name, message: $"Missing required path: paths.($path_name)" }) } else { # Check if path is absolute if not ($path_value | str starts-with "/") { $warnings = ($warnings | append { type: "relative_path", severity: "warning", path: $path_name, value: $path_value, message: $"Path paths.($path_name) should be absolute, got: ($path_value)" }) } # Check if base path exists (critical for system operation) if $path_name == "base" { if not ($path_value | path exists) { $errors = ($errors | append { type: "path_not_exists", severity: "error", path: $path_name, value: $path_value, message: $"Base path does not exist: ($path_value)" }) } } } } { valid: (($errors | length) == 0), errors: $errors, warnings: $warnings } } # Validate data types - checks configuration values have correct types export def validate-data-types [ config: record ] { mut errors = [] mut warnings = [] # Validate core.version follows semantic versioning pattern let core_version = ($config | get -o core.version) if ($core_version | is-not-empty) { let version_pattern = "^\\d+\\.\\d+\\.\\d+(-.+)?$" let version_parts = ($core_version | split row ".") if (($version_parts | length) < 3) { $errors = ($errors | append { type: "invalid_version", severity: "error", field: "core.version", value: $core_version, message: $"core.version must follow semantic versioning format, got: ($core_version)" }) } } # Validate debug.enabled is boolean let debug_enabled = ($config | get -o debug.enabled) if ($debug_enabled | is-not-empty) { if (($debug_enabled | describe) != "bool") { $errors = ($errors | append { type: "invalid_type", severity: "error", field: "debug.enabled", value: $debug_enabled, expected: "bool", actual: ($debug_enabled | describe), message: $"debug.enabled must be boolean, got: ($debug_enabled | describe)" }) } } # Validate debug.metadata is boolean let debug_metadata = ($config | get -o debug.metadata) if ($debug_metadata | is-not-empty) { if (($debug_metadata | describe) != "bool") { $errors = ($errors | append { type: "invalid_type", severity: "error", field: "debug.metadata", value: $debug_metadata, expected: "bool", actual: ($debug_metadata | describe), message: $"debug.metadata must be boolean, got: ($debug_metadata | describe)" }) } } # Validate sops.use_sops is boolean let sops_use = ($config | get -o sops.use_sops) if ($sops_use | is-not-empty) { if (($sops_use | describe) != "bool") { $errors = ($errors | append { type: "invalid_type", severity: "error", field: "sops.use_sops", value: $sops_use, expected: "bool", actual: ($sops_use | describe), message: $"sops.use_sops must be boolean, got: ($sops_use | describe)" }) } } { valid: (($errors | length) == 0), errors: $errors, warnings: $warnings } } # Validate semantic rules - business logic validation export def validate-semantic-rules [ config: record ] { mut errors = [] mut warnings = [] # Validate provider configuration let providers = ($config | get -o providers | default {}) let default_provider = ($providers | get -o default) if ($default_provider | is-not-empty) { let valid_providers = ["aws", "upcloud", "local"] if not ($default_provider in $valid_providers) { $errors = ($errors | append { type: "invalid_provider", severity: "error", field: "providers.default", value: $default_provider, valid_options: $valid_providers, message: $"Invalid default provider: ($default_provider). Valid options: ($valid_providers | str join ', ')" }) } } # Validate log level let log_level = ($config | get -o debug.log_level) if ($log_level | is-not-empty) { let valid_levels = ["trace", "debug", "info", "warn", "error"] if not ($log_level in $valid_levels) { $warnings = ($warnings | append { type: "invalid_log_level", severity: "warning", field: "debug.log_level", value: $log_level, valid_options: $valid_levels, message: $"Invalid log level: ($log_level). Valid options: ($valid_levels | str join ', ')" }) } } # Validate output format let output_format = ($config | get -o output.format) if ($output_format | is-not-empty) { let valid_formats = ["json", "yaml", "toml", "text"] if not ($output_format in $valid_formats) { $warnings = ($warnings | append { type: "invalid_output_format", severity: "warning", field: "output.format", value: $output_format, valid_options: $valid_formats, message: $"Invalid output format: ($output_format). Valid options: ($valid_formats | str join ', ')" }) } } { valid: (($errors | length) == 0), errors: $errors, warnings: $warnings } } # Validate file existence - checks referenced files exist export def validate-file-existence [ config: record ] { mut errors = [] mut warnings = [] # Check SOPS configuration file let sops_config = ($config | get -o sops.config_path) if ($sops_config | is-not-empty) { if not ($sops_config | path exists) { $warnings = ($warnings | append { type: "missing_sops_config", severity: "warning", field: "sops.config_path", value: $sops_config, message: $"SOPS config file not found: ($sops_config)" }) } } # Check SOPS key files let key_paths = ($config | get -o sops.key_search_paths | default []) mut found_key = false for key_path in $key_paths { let expanded_path = ($key_path | str replace "~" $env.HOME) if ($expanded_path | path exists) { $found_key = true break } } if not $found_key and ($key_paths | length) > 0 { $warnings = ($warnings | append { type: "missing_sops_keys", severity: "warning", field: "sops.key_search_paths", value: $key_paths, message: $"No SOPS key files found in search paths: ($key_paths | str join ', ')" }) } # Check critical configuration files let settings_file = ($config | get -o paths.files.settings) if ($settings_file | is-not-empty) { if not ($settings_file | path exists) { $errors = ($errors | append { type: "missing_settings_file", severity: "error", field: "paths.files.settings", value: $settings_file, message: $"Settings file not found: ($settings_file)" }) } } { valid: (($errors | length) == 0), errors: $errors, warnings: $warnings } } # Enhanced main validation function export def validate-config [ config: record --detailed = false # Show detailed validation results --strict = false # Treat warnings as errors ] { # Run all validation checks let structure_result = (validate-config-structure $config) let paths_result = (validate-path-values $config) let types_result = (validate-data-types $config) let semantic_result = (validate-semantic-rules $config) let files_result = (validate-file-existence $config) # Combine all results let all_errors = ( $structure_result.errors | append $paths_result.errors | append $types_result.errors | append $semantic_result.errors | append $files_result.errors ) let all_warnings = ( $structure_result.warnings | append $paths_result.warnings | append $types_result.warnings | append $semantic_result.warnings | append $files_result.warnings ) let has_errors = ($all_errors | length) > 0 let has_warnings = ($all_warnings | length) > 0 # In strict mode, treat warnings as errors let final_valid = if $strict { not $has_errors and not $has_warnings } else { not $has_errors } # Throw error if validation fails and not in detailed mode if not $detailed and not $final_valid { let error_messages = ($all_errors | each { |err| $err.message }) let warning_messages = if $strict { ($all_warnings | each { |warn| $warn.message }) } else { [] } let combined_messages = ($error_messages | append $warning_messages) error make { msg: ($combined_messages | str join "; ") } } # Return detailed results { valid: $final_valid, errors: $all_errors, warnings: $all_warnings, summary: { total_errors: ($all_errors | length), total_warnings: ($all_warnings | length), checks_run: 5, structure_valid: $structure_result.valid, paths_valid: $paths_result.valid, types_valid: $types_result.valid, semantic_valid: $semantic_result.valid, files_valid: $files_result.valid } } } # Helper function to create directory structure for user config export def init-user-config [ --template: string = "user" # Template type: user, dev, prod, test --force = false # Overwrite existing config ] { let config_dir = ($env.HOME | path join ".config" | path join "provisioning") if not ($config_dir | path exists) { mkdir $config_dir print $"Created user config directory: ($config_dir)" } let user_config_path = ($config_dir | path join "config.toml") # Determine template file based on template parameter let template_file = match $template { "user" => "config.user.toml.example" "dev" => "config.dev.toml.example" "prod" => "config.prod.toml.example" "test" => "config.test.toml.example" _ => { print $"❌ Unknown template: ($template). Valid options: user, dev, prod, test" return } } # Find the template file in the project let project_root = (get-project-root) let template_path = ($project_root | path join $template_file) if not ($template_path | path exists) { print $"❌ Template file not found: ($template_path)" print "Available templates should be in the project root directory" return } # Check if config already exists if ($user_config_path | path exists) and not $force { print $"⚠️ User config already exists: ($user_config_path)" print "Use --force to overwrite or choose a different template" print $"Current template: ($template)" return } # Copy template to user config cp $template_path $user_config_path print $"✅ Created user config from ($template) template: ($user_config_path)" print "" print "📝 Next steps:" print $" 1. Edit the config file: ($user_config_path)" print " 2. Update paths.base to point to your provisioning installation" print " 3. Configure your preferred providers and settings" print " 4. Test the configuration: ./core/nulib/provisioning validate config" print "" print $"💡 Template used: ($template_file)" # Show template-specific guidance match $template { "dev" => { print "🔧 Development template configured with:" print " • Enhanced debugging enabled" print " • Local provider as default" print " • JSON output format" print " • Check mode enabled by default" } "prod" => { print "🏭 Production template configured with:" print " • Minimal logging for security" print " • AWS provider as default" print " • Strict validation enabled" print " • Backup and monitoring settings" } "test" => { print "🧪 Testing template configured with:" print " • Mock providers and safe defaults" print " • Test isolation settings" print " • CI/CD friendly configurations" print " • Automatic cleanup enabled" } _ => { print "👤 User template configured with:" print " • Balanced settings for general use" print " • Comprehensive documentation" print " • Safe defaults for all scenarios" } } } # Helper function to get project root directory def get-project-root [] { # Try to find project root by looking for key files let potential_roots = [ $env.PWD ($env.PWD | path dirname) ($env.PWD | path dirname | path dirname) ($env.PWD | path dirname | path dirname | path dirname) ($env.PWD | path dirname | path dirname | path dirname | path dirname) ] for root in $potential_roots { # Check for provisioning project indicators if (($root | path join "config.defaults.toml" | path exists) or ($root | path join "kcl.mod" | path exists) or ($root | path join "core" "nulib" "provisioning" | path exists)) { return $root } } # Fallback to current directory $env.PWD } # Enhanced interpolation function with comprehensive pattern support def interpolate-all-paths [ config: record base_path: string ] { # Convert to JSON for efficient string processing let json_str = ($config | to json) # Start with existing pattern mut interpolated_json = ($json_str | str replace --all "{{paths.base}}" $base_path) # Apply enhanced interpolation patterns $interpolated_json = (apply-enhanced-interpolation $interpolated_json $config) # Convert back to record ($interpolated_json | from json) } # Apply enhanced interpolation patterns with security validation def apply-enhanced-interpolation [ json_str: string config: record ] { mut result = $json_str # Environment variable interpolation with security checks $result = (interpolate-env-variables $result) # Date and time interpolation $result = (interpolate-datetime $result) # Git information interpolation $result = (interpolate-git-info $result) # SOPS configuration interpolation $result = (interpolate-sops-config $result $config) # Cross-section provider references $result = (interpolate-provider-refs $result $config) # Advanced features: conditionals and functions $result = (interpolate-advanced-features $result $config) $result } # Interpolate environment variables with security validation def interpolate-env-variables [ text: string ] { mut result = $text # Safe environment variables list (security) let safe_env_vars = [ "HOME" "USER" "HOSTNAME" "PWD" "SHELL" "PROVISIONING" "PROVISIONING_KLOUD_PATH" "PROVISIONING_INFRA_PATH" "PROVISIONING_SOPS" "PROVISIONING_KAGE" ] for env_var in $safe_env_vars { let pattern = $"\\{\\{env\\.($env_var)\\}\\}" let env_value = ($env | get -o $env_var | default "") if ($env_value | is-not-empty) { $result = ($result | str replace --regex $pattern $env_value) } } # Handle conditional environment variables like {{env.HOME || "/tmp"}} $result = (interpolate-conditional-env $result) $result } # Handle conditional environment variable interpolation def interpolate-conditional-env [ text: string ] { mut result = $text # For now, implement basic conditional logic for common patterns if ($result | str contains "{{env.HOME || \"/tmp\"}}") { let home_value = ($env.HOME? | default "/tmp") $result = ($result | str replace --all "{{env.HOME || \"/tmp\"}}" $home_value) } if ($result | str contains "{{env.USER || \"unknown\"}}") { let user_value = ($env.USER? | default "unknown") $result = ($result | str replace --all "{{env.USER || \"unknown\"}}" $user_value) } $result } # Interpolate date and time values def interpolate-datetime [ text: string ] { mut result = $text # Current date in YYYY-MM-DD format let current_date = (date now | format date "%Y-%m-%d") $result = ($result | str replace --all "{{now.date}}" $current_date) # Current timestamp (Unix timestamp) let current_timestamp = (date now | format date "%s") $result = ($result | str replace --all "{{now.timestamp}}" $current_timestamp) # ISO 8601 timestamp let iso_timestamp = (date now | format date "%Y-%m-%dT%H:%M:%SZ") $result = ($result | str replace --all "{{now.iso}}" $iso_timestamp) $result } # Interpolate git information def interpolate-git-info [ text: string ] { mut result = $text # Get git branch (safely) let git_branch = ( try { git branch --show-current } catch { "unknown" } ) $result = ($result | str replace --all "{{git.branch}}" $git_branch) # Get git commit hash (safely) let git_commit = ( try { git rev-parse --short HEAD } catch { "unknown" } ) $result = ($result | str replace --all "{{git.commit}}" $git_commit) # Get git remote origin URL (safely) let git_origin = ( try { git remote get-url origin } catch { "unknown" } ) $result = ($result | str replace --all "{{git.origin}}" $git_origin) $result } # Interpolate SOPS configuration references def interpolate-sops-config [ text: string config: record ] { mut result = $text # SOPS key file path let sops_key_file = ($config | get -o sops.age_key_file | default "") if ($sops_key_file | is-not-empty) { $result = ($result | str replace --all "{{sops.key_file}}" $sops_key_file) } # SOPS config path let sops_config_path = ($config | get -o sops.config_path | default "") if ($sops_config_path | is-not-empty) { $result = ($result | str replace --all "{{sops.config_path}}" $sops_config_path) } $result } # Interpolate cross-section provider references def interpolate-provider-refs [ text: string config: record ] { mut result = $text # AWS provider region let aws_region = ($config | get -o providers.aws.region | default "") if ($aws_region | is-not-empty) { $result = ($result | str replace --all "{{providers.aws.region}}" $aws_region) } # Default provider let default_provider = ($config | get -o providers.default | default "") if ($default_provider | is-not-empty) { $result = ($result | str replace --all "{{providers.default}}" $default_provider) } # UpCloud zone let upcloud_zone = ($config | get -o providers.upcloud.zone | default "") if ($upcloud_zone | is-not-empty) { $result = ($result | str replace --all "{{providers.upcloud.zone}}" $upcloud_zone) } $result } # Interpolate advanced features (function calls, environment-aware paths) def interpolate-advanced-features [ text: string config: record ] { mut result = $text # Function call: {{path.join(paths.base, "custom")}} if ($result | str contains "{{path.join(paths.base") { let base_path = ($config | get -o paths.base | default "") # Simple implementation for path.join with base path $result = ($result | str replace --regex "\\{\\{path\\.join\\(paths\\.base,\\s*\"([^\"]+)\"\\)\\}\\}" $"($base_path)/$1") } # Environment-aware paths: {{paths.base.${env}}} let current_env = ($config | get -o current_environment | default "dev") $result = ($result | str replace --all "{{paths.base.${env}}}" $"{{paths.base}}.($current_env)") $result } # Validate interpolation patterns and detect potential issues export def validate-interpolation [ config: record --detailed = false # Show detailed validation results ] { mut errors = [] mut warnings = [] # Convert config to JSON for pattern detection let json_str = ($config | to json) # Check for unresolved interpolation patterns let unresolved_patterns = (detect-unresolved-patterns $json_str) if ($unresolved_patterns | length) > 0 { $errors = ($errors | append { type: "unresolved_interpolation" severity: "error" patterns: $unresolved_patterns message: $"Unresolved interpolation patterns found: ($unresolved_patterns | str join ', ')" }) } # Check for circular dependencies let circular_deps = (detect-circular-dependencies $json_str) if ($circular_deps | length) > 0 { $errors = ($errors | append { type: "circular_dependency" severity: "error" dependencies: $circular_deps message: $"Circular interpolation dependencies detected: ($circular_deps | str join ', ')" }) } # Check for unsafe environment variable access let unsafe_env_vars = (detect-unsafe-env-patterns $json_str) if ($unsafe_env_vars | length) > 0 { $warnings = ($warnings | append { type: "unsafe_env_access" severity: "warning" variables: $unsafe_env_vars message: $"Potentially unsafe environment variable access: ($unsafe_env_vars | str join ', ')" }) } # Validate git repository context let git_validation = (validate-git-context $json_str) if not $git_validation.valid { $warnings = ($warnings | append { type: "git_context" severity: "warning" message: $git_validation.message }) } let has_errors = ($errors | length) > 0 let has_warnings = ($warnings | length) > 0 if not $detailed and $has_errors { let error_messages = ($errors | each { |err| $err.message }) error make { msg: ($error_messages | str join "; ") } } { valid: (not $has_errors), errors: $errors, warnings: $warnings, summary: { total_errors: ($errors | length), total_warnings: ($warnings | length), interpolation_patterns_detected: (count-interpolation-patterns $json_str) } } } # Detect unresolved interpolation patterns def detect-unresolved-patterns [ text: string ] { # Find patterns that look like interpolation but might not be handled let unknown_patterns = ($text | str replace --regex "\\{\\{([^}]+)\\}\\}" "") # Known patterns that should be resolved let known_patterns = [ "paths.base" "env\\." "now\\." "git\\." "sops\\." "providers\\." "path\\.join" ] mut unresolved = [] # Check for patterns that don't match known types let all_matches = ($text | str replace --regex "\\{\\{([^}]+)\\}\\}" "$1") if ($all_matches | str contains "{{") { # Basic detection - in a real implementation, this would be more sophisticated let potential_unknown = ($text | str replace --regex "\\{\\{(\\w+\\.\\w+)\\}\\}" "") if ($text | str contains "{{unknown.") { $unresolved = ($unresolved | append "unknown.*") } } $unresolved } # Detect circular interpolation dependencies def detect-circular-dependencies [ text: string ] { mut circular_deps = [] # Simple detection for self-referencing patterns if (($text | str contains "{{paths.base}}") and ($text | str contains "paths.base.*{{paths.base}}")) { $circular_deps = ($circular_deps | append "paths.base -> paths.base") } $circular_deps } # Detect unsafe environment variable patterns def detect-unsafe-env-patterns [ text: string ] { mut unsafe_vars = [] # Patterns that might be dangerous let dangerous_patterns = ["PATH" "LD_LIBRARY_PATH" "PYTHONPATH" "SHELL" "PS1"] for pattern in $dangerous_patterns { if ($text | str contains $"{{env.($pattern)}}") { $unsafe_vars = ($unsafe_vars | append $pattern) } } $unsafe_vars } # Validate git repository context for git interpolations def validate-git-context [ text: string ] { if ($text | str contains "{{git.") { # Check if we're in a git repository let is_git_repo = ( try { git rev-parse --git-dir | complete | get exit_code } catch { 1 } ) == 0 if not $is_git_repo { return { valid: false message: "Git interpolation patterns detected but not in a git repository" } } } { valid: true, message: "" } } # Count interpolation patterns for metrics def count-interpolation-patterns [ text: string ] { # Count all {{...}} patterns by finding matches # Simple approximation: count occurrences of "{{" let pattern_count = ($text | str replace --all "{{" "\n{{" | lines | where ($it | str contains "{{") | length) $pattern_count } # Test interpolation with sample data export def test-interpolation [ --sample: string = "basic" # Sample test data: basic, advanced, all ] { print "🧪 Testing Enhanced Interpolation System" print "" # Define test configurations based on sample type let test_config = match $sample { "basic" => { paths: { base: "/usr/local/provisioning" } test_patterns: { simple_path: "{{paths.base}}/config" env_home: "{{env.HOME}}/configs" current_date: "backup-{{now.date}}" } } "advanced" => { paths: { base: "/usr/local/provisioning" } providers: { aws: { region: "us-west-2" }, default: "aws" } sops: { key_file: "{{env.HOME}}/.age/key.txt" } test_patterns: { complex_path: "{{path.join(paths.base, \"custom\")}}" provider_ref: "Region: {{providers.aws.region}}" git_info: "Build: {{git.branch}}-{{git.commit}}" conditional: "{{env.HOME || \"/tmp\"}}/cache" } } _ => { paths: { base: "/usr/local/provisioning" } providers: { aws: { region: "us-west-2" }, default: "aws" } sops: { key_file: "{{env.HOME}}/.age/key.txt", config_path: "/etc/sops.yaml" } current_environment: "test" test_patterns: { all_patterns: "{{paths.base}}/{{env.USER}}/{{now.date}}/{{git.branch}}/{{providers.default}}" function_call: "{{path.join(paths.base, \"providers\")}}" sops_refs: "Key: {{sops.key_file}}, Config: {{sops.config_path}}" datetime: "{{now.date}} at {{now.timestamp}}" } } } # Test interpolation print $"Testing with ($sample) sample configuration..." print "" let base_path = "/usr/local/provisioning" let interpolated_config = (interpolate-all-paths $test_config $base_path) # Show results print "📋 Original patterns:" for key in ($test_config.test_patterns | columns) { let original = ($test_config.test_patterns | get $key) print $" ($key): ($original)" } print "" print "✨ Interpolated results:" for key in ($interpolated_config.test_patterns | columns) { let interpolated = ($interpolated_config.test_patterns | get $key) print $" ($key): ($interpolated)" } print "" # Validate interpolation let validation = (validate-interpolation $test_config --detailed true) if $validation.valid { print "✅ Interpolation validation passed" } else { print "❌ Interpolation validation failed:" for error in $validation.errors { print $" Error: ($error.message)" } } if ($validation.warnings | length) > 0 { print "⚠️ Warnings:" for warning in $validation.warnings { print $" Warning: ($warning.message)" } } print "" print $"📊 Summary: ($validation.summary.interpolation_patterns_detected) interpolation patterns processed" $interpolated_config } # Security-hardened interpolation with input validation export def secure-interpolation [ config: record --allow-unsafe = false # Allow potentially unsafe patterns --max-depth = 5 # Maximum interpolation depth ] { # Security checks before interpolation let security_validation = (validate-interpolation-security $config $allow_unsafe) if not $security_validation.valid { error make { msg: $"Security validation failed: ($security_validation.errors | str join '; ')" } } # Apply interpolation with depth limiting let base_path = ($config | get -o paths.base | default "") if ($base_path | is-not-empty) { interpolate-with-depth-limit $config $base_path $max_depth } else { $config } } # Validate interpolation security def validate-interpolation-security [ config: record allow_unsafe: bool ] { mut errors = [] let json_str = ($config | to json) # Check for code injection patterns let dangerous_patterns = [ "\\$\\(" "\\`" "\\;" "\\|\\|" "\\&&" "rm " "sudo " "eval " "exec " ] for pattern in $dangerous_patterns { if ($json_str =~ $pattern) { $errors = ($errors | append $"Potential code injection pattern detected: ($pattern)") } } # Check for unsafe environment variable access if not $allow_unsafe { let unsafe_env_vars = ["PATH" "LD_LIBRARY_PATH" "PYTHONPATH" "PS1" "PROMPT_COMMAND"] for var in $unsafe_env_vars { if ($json_str | str contains $"{{env.($var)}}") { $errors = ($errors | append $"Unsafe environment variable access: ($var)") } } } # Check for path traversal attempts if (($json_str | str contains "../") or ($json_str | str contains "..\\")) { $errors = ($errors | append "Path traversal attempt detected") } { valid: (($errors | length) == 0) errors: $errors } } # Interpolate with depth limiting to prevent infinite recursion def interpolate-with-depth-limit [ config: record base_path: string max_depth: int ] { mut result = $config mut current_depth = 0 # Track interpolation patterns to detect loops mut seen_patterns = [] while $current_depth < $max_depth { let pre_interpolation = ($result | to json) $result = (interpolate-all-paths $result $base_path) let post_interpolation = ($result | to json) # If no changes, we're done if $pre_interpolation == $post_interpolation { break } # Check for circular dependencies if ($post_interpolation in $seen_patterns) { error make { msg: $"Circular interpolation dependency detected at depth ($current_depth)" } } $seen_patterns = ($seen_patterns | append $post_interpolation) $current_depth = ($current_depth + 1) } if $current_depth >= $max_depth { error make { msg: $"Maximum interpolation depth ($max_depth) exceeded - possible infinite recursion" } } $result } # Create comprehensive interpolation test suite export def create-interpolation-test-suite [ --output-file: string = "interpolation_test_results.json" ] { print "🧪 Creating Comprehensive Interpolation Test Suite" print "==================================================" print "" mut test_results = [] # Test 1: Basic patterns print "🔍 Test 1: Basic Interpolation Patterns" let basic_test = (run-interpolation-test "basic") $test_results = ($test_results | append { test_name: "basic_patterns" passed: $basic_test.passed details: $basic_test.details timestamp: (date now | format date "%Y-%m-%d %H:%M:%S") }) # Test 2: Environment variables print "🔍 Test 2: Environment Variable Interpolation" let env_test = (run-interpolation-test "environment") $test_results = ($test_results | append { test_name: "environment_variables" passed: $env_test.passed details: $env_test.details timestamp: (date now | format date "%Y-%m-%d %H:%M:%S") }) # Test 3: Security validation print "🔍 Test 3: Security Validation" let security_test = (run-security-test) $test_results = ($test_results | append { test_name: "security_validation" passed: $security_test.passed details: $security_test.details timestamp: (date now | format date "%Y-%m-%d %H:%M:%S") }) # Test 4: Advanced patterns print "🔍 Test 4: Advanced Interpolation Features" let advanced_test = (run-interpolation-test "advanced") $test_results = ($test_results | append { test_name: "advanced_patterns" passed: $advanced_test.passed details: $advanced_test.details timestamp: (date now | format date "%Y-%m-%d %H:%M:%S") }) # Save results $test_results | to json | save --force $output_file # Summary let total_tests = ($test_results | length) let passed_tests = ($test_results | where passed == true | length) let failed_tests = ($total_tests - $passed_tests) print "" print "📊 Test Suite Summary" print "====================" print $" Total tests: ($total_tests)" print $" Passed: ($passed_tests)" print $" Failed: ($failed_tests)" print "" if $failed_tests == 0 { print "✅ All interpolation tests passed!" } else { print "❌ Some interpolation tests failed!" print "" print "Failed tests:" for test in ($test_results | where passed == false) { print $" • ($test.test_name): ($test.details.error)" } } print "" print $"📄 Detailed results saved to: ($output_file)" { total: $total_tests passed: $passed_tests failed: $failed_tests success_rate: (($passed_tests * 100) / $total_tests) results: $test_results } } # Run individual interpolation test def run-interpolation-test [ test_type: string ] { try { match $test_type { "basic" => { let test_config = { paths: { base: "/test/path" } test_value: "{{paths.base}}/config" } let result = (interpolate-all-paths $test_config "/test/path") let expected = "/test/path/config" let actual = ($result.test_value) if $actual == $expected { { passed: true, details: { expected: $expected, actual: $actual } } } else { { passed: false, details: { expected: $expected, actual: $actual, error: "Value mismatch" } } } } "environment" => { let test_config = { paths: { base: "/test/path" } test_value: "{{env.USER}}/config" } let result = (interpolate-all-paths $test_config "/test/path") let expected_pattern = ".*/config" # USER should be replaced with something if ($result.test_value | str contains "/config") and not ($result.test_value | str contains "{{env.USER}}") { { passed: true, details: { pattern: $expected_pattern, actual: $result.test_value } } } else { { passed: false, details: { pattern: $expected_pattern, actual: $result.test_value, error: "Environment variable not interpolated" } } } } "advanced" => { let test_config = { paths: { base: "/test/path" } current_environment: "test" test_values: { date_test: "backup-{{now.date}}" git_test: "build-{{git.branch}}" } } let result = (interpolate-all-paths $test_config "/test/path") # Check if date was interpolated (should not contain {{now.date}}) let date_ok = not ($result.test_values.date_test | str contains "{{now.date}}") # Check if git was interpolated (should not contain {{git.branch}}) let git_ok = not ($result.test_values.git_test | str contains "{{git.branch}}") if $date_ok and $git_ok { { passed: true, details: { date_result: $result.test_values.date_test, git_result: $result.test_values.git_test } } } else { { passed: false, details: { date_result: $result.test_values.date_test, git_result: $result.test_values.git_test, error: "Advanced patterns not interpolated" } } } } _ => { { passed: false, details: { error: $"Unknown test type: ($test_type)" } } } } } catch { |e| { passed: false, details: { error: $"Test execution failed: ($e.msg)" } } } } # Run security validation test def run-security-test [] { try { # Test 1: Safe configuration should pass let safe_config = { paths: { base: "/safe/path" } test_value: "{{env.HOME}}/config" } let safe_result = (validate-interpolation-security $safe_config false) # Test 2: Unsafe configuration should fail let unsafe_config = { paths: { base: "/unsafe/path" } test_value: "{{env.PATH}}/config" # PATH is considered unsafe } let unsafe_result = (validate-interpolation-security $unsafe_config false) if $safe_result.valid and (not $unsafe_result.valid) { { passed: true, details: { safe_passed: $safe_result.valid, unsafe_blocked: (not $unsafe_result.valid) } } } else { { passed: false, details: { safe_passed: $safe_result.valid, unsafe_blocked: (not $unsafe_result.valid), error: "Security validation not working correctly" } } } } catch { |e| { passed: false, details: { error: $"Security test execution failed: ($e.msg)" } } } } # Environment detection and management functions # Detect current environment from various sources export def detect-current-environment [] { # Priority order for environment detection: # 1. PROVISIONING_ENV environment variable # 2. Environment-specific markers # 3. Directory-based detection # 4. Default fallback # Check explicit environment variable if ($env.PROVISIONING_ENV? | is-not-empty) { return $env.PROVISIONING_ENV } # Check CI/CD environments if ($env.CI? | is-not-empty) { if ($env.GITHUB_ACTIONS? | is-not-empty) { return "ci" } if ($env.GITLAB_CI? | is-not-empty) { return "ci" } if ($env.JENKINS_URL? | is-not-empty) { return "ci" } return "test" # Default for CI environments } # Check for development indicators if (($env.PWD | path join ".git" | path exists) or ($env.PWD | path join "development" | path exists) or ($env.PWD | path join "dev" | path exists)) { return "dev" } # Check for production indicators if (($env.HOSTNAME? | default "" | str contains "prod") or ($env.NODE_ENV? | default "" | str downcase) == "production" or ($env.ENVIRONMENT? | default "" | str downcase) == "production") { return "prod" } # Check for test indicators if (($env.NODE_ENV? | default "" | str downcase) == "test" or ($env.ENVIRONMENT? | default "" | str downcase) == "test") { return "test" } # Default to development for interactive usage if ($env.TERM? | is-not-empty) { return "dev" } # Fallback return "dev" } # Get available environments from configuration export def get-available-environments [ config: record ] { let environments_section = ($config | get -o "environments" | default {}) $environments_section | columns } # Validate environment name export def validate-environment [ environment: string config: record ] { let valid_environments = ["dev" "test" "prod" "ci" "staging" "local"] let configured_environments = (get-available-environments $config) let all_valid = ($valid_environments | append $configured_environments | uniq) if ($environment in $all_valid) { { valid: true, message: "" } } else { { valid: false, message: $"Invalid environment '($environment)'. Valid options: ($all_valid | str join ', ')" } } } # Apply environment variable overrides to configuration export def apply-environment-variable-overrides [ config: record debug = false ] { mut result = $config # Map of environment variables to config paths with type conversion let env_mappings = { "PROVISIONING_DEBUG": { path: "debug.enabled", type: "bool" }, "PROVISIONING_LOG_LEVEL": { path: "debug.log_level", type: "string" }, "PROVISIONING_NO_TERMINAL": { path: "debug.no_terminal", type: "bool" }, "PROVISIONING_CHECK": { path: "debug.check", type: "bool" }, "PROVISIONING_METADATA": { path: "debug.metadata", type: "bool" }, "PROVISIONING_OUTPUT_FORMAT": { path: "output.format", type: "string" }, "PROVISIONING_FILE_VIEWER": { path: "output.file_viewer", type: "string" }, "PROVISIONING_USE_SOPS": { path: "sops.use_sops", type: "bool" }, "PROVISIONING_PROVIDER": { path: "providers.default", type: "string" }, "PROVISIONING_KLOUD_PATH": { path: "paths.kloud", type: "string" }, "PROVISIONING_INFRA_PATH": { path: "paths.infra", type: "string" }, "PROVISIONING_SOPS": { path: "sops.config_path", type: "string" }, "PROVISIONING_KAGE": { path: "sops.age_key_file", type: "string" } } for env_var in ($env_mappings | columns) { let env_value = ($env | get -o $env_var) if ($env_value | is-not-empty) { let mapping = ($env_mappings | get $env_var) let config_path = $mapping.path let config_type = $mapping.type # Convert value to appropriate type let converted_value = match $config_type { "bool" => { if ($env_value | describe) == "string" { match ($env_value | str downcase) { "true" | "1" | "yes" | "on" => true "false" | "0" | "no" | "off" => false _ => false } } else { $env_value | into bool } } "string" => $env_value _ => $env_value } if $debug { # log debug $"Applying env override: ($env_var) -> ($config_path) = ($converted_value)" } $result = (set-config-value $result $config_path $converted_value) } } $result } # Set a configuration value using dot notation def set-config-value [ config: record path: string value: any ] { let path_parts = ($path | split row ".") mut result = $config if ($path_parts | length) == 1 { $result | upsert ($path_parts | first) $value } else if ($path_parts | length) == 2 { let section = ($path_parts | first) let key = ($path_parts | last) let section_data = ($result | get -o $section | default {}) $result | upsert $section ($section_data | upsert $key $value) } else if ($path_parts | length) == 3 { let section = ($path_parts | first) let subsection = ($path_parts | get 1) let key = ($path_parts | last) let section_data = ($result | get -o $section | default {}) let subsection_data = ($section_data | get -o $subsection | default {}) $result | upsert $section ($section_data | upsert $subsection ($subsection_data | upsert $key $value)) } else { # For deeper nesting, use recursive approach set-config-value-recursive $result $path_parts $value } } # Recursive helper for deep config value setting def set-config-value-recursive [ config: record path_parts: list value: any ] { if ($path_parts | length) == 1 { $config | upsert ($path_parts | first) $value } else { let current_key = ($path_parts | first) let remaining_parts = ($path_parts | skip 1) let current_section = ($config | get -o $current_key | default {}) $config | upsert $current_key (set-config-value-recursive $current_section $remaining_parts $value) } } # Bootstrap function to get defaults config path # This function must avoid circular dependencies during initial config loading def get-defaults-config-path [] { # Use environment variable fallback for bootstrap let base_path = ($env.PROVISIONING? | default "/usr/local/provisioning") $base_path | path join "config.defaults.toml" }