diff --git a/docs/pages/quickstarts/kubernetes.mdx b/docs/pages/quickstarts/kubernetes.mdx index e76a81318..18d74cf15 100644 --- a/docs/pages/quickstarts/kubernetes.mdx +++ b/docs/pages/quickstarts/kubernetes.mdx @@ -35,7 +35,7 @@ Local Kubernetes setup for Outpost using Minikube. This setup includes: 3. Install dependencies: ```sh - cd outpost/deployments/kubernetes + cd outpost/examples/kubernetes ./setup-dependencies.sh ``` @@ -48,7 +48,7 @@ Local Kubernetes setup for Outpost using Minikube. This setup includes: ::: ```sh - helm install outpost ../../deployments/kubernetes/charts/outpost -f values.yaml + helm install outpost ../../deployments/kubernetes/charts/outpost -f outpost.yaml ``` ## Verify Installation diff --git a/examples/kubernetes/values.yaml b/examples/kubernetes/outpost.yaml similarity index 100% rename from examples/kubernetes/values.yaml rename to examples/kubernetes/outpost.yaml diff --git a/examples/kubernetes/postgresql.yaml b/examples/kubernetes/postgresql.yaml new file mode 100644 index 000000000..8c08d9f4c --- /dev/null +++ b/examples/kubernetes/postgresql.yaml @@ -0,0 +1,84 @@ +apiVersion: v1 +kind: Service +metadata: + name: outpost-postgresql +spec: + type: ClusterIP + ports: + - port: 5432 + targetPort: 5432 + selector: + app: outpost-postgresql +--- +apiVersion: v1 +kind: Service +metadata: + name: outpost-postgresql-headless +spec: + clusterIP: None + ports: + - port: 5432 + targetPort: 5432 + selector: + app: outpost-postgresql +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: outpost-postgresql +spec: + serviceName: outpost-postgresql-headless + replicas: 1 + selector: + matchLabels: + app: outpost-postgresql + template: + metadata: + labels: + app: outpost-postgresql + spec: + containers: + - name: postgresql + image: postgres:16-alpine + env: + - name: POSTGRES_USER + value: "outpost" + - name: POSTGRES_DB + value: "outpost" + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: outpost-postgresql + key: password + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + ports: + - containerPort: 5432 + name: postgresql + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + livenessProbe: + exec: + command: + - pg_isready + - -U + - outpost + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + exec: + command: + - pg_isready + - -U + - outpost + initialDelaySeconds: 5 + periodSeconds: 5 + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 8Gi \ No newline at end of file diff --git a/examples/kubernetes/rabbitmq.yaml b/examples/kubernetes/rabbitmq.yaml new file mode 100644 index 000000000..5df6a8e54 --- /dev/null +++ b/examples/kubernetes/rabbitmq.yaml @@ -0,0 +1,93 @@ +apiVersion: v1 +kind: Service +metadata: + name: outpost-rabbitmq +spec: + type: ClusterIP + ports: + - name: amqp + port: 5672 + targetPort: 5672 + - name: management + port: 15672 + targetPort: 15672 + selector: + app: outpost-rabbitmq +--- +apiVersion: v1 +kind: Service +metadata: + name: outpost-rabbitmq-headless +spec: + type: ClusterIP + clusterIP: None + ports: + - name: amqp + port: 5672 + targetPort: 5672 + selector: + app: outpost-rabbitmq +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: outpost-rabbitmq +spec: + serviceName: outpost-rabbitmq-headless + replicas: 1 + selector: + matchLabels: + app: outpost-rabbitmq + template: + metadata: + labels: + app: outpost-rabbitmq + spec: + containers: + - name: rabbitmq + image: rabbitmq:3.13-management-alpine + env: + - name: RABBITMQ_DEFAULT_USER + value: "outpost" + - name: RABBITMQ_DEFAULT_PASS + valueFrom: + secretKeyRef: + name: outpost-rabbitmq + key: rabbitmq-password + - name: RABBITMQ_ERLANG_COOKIE + valueFrom: + secretKeyRef: + name: outpost-rabbitmq + key: rabbitmq-erlang-cookie + ports: + - containerPort: 5672 + name: amqp + - containerPort: 15672 + name: management + volumeMounts: + - name: data + mountPath: /var/lib/rabbitmq + livenessProbe: + exec: + command: + - rabbitmq-diagnostics + - ping + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 10 + readinessProbe: + exec: + command: + - rabbitmq-diagnostics + - check_port_connectivity + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 5 + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 8Gi \ No newline at end of file diff --git a/examples/kubernetes/redis.yaml b/examples/kubernetes/redis.yaml new file mode 100644 index 000000000..0ec221a36 --- /dev/null +++ b/examples/kubernetes/redis.yaml @@ -0,0 +1,70 @@ +apiVersion: v1 +kind: Service +metadata: + name: outpost-redis-master +spec: + type: ClusterIP + ports: + - port: 6379 + targetPort: 6379 + selector: + app: outpost-redis +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: outpost-redis +spec: + serviceName: outpost-redis-master + replicas: 1 + selector: + matchLabels: + app: outpost-redis + template: + metadata: + labels: + app: outpost-redis + spec: + containers: + - name: redis + image: redis:7-alpine + command: + - redis-server + - --requirepass + - $(REDIS_PASSWORD) + env: + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: outpost-redis + key: redis-password + ports: + - containerPort: 6379 + name: redis + volumeMounts: + - name: data + mountPath: /data + livenessProbe: + exec: + command: + - sh + - -c + - redis-cli -a "${REDIS_PASSWORD}" ping + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + exec: + command: + - sh + - -c + - redis-cli -a "${REDIS_PASSWORD}" ping + initialDelaySeconds: 5 + periodSeconds: 5 + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 8Gi \ No newline at end of file diff --git a/examples/kubernetes/setup-dependencies.sh b/examples/kubernetes/setup-dependencies.sh old mode 100644 new mode 100755 index 14a5bde27..72dfad71e --- a/examples/kubernetes/setup-dependencies.sh +++ b/examples/kubernetes/setup-dependencies.sh @@ -3,68 +3,206 @@ set -e # Exit on any error echo "🚀 Setting up Outpost dependencies..." -# Helper function to check if helm release exists -helm_release_exists() { - helm list | grep -q "^$1" +# Helper function to print pod diagnostics +print_pod_diagnostics() { + local pod_name=$1 + local resource_name=$2 + local cleanup_commands=$3 + + echo "" + echo "❌ $resource_name pod failed to become ready!" + echo "" + echo "Pod status:" + kubectl get pod "$pod_name" + echo "" + echo "Recent events:" + kubectl describe pod "$pod_name" | grep -A 10 "Events:" || true + echo "" + echo "Container logs (last 20 lines):" + kubectl logs "$pod_name" --tail=20 2>/dev/null || echo " (no logs available)" + echo "" + echo "⚠️ To fix this issue, clean up and re-run:" + echo "$cleanup_commands" + echo "" } -# Helper function to check if secret exists -secret_exists() { - kubectl get secret "$1" >/dev/null 2>&1 +# Helper function to check if pod is healthy +pod_is_healthy() { + local pod_name=$1 + if kubectl get pod "$pod_name" >/dev/null 2>&1; then + local status=$(kubectl get pod "$pod_name" -o jsonpath='{.status.phase}') + local ready=$(kubectl get pod "$pod_name" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}') + + # Check init containers for ERROR states only (not normal waiting states like "PodInitializing") + local init_container_status=$(kubectl get pod "$pod_name" -o jsonpath='{.status.initContainerStatuses[*].state.waiting.reason}' 2>/dev/null) + if [[ "$init_container_status" =~ (ImagePullBackOff|ErrImagePull|CrashLoopBackOff) ]]; then + return 1 + fi + + # Check main containers for error states + local container_status=$(kubectl get pod "$pod_name" -o jsonpath='{.status.containerStatuses[0].state}' 2>/dev/null) + if echo "$container_status" | grep -q "waiting"; then + local waiting_reason=$(kubectl get pod "$pod_name" -o jsonpath='{.status.containerStatuses[0].state.waiting.reason}') + if [[ "$waiting_reason" =~ (ImagePullBackOff|ErrImagePull|CrashLoopBackOff) ]]; then + return 1 + fi + fi + + [[ "$status" == "Running" && "$ready" == "True" ]] + else + return 1 + fi } -# Add Bitnami repo if not exists -if ! helm repo list | grep -q "bitnami"; then - echo "📦 Adding Bitnami Helm repo..." - helm repo add bitnami https://charts.bitnami.com/bitnami >/dev/null - helm repo update >/dev/null -fi +# Helper function to wait for pod with periodic error checking +wait_for_pod_ready() { + local pod_name=$1 + local timeout=$2 + local elapsed=0 + local check_interval=5 + + while [ $elapsed -lt $timeout ]; do + # Check if pod is ready + if kubectl wait --for=condition=ready "pod/$pod_name" --timeout=${check_interval}s 2>/dev/null; then + return 0 + fi + + # Check for actual error states only (not just "not ready yet") + if kubectl get pod "$pod_name" >/dev/null 2>&1; then + local init_container_status=$(kubectl get pod "$pod_name" -o jsonpath='{.status.initContainerStatuses[*].state.waiting.reason}' 2>/dev/null) + if [[ "$init_container_status" =~ (ImagePullBackOff|ErrImagePull|CrashLoopBackOff) ]]; then + return 1 + fi + + local container_status=$(kubectl get pod "$pod_name" -o jsonpath='{.status.containerStatuses[0].state.waiting.reason}' 2>/dev/null) + if [[ "$container_status" =~ (ImagePullBackOff|ErrImagePull|CrashLoopBackOff) ]]; then + return 1 + fi + fi + + elapsed=$((elapsed + check_interval)) + done + + return 1 +} + +echo "🚀 Using official Docker images for improved reliability" +echo "" + +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# Install PostgreSQL with custom config -if ! helm_release_exists "outpost-postgresql"; then - echo "🐘 Installing PostgreSQL..." - helm install outpost-postgresql bitnami/postgresql \ - --set auth.username=outpost \ - --set auth.database=outpost \ - --set auth.postgresPassword="" >/dev/null +# Install PostgreSQL using direct Kubernetes manifests +if ! kubectl get statefulset outpost-postgresql >/dev/null 2>&1; then + echo "🐘 Installing PostgreSQL (using official postgres:16-alpine image)..." + + # Generate a random password + POSTGRES_PASSWORD=$(openssl rand -base64 24) + + # Create PostgreSQL secret + kubectl create secret generic outpost-postgresql \ + --from-literal=password="$POSTGRES_PASSWORD" \ + --dry-run=client -o yaml | kubectl apply -f - >/dev/null + + # Apply PostgreSQL manifests + kubectl apply -f "$SCRIPT_DIR/postgresql.yaml" >/dev/null + + echo "⏳ Waiting for PostgreSQL to be ready (timeout: 120s)..." + if ! wait_for_pod_ready "outpost-postgresql-0" 120; then + print_pod_diagnostics "outpost-postgresql-0" "PostgreSQL" \ +" kubectl delete -f $SCRIPT_DIR/postgresql.yaml + kubectl delete pvc data-outpost-postgresql-0 + kubectl delete secret outpost-postgresql" + exit 1 + fi +elif ! pod_is_healthy "outpost-postgresql-0"; then + print_pod_diagnostics "outpost-postgresql-0" "PostgreSQL" \ +" kubectl delete -f $SCRIPT_DIR/postgresql.yaml + kubectl delete pvc data-outpost-postgresql-0 + kubectl delete secret outpost-postgresql" + exit 1 else - echo "🐘 PostgreSQL already installed, skipping..." + echo "🐘 PostgreSQL already installed and healthy, skipping..." fi -echo "⏳ Waiting for PostgreSQL to be ready..." -kubectl wait --for=condition=ready pod/outpost-postgresql-0 --timeout=120s >/dev/null 2>&1 POSTGRES_PASSWORD=$(kubectl get secret outpost-postgresql -o jsonpath="{.data.password}" | base64 -d) POSTGRES_URL="postgresql://outpost:${POSTGRES_PASSWORD}@outpost-postgresql:5432/outpost?sslmode=disable" -# Install Redis -if ! helm_release_exists "outpost-redis"; then - echo "🔴 Installing Redis..." - helm install outpost-redis bitnami/redis >/dev/null +# Install Redis using direct Kubernetes manifests +if ! kubectl get statefulset outpost-redis >/dev/null 2>&1; then + echo "🔴 Installing Redis (using official redis:7-alpine image)..." + + # Generate a random password + REDIS_PASSWORD=$(openssl rand -base64 24) + + # Create Redis secret + kubectl create secret generic outpost-redis \ + --from-literal=redis-password="$REDIS_PASSWORD" \ + --dry-run=client -o yaml | kubectl apply -f - >/dev/null + + # Apply Redis manifests + kubectl apply -f "$SCRIPT_DIR/redis.yaml" >/dev/null + + echo "⏳ Waiting for Redis to be ready (timeout: 120s)..." + if ! wait_for_pod_ready "outpost-redis-0" 120; then + print_pod_diagnostics "outpost-redis-0" "Redis" \ +" kubectl delete -f $SCRIPT_DIR/redis.yaml + kubectl delete pvc data-outpost-redis-0 + kubectl delete secret outpost-redis" + exit 1 + fi +elif ! pod_is_healthy "outpost-redis-0"; then + print_pod_diagnostics "outpost-redis-0" "Redis" \ +" kubectl delete -f $SCRIPT_DIR/redis.yaml + kubectl delete pvc data-outpost-redis-0 + kubectl delete secret outpost-redis" + exit 1 else - echo "🔴 Redis already installed, skipping..." + echo "🔴 Redis already installed and healthy, skipping..." fi -echo "⏳ Waiting for Redis to be ready..." -kubectl wait --for=condition=ready pod/outpost-redis-master-0 --timeout=120s >/dev/null 2>&1 REDIS_PASSWORD=$(kubectl get secret outpost-redis -o jsonpath="{.data.redis-password}" | base64 -d) -# Install RabbitMQ with custom config -if ! helm_release_exists "outpost-rabbitmq"; then - echo "🐰 Installing RabbitMQ..." - helm install outpost-rabbitmq bitnami/rabbitmq \ - --set auth.username=outpost \ - --set auth.password="" >/dev/null +# Install RabbitMQ using direct Kubernetes manifests +if ! kubectl get statefulset outpost-rabbitmq >/dev/null 2>&1; then + echo "🐰 Installing RabbitMQ (using official rabbitmq:3.13-management-alpine image)..." + + # Generate passwords + RABBITMQ_PASSWORD=$(openssl rand -base64 24) + RABBITMQ_ERLANG_COOKIE=$(openssl rand -base64 48) + + # Create RabbitMQ secret + kubectl create secret generic outpost-rabbitmq \ + --from-literal=rabbitmq-password="$RABBITMQ_PASSWORD" \ + --from-literal=rabbitmq-erlang-cookie="$RABBITMQ_ERLANG_COOKIE" \ + --dry-run=client -o yaml | kubectl apply -f - >/dev/null + + # Apply RabbitMQ manifests + kubectl apply -f "$SCRIPT_DIR/rabbitmq.yaml" >/dev/null + + echo "⏳ Waiting for RabbitMQ to be ready (timeout: 120s)..." + if ! wait_for_pod_ready "outpost-rabbitmq-0" 120; then + print_pod_diagnostics "outpost-rabbitmq-0" "RabbitMQ" \ +" kubectl delete -f $SCRIPT_DIR/rabbitmq.yaml + kubectl delete pvc data-outpost-rabbitmq-0 + kubectl delete secret outpost-rabbitmq" + exit 1 + fi +elif ! pod_is_healthy "outpost-rabbitmq-0"; then + print_pod_diagnostics "outpost-rabbitmq-0" "RabbitMQ" \ +" kubectl delete -f $SCRIPT_DIR/rabbitmq.yaml + kubectl delete pvc data-outpost-rabbitmq-0 + kubectl delete secret outpost-rabbitmq" + exit 1 else - echo "🐰 RabbitMQ already installed, skipping..." + echo "🐰 RabbitMQ already installed and healthy, skipping..." fi -echo "⏳ Waiting for RabbitMQ to be ready..." -kubectl wait --for=condition=ready pod/outpost-rabbitmq-0 --timeout=120s >/dev/null 2>&1 RABBITMQ_PASSWORD=$(kubectl get secret outpost-rabbitmq -o jsonpath="{.data.rabbitmq-password}" | base64 -d) RABBITMQ_SERVER_URL="amqp://outpost:${RABBITMQ_PASSWORD}@outpost-rabbitmq:5672" # Generate application secrets echo "🔑 Generating application secrets..." -API_KEY=$(openssl rand -hex 16) -API_JWT_SECRET=$(openssl rand -hex 32) -AES_ENCRYPTION_SECRET=$(openssl rand -hex 32) +API_KEY=$(openssl rand -base64 24) +API_JWT_SECRET=$(openssl rand -base64 48) +AES_ENCRYPTION_SECRET=$(openssl rand -base64 48) # Create or update Kubernetes secret echo "🔒 Creating/updating Kubernetes secrets..."