386 lines
13 KiB
Bash
386 lines
13 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"
|
|
REGISTRY="192.168.123.18:3001"
|
|
IMAGE_NAME="umbragit/umbradatastore"
|
|
IMAGE_TAG="latest"
|
|
|
|
# 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"
|
|
}
|
|
|
|
# Build and push database container image
|
|
build_database_image() {
|
|
log_info "Building database container image: ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}"
|
|
|
|
# Check if Docker is available
|
|
if ! command -v docker &> /dev/null; then
|
|
log_error "docker not found. Please install Docker."
|
|
exit 1
|
|
fi
|
|
|
|
# Build the image
|
|
log_info "Building Docker image..."
|
|
docker build -t "${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" \
|
|
-t "${REGISTRY}/${IMAGE_NAME}:${POSTGRES_VERSION}" \
|
|
-f Dockerfile \
|
|
/home/umbra/src/UmbraDatastore
|
|
|
|
# Try to push to registry (requires Docker daemon configured for insecure registry)
|
|
log_info "Pushing image to registry: ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}"
|
|
log_warning "Note: Requires Docker daemon configured with 'insecure-registries: [\"192.168.123.18:3001\"]' in /etc/docker/daemon.json"
|
|
|
|
if docker push "${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" && \
|
|
docker push "${REGISTRY}/${IMAGE_NAME}:${POSTGRES_VERSION}"; then
|
|
log_success "Image pushed successfully"
|
|
else
|
|
log_warning "Failed to push image to registry. This may be due to Docker daemon not configured for insecure registry."
|
|
log_warning "To fix: Add 'insecure-registries: [\"192.168.123.18:3001\"]' to /etc/docker/daemon.json and restart Docker"
|
|
log_warning "Then run: docker push ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} && docker push ${REGISTRY}/${IMAGE_NAME}:${POSTGRES_VERSION}"
|
|
fi
|
|
}
|
|
|
|
# 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
|
|
build_database_image
|
|
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 |