package generator import ( "fmt" "kforge/internal/config" "kforge/pkg/interpolate" ) // GenerateInfrastructure produces manifests for all enabled // infrastructure services for a given environment. // Returns a slice of (resourceName, yamlContent) pairs so the // caller can write them to separate files if desired. func GenerateInfrastructure(env *config.ResolvedEnvironment) ([]InfraManifest, error) { var manifests []InfraManifest infra := env.Infrastructure if infra.Database != nil { m, err := generateDatabase(env, infra.Database) if err != nil { return nil, fmt.Errorf("database: %w", err) } manifests = append(manifests, m...) } if infra.Cache != nil { m, err := generateCache(env, infra.Cache) if err != nil { return nil, fmt.Errorf("cache: %w", err) } manifests = append(manifests, m...) } if infra.Storage != nil { m, err := generateStorage(env, infra.Storage) if err != nil { return nil, fmt.Errorf("storage: %w", err) } manifests = append(manifests, m...) } if infra.Queue != nil { m, err := generateQueue(env, infra.Queue) if err != nil { return nil, fmt.Errorf("queue: %w", err) } manifests = append(manifests, m...) } if infra.Search != nil { m, err := generateSearch(env, infra.Search) if err != nil { return nil, fmt.Errorf("search: %w", err) } manifests = append(manifests, m...) } if infra.Monitoring != nil { m, err := generateMonitoring(env, infra.Monitoring) if err != nil { return nil, fmt.Errorf("monitoring: %w", err) } manifests = append(manifests, m...) } return manifests, nil } // InfraManifest is a named manifest produced by an infra generator. type InfraManifest struct { // Name is a short identifier for the manifest (used as filename suffix). Name string Content string // EnvVars are the env vars that should be injected into the // deployment to connect to this infrastructure service. EnvVars []config.EnvVarConfig } // ------------------------------------------------------------ // Database — CNPG (centralized cluster) // ------------------------------------------------------------ func generateDatabase(env *config.ResolvedEnvironment, db *config.DatabaseInfraConfig) ([]InfraManifest, error) { dbName := db.DatabaseName if dbName == "" { dbName = interpolate.PGIdentifier(env.FullName) } // pgUser must be a valid unquoted PostgreSQL identifier; same as // the username stored in the db-credentials Secret by `kforge secrets apply`. pgUser := interpolate.PGIdentifier(env.FullName) secretName := env.FullName + "-db-credentials" cnpgHost := env.CNPGHost // CNPG Database CR — declaratively manages the database lifecycle. // The owner role is created by the db-init Job below. dbManifest := fmt.Sprintf(`apiVersion: postgresql.cnpg.io/v1 kind: Database metadata: name: %s namespace: %s labels: app: %s managed-by: kforge spec: name: %s owner: %s cluster: name: %s `, dbName, env.Namespace, env.FullName, dbName, pgUser, env.CNPGClusterName) // db-init Job — runs on every deploy to ensure the PostgreSQL role // exists and its password matches the Secret. The password stored by // `kforge secrets apply` is alphanumeric-only so it is safe to // embed in a shell command without additional escaping. // // Prerequisites: // - The CNPG superuser Secret must exist in the same namespace // (or copy it there as part of cluster bootstrap). // - `kforge secrets apply` must have run before this Job. jobManifest := fmt.Sprintf(`apiVersion: batch/v1 kind: Job metadata: name: %s-db-init namespace: %s labels: app: %s managed-by: kforge spec: ttlSecondsAfterFinished: 600 template: metadata: labels: app: %s spec: restartPolicy: OnFailure containers: - name: db-init image: postgres:16-alpine command: - /bin/sh - -c - | set -e echo "Ensuring role $DB_USER exists..." PGPASSWORD="$ADMIN_PASSWORD" psql -h "$DB_HOST" -U "$ADMIN_USER" postgres \ -c "SELECT 1 FROM pg_roles WHERE rolname = '$DB_USER'" | grep -q 1 || \ PGPASSWORD="$ADMIN_PASSWORD" psql -h "$DB_HOST" -U "$ADMIN_USER" postgres \ -c "CREATE ROLE $DB_USER WITH LOGIN PASSWORD '$DB_PASSWORD';" echo "Syncing password for $DB_USER..." PGPASSWORD="$ADMIN_PASSWORD" psql -h "$DB_HOST" -U "$ADMIN_USER" postgres \ -c "ALTER ROLE $DB_USER WITH PASSWORD '$DB_PASSWORD';" echo "Done." env: - name: DB_HOST value: %s - name: DB_USER valueFrom: secretKeyRef: name: %s key: username - name: DB_PASSWORD valueFrom: secretKeyRef: name: %s key: password - name: ADMIN_USER valueFrom: secretKeyRef: name: %s key: username - name: ADMIN_PASSWORD valueFrom: secretKeyRef: name: %s key: password `, env.FullName, env.Namespace, env.FullName, env.FullName, cnpgHost, secretName, secretName, env.CNPGSuperuserSecret, env.CNPGSuperuserSecret, ) // DATABASE_URL uses Kubernetes $(VAR_NAME) substitution — DB_USER and // DB_PASSWORD must be defined earlier in the env list. envVars := []config.EnvVarConfig{ {Name: "DB_HOST", Type: config.EnvVarTypePlain, Value: cnpgHost}, {Name: "DB_PORT", Type: config.EnvVarTypePlain, Value: "5432"}, {Name: "DB_NAME", Type: config.EnvVarTypePlain, Value: dbName}, {Name: "DB_USER", Type: config.EnvVarTypeSecretRef, SecretName: secretName, SecretKey: "username"}, {Name: "DB_PASSWORD", Type: config.EnvVarTypeSecretRef, SecretName: secretName, SecretKey: "password"}, {Name: "DATABASE_URL", Type: config.EnvVarTypePlain, Value: fmt.Sprintf("postgresql://$(DB_USER):$(DB_PASSWORD)@%s/%s", cnpgHost, dbName)}, } return []InfraManifest{ {Name: "cnpg-database", Content: dbManifest, EnvVars: envVars}, {Name: "cnpg-db-init", Content: jobManifest}, }, nil } // ------------------------------------------------------------ // Cache — Valkey or Redis (standalone or cluster) // ------------------------------------------------------------ func generateCache(env *config.ResolvedEnvironment, cache *config.CacheInfraConfig) ([]InfraManifest, error) { name := env.FullName + "-cache" secretName := env.FullName + "-cache-credentials" image := "valkey/valkey:7-alpine" if cache.Provider == "redis" { image = "redis:7-alpine" } replicas := 1 if cache.Replicas != nil { replicas = *cache.Replicas } var manifest string if cache.Mode == "cluster" && replicas > 1 { manifest = generateCacheStatefulSet(name, env.Namespace, env.FullName, image, secretName, replicas) } else { manifest = generateCacheDeployment(name, env.Namespace, env.FullName, image, secretName) } // Service svcManifest := fmt.Sprintf(`apiVersion: v1 kind: Service metadata: name: %s namespace: %s labels: app: %s managed-by: kforge spec: type: ClusterIP ports: - port: 6379 targetPort: 6379 selector: app: %s `, name, env.Namespace, env.FullName, name) // Password secret placeholder — actual password generated by // `kforge secrets apply`. secretManifest := fmt.Sprintf(`# Secret %q — generated by: kforge secrets apply --env %s # Do not commit generated passwords. This comment is a placeholder. `, secretName, env.EnvKey) envVars := []config.EnvVarConfig{ { Name: "CACHE_URL", Type: config.EnvVarTypePlain, Value: fmt.Sprintf("redis://:%s@%s:6379", "$(CACHE_PASSWORD)", name), }, { Name: "CACHE_HOST", Type: config.EnvVarTypePlain, Value: name, }, { Name: "CACHE_PORT", Type: config.EnvVarTypePlain, Value: "6379", }, { Name: "CACHE_PASSWORD", Type: config.EnvVarTypeSecretRef, SecretName: secretName, SecretKey: "password", }, } return []InfraManifest{ {Name: "cache", Content: manifest}, {Name: "cache-svc", Content: svcManifest}, {Name: "cache-secret", Content: secretManifest, EnvVars: envVars}, }, nil } func generateCacheDeployment(name, namespace, appLabel, image, secretName string) string { return fmt.Sprintf(`apiVersion: apps/v1 kind: Deployment metadata: name: %s namespace: %s labels: app: %s managed-by: kforge spec: replicas: 1 selector: matchLabels: app: %s template: metadata: labels: app: %s spec: containers: - name: %s image: %s ports: - containerPort: 6379 command: ["valkey-server", "--requirepass", "$(CACHE_PASSWORD)"] env: - name: CACHE_PASSWORD valueFrom: secretKeyRef: name: %s key: password resources: requests: cpu: 50m memory: 64Mi limits: cpu: 200m memory: 256Mi `, name, namespace, appLabel, name, name, name, image, secretName) } func generateCacheStatefulSet(name, namespace, appLabel, image, secretName string, replicas int) string { return fmt.Sprintf(`apiVersion: apps/v1 kind: StatefulSet metadata: name: %s namespace: %s labels: app: %s managed-by: kforge spec: replicas: %d serviceName: %s selector: matchLabels: app: %s template: metadata: labels: app: %s spec: containers: - name: %s image: %s ports: - containerPort: 6379 command: ["valkey-server", "--requirepass", "$(CACHE_PASSWORD)", "--cluster-enabled", "yes"] env: - name: CACHE_PASSWORD valueFrom: secretKeyRef: name: %s key: password resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 512Mi `, name, namespace, appLabel, replicas, name, name, name, name, image, secretName) } // ------------------------------------------------------------ // Storage — Minio // ------------------------------------------------------------ func generateStorage(env *config.ResolvedEnvironment, storage *config.StorageInfraConfig) ([]InfraManifest, error) { name := env.FullName + "-storage" secretName := env.FullName + "-storage-credentials" manifest := fmt.Sprintf(`apiVersion: apps/v1 kind: Deployment metadata: name: %s namespace: %s labels: app: %s managed-by: kforge spec: replicas: 1 selector: matchLabels: app: %s template: metadata: labels: app: %s spec: containers: - name: %s image: minio/minio:latest args: ["server", "/data", "--console-address", ":9001"] ports: - containerPort: 9000 name: api - containerPort: 9001 name: console env: - name: MINIO_ROOT_USER valueFrom: secretKeyRef: name: %s key: access_key - name: MINIO_ROOT_PASSWORD valueFrom: secretKeyRef: name: %s key: secret_key resources: requests: cpu: 100m memory: 256Mi limits: cpu: 500m memory: 1Gi volumeMounts: - name: data mountPath: /data volumes: - name: data emptyDir: {} `, name, env.Namespace, env.FullName, name, name, name, secretName, secretName) svcManifest := fmt.Sprintf(`apiVersion: v1 kind: Service metadata: name: %s namespace: %s labels: app: %s managed-by: kforge spec: type: ClusterIP ports: - name: api port: 9000 targetPort: 9000 - name: console port: 9001 targetPort: 9001 selector: app: %s `, name, env.Namespace, env.FullName, name) envVars := []config.EnvVarConfig{ {Name: "STORAGE_ENDPOINT", Type: config.EnvVarTypePlain, Value: fmt.Sprintf("http://%s:9000", name)}, {Name: "STORAGE_ACCESS_KEY", Type: config.EnvVarTypeSecretRef, SecretName: secretName, SecretKey: "access_key"}, {Name: "STORAGE_SECRET_KEY", Type: config.EnvVarTypeSecretRef, SecretName: secretName, SecretKey: "secret_key"}, } return []InfraManifest{ {Name: "storage", Content: manifest}, {Name: "storage-svc", Content: svcManifest, EnvVars: envVars}, }, nil } // ------------------------------------------------------------ // Queue — NATS or RabbitMQ // ------------------------------------------------------------ func generateQueue(env *config.ResolvedEnvironment, queue *config.QueueInfraConfig) ([]InfraManifest, error) { if queue.Provider == "rabbitmq" { return generateRabbitMQ(env) } return generateNATS(env) } func generateNATS(env *config.ResolvedEnvironment) ([]InfraManifest, error) { name := env.FullName + "-queue" manifest := fmt.Sprintf(`apiVersion: apps/v1 kind: Deployment metadata: name: %s namespace: %s labels: app: %s managed-by: kforge spec: replicas: 1 selector: matchLabels: app: %s template: metadata: labels: app: %s spec: containers: - name: %s image: nats:2-alpine ports: - containerPort: 4222 name: client - containerPort: 8222 name: monitor resources: requests: cpu: 25m memory: 32Mi limits: cpu: 100m memory: 128Mi `, name, env.Namespace, env.FullName, name, name, name) svcManifest := fmt.Sprintf(`apiVersion: v1 kind: Service metadata: name: %s namespace: %s labels: app: %s managed-by: kforge spec: type: ClusterIP ports: - name: client port: 4222 targetPort: 4222 selector: app: %s `, name, env.Namespace, env.FullName, name) envVars := []config.EnvVarConfig{ {Name: "QUEUE_URL", Type: config.EnvVarTypePlain, Value: fmt.Sprintf("nats://%s:4222", name)}, {Name: "QUEUE_HOST", Type: config.EnvVarTypePlain, Value: name}, } return []InfraManifest{ {Name: "queue-nats", Content: manifest}, {Name: "queue-svc", Content: svcManifest, EnvVars: envVars}, }, nil } func generateRabbitMQ(env *config.ResolvedEnvironment) ([]InfraManifest, error) { name := env.FullName + "-queue" secretName := env.FullName + "-queue-credentials" manifest := fmt.Sprintf(`apiVersion: apps/v1 kind: Deployment metadata: name: %s namespace: %s labels: app: %s managed-by: kforge spec: replicas: 1 selector: matchLabels: app: %s template: metadata: labels: app: %s spec: containers: - name: %s image: rabbitmq:3-management-alpine ports: - containerPort: 5672 name: amqp - containerPort: 15672 name: management env: - name: RABBITMQ_DEFAULT_USER valueFrom: secretKeyRef: name: %s key: username - name: RABBITMQ_DEFAULT_PASS valueFrom: secretKeyRef: name: %s key: password resources: requests: cpu: 100m memory: 256Mi limits: cpu: 500m memory: 512Mi `, name, env.Namespace, env.FullName, name, name, name, secretName, secretName) envVars := []config.EnvVarConfig{ { Name: "QUEUE_URL", Type: config.EnvVarTypePlain, Value: fmt.Sprintf("amqp://$(QUEUE_USER):$(QUEUE_PASSWORD)@%s:5672", name), }, {Name: "QUEUE_USER", Type: config.EnvVarTypeSecretRef, SecretName: secretName, SecretKey: "username"}, {Name: "QUEUE_PASSWORD", Type: config.EnvVarTypeSecretRef, SecretName: secretName, SecretKey: "password"}, } return []InfraManifest{ {Name: "queue-rabbitmq", Content: manifest, EnvVars: envVars}, }, nil } // ------------------------------------------------------------ // Search — Meilisearch // ------------------------------------------------------------ func generateSearch(env *config.ResolvedEnvironment, search *config.SearchInfraConfig) ([]InfraManifest, error) { name := env.FullName + "-search" secretName := env.FullName + "-search-credentials" manifest := fmt.Sprintf(`apiVersion: apps/v1 kind: Deployment metadata: name: %s namespace: %s labels: app: %s managed-by: kforge spec: replicas: 1 selector: matchLabels: app: %s template: metadata: labels: app: %s spec: containers: - name: %s image: getmeili/meilisearch:latest ports: - containerPort: 7700 env: - name: MEILI_MASTER_KEY valueFrom: secretKeyRef: name: %s key: master_key - name: MEILI_ENV value: production resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 512Mi `, name, env.Namespace, env.FullName, name, name, name, secretName) svcManifest := fmt.Sprintf(`apiVersion: v1 kind: Service metadata: name: %s namespace: %s labels: app: %s managed-by: kforge spec: type: ClusterIP ports: - port: 7700 targetPort: 7700 selector: app: %s `, name, env.Namespace, env.FullName, name) envVars := []config.EnvVarConfig{ {Name: "SEARCH_URL", Type: config.EnvVarTypePlain, Value: fmt.Sprintf("http://%s:7700", name)}, {Name: "SEARCH_MASTER_KEY", Type: config.EnvVarTypeSecretRef, SecretName: secretName, SecretKey: "master_key"}, } return []InfraManifest{ {Name: "search", Content: manifest}, {Name: "search-svc", Content: svcManifest, EnvVars: envVars}, }, nil } // ------------------------------------------------------------ // Monitoring — Prometheus ServiceMonitor // ------------------------------------------------------------ func generateMonitoring(env *config.ResolvedEnvironment, mon *config.MonitoringInfraConfig) ([]InfraManifest, error) { port := env.Port if mon.MetricsPort != nil { port = *mon.MetricsPort } // ServiceMonitor CR (requires Prometheus Operator). manifest := fmt.Sprintf(`apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: %s-monitor namespace: %s labels: app: %s managed-by: kforge spec: selector: matchLabels: app: %s endpoints: - path: %s port: %d interval: 30s `, env.FullName, env.Namespace, env.FullName, env.FullName, mon.MetricsPath, port) return []InfraManifest{ {Name: "servicemonitor", Content: manifest}, }, nil }