box/internal/app/db_provisioning.go
2026-05-14 22:29:28 +02:00

84 lines
2.3 KiB
Go

package app
import (
"context"
"database/sql"
"fmt"
"net/url"
"strings"
)
func (a *App) ensureProjectDatabase(ctx context.Context, p *Project) error {
if !a.cfg.DBProvisionEnabled {
return nil
}
adminDB, err := sql.Open("pgx", a.cfg.DBProvisionAdminURL)
if err != nil {
return fmt.Errorf("db provisioning connect failed: %w", err)
}
defer adminDB.Close()
if err := adminDB.PingContext(ctx); err != nil {
return fmt.Errorf("db provisioning ping failed: %w", err)
}
if p.DBName == "" {
p.DBName = fmt.Sprintf("app_%d", p.ID)
}
if p.DBUser == "" {
p.DBUser = fmt.Sprintf("appu_%d", p.ID)
}
if p.DBPassword == "" {
pw, err := newDeployToken()
if err != nil {
return err
}
p.DBPassword = pw
}
if _, err := adminDB.ExecContext(ctx, fmt.Sprintf(`DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = %s) THEN
CREATE ROLE %s LOGIN PASSWORD %s;
ELSE
ALTER ROLE %s LOGIN PASSWORD %s;
END IF;
END $$;`, sqlQuoteLiteral(p.DBUser), sqlQuoteIdent(p.DBUser), sqlQuoteLiteral(p.DBPassword), sqlQuoteIdent(p.DBUser), sqlQuoteLiteral(p.DBPassword))); err != nil {
return fmt.Errorf("create/alter role failed: %w", err)
}
var dbExists bool
if err := adminDB.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM pg_database WHERE datname = $1)`, p.DBName).Scan(&dbExists); err != nil {
return fmt.Errorf("check database existence failed: %w", err)
}
if !dbExists {
if _, err := adminDB.ExecContext(ctx, fmt.Sprintf(`CREATE DATABASE %s OWNER %s`, sqlQuoteIdent(p.DBName), sqlQuoteIdent(p.DBUser))); err != nil {
return fmt.Errorf("create database failed: %w", err)
}
}
if _, err := a.db.ExecContext(ctx, `UPDATE projects SET db_name=$1, db_user=$2, db_password=$3 WHERE id=$4`, p.DBName, p.DBUser, p.DBPassword, p.ID); err != nil {
return fmt.Errorf("persist db credentials failed: %w", err)
}
return nil
}
func (a *App) projectDatabaseURL(p Project) (string, error) {
if p.DBName == "" || p.DBUser == "" || p.DBPassword == "" {
return "", nil
}
base, err := url.Parse(a.cfg.DBProvisionAdminURL)
if err != nil {
return "", err
}
base.User = url.UserPassword(p.DBUser, p.DBPassword)
base.Path = "/" + p.DBName
return base.String(), nil
}
func sqlQuoteIdent(s string) string {
return `"` + strings.ReplaceAll(s, `"`, `""`) + `"`
}
func sqlQuoteLiteral(s string) string {
return `'` + strings.ReplaceAll(s, `'`, `''`) + `'`
}