// ABOUTME: YAML configuration parser for EdgeConnect apply command with comprehensive validation // ABOUTME: Handles parsing and validation of EdgeConnectConfig files with detailed error messages package config import ( "fmt" "os" "path/filepath" "gopkg.in/yaml.v3" ) // Parser defines the interface for configuration parsing type Parser interface { ParseFile(filename string) (*EdgeConnectConfig, string, error) ParseBytes(data []byte) (*EdgeConnectConfig, error) Validate(config *EdgeConnectConfig) error } // ConfigParser implements the Parser interface type ConfigParser struct{} // NewParser creates a new configuration parser func NewParser() Parser { return &ConfigParser{} } // ParseFile parses an EdgeConnectConfig from a YAML file func (p *ConfigParser) ParseFile(filename string) (*EdgeConnectConfig, string, error) { if filename == "" { return nil, "", fmt.Errorf("filename cannot be empty") } // Check if file exists if _, err := os.Stat(filename); os.IsNotExist(err) { return nil, "", fmt.Errorf("configuration file does not exist: %s", filename) } // Read file contents data, err := os.ReadFile(filename) if err != nil { return nil, "", fmt.Errorf("failed to read configuration file %s: %w", filename, err) } // Parse YAML without validation first config, err := p.parseYAMLOnly(data) if err != nil { return nil, "", fmt.Errorf("failed to parse configuration file %s: %w", filename, err) } // Resolve relative paths relative to config file directory configDir := filepath.Dir(filename) if err := p.resolveRelativePaths(config, configDir); err != nil { return nil, "", fmt.Errorf("failed to resolve paths in %s: %w", filename, err) } // Now validate with resolved paths if err := p.Validate(config); err != nil { return nil, "", fmt.Errorf("configuration validation failed in %s: %w", filename, err) } manifest, err := p.readManifestFiles(config) if err != nil { return nil, "", fmt.Errorf("failed to read manifest files: %w", err) } return config, manifest, nil } // parseYAMLOnly parses YAML without validation func (p *ConfigParser) parseYAMLOnly(data []byte) (*EdgeConnectConfig, error) { if len(data) == 0 { return nil, fmt.Errorf("configuration data cannot be empty") } var config EdgeConnectConfig // Parse YAML with strict mode decoder := yaml.NewDecoder(nil) decoder.KnownFields(true) // Fail on unknown fields if err := yaml.Unmarshal(data, &config); err != nil { return nil, fmt.Errorf("YAML parsing failed: %w", err) } return &config, nil } // ParseBytes parses an EdgeConnectConfig from YAML bytes func (p *ConfigParser) ParseBytes(data []byte) (*EdgeConnectConfig, error) { // Parse YAML only config, err := p.parseYAMLOnly(data) if err != nil { return nil, err } // Validate the parsed configuration if err := p.Validate(config); err != nil { return nil, fmt.Errorf("configuration validation failed: %w", err) } return config, nil } // Validate performs comprehensive validation of the configuration func (p *ConfigParser) Validate(config *EdgeConnectConfig) error { if config == nil { return fmt.Errorf("configuration cannot be nil") } return config.Validate() } // resolveRelativePaths converts relative paths to absolute paths based on config directory func (p *ConfigParser) resolveRelativePaths(config *EdgeConnectConfig, configDir string) error { if config.Spec.K8sApp != nil { resolved := config.Spec.K8sApp.GetManifestPath(configDir) config.Spec.K8sApp.ManifestFile = resolved } if config.Spec.DockerApp != nil && config.Spec.DockerApp.ManifestFile != "" { resolved := config.Spec.DockerApp.GetManifestPath(configDir) config.Spec.DockerApp.ManifestFile = resolved } return nil } // ValidateManifestFiles performs additional validation on manifest files func (p *ConfigParser) readManifestFiles(config *EdgeConnectConfig) (string, error) { var manifestFile string if config.Spec.K8sApp != nil { manifestFile = config.Spec.K8sApp.ManifestFile } else if config.Spec.DockerApp != nil { manifestFile = config.Spec.DockerApp.ManifestFile } if manifestFile == "" { return "", nil } if manifestFile != "" { if err := p.validateManifestFile(manifestFile); err != nil { return "", fmt.Errorf("manifest file validation failed: %w", err) } } // Try to read the file to ensure it's accessible content, err := os.ReadFile(manifestFile) if err != nil { return "", fmt.Errorf("cannot read manifest file %s: %w", manifestFile, err) } return string(content), nil } // validateManifestFile checks if the manifest file is valid and readable func (p *ConfigParser) validateManifestFile(filename string) error { info, err := os.Stat(filename) if err != nil { return fmt.Errorf("cannot access manifest file %s: %w", filename, err) } if info.IsDir() { return fmt.Errorf("manifest file cannot be a directory: %s", filename) } if info.Size() == 0 { return fmt.Errorf("manifest file cannot be empty: %s", filename) } return nil }