+129
-55
@@ -1,5 +1,12 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"kforge/pkg/interpolate"
|
||||
)
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Default values — single source of truth for every default
|
||||
// referenced in the schema. Change a default here and it
|
||||
@@ -10,6 +17,8 @@ const (
|
||||
DefaultTLSIssuer = "letsencrypt-prod"
|
||||
DefaultIngressClass = "nginx"
|
||||
DefaultCNPGHost = "cnpg-main-rw.default.svc.cluster.local"
|
||||
DefaultCNPGClusterName = "cnpg-main"
|
||||
DefaultCNPGSuperuserSecret = "cnpg-main-superuser"
|
||||
DefaultNamespacePattern = "${env}"
|
||||
DefaultImagePullPolicy = "Always"
|
||||
DefaultServiceType = "ClusterIP"
|
||||
@@ -19,15 +28,15 @@ const (
|
||||
DefaultHealthCheckPath = "/healthcheck"
|
||||
DefaultRegistryURL = "registry.natelubitz.com"
|
||||
|
||||
DefaultPort = 3000
|
||||
DefaultReplicas = 1
|
||||
DefaultInitialDelaySecs = 15
|
||||
DefaultPeriodSecs = 10
|
||||
DefaultTimeoutSecs = 5
|
||||
DefaultFailureThreshold = 3
|
||||
DefaultDeleteGraceSecs = 300
|
||||
DefaultSuccessfulJobsHist = 3
|
||||
DefaultFailedJobsHist = 1
|
||||
DefaultPort = 3000
|
||||
DefaultReplicas = 1
|
||||
DefaultInitialDelaySecs = 15
|
||||
DefaultPeriodSecs = 10
|
||||
DefaultTimeoutSecs = 5
|
||||
DefaultFailureThreshold = 3
|
||||
DefaultDeleteGraceSecs = 300
|
||||
DefaultSuccessfulJobsHist = 3
|
||||
DefaultFailedJobsHist = 1
|
||||
|
||||
DefaultCacheProvider = "valkey"
|
||||
DefaultCacheMode = "standalone"
|
||||
@@ -40,6 +49,8 @@ const (
|
||||
|
||||
DefaultRestartPolicy = "OnFailure"
|
||||
DefaultConcurrencyPolicy = "Forbid"
|
||||
|
||||
DefaultPreviewNamespacePrefix = "preview-pr"
|
||||
)
|
||||
|
||||
// boolPtr / intPtr are helpers for pointer defaults.
|
||||
@@ -76,6 +87,12 @@ func applyClusterDefaults(c *ClusterConfig) {
|
||||
if c.CNPG.Host == "" {
|
||||
c.CNPG.Host = DefaultCNPGHost
|
||||
}
|
||||
if c.CNPG.ClusterName == "" {
|
||||
c.CNPG.ClusterName = DefaultCNPGClusterName
|
||||
}
|
||||
if c.CNPG.SuperuserSecret == "" {
|
||||
c.CNPG.SuperuserSecret = DefaultCNPGSuperuserSecret
|
||||
}
|
||||
if c.NamespacePattern == "" {
|
||||
c.NamespacePattern = DefaultNamespacePattern
|
||||
}
|
||||
@@ -156,9 +173,7 @@ func applyResourceDefaults(r *ResourceConfig) {
|
||||
}
|
||||
|
||||
// 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.
|
||||
// infrastructure block.
|
||||
func applyRootInfraDefaults(infra *InfrastructureConfig) {
|
||||
if infra.Cache != nil {
|
||||
if infra.Cache.Provider == "" {
|
||||
@@ -240,9 +255,15 @@ type ResolvedEnvironment struct {
|
||||
CronJobs []ResolvedCronJob
|
||||
|
||||
// Cluster-level settings (carried through for generators)
|
||||
TLSIssuer string
|
||||
IngressClass string
|
||||
CNPGHost string
|
||||
TLSIssuer string
|
||||
IngressClass string
|
||||
CNPGHost string
|
||||
CNPGClusterName string
|
||||
CNPGSuperuserSecret string
|
||||
|
||||
// DNS (for external-dns Ingress annotations)
|
||||
DNSTarget string
|
||||
SkipDNS bool
|
||||
|
||||
// Lifecycle
|
||||
Lifecycle LifecycleConfig
|
||||
@@ -318,30 +339,101 @@ func ResolveEnvironment(cfg *KforgeConfig, envKey string) (ResolvedEnvironment,
|
||||
Auth: auth,
|
||||
}
|
||||
|
||||
// Resolve DNS target token (${KFORGE_NODE_IP} etc.) from env.
|
||||
tokens := interpolate.FromEnvironment(
|
||||
cfg.Meta.Name, cfg.Meta.Tenant, envKey, prefix, fullName, imageTag, namespace,
|
||||
)
|
||||
dnsTarget := interpolate.Apply(cfg.DNS.Target, tokens)
|
||||
|
||||
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,
|
||||
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,
|
||||
CNPGClusterName: cfg.Cluster.CNPG.ClusterName,
|
||||
CNPGSuperuserSecret: cfg.Cluster.CNPG.SuperuserSecret,
|
||||
DNSTarget: dnsTarget,
|
||||
SkipDNS: cfg.DNS.SkipDNS,
|
||||
Lifecycle: lifecycle,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// SynthesizePreviewEnvironment creates a ResolvedEnvironment
|
||||
// for a PR preview from the preview config + a base environment.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
// SynthesizePreviewEnvironment derives a preview environment by
|
||||
// cloning the base environment and overriding namespace, hostname,
|
||||
// full name, and image tag for the given PR number.
|
||||
func SynthesizePreviewEnvironment(cfg *KforgeConfig, prNumber string) (ResolvedEnvironment, error) {
|
||||
if !cfg.Preview.Enabled {
|
||||
return ResolvedEnvironment{}, fmt.Errorf("preview is not enabled in kforge.yml")
|
||||
}
|
||||
if cfg.Preview.BaseEnvironment == "" {
|
||||
return ResolvedEnvironment{}, fmt.Errorf("preview.base_environment is required")
|
||||
}
|
||||
|
||||
env, err := ResolveEnvironment(cfg, cfg.Preview.BaseEnvironment)
|
||||
if err != nil {
|
||||
return ResolvedEnvironment{}, fmt.Errorf("resolving base environment %q: %w", cfg.Preview.BaseEnvironment, err)
|
||||
}
|
||||
|
||||
nsPrefix := cfg.Preview.NamespacePrefix
|
||||
if nsPrefix == "" {
|
||||
nsPrefix = DefaultPreviewNamespacePrefix
|
||||
}
|
||||
|
||||
env.EnvKey = "preview-" + prNumber
|
||||
env.Namespace = nsPrefix + "-" + prNumber
|
||||
env.FullName = "pr" + prNumber + "-" + cfg.Meta.Tenant + "-" + cfg.Meta.Name
|
||||
|
||||
// Override image tag to a PR-specific tag.
|
||||
if idx := strings.LastIndex(env.Image, ":"); idx >= 0 {
|
||||
env.Image = env.Image[:idx+1] + "pr-" + prNumber
|
||||
}
|
||||
|
||||
// Override ingress hostname using the template.
|
||||
hostnameTemplate := cfg.Preview.HostnameTemplate
|
||||
if hostnameTemplate == "" {
|
||||
hostnameTemplate = "pr-${PR_NUMBER}.${name}.example.com"
|
||||
}
|
||||
previewTokens := interpolate.Tokens{
|
||||
"PR_NUMBER": prNumber,
|
||||
"name": cfg.Meta.Name,
|
||||
"tenant": cfg.Meta.Tenant,
|
||||
}
|
||||
hostname := interpolate.Apply(hostnameTemplate, previewTokens)
|
||||
|
||||
env.Ingress = IngressConfig{
|
||||
Hosts: []IngressHost{{
|
||||
Hostname: hostname,
|
||||
TLS: true,
|
||||
DNSRecord: true,
|
||||
}},
|
||||
Auth: IngressAuth{
|
||||
SecretName: env.FullName + "-basic-auth",
|
||||
},
|
||||
}
|
||||
|
||||
return env, nil
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Internal resolution helpers
|
||||
// ------------------------------------------------------------
|
||||
@@ -350,7 +442,6 @@ 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
|
||||
}
|
||||
@@ -361,13 +452,10 @@ 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 == "" {
|
||||
if pattern == "" {
|
||||
return envKey
|
||||
}
|
||||
return result
|
||||
return pattern
|
||||
}
|
||||
|
||||
func resolveFullName(cfg *KforgeConfig, prefix string, env EnvironmentConfig) string {
|
||||
@@ -379,7 +467,6 @@ func resolveFullName(cfg *KforgeConfig, prefix string, env EnvironmentConfig) st
|
||||
|
||||
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
|
||||
}
|
||||
@@ -428,15 +515,6 @@ func mergeEnvVars(base, override []EnvVarConfig) []EnvVarConfig {
|
||||
|
||||
// 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),
|
||||
@@ -635,14 +713,12 @@ func resolveCronJobs(jobs []CronJobConfig, deploymentImage string, deploymentEnv
|
||||
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)
|
||||
@@ -650,12 +726,10 @@ func resolveCronJobs(jobs []CronJobConfig, deploymentImage string, deploymentEnv
|
||||
r.EnvVars = job.EnvVars
|
||||
}
|
||||
|
||||
// Resources
|
||||
if job.Resources == nil {
|
||||
r.Resources = &defaultResources
|
||||
}
|
||||
|
||||
// Tuning defaults
|
||||
if r.RestartPolicy == "" {
|
||||
r.RestartPolicy = DefaultRestartPolicy
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user