- 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>
248 lines
No EOL
7.1 KiB
Go
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
|
|
} |