@@ -2,7 +2,6 @@ package generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"kforge/internal/config"
|
||||
"kforge/pkg/interpolate"
|
||||
@@ -79,18 +78,22 @@ type InfraManifest struct {
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Database — CNPG
|
||||
// Database — CNPG (centralized cluster)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
func generateDatabase(env *config.ResolvedEnvironment, db *config.DatabaseInfraConfig) ([]InfraManifest, error) {
|
||||
dbName := db.DatabaseName
|
||||
if dbName == "" {
|
||||
dbName = interpolate.Slug(env.FullName)
|
||||
dbName = interpolate.PGIdentifier(env.FullName)
|
||||
}
|
||||
roleName := dbName + "_role"
|
||||
// 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
|
||||
// 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:
|
||||
@@ -103,47 +106,96 @@ spec:
|
||||
name: %s
|
||||
owner: %s
|
||||
cluster:
|
||||
name: cnpg-main
|
||||
`, dbName, env.Namespace, env.FullName, dbName, roleName)
|
||||
name: %s
|
||||
`, dbName, env.Namespace, env.FullName, dbName, pgUser, env.CNPGClusterName)
|
||||
|
||||
// CNPG Role CR — CNPG creates and rotates the password,
|
||||
// storing it in the secret named below.
|
||||
roleManifest := fmt.Sprintf(`apiVersion: postgresql.cnpg.io/v1
|
||||
kind: DatabaseRole
|
||||
// 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
|
||||
name: %s-db-init
|
||||
namespace: %s
|
||||
labels:
|
||||
app: %s
|
||||
managed-by: kforge
|
||||
spec:
|
||||
name: %s
|
||||
passwordSecret:
|
||||
name: %s
|
||||
login: true
|
||||
superuser: false
|
||||
createdb: false
|
||||
`, roleName, env.Namespace, env.FullName, roleName, secretName)
|
||||
|
||||
// The env vars reference the CNPG-managed secret.
|
||||
// CNPG populates: username, password keys in the secret.
|
||||
// We assemble DATABASE_URL from the known CNPG host + db name.
|
||||
cnpgHost := env.CNPGHost
|
||||
dbURL := fmt.Sprintf("postgresql://$(%s_USER):$(%s_PASSWORD)@%s/%s",
|
||||
strings.ToUpper(env.FullName), strings.ToUpper(env.FullName), cnpgHost, dbName)
|
||||
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: dbURL},
|
||||
{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-role", Content: roleManifest},
|
||||
{Name: "cnpg-db-init", Content: jobManifest},
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user