Files
UmbraDatastore/scripts/deploy-database.sh
umbra audio cce2ce55dc Initial commit: UmbraDatastore configuration and deployment scripts
- Database deployment script (scripts/deploy-database.sh)
- Kubernetes manifests for CNPG PostgreSQL cluster (k8s/base/)
- Documentation (docs/DEPLOYMENT_GUIDE.md, DATASTORE_SPEC.md, INTEGRATION_SPEC.md)
- README with architecture overview and operations guide
2026-10-31 19:47:27 -07:00

351 lines
11 KiB
Bash

#!/usr/bin/env bash
# Copyright 2026 Umbra Tools (umbratools.com)
# SPDX-License-Identifier: Apache-2.0
# Deploy UmbraTrans PostgreSQL Database
# This script installs CNPG operator, creates secrets, deploys the cluster,
# runs migrations, and verifies the deployment.
set -euo pipefail
# Configuration
NAMESPACE="umbratrans"
CLUSTER_NAME="umbratrans-db"
CNPG_VERSION="1.24.0"
POSTGRES_VERSION="16.4"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $*"; }
log_warning() { echo -e "${YELLOW}[WARNING]${NC} $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
# Check prerequisites
check_prerequisites() {
log_info "Checking prerequisites..."
if ! command -v kubectl &> /dev/null; then
log_error "kubectl not found. Please install kubectl."
exit 1
fi
if ! kubectl cluster-info &> /dev/null; then
log_error "Cannot connect to Kubernetes cluster. Check kubeconfig."
exit 1
fi
log_success "Prerequisites check passed"
}
# Install CNPG operator
install_cnpg_operator() {
log_info "Installing CloudNativePG operator v${CNPG_VERSION}..."
if kubectl get crd clusters.postgresql.cnpg.io &> /dev/null; then
log_warning "CNPG operator already installed (CRD exists)"
return 0
fi
kubectl apply -f "https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-${CNPG_VERSION}/releases/cnpg-${CNPG_VERSION}.yaml"
log_info "Waiting for CNPG operator to be ready..."
kubectl wait --for=condition=Available deployment/cnpg-controller-manager \
-n cnpg-system --timeout=120s
log_success "CNPG operator installed"
}
# Create namespace
create_namespace() {
log_info "Creating namespace: ${NAMESPACE}"
kubectl create namespace "${NAMESPACE}" --dry-run=client -o yaml | kubectl apply -f -
log_success "Namespace ready"
}
# Generate secure passwords
generate_password() {
openssl rand -base64 32 | tr -d '\n'
}
# Create secrets
create_secrets() {
log_info "Creating database secrets..."
# Main database credentials
DB_PASSWORD=$(generate_password)
DB_DSN="postgresql://umbratrans:${DB_PASSWORD}@${CLUSTER_NAME}-rw:5432/umbratrans?sslmode=require"
kubectl create secret generic umbratrans-db-credentials \
-n "${NAMESPACE}" \
--from-literal=username=umbratrans \
--from-literal=password="${DB_PASSWORD}" \
--from-literal=dsn="${DB_DSN}" \
--dry-run=client -o yaml | kubectl apply -f -
# PgBouncer admin credentials
PGBOUNCER_PASSWORD=$(generate_password)
kubectl create secret generic umbratrans-pgbouncer-admin \
-n "${NAMESPACE}" \
--from-literal=username=pgbouncer_admin \
--from-literal=password="${PGBOUNCER_PASSWORD}" \
--dry-run=client -o yaml | kubectl apply -f -
# Application secrets (token pepper)
TOKEN_PEPPER=$(generate_password)
kubectl create secret generic umbratrans-secrets \
-n "${NAMESPACE}" \
--from-literal=token_pepper="${TOKEN_PEPPER}" \
--dry-run=client -o yaml | kubectl apply -f -
# SMTP credentials (placeholder - update with real values)
kubectl create secret generic umbratrans-smtp \
-n "${NAMESPACE}" \
--from-literal=host=smtp.example.com \
--from-literal=port=587 \
--from-literal=username=umbra@example.com \
--from-literal=password=CHANGE_ME \
--from-literal=sender=umbratrans@umbra.example.com \
--dry-run=client -o yaml | kubectl apply -f -
# Backup credentials (placeholder - update with real S3 credentials)
kubectl create secret generic umbratrans-backup-credentials \
-n "${NAMESPACE}" \
--from-literal=ACCESS_KEY_ID=CHANGE_ME \
--from-literal=SECRET_ACCESS_KEY=CHANGE_ME \
--dry-run=client -o yaml | kubectl apply -f -
log_success "Secrets created"
log_warning "Remember to update placeholder secrets (SMTP, S3) with real values!"
}
# Deploy database cluster
deploy_cluster() {
log_info "Deploying PostgreSQL cluster: ${CLUSTER_NAME}"
kubectl apply -f k8s/base/01-postgresql.yaml
log_info "Waiting for cluster to be ready (this may take several minutes)..."
kubectl wait --for=condition=Ready cluster/${CLUSTER_NAME} \
-n "${NAMESPACE}" --timeout=600s
log_success "PostgreSQL cluster is ready"
}
# Verify database connectivity
verify_database() {
log_info "Verifying database connectivity..."
# Get the primary pod
PRIMARY_POD=$(kubectl get pods -n "${NAMESPACE}" \
-l "postgresql=${CLUSTER_NAME},role=primary" \
-o jsonpath='{.items[0].metadata.name}')
if [[ -z "${PRIMARY_POD}" ]]; then
log_error "Could not find primary pod"
exit 1
fi
log_info "Primary pod: ${PRIMARY_POD}"
# Test connection
DB_PASSWORD=$(kubectl get secret umbratrans-db-credentials -n "${NAMESPACE}" -o jsonpath='{.data.password}' | base64 -d)
kubectl exec -n "${NAMESPACE}" "${PRIMARY_POD}" -- \
psql -U umbratrans -d umbratrans -c "SELECT version();" \
PGPASSWORD="${DB_PASSWORD}"
log_success "Database connection verified"
}
# Run database migrations
run_migrations() {
log_info "Running database migrations..."
# Create a job to run migrations
cat <<EOF | kubectl apply -f -
apiVersion: batch/v1
kind: Job
metadata:
name: umbratrans-migrate-$(date +%s)
namespace: ${NAMESPACE}
spec:
ttlSecondsAfterFinished: 3600
backoffLimit: 3
template:
spec:
restartPolicy: OnFailure
serviceAccountName: umbratrans-worker
containers:
- name: migrate
image: umbratrans-web:latest
imagePullPolicy: IfNotPresent
command: ["python", "-m", "umbratrans_web.cli", "migrate", "--allow-production"]
env:
- name: UMBRA_ENV
value: "production"
- name: UMBRATRANS_DATABASE_DSN
valueFrom:
secretKeyRef:
name: umbratrans-db-credentials
key: dsn
- name: UMBRATRANS_REPOSITORY
value: "postgres"
EOF
log_info "Waiting for migration job to complete..."
kubectl wait --for=condition=complete job -l job-name=umbratrans-migrate \
-n "${NAMESPACE}" --timeout=300s
# Check job status
JOB_STATUS=$(kubectl get jobs -n "${NAMESPACE}" -l job-name=umbratrans-migrate -o jsonpath='{.items[0].status.conditions[?(@.type=="Complete")].status}')
if [[ "${JOB_STATUS}" != "True" ]]; then
log_error "Migration job failed"
kubectl logs -n "${NAMESPACE}" -l job-name=umbratrans-migrate
exit 1
fi
log_success "Migrations completed successfully"
}
# Verify schema
verify_schema() {
log_info "Verifying database schema..."
PRIMARY_POD=$(kubectl get pods -n "${NAMESPACE}" \
-l "postgresql=${CLUSTER_NAME},role=primary" \
-o jsonpath='{.items[0].metadata.name}')
DB_PASSWORD=$(kubectl get secret umbratrans-db-credentials -n "${NAMESPACE}" -o jsonpath='{.data.password}' | base64 -d)
# Check key tables exist
kubectl exec -n "${NAMESPACE}" "${PRIMARY_POD}" -- \
psql -U umbratrans -d umbratrans -c "
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name IN ('users', 'organizations', 'projects', 'jobs', 'artifacts', 'compute_nodes');
" PGPASSWORD="${DB_PASSWORD}"
log_success "Schema verification passed"
}
# Create initial admin user (if needed)
create_admin_user() {
log_info "Creating initial admin user (if needed)..."
PRIMARY_POD=$(kubectl get pods -n "${NAMESPACE}" \
-l "postgresql=${CLUSTER_NAME},role=primary" \
-o jsonpath='{.items[0].metadata.name}')
DB_PASSWORD=$(kubectl get secret umbratrans-db-credentials -n "${NAMESPACE}" -o jsonpath='{.data.password}' | base64 -d)
# Check if admin exists
ADMIN_EXISTS=$(kubectl exec -n "${NAMESPACE}" "${PRIMARY_POD}" -- \
psql -U umbratrans -d umbratrans -t -c "
SELECT COUNT(*) FROM users WHERE role = 'platform_admin';
" PGPASSWORD="${DB_PASSWORD}" | tr -d ' ')
if [[ "${ADMIN_EXISTS}" -eq 0 ]]; then
log_info "No admin user found. Creating bootstrap admin..."
ADMIN_PASSWORD=$(generate_password)
# Run bootstrap admin creation
cat <<EOF | kubectl apply -f -
apiVersion: batch/v1
kind: Job
metadata:
name: umbratrans-bootstrap-admin-$(date +%s)
namespace: ${NAMESPACE}
spec:
ttlSecondsAfterFinished: 3600
backoffLimit: 1
template:
spec:
restartPolicy: OnFailure
containers:
- name: bootstrap
image: umbratrans-web:latest
imagePullPolicy: IfNotPresent
command: ["python", "-m", "umbratrans_web.cli", "bootstrap-admin"]
env:
- name: UMBRA_ENV
value: "production"
- name: UMBRATRANS_DATABASE_DSN
valueFrom:
secretKeyRef:
name: umbratrans-db-credentials
key: dsn
- name: UMBRATRANS_REPOSITORY
value: "postgres"
stdin: true
stdinOnce: true
EOF
log_warning "Bootstrap admin job created. Check logs for the one-time password."
log_warning "Run: kubectl logs -n ${NAMESPACE} -l job-name=umbratrans-bootstrap-admin -f"
else
log_info "Admin user already exists"
fi
}
# Display connection info
show_connection_info() {
log_info "Database deployment complete!"
echo
echo "Connection Information:"
echo " Cluster: ${CLUSTER_NAME}"
echo " Namespace: ${NAMESPACE}"
echo " Primary Service: ${CLUSTER_NAME}-rw.${NAMESPACE}.svc.cluster.local:5432"
echo " Replica Service: ${CLUSTER_NAME}-ro.${NAMESPACE}.svc.cluster.local:5432"
echo " PgBouncer Service: ${CLUSTER_NAME}-pgbouncer.${NAMESPACE}.svc.cluster.local:6432"
echo
echo "Database: umbratrans"
echo "User: umbratrans"
echo "Password: (stored in secret umbratrans-db-credentials)"
echo
echo "Connection string (internal):"
echo " postgresql://umbratrans:<password>@${CLUSTER_NAME}-rw.${NAMESPACE}.svc.cluster.local:5432/umbratrans?sslmode=require"
echo
echo "Connection string (via PgBouncer):"
echo " postgresql://umbratrans:<password>@${CLUSTER_NAME}-pgbouncer.${NAMESPACE}.svc.cluster.local:6432/umbratrans?sslmode=require"
echo
echo "To connect from outside the cluster:"
echo " kubectl port-forward -n ${NAMESPACE} svc/${CLUSTER_NAME}-rw 5432:5432"
echo " Then connect to localhost:5432"
echo
echo "To check cluster status:"
echo " kubectl get cluster ${CLUSTER_NAME} -n ${NAMESPACE}"
echo
echo "To view logs:"
echo " kubectl logs -n ${NAMESPACE} -l postgresql=${CLUSTER_NAME},role=primary -f"
}
# Main deployment flow
main() {
log_info "Starting UmbraTrans database deployment"
echo
check_prerequisites
install_cnpg_operator
create_namespace
create_secrets
deploy_cluster
verify_database
run_migrations
verify_schema
create_admin_user
show_connection_info
log_success "Database deployment completed successfully!"
}
# Run main if script is executed directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi