package config import ( "fmt" "os" "gopkg.in/yaml.v3" ) // Load reads a kforge.yml file from disk, applies all defaults, // and returns the parsed config ready for environment resolution. func Load(path string) (*KforgeConfig, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("reading %s: %w", path, err) } return LoadBytes(data) } // LoadBytes parses kforge.yml from a byte slice. Useful in tests. func LoadBytes(data []byte) (*KforgeConfig, error) { cfg := &KforgeConfig{} if err := yaml.Unmarshal(data, cfg); err != nil { return nil, fmt.Errorf("parsing kforge.yml: %w", err) } if err := validate(cfg); err != nil { return nil, err } ApplyDefaults(cfg) return cfg, nil } // validate performs structural validation before defaults are // applied. Returns the first error found. func validate(cfg *KforgeConfig) error { if cfg.Meta.Name == "" { return fmt.Errorf("meta.name is required") } if cfg.Meta.Tenant == "" { return fmt.Errorf("meta.tenant is required") } if len(cfg.Environments) == 0 { return fmt.Errorf("at least one environment must be defined") } for envKey, env := range cfg.Environments { if len(env.Ingress.Hosts) == 0 { return fmt.Errorf("environments.%s: at least one ingress host is required", envKey) } for _, host := range env.Ingress.Hosts { if host.Hostname == "" { return fmt.Errorf("environments.%s: ingress host hostname cannot be empty", envKey) } } for _, job := range env.CronJobs { if job.Name == "" { return fmt.Errorf("environments.%s: cron job is missing a name", envKey) } if job.Schedule == "" { return fmt.Errorf("environments.%s.cron_jobs.%s: schedule is required", envKey, job.Name) } if len(job.Command) == 0 { return fmt.Errorf("environments.%s.cron_jobs.%s: command is required", envKey, job.Name) } } for _, v := range env.EnvVars { if err := validateEnvVar(envKey, v); err != nil { return err } } } for _, v := range cfg.Defaults.EnvVars { if err := validateEnvVar("defaults", v); err != nil { return err } } return nil } func validateEnvVar(context string, v EnvVarConfig) error { if v.Name == "" { return fmt.Errorf("%s: env var is missing a name", context) } switch v.Type { case EnvVarTypePlain, "": // value can technically be empty (empty string env var is valid) case EnvVarTypeSecretRef: if v.SecretName == "" || v.SecretKey == "" { return fmt.Errorf("%s: env var %q (secret_ref) requires secret_name and secret_key", context, v.Name) } case EnvVarTypeConfigMapRef: if v.ConfigMapName == "" || v.ConfigMapKey == "" { return fmt.Errorf("%s: env var %q (configmap_ref) requires configmap_name and configmap_key", context, v.Name) } default: return fmt.Errorf("%s: env var %q has unknown type %q (valid: plain, secret_ref, configmap_ref)", context, v.Name, v.Type) } return nil } // EnvironmentKeys returns the sorted list of environment names. func EnvironmentKeys(cfg *KforgeConfig) []string { keys := make([]string, 0, len(cfg.Environments)) for k := range cfg.Environments { keys = append(keys, k) } return keys }