edge-connect-client/internal/config/parser.go

174 lines
4.9 KiB
Go

// 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
}