edge-connect-client/sdk/internal/config/parser.go
Waldemar 1e1574e6a6
feat(apply): Implement EdgeConnect configuration parsing foundation
- Add comprehensive YAML configuration types for EdgeConnectConfig
- Implement robust parser with validation and path resolution
- Support both k8sApp and dockerApp configurations
- Add comprehensive test coverage with real example parsing
- Create validation for infrastructure uniqueness and port ranges
- Generate instance names following pattern: appName-appVersion-instance

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-29 16:18:35 +02:00

248 lines
No EOL
7.1 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, 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, 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)
}
return config, 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) ValidateManifestFiles(config *EdgeConnectConfig) 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 != "" {
if err := p.validateManifestFile(manifestFile); err != nil {
return fmt.Errorf("manifest file validation failed: %w", err)
}
}
return 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)
}
// Try to read the file to ensure it's accessible
if _, err := os.ReadFile(filename); err != nil {
return fmt.Errorf("cannot read manifest file %s: %w", filename, err)
}
return nil
}
// GetInstanceName generates the instance name following the pattern: appName-appVersion-instance
func GetInstanceName(appName, appVersion string) string {
return fmt.Sprintf("%s-%s-instance", appName, appVersion)
}
// ValidateInfrastructureUniqueness ensures no duplicate infrastructure targets
func (p *ConfigParser) ValidateInfrastructureUniqueness(config *EdgeConnectConfig) error {
seen := make(map[string]bool)
for i, infra := range config.Spec.InfraTemplate {
key := fmt.Sprintf("%s:%s:%s:%s",
infra.Organization,
infra.Region,
infra.CloudletOrg,
infra.CloudletName)
if seen[key] {
return fmt.Errorf("duplicate infrastructure target at index %d: org=%s, region=%s, cloudletOrg=%s, cloudletName=%s",
i, infra.Organization, infra.Region, infra.CloudletOrg, infra.CloudletName)
}
seen[key] = true
}
return nil
}
// ValidatePortRanges ensures port ranges don't overlap in network configuration
func (p *ConfigParser) ValidatePortRanges(config *EdgeConnectConfig) error {
if config.Spec.Network == nil {
return nil
}
connections := config.Spec.Network.OutboundConnections
for i := 0; i < len(connections); i++ {
for j := i + 1; j < len(connections); j++ {
conn1 := connections[i]
conn2 := connections[j]
// Only check same protocol and CIDR
if conn1.Protocol == conn2.Protocol && conn1.RemoteCIDR == conn2.RemoteCIDR {
if portRangesOverlap(conn1.PortRangeMin, conn1.PortRangeMax, conn2.PortRangeMin, conn2.PortRangeMax) {
return fmt.Errorf("overlapping port ranges for protocol %s and CIDR %s: [%d-%d] overlaps with [%d-%d]",
conn1.Protocol, conn1.RemoteCIDR,
conn1.PortRangeMin, conn1.PortRangeMax,
conn2.PortRangeMin, conn2.PortRangeMax)
}
}
}
}
return nil
}
// portRangesOverlap checks if two port ranges overlap
func portRangesOverlap(min1, max1, min2, max2 int) bool {
return max1 >= min2 && max2 >= min1
}
// ComprehensiveValidate performs all validation checks including extended ones
func (p *ConfigParser) ComprehensiveValidate(config *EdgeConnectConfig) error {
// Basic validation
if err := p.Validate(config); err != nil {
return err
}
// Manifest file validation
if err := p.ValidateManifestFiles(config); err != nil {
return err
}
// Infrastructure uniqueness validation
if err := p.ValidateInfrastructureUniqueness(config); err != nil {
return err
}
// Port range validation
if err := p.ValidatePortRanges(config); err != nil {
return err
}
return nil
}