Files
kforge/internal/config/loader.go
nate.lubitz 1881ea2e99
Publish Action Image / build (push) Successful in 24s
Publish Action Image / publish (push) Successful in 27s
more updates
2026-06-29 16:29:16 +10:00

112 lines
3.4 KiB
Go

package config
import (
"fmt"
"os"
"strings"
"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 cfg.ActionRef != "" && (strings.Contains(cfg.ActionRef, "@") && !strings.Contains(cfg.ActionRef, "@sha256:")) {
return fmt.Errorf("action_ref %q looks like a git ref — use a Docker image tag instead (e.g. registry.example.com/infra/kforge:latest)", cfg.ActionRef)
}
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
}