package config // ------------------------------------------------------------ // Default values — single source of truth for every default // referenced in the schema. Change a default here and it // propagates everywhere automatically. // ------------------------------------------------------------ const ( DefaultTLSIssuer = "letsencrypt-prod" DefaultIngressClass = "nginx" DefaultCNPGHost = "cnpg-main-rw.default.svc.cluster.local" DefaultNamespacePattern = "${env}" DefaultImagePullPolicy = "Always" DefaultServiceType = "ClusterIP" DefaultDockerfile = "Dockerfile" DefaultImageTag = "latest" DefaultPullSecret = "regcred" DefaultHealthCheckPath = "/healthcheck" DefaultRegistryURL = "registry.natelubitz.com" DefaultPort = 3000 DefaultReplicas = 1 DefaultInitialDelaySecs = 15 DefaultPeriodSecs = 10 DefaultTimeoutSecs = 5 DefaultFailureThreshold = 3 DefaultDeleteGraceSecs = 300 DefaultSuccessfulJobsHist = 3 DefaultFailedJobsHist = 1 DefaultCacheProvider = "valkey" DefaultCacheMode = "standalone" DefaultStorageProvider = "minio" DefaultStorageMode = "standalone" DefaultQueueProvider = "nats" DefaultSearchProvider = "meilisearch" DefaultMonProvider = "prometheus" DefaultMetricsPath = "/metrics" DefaultRestartPolicy = "OnFailure" DefaultConcurrencyPolicy = "Forbid" ) // boolPtr / intPtr are helpers for pointer defaults. func boolPtr(b bool) *bool { return &b } func intPtr(i int) *int { return &i } // isEnabled returns true if the InfraBase has no explicit Enabled // value set (nil = inherits root default, which is true when the // block exists) or if Enabled is explicitly true. func isEnabled(b *bool) bool { return b == nil || *b } // ------------------------------------------------------------ // ApplyDefaults fills in every zero/nil value in the config // with the canonical default. Called once after unmarshalling, // before any environment resolution. // ------------------------------------------------------------ func ApplyDefaults(cfg *KforgeConfig) { applyClusterDefaults(&cfg.Cluster) applyRegistryDefaults(&cfg.Registry, &cfg.Meta) applyDeploymentDefaults(&cfg.Defaults) applyRootInfraDefaults(&cfg.Infrastructure) } func applyClusterDefaults(c *ClusterConfig) { if c.TLSIssuer == "" { c.TLSIssuer = DefaultTLSIssuer } if c.IngressClass == "" { c.IngressClass = DefaultIngressClass } if c.CNPG.Host == "" { c.CNPG.Host = DefaultCNPGHost } if c.NamespacePattern == "" { c.NamespacePattern = DefaultNamespacePattern } } func applyRegistryDefaults(r *RegistryConfig, m *MetaConfig) { if r.URL == "" { r.URL = DefaultRegistryURL } if r.PullSecret == "" { r.PullSecret = DefaultPullSecret } if r.Repository == "" { r.Repository = "${tenant}/${name}" } } func applyDeploymentDefaults(d *DefaultsConfig) { if d.ImagePullPolicy == "" { d.ImagePullPolicy = DefaultImagePullPolicy } if d.Replicas == nil { d.Replicas = intPtr(DefaultReplicas) } if d.Port == nil { d.Port = intPtr(DefaultPort) } if d.ServiceType == "" { d.ServiceType = DefaultServiceType } if d.Dockerfile == "" { d.Dockerfile = DefaultDockerfile } applyHealthCheckDefaults(&d.HealthCheck, d.Port) applyResourceDefaults(&d.Resources) } func applyHealthCheckDefaults(h *HealthCheckConfig, defaultPort *int) { if h.Path == "" { h.Path = DefaultHealthCheckPath } if h.Port == nil { h.Port = defaultPort } if h.InitialDelaySeconds == 0 { h.InitialDelaySeconds = DefaultInitialDelaySecs } if h.PeriodSeconds == 0 { h.PeriodSeconds = DefaultPeriodSecs } if h.TimeoutSeconds == 0 { h.TimeoutSeconds = DefaultTimeoutSecs } if h.FailureThreshold == 0 { h.FailureThreshold = DefaultFailureThreshold } if h.Liveness == nil { h.Liveness = boolPtr(true) } if h.Readiness == nil { h.Readiness = boolPtr(true) } } func applyResourceDefaults(r *ResourceConfig) { if r.Requests.CPU == "" { r.Requests.CPU = "100m" } if r.Requests.Memory == "" { r.Requests.Memory = "128Mi" } if r.Limits.CPU == "" { r.Limits.CPU = "500m" } if r.Limits.Memory == "" { r.Limits.Memory = "512Mi" } } // applyRootInfraDefaults sets provider/mode defaults on the root // infrastructure block. The enabled flag is handled by the merge // step: if a block exists at root with no explicit enabled:false, // it is considered enabled. func applyRootInfraDefaults(infra *InfrastructureConfig) { if infra.Cache != nil { if infra.Cache.Provider == "" { infra.Cache.Provider = DefaultCacheProvider } if infra.Cache.Mode == "" { infra.Cache.Mode = DefaultCacheMode } if infra.Cache.Replicas == nil { infra.Cache.Replicas = intPtr(1) } } if infra.Storage != nil { if infra.Storage.Provider == "" { infra.Storage.Provider = DefaultStorageProvider } if infra.Storage.Mode == "" { infra.Storage.Mode = DefaultStorageMode } } if infra.Queue != nil { if infra.Queue.Provider == "" { infra.Queue.Provider = DefaultQueueProvider } } if infra.Search != nil { if infra.Search.Provider == "" { infra.Search.Provider = DefaultSearchProvider } } if infra.Monitoring != nil { if infra.Monitoring.Provider == "" { infra.Monitoring.Provider = DefaultMonProvider } if infra.Monitoring.MetricsPath == "" { infra.Monitoring.MetricsPath = DefaultMetricsPath } } } // ------------------------------------------------------------ // ResolveEnvironment produces a fully-merged, fully-defaulted // ResolvedEnvironment for a given environment key. // // Merge order (last wins): // 1. Compiled defaults (from ApplyDefaults) // 2. Root infrastructure block // 3. Per-environment overrides // ------------------------------------------------------------ // ResolvedEnvironment is the flattened, ready-to-generate view // of a single environment. Generators consume this type only — // they never touch raw config or merge logic. type ResolvedEnvironment struct { // Identity EnvKey string EnvPrefix string Namespace string FullName string // ${env_prefix}-${tenant}-${name} // Deployment Image string ImagePullPolicy string ImagePullSecret string Replicas int Port int ServiceType string HealthCheck HealthCheckConfig Resources ResourceConfig EnvVars []EnvVarConfig // Ingress Ingress IngressConfig // Infrastructure (merged root + env override) Infrastructure ResolvedInfrastructure // Cron jobs CronJobs []ResolvedCronJob // Cluster-level settings (carried through for generators) TLSIssuer string IngressClass string CNPGHost string // Lifecycle Lifecycle LifecycleConfig } // ResolvedInfrastructure holds the fully-merged infra state for // one environment. Each field is nil if the service is disabled. type ResolvedInfrastructure struct { Database *DatabaseInfraConfig Cache *CacheInfraConfig Storage *StorageInfraConfig Queue *QueueInfraConfig Search *SearchInfraConfig Monitoring *MonitoringInfraConfig } // ResolvedCronJob is a fully-defaulted cron job ready for the // CronJob manifest generator. type ResolvedCronJob struct { CronJobConfig Image string EnvVars []EnvVarConfig // merged: deployment vars + job-specific vars } // ResolveEnvironment merges root config + env overrides into a // single ResolvedEnvironment. cfg must have had ApplyDefaults // called on it first. func ResolveEnvironment(cfg *KforgeConfig, envKey string) (ResolvedEnvironment, error) { env, ok := cfg.Environments[envKey] if !ok { return ResolvedEnvironment{}, &UnknownEnvironmentError{Key: envKey} } prefix := resolveEnvPrefix(envKey, env.EnvPrefix) namespace := resolveNamespace(envKey, env.Namespace, cfg.Cluster.NamespacePattern) fullName := resolveFullName(cfg, prefix, env) registry := resolveRegistry(cfg, env) imageTag := env.ImageTag if imageTag == "" { imageTag = DefaultImageTag } image := registry.URL + "/" + registry.Repository + ":" + imageTag replicas := *cfg.Defaults.Replicas if env.Replicas != nil { replicas = *env.Replicas } port := *cfg.Defaults.Port hc := cfg.Defaults.HealthCheck if hc.Port == nil || *hc.Port == 0 { hc.Port = intPtr(port) } envVars := mergeEnvVars(cfg.Defaults.EnvVars, env.EnvVars) infra := mergeInfrastructure(&cfg.Infrastructure, &env.Infrastructure) cronJobs := resolveCronJobs(env.CronJobs, image, envVars, cfg.Defaults.Resources) lifecycle := env.Lifecycle if lifecycle.DeleteGraceSeconds == 0 { lifecycle.DeleteGraceSeconds = DefaultDeleteGraceSecs } auth := env.Ingress.Auth if auth.SecretName == "" { auth.SecretName = fullName + "-basic-auth" } ingress := IngressConfig{ Hosts: env.Ingress.Hosts, Auth: auth, } return ResolvedEnvironment{ EnvKey: envKey, EnvPrefix: prefix, Namespace: namespace, FullName: fullName, Image: image, ImagePullPolicy: cfg.Defaults.ImagePullPolicy, ImagePullSecret: registry.PullSecret, Replicas: replicas, Port: port, ServiceType: cfg.Defaults.ServiceType, HealthCheck: hc, Resources: cfg.Defaults.Resources, EnvVars: envVars, Ingress: ingress, Infrastructure: infra, CronJobs: cronJobs, TLSIssuer: cfg.Cluster.TLSIssuer, IngressClass: cfg.Cluster.IngressClass, CNPGHost: cfg.Cluster.CNPG.Host, Lifecycle: lifecycle, }, nil } // ------------------------------------------------------------ // Internal resolution helpers // ------------------------------------------------------------ func resolveEnvPrefix(envKey string, override *string) string { if override != nil && *override != "" { return *override } // Default: first 4 chars of envKey, or full key if shorter. if len(envKey) <= 4 { return envKey } return envKey[:4] } func resolveNamespace(envKey, explicit, pattern string) string { if explicit != "" { return explicit } // Apply the namespace pattern (simple token replace here; // full interpolation runs later via the interpolate package). result := pattern if result == "" { return envKey } return result } func resolveFullName(cfg *KforgeConfig, prefix string, env EnvironmentConfig) string { if cfg.Meta.NameOverride != nil && *cfg.Meta.NameOverride != "" { return *cfg.Meta.NameOverride } return prefix + "-" + cfg.Meta.Tenant + "-" + cfg.Meta.Name } func resolveRegistry(cfg *KforgeConfig, env EnvironmentConfig) RegistryConfig { base := cfg.Registry // Resolve the repository token now that we have meta values. if base.Repository == "${tenant}/${name}" || base.Repository == "" { base.Repository = cfg.Meta.Tenant + "/" + cfg.Meta.Name } if env.Registry != nil { r := *env.Registry if r.URL == "" { r.URL = base.URL } if r.Repository == "" { r.Repository = base.Repository } if r.PullSecret == "" { r.PullSecret = base.PullSecret } return r } return base } // MergeEnvVars is the exported form of mergeEnvVars, used by the // generate command to inject infrastructure env vars into deployments. func MergeEnvVars(base, override []EnvVarConfig) []EnvVarConfig { return mergeEnvVars(base, override) } // mergeEnvVars combines base and override slices. If an env var // with the same Name appears in both, the override wins. func mergeEnvVars(base, override []EnvVarConfig) []EnvVarConfig { merged := make([]EnvVarConfig, 0, len(base)+len(override)) seen := make(map[string]int) // name → index in merged for _, v := range base { seen[v.Name] = len(merged) merged = append(merged, v) } for _, v := range override { if idx, ok := seen[v.Name]; ok { merged[idx] = v // override } else { seen[v.Name] = len(merged) merged = append(merged, v) } } return merged } // mergeInfrastructure performs a shallow merge of root infra // defaults with per-environment overrides. // // Rules: // - If root has a service block with no explicit enabled:false, // it is enabled in every environment. // - An env block with enabled:false disables the service. // - An env block with partial fields overrides only those fields; // everything else inherits from root. // - If root has no block for a service, env can still enable it // by providing its own block (enabled defaults to true if present). func mergeInfrastructure(root, env *InfrastructureConfig) ResolvedInfrastructure { return ResolvedInfrastructure{ Database: mergeDatabase(root.Database, env.Database), Cache: mergeCache(root.Cache, env.Cache), Storage: mergeStorage(root.Storage, env.Storage), Queue: mergeQueue(root.Queue, env.Queue), Search: mergeSearch(root.Search, env.Search), Monitoring: mergeMonitoring(root.Monitoring, env.Monitoring), } } func mergeDatabase(root, env *DatabaseInfraConfig) *DatabaseInfraConfig { if root == nil && env == nil { return nil } merged := &DatabaseInfraConfig{} if root != nil { *merged = *root } if env != nil { if env.Enabled != nil { merged.Enabled = env.Enabled } if env.Provider != "" { merged.Provider = env.Provider } if env.DatabaseName != "" { merged.DatabaseName = env.DatabaseName } } if !isEnabled(merged.Enabled) { return nil } if merged.Provider == "" { merged.Provider = "cnpg" } return merged } func mergeCache(root, env *CacheInfraConfig) *CacheInfraConfig { if root == nil && env == nil { return nil } merged := &CacheInfraConfig{} if root != nil { *merged = *root } if env != nil { if env.Enabled != nil { merged.Enabled = env.Enabled } if env.Provider != "" { merged.Provider = env.Provider } if env.Mode != "" { merged.Mode = env.Mode } if env.Replicas != nil { merged.Replicas = env.Replicas } } if !isEnabled(merged.Enabled) { return nil } if merged.Provider == "" { merged.Provider = DefaultCacheProvider } if merged.Mode == "" { merged.Mode = DefaultCacheMode } if merged.Replicas == nil { merged.Replicas = intPtr(1) } return merged } func mergeStorage(root, env *StorageInfraConfig) *StorageInfraConfig { if root == nil && env == nil { return nil } merged := &StorageInfraConfig{} if root != nil { *merged = *root } if env != nil { if env.Enabled != nil { merged.Enabled = env.Enabled } if env.Provider != "" { merged.Provider = env.Provider } if env.Mode != "" { merged.Mode = env.Mode } } if !isEnabled(merged.Enabled) { return nil } if merged.Provider == "" { merged.Provider = DefaultStorageProvider } if merged.Mode == "" { merged.Mode = DefaultStorageMode } return merged } func mergeQueue(root, env *QueueInfraConfig) *QueueInfraConfig { if root == nil && env == nil { return nil } merged := &QueueInfraConfig{} if root != nil { *merged = *root } if env != nil { if env.Enabled != nil { merged.Enabled = env.Enabled } if env.Provider != "" { merged.Provider = env.Provider } } if !isEnabled(merged.Enabled) { return nil } if merged.Provider == "" { merged.Provider = DefaultQueueProvider } return merged } func mergeSearch(root, env *SearchInfraConfig) *SearchInfraConfig { if root == nil && env == nil { return nil } merged := &SearchInfraConfig{} if root != nil { *merged = *root } if env != nil { if env.Enabled != nil { merged.Enabled = env.Enabled } if env.Provider != "" { merged.Provider = env.Provider } } if !isEnabled(merged.Enabled) { return nil } if merged.Provider == "" { merged.Provider = DefaultSearchProvider } return merged } func mergeMonitoring(root, env *MonitoringInfraConfig) *MonitoringInfraConfig { if root == nil && env == nil { return nil } merged := &MonitoringInfraConfig{} if root != nil { *merged = *root } if env != nil { if env.Enabled != nil { merged.Enabled = env.Enabled } if env.Provider != "" { merged.Provider = env.Provider } if env.MetricsPath != "" { merged.MetricsPath = env.MetricsPath } if env.MetricsPort != nil { merged.MetricsPort = env.MetricsPort } } if !isEnabled(merged.Enabled) { return nil } if merged.Provider == "" { merged.Provider = DefaultMonProvider } if merged.MetricsPath == "" { merged.MetricsPath = DefaultMetricsPath } return merged } // resolveCronJobs applies defaults to each cron job and merges // env vars from the parent deployment. func resolveCronJobs(jobs []CronJobConfig, deploymentImage string, deploymentEnvVars []EnvVarConfig, defaultResources ResourceConfig) []ResolvedCronJob { resolved := make([]ResolvedCronJob, 0, len(jobs)) for _, job := range jobs { r := ResolvedCronJob{CronJobConfig: job} // Image if job.ImageOverride != nil && *job.ImageOverride != "" { r.Image = *job.ImageOverride } else { r.Image = deploymentImage } // Env vars: deployment vars first, then job-specific (with merge) inheritEnv := job.InheritEnv == nil || *job.InheritEnv if inheritEnv { r.EnvVars = mergeEnvVars(deploymentEnvVars, job.EnvVars) } else { r.EnvVars = job.EnvVars } // Resources if job.Resources == nil { r.Resources = &defaultResources } // Tuning defaults if r.RestartPolicy == "" { r.RestartPolicy = DefaultRestartPolicy } if r.ConcurrencyPolicy == "" { r.ConcurrencyPolicy = DefaultConcurrencyPolicy } if r.SuccessfulJobsHistoryLimit == nil { r.SuccessfulJobsHistoryLimit = intPtr(DefaultSuccessfulJobsHist) } if r.FailedJobsHistoryLimit == nil { r.FailedJobsHistoryLimit = intPtr(DefaultFailedJobsHist) } resolved = append(resolved, r) } return resolved } // ------------------------------------------------------------ // Errors // ------------------------------------------------------------ type UnknownEnvironmentError struct { Key string } func (e *UnknownEnvironmentError) Error() string { return "unknown environment: " + e.Key }