package config import ( "fmt" "strings" ) const ( minPort = 1 maxPort = 65535 ) // ValidationError represents a configuration validation error with context. type ValidationError struct { Field string // The field that failed validation Message string // Error message Context map[string]string // Additional context information } // Error implements the error interface. func (e ValidationError) Error() string { return e.Message } // Validator validates configuration files. type Validator struct{} // NewValidator creates a new Validator instance. func NewValidator() *Validator { return &Validator{} } // ValidateConfig validates the entire configuration and returns all errors found. func (v *Validator) ValidateConfig(cfg *Config) []ValidationError { var errs []ValidationError if cfg == nil { return []ValidationError{{ Field: "config", Message: "Configuration is nil", }} } // Validate structure errs = append(errs, v.validateStructure(cfg)...) // Validate each forward for _, ctx := range cfg.Contexts { for _, ns := range ctx.Namespaces { for _, fwd := range ns.Forwards { errs = append(errs, v.validateForward(&fwd)...) } } } // Check for duplicate local ports errs = append(errs, v.validateDuplicatePorts(cfg)...) return errs } // validateStructure validates the basic structure of the configuration. func (v *Validator) validateStructure(cfg *Config) []ValidationError { var errs []ValidationError if len(cfg.Contexts) == 0 { errs = append(errs, ValidationError{ Field: "contexts", Message: "Configuration must have at least one context", }) return errs } for i, ctx := range cfg.Contexts { if ctx.Name == "" { errs = append(errs, ValidationError{ Field: fmt.Sprintf("contexts[%d].name", i), Message: "Context name cannot be empty", }) } if len(ctx.Namespaces) == 0 { errs = append(errs, ValidationError{ Field: fmt.Sprintf("contexts[%d].namespaces", i), Message: fmt.Sprintf("Context '%s' must have at least one namespace", ctx.Name), }) continue } for j, ns := range ctx.Namespaces { if ns.Name == "" { errs = append(errs, ValidationError{ Field: fmt.Sprintf("contexts[%d].namespaces[%d].name", i, j), Message: fmt.Sprintf("Namespace name cannot be empty in context '%s'", ctx.Name), }) } if len(ns.Forwards) == 0 { errs = append(errs, ValidationError{ Field: fmt.Sprintf("contexts[%d].namespaces[%d].forwards", i, j), Message: fmt.Sprintf("Namespace '%s/%s' must have at least one forward", ctx.Name, ns.Name), }) } } } return errs } // validateForward validates a single forward configuration. func (v *Validator) validateForward(fwd *Forward) []ValidationError { var errs []ValidationError // Validate resource if fwd.Resource == "" { errs = append(errs, ValidationError{ Field: "resource", Message: fmt.Sprintf("Resource cannot be empty for forward %s", fwd.ID()), }) } else { errs = append(errs, v.validateResource(fwd)...) } // Validate protocol if fwd.Protocol != "" && fwd.Protocol != "tcp" && fwd.Protocol != "udp" { errs = append(errs, ValidationError{ Field: "protocol", Message: fmt.Sprintf("Invalid protocol '%s' for forward %s (must be 'tcp' or 'udp')", fwd.Protocol, fwd.ID()), }) } // Validate ports if fwd.Port < minPort || fwd.Port > maxPort { errs = append(errs, ValidationError{ Field: "port", Message: fmt.Sprintf("Invalid port %d for forward %s (must be between %d and %d)", fwd.Port, fwd.ID(), minPort, maxPort), }) } if fwd.LocalPort < minPort || fwd.LocalPort > maxPort { errs = append(errs, ValidationError{ Field: "localPort", Message: fmt.Sprintf("Invalid localPort %d for forward %s (must be between %d and %d)", fwd.LocalPort, fwd.ID(), minPort, maxPort), }) } return errs } // validateResource validates the resource field format and selector usage. func (v *Validator) validateResource(fwd *Forward) []ValidationError { var errs []ValidationError parts := strings.SplitN(fwd.Resource, "/", 2) resourceType := parts[0] // Valid resource types: pod, service if resourceType != "pod" && resourceType != "service" { errs = append(errs, ValidationError{ Field: "resource", Message: fmt.Sprintf("Invalid resource type '%s' for forward %s (must be 'pod' or 'service')", resourceType, fwd.ID()), }) return errs } // For pod resources if resourceType == "pod" { if len(parts) == 2 { // pod/name format - should not have selector if fwd.Selector != "" { errs = append(errs, ValidationError{ Field: "selector", Message: fmt.Sprintf("Forward %s uses explicit pod name (%s) and should not have a selector", fwd.ID(), fwd.Resource), }) } // Validate pod name is not empty if parts[1] == "" { errs = append(errs, ValidationError{ Field: "resource", Message: fmt.Sprintf("Pod name cannot be empty for forward %s", fwd.ID()), }) } } else { // pod (no name) - must have selector if fwd.Selector == "" { errs = append(errs, ValidationError{ Field: "selector", Message: fmt.Sprintf("Forward %s uses generic 'pod' resource and must have a selector", fwd.ID()), }) } } } // For service resources if resourceType == "service" { if len(parts) < 2 || parts[1] == "" { errs = append(errs, ValidationError{ Field: "resource", Message: fmt.Sprintf("Service name cannot be empty for forward %s", fwd.ID()), }) } if fwd.Selector != "" { errs = append(errs, ValidationError{ Field: "selector", Message: fmt.Sprintf("Forward %s uses service resource and should not have a selector", fwd.ID()), }) } } return errs } // validateDuplicatePorts checks for duplicate local ports across all forwards. func (v *Validator) validateDuplicatePorts(cfg *Config) []ValidationError { var errs []ValidationError portMap := make(map[int][]string) // port -> list of forward IDs for _, ctx := range cfg.Contexts { for _, ns := range ctx.Namespaces { for _, fwd := range ns.Forwards { portMap[fwd.LocalPort] = append(portMap[fwd.LocalPort], fwd.ID()) } } } // Find duplicates for port, forwards := range portMap { if len(forwards) > 1 { errs = append(errs, ValidationError{ Field: "localPort", Message: fmt.Sprintf("Duplicate local port %d used by multiple forwards", port), Context: map[string]string{ "port": fmt.Sprintf("%d", port), "forwards": strings.Join(forwards, ", "), }, }) } } return errs } // FormatValidationErrors formats validation errors into a human-readable string. func FormatValidationErrors(errs []ValidationError) string { if len(errs) == 0 { return "" } var sb strings.Builder sb.WriteString("\nConfiguration Validation Errors:\n") sb.WriteString(strings.Repeat("=", 50) + "\n\n") for i, err := range errs { sb.WriteString(fmt.Sprintf("%d. %s\n", i+1, err.Message)) if len(err.Context) > 0 { for k, v := range err.Context { sb.WriteString(fmt.Sprintf(" %s: %s\n", k, v)) } } sb.WriteString("\n") } return sb.String() }