package config import ( "fmt" "strings" "k8s.io/apimachinery/pkg/labels" apivalidation "k8s.io/apimachinery/pkg/util/validation" "github.com/stakater/Reloader/internal/pkg/workload" ) // ValidationError represents a configuration validation error. type ValidationError struct { Field string Message string } func (e ValidationError) Error() string { return fmt.Sprintf("config.%s: %s", e.Field, e.Message) } // ValidationErrors is a collection of validation errors. type ValidationErrors []ValidationError func (e ValidationErrors) Error() string { if len(e) == 0 { return "" } if len(e) == 1 { return e[0].Error() } var b strings.Builder b.WriteString("multiple configuration errors:\n") for _, err := range e { b.WriteString(" - ") b.WriteString(err.Error()) b.WriteString("\n") } return b.String() } // Validate checks the configuration for errors and normalizes values. func (c *Config) Validate() error { var errs ValidationErrors switch c.ReloadStrategy { case ReloadStrategyEnvVars, ReloadStrategyAnnotations: // valid case "": c.ReloadStrategy = ReloadStrategyEnvVars default: errs = append( errs, ValidationError{ Field: "ReloadStrategy", Message: fmt.Sprintf("invalid value %q, must be %q or %q", c.ReloadStrategy, ReloadStrategyEnvVars, ReloadStrategyAnnotations), }, ) } switch c.ArgoRolloutStrategy { case ArgoRolloutStrategyRestart, ArgoRolloutStrategyRollout: // valid case "": c.ArgoRolloutStrategy = ArgoRolloutStrategyRollout default: errs = append( errs, ValidationError{ Field: "ArgoRolloutStrategy", Message: fmt.Sprintf( "invalid value %q, must be %q or %q", c.ArgoRolloutStrategy, ArgoRolloutStrategyRestart, ArgoRolloutStrategyRollout, ), }, ) } switch strings.ToLower(c.LogLevel) { case "trace", "debug", "info", "warn", "warning", "error", "fatal", "panic", "": // valid default: errs = append( errs, ValidationError{ Field: "LogLevel", Message: fmt.Sprintf("invalid log level %q", c.LogLevel), }, ) } switch strings.ToLower(c.LogFormat) { case "json", "": // valid default: errs = append( errs, ValidationError{ Field: "LogFormat", Message: fmt.Sprintf("invalid log format %q, must be \"json\" or empty", c.LogFormat), }, ) } // Watched namespaces must be valid DNS-1123 labels; an invalid entry would // otherwise fail deep inside the controller-runtime cache with an opaque error. for _, ns := range c.WatchedNamespaces { if msgs := apivalidation.IsDNS1123Label(ns); len(msgs) > 0 { errs = append( errs, ValidationError{ Field: "WatchedNamespaces", Message: fmt.Sprintf("invalid namespace %q: %s", ns, strings.Join(msgs, "; ")), }, ) } } c.IgnoredResources = normalizeToLower(c.IgnoredResources) // Normalize ignored workloads to canonical Kind values (e.g., "cronjobs" -> "CronJob") c.IgnoredWorkloads = normalizeToLower(c.IgnoredWorkloads) normalizedWorkloads := make([]string, 0, len(c.IgnoredWorkloads)) for _, w := range c.IgnoredWorkloads { kind, err := workload.KindFromString(w) if err != nil { errs = append( errs, ValidationError{ Field: "IgnoredWorkloads", Message: fmt.Sprintf("unknown workload type %q", w), }, ) } else { normalizedWorkloads = append(normalizedWorkloads, string(kind)) } } c.IgnoredWorkloads = normalizedWorkloads if len(errs) > 0 { return errs } return nil } // normalizeToLower converts all strings in the slice to lowercase and removes empty strings. func normalizeToLower(items []string) []string { if len(items) == 0 { return items } result := make([]string, 0, len(items)) for _, item := range items { item = strings.TrimSpace(strings.ToLower(item)) if item != "" { result = append(result, item) } } return result } // ParseSelectors parses a slice of selector strings into label selectors. func ParseSelectors(selectorStrings []string) ([]labels.Selector, error) { if len(selectorStrings) == 0 { return nil, nil } selectors := make([]labels.Selector, 0, len(selectorStrings)) for _, s := range selectorStrings { s = strings.TrimSpace(s) if s == "" { continue } selector, err := labels.Parse(s) if err != nil { return nil, fmt.Errorf("invalid selector %q: %w", s, err) } selectors = append(selectors, selector) } return selectors, nil }