Files
nate.lubitz 532b912ffb
Build and Deploy / build-and-deploy (push) Failing after 20s
add tool to gitea
2026-06-05 02:34:00 +10:00

688 lines
18 KiB
Go

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
}