#!/usr/bin/env bash set -euo pipefail # runmefirst.sh — Full CI/CD pipeline infrastructure install # Installs: Gitea config → Woodpecker CI → ArgoCD → Kargo # Persists state on NFS volumes at nfs://192.168.42.8:/volume1/Kubernetes/pipelines SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" GITEA_AUTH="Authorization: Basic $(echo -n 'admin:St0nks0racl3!' | base64)" GITEA_API="http://10.1.1.12:30300/api/v1" # ------------------------------------------------------- # 1. Create namespaces # ------------------------------------------------------- echo "--- Step 1: Creating namespaces ---" for ns in woodpecker argocd kargo stonks-beta stonks-paper harbor-service; do kubectl create namespace "$ns" --dry-run=client -o yaml | kubectl apply -f - echo " ✓ namespace/$ns" done echo "" # ------------------------------------------------------- # 2. Ensure proxy-ca-cert ConfigMap + Kyverno policies # ------------------------------------------------------- echo "--- Step 2: Proxy CA cert and Kyverno policies ---" CA_CERT_PATH="${SCRIPT_DIR}/home.crt" curl -sf http://192.168.42.1/home.crt -o "$CA_CERT_PATH" for ns in woodpecker argocd kargo harbor-service; do if ! kubectl get configmap proxy-ca-cert -n "$ns" > /dev/null 2>&1; then kubectl create configmap proxy-ca-cert --from-file=ca.crt="$CA_CERT_PATH" -n "$ns" echo " ✓ proxy-ca-cert created in $ns" else echo " ✓ proxy-ca-cert already exists in $ns" fi done # Apply Kyverno policy BEFORE Woodpecker install so pods get injected on creation kubectl apply -f woodpecker/kyverno-proxy-ca.yaml echo " ✓ Kyverno woodpecker-proxy-ca policy applied" # Docker Hub auth for builder pods (avoids rate limits) if ! kubectl get secret dockerhub-config -n woodpecker > /dev/null 2>&1; then kubectl create secret generic dockerhub-config -n woodpecker \ --from-literal=config.json='{"auths":{"https://index.docker.io/v1/":{"auth":"'"$(echo -n 'celesrenata:dckr_pat_rDJs5PbzGll_jyFyL9_NGEk_bJI' | base64)"'"}}}' echo " ✓ dockerhub-config secret created" else echo " ✓ dockerhub-config secret already exists" fi echo "" # ------------------------------------------------------- # 3. Apply NFS PersistentVolumes # ------------------------------------------------------- echo "--- Step 3: Applying NFS PersistentVolumes ---" kubectl apply -f pvs/argocd-pv.yaml kubectl apply -f pvs/kargo-pv.yaml kubectl apply -f pvs/woodpecker-pv.yaml kubectl apply -f pvs/harbor-pv.yaml echo " ✓ PVs applied" echo "" # ------------------------------------------------------- # 3b. Install Harbor container registry # ------------------------------------------------------- echo "--- Step 3b: Installing Harbor ---" kubectl create namespace harbor-service --dry-run=client -o yaml | kubectl apply -f - # Remove old plain Docker Registry ingress (registry.celestium.life) if it exists # Harbor will take over that domain if kubectl get ingress registry-ingress -n git-server > /dev/null 2>&1; then echo " Removing old registry ingress from git-server namespace..." kubectl delete ingress registry-ingress -n git-server echo " ✓ Old registry ingress removed" fi # Create NFS directories on the NAS (via a temporary pod) echo " Ensuring NFS directories exist..." ssh root@gremlin-1 " mkdir -p /tmp/harbor-nfs-init mount -t nfs 192.168.42.8:/volume1/Kubernetes/harbor /tmp/harbor-nfs-init 2>/dev/null || true mkdir -p /tmp/harbor-nfs-init/data/registry mkdir -p /tmp/harbor-nfs-init/data/database mkdir -p /tmp/harbor-nfs-init/data/redis mkdir -p /tmp/harbor-nfs-init/data/jobservice mkdir -p /tmp/harbor-nfs-init/data/trivy umount /tmp/harbor-nfs-init 2>/dev/null || true rmdir /tmp/harbor-nfs-init 2>/dev/null || true " 2>/dev/null || echo " ⚠ Could not create NFS dirs via SSH (non-fatal, they may already exist)" # Apply PVCs kubectl apply -f harbor/pvcs.yaml echo " ✓ Harbor PVCs applied" # Install/upgrade Harbor via Helm helm repo add harbor https://helm.goharbor.io 2>/dev/null || true helm repo update harbor 2>/dev/null || true HARBOR_EXISTS=$(helm list -n harbor-service -q 2>/dev/null | grep -c harbor || true) if [ "${HARBOR_EXISTS:-0}" -gt 0 ]; then echo " Harbor already installed — upgrading..." else echo " Fresh Harbor install..." fi helm upgrade --install harbor harbor/harbor \ --namespace harbor-service \ --values harbor/values.yaml \ --timeout 10m \ --wait echo " Waiting for Harbor core to be ready..." kubectl wait --for=condition=ready pod -l app=harbor,component=core -n harbor-service --timeout=180s > /dev/null 2>&1 || true echo " ✓ Harbor installed at https://registry.celestium.life" echo " Default login: admin / St0nks0racl3!" echo "" # ------------------------------------------------------- # 4. Configure Gitea (admin user, repo, webhook config) # ------------------------------------------------------- echo "--- Step 4: Configuring Gitea ---" bash gitea/setup.sh echo " ✓ Gitea configured" GITEA_POD=$(kubectl get pods -n git-server -l app=gitea -o jsonpath='{.items[0].metadata.name}') if ! kubectl exec -n git-server "$GITEA_POD" -- grep -q '\[webhook\]' /data/gitea/conf/app.ini 2>/dev/null; then kubectl exec -n git-server "$GITEA_POD" -- sh -c 'printf "\n[webhook]\nALLOWED_HOST_LIST = *\nSKIP_TLS_VERIFY = true\n" >> /data/gitea/conf/app.ini' kubectl rollout restart deployment/gitea -n git-server kubectl rollout status deployment/gitea -n git-server --timeout=60s echo " ✓ Gitea webhook config added" else echo " ✓ Gitea webhook config already present" fi echo "" # ------------------------------------------------------- # 5. Install Woodpecker CI via Helm # ------------------------------------------------------- echo "--- Step 5: Installing Woodpecker CI ---" WOODPECKER_EXISTS=$(helm list -n woodpecker -q 2>/dev/null | grep -c woodpecker || true) if [ "${WOODPECKER_EXISTS:-0}" -gt 0 ]; then echo " Woodpecker already installed — upgrading (preserving OAuth2 grants)..." helm upgrade woodpecker oci://ghcr.io/woodpecker-ci/helm/woodpecker \ --namespace woodpecker \ --values woodpecker/values.yaml \ --timeout 5m else echo " Fresh Woodpecker install..." # Delete stale OAuth2 app in Gitea (if any) EXISTING_APP_ID=$(curl -s -H "$GITEA_AUTH" "$GITEA_API/user/applications/oauth2" | python3 -c ' import sys, json for app in json.loads(sys.stdin.read()): if app.get("name") == "woodpecker-ci": print(app["id"]) break ' 2>/dev/null || echo "") if [ -n "$EXISTING_APP_ID" ]; then curl -s -X DELETE -H "$GITEA_AUTH" "$GITEA_API/user/applications/oauth2/$EXISTING_APP_ID" > /dev/null echo " Deleted stale OAuth2 app (id=$EXISTING_APP_ID)" fi # Create fresh OAuth2 app OAUTH2_RESP=$(curl -s -X POST "$GITEA_API/user/applications/oauth2" \ -H "$GITEA_AUTH" -H "Content-Type: application/json" \ -d '{"name":"woodpecker-ci","redirect_uris":["https://stonks-ci.celestium.life/authorize"],"confidential_client":true}') GITEA_CLIENT_ID=$(echo "$OAUTH2_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['client_id'])") GITEA_CLIENT_SECRET=$(echo "$OAUTH2_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['client_secret'])") echo " ✓ OAuth2 app created (client_id: $GITEA_CLIENT_ID)" helm install woodpecker oci://ghcr.io/woodpecker-ci/helm/woodpecker \ --namespace woodpecker \ --values woodpecker/values.yaml \ --set server.env.WOODPECKER_GITEA_CLIENT="${GITEA_CLIENT_ID}" \ --set server.env.WOODPECKER_GITEA_SECRET="${GITEA_CLIENT_SECRET}" \ --timeout 5m fi # Wait for server to be ready (don't use --wait, agents may take longer) echo " Waiting for Woodpecker server..." kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=server -n woodpecker --timeout=120s > /dev/null 2>&1 || true echo " Waiting for Woodpecker agents..." kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=agent -n woodpecker --timeout=120s > /dev/null 2>&1 || true # Apply agent RBAC (grants cluster-admin to default + woodpecker-agent SAs) kubectl apply -f woodpecker/agent-rbac.yaml echo " ✓ Woodpecker CI installed + RBAC applied" # ------------------------------------------------------- # 5b. Activate repo in Woodpecker (if fresh install) # ------------------------------------------------------- if [ "${WOODPECKER_EXISTS:-0}" -eq 0 ]; then echo " Activating repo in Woodpecker..." # Wait for server to be ready kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=server -n woodpecker --timeout=60s > /dev/null 2>&1 # Port-forward to Woodpecker API kubectl port-forward -n woodpecker svc/woodpecker-server 18080:80 > /dev/null 2>&1 & PF_PID=$! sleep 5 # Login via OAuth2 to get a user token — use the Gitea token approach # Create a Gitea personal access token for API bootstrap GITEA_TOKEN_RESP=$(curl -s -X POST "$GITEA_API/users/admin/tokens" \ -H "$GITEA_AUTH" -H "Content-Type: application/json" \ -d '{"name":"woodpecker-bootstrap-'"$(date +%s)"'","scopes":["all"]}') GITEA_PAT=$(echo "$GITEA_TOKEN_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['sha1'])") # Use Woodpecker's OAuth2 flow to get a session — this is complex via CLI # Instead, just tell the user to activate via UI on first install echo " ⚠ First install: please log in to https://stonks-ci.celestium.life" echo " 1. Activate the admin/stonks-oracle repo" echo " 2. Mark it as trusted (Settings → General → Trusted)" echo " 3. Add 'github_ssh_key' secret (Settings → Secrets)" kill $PF_PID 2>/dev/null || true wait $PF_PID 2>/dev/null || true # Fix webhook to internal URL after user activates echo " After activating, run: bash pipelines/fix-webhook.sh" fi echo "" # ------------------------------------------------------- # 6. Install ArgoCD via Helm # ------------------------------------------------------- echo "--- Step 6: Installing ArgoCD ---" ARGOCD_EXISTS=$(helm list -n argocd -q 2>/dev/null | grep -c argocd || true) if [ "${ARGOCD_EXISTS:-0}" -eq 0 ]; then kubectl delete crd applications.argoproj.io applicationsets.argoproj.io appprojects.argoproj.io \ --ignore-not-found --timeout=30s > /dev/null 2>&1 || true kubectl delete sa --all -n argocd --ignore-not-found --timeout=10s > /dev/null 2>&1 || true kubectl delete role --all -n argocd --ignore-not-found --timeout=10s > /dev/null 2>&1 || true kubectl delete rolebinding --all -n argocd --ignore-not-found --timeout=10s > /dev/null 2>&1 || true fi helm repo add argo https://argoproj.github.io/argo-helm || true helm repo update helm upgrade --install argocd argo/argo-cd \ --namespace argocd \ --values argocd/values.yaml \ --wait --timeout 5m echo " ✓ ArgoCD installed" kubectl apply -f argocd/repo-secret.yaml kubectl apply -f argocd/apps/stonks-beta.yaml kubectl apply -f argocd/apps/stonks-paper.yaml kubectl apply -f argocd/apps/stonks-live.yaml echo " ✓ ArgoCD repo secret and Applications applied" echo "" # ------------------------------------------------------- # 6b. Install Argo Rollouts (CRDs + controller for Kargo verification) # ------------------------------------------------------- echo "--- Step 6b: Installing Argo Rollouts ---" kubectl create namespace argo-rollouts --dry-run=client -o yaml | kubectl apply -f - kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml kubectl rollout status deployment/argo-rollouts -n argo-rollouts --timeout=120s > /dev/null 2>&1 || true echo " ✓ Argo Rollouts installed (provides AnalysisRun CRD for Kargo verification)" echo "" # ------------------------------------------------------- # 6c. Seed secrets for beta, paper, and live namespaces # ------------------------------------------------------- echo "--- Step 6c: Seeding secrets for beta/paper/live ---" # These secrets are NOT managed by Helm (ArgoCD ignoreDifferences prevents overwrite). # Source credentials from the cluster's infrastructure services. REDIS_PW=$(kubectl get secret -n redis-service redis -o jsonpath='{.data.redis-password}' | base64 -d 2>/dev/null || echo "") MINIO_AK=$(kubectl get secret -n minio-service minio-secrets -o jsonpath='{.data.MINIO_ACCESS_KEY}' | base64 -d 2>/dev/null || echo "minioadmin") MINIO_SK=$(kubectl get secret -n minio-service minio-secrets -o jsonpath='{.data.MINIO_SECRET_KEY}' | base64 -d 2>/dev/null || echo "minioadmin") ALPACA_KEY=$(cat /root/sources/celesrenata/stonks-oracle/alpaca.key 2>/dev/null || echo "") ALPACA_SECRET=$(cat /root/sources/celesrenata/stonks-oracle/alpaca.secret 2>/dev/null || echo "") ALPACA_URL=$(cat /root/sources/celesrenata/stonks-oracle/alpaca.url 2>/dev/null || echo "https://paper-api.alpaca.markets") POLYGON_KEY=$(cat /root/sources/celesrenata/stonks-oracle/polygon.io.key 2>/dev/null || echo "") for ns in stonks-beta stonks-paper stonks-oracle; do kubectl create secret generic stonks-core-secrets \ --from-literal=POSTGRES_PASSWORD='St0nks0racl3!' \ --from-literal=REDIS_PASSWORD="$REDIS_PW" \ --from-literal=MINIO_ACCESS_KEY="$MINIO_AK" \ --from-literal=MINIO_SECRET_KEY="$MINIO_SK" \ -n "$ns" --dry-run=client -o yaml | kubectl apply -f - kubectl create secret generic stonks-broker-secrets \ --from-literal=BROKER_API_KEY="$ALPACA_KEY" \ --from-literal=BROKER_API_SECRET="$ALPACA_SECRET" \ --from-literal=BROKER_BASE_URL="$ALPACA_URL" \ -n "$ns" --dry-run=client -o yaml | kubectl apply -f - kubectl create secret generic stonks-market-secrets \ --from-literal=MARKET_DATA_API_KEY="$POLYGON_KEY" \ -n "$ns" --dry-run=client -o yaml | kubectl apply -f - kubectl create secret generic stonks-gmail-secrets \ --from-literal=GMAIL_SENDER='celes@celestium.life' \ --from-literal=GMAIL_RECIPIENT='celes@celestium.life' \ --from-literal=GMAIL_APP_PASSWORD='' \ -n "$ns" --dry-run=client -o yaml | kubectl apply -f - kubectl create secret generic stonks-dashboard-secrets \ --from-literal=SUPERSET_SECRET_KEY='stonks-superset-key' \ --from-literal=SUPERSET_ADMIN_PASSWORD='St0nks0racl3!' \ -n "$ns" --dry-run=client -o yaml | kubectl apply -f - echo " ✓ Secrets seeded in $ns" done echo "" # ------------------------------------------------------- # 6d. Create isolated databases for beta and paper stages # ------------------------------------------------------- echo "--- Step 6d: Creating stage-isolated databases ---" PG_POD=$(kubectl get pods -n postgresql-service -l cnpg.io/cluster=postgresql,role=primary -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "postgresql-1") # Create databases (idempotent) for db in stonks_beta stonks_paper; do kubectl exec -n postgresql-service "$PG_POD" -c postgres -- \ psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname='$db'" | grep -q 1 || \ kubectl exec -n postgresql-service "$PG_POD" -c postgres -- \ psql -U postgres -c "CREATE DATABASE $db OWNER stonks;" echo " ✓ Database $db exists" done # Run migrations on all three databases (idempotent — IF NOT EXISTS in DDL) REPO_ROOT="${SCRIPT_DIR}/../stonks-oracle" if [ -d "$REPO_ROOT/infra/migrations" ]; then for db in stonks stonks_beta stonks_paper; do echo " Running migrations on $db..." for migration in $(ls "$REPO_ROOT/infra/migrations/"*.sql 2>/dev/null | sort); do kubectl exec -i -n postgresql-service "$PG_POD" -c postgres -- \ psql -U postgres -d "$db" < "$migration" > /dev/null 2>&1 || true done echo " ✓ Migrations applied to $db" done else echo " ⚠ Migrations directory not found at $REPO_ROOT/infra/migrations — skipping" fi # Seed symbol registry data on all three databases if [ -d "$REPO_ROOT/services/symbol_registry" ]; then for db in stonks stonks_beta stonks_paper; do echo " Seeding $db..." POSTGRES_HOST=postgresql-rw.postgresql-service.svc.cluster.local \ POSTGRES_PASSWORD='St0nks0racl3!' POSTGRES_USER=stonks POSTGRES_DB="$db" \ python3 -m services.symbol_registry.seed 2>/dev/null || true echo " ✓ Seed data applied to $db" done fi echo "" # ------------------------------------------------------- # 7. Install Kargo via Helm # ------------------------------------------------------- echo "--- Step 7: Installing Kargo ---" KARGO_EXISTS=$(helm list -n kargo -q 2>/dev/null | grep -c kargo || true) if [ "${KARGO_EXISTS:-0}" -eq 0 ]; then kubectl delete crd freights.kargo.akuity.io projects.kargo.akuity.io stages.kargo.akuity.io \ warehouses.kargo.akuity.io promotions.kargo.akuity.io promotiontasks.kargo.akuity.io \ clusterpromotiontasks.kargo.akuity.io projectconfigs.kargo.akuity.io \ clusterconfigs.kargo.akuity.io --ignore-not-found --timeout=30s > /dev/null 2>&1 || true kubectl delete sa --all -n kargo --ignore-not-found --timeout=10s > /dev/null 2>&1 || true kubectl delete role --all -n kargo --ignore-not-found --timeout=10s > /dev/null 2>&1 || true kubectl delete rolebinding --all -n kargo --ignore-not-found --timeout=10s > /dev/null 2>&1 || true fi # Kargo chart bug: controller deployment references SA 'kargo-controller' but chart doesn't create it # Pre-create with Helm labels so it doesn't conflict with the chart kubectl apply -f - <<'SAEOF' apiVersion: v1 kind: ServiceAccount metadata: name: kargo-controller namespace: kargo labels: app.kubernetes.io/managed-by: Helm annotations: meta.helm.sh/release-name: kargo meta.helm.sh/release-namespace: kargo SAEOF helm upgrade --install kargo oci://ghcr.io/akuity/kargo-charts/kargo \ --namespace kargo \ --values kargo/values.yaml \ --timeout 5m || true echo " Waiting for kargo-controller..." for i in $(seq 1 24); do if kubectl get pods -n kargo -l app.kubernetes.io/component=controller -o jsonpath='{.items[0].status.containerStatuses[0].ready}' 2>/dev/null | grep -q true; then break fi kubectl delete pod -n kargo -l app.kubernetes.io/component=controller --field-selector=status.phase=Failed --ignore-not-found > /dev/null 2>&1 || true sleep 5 done echo " ✓ Kargo installed" # Fix Kargo admin password — Helm chart leaves the secret empty on fresh install. # The hash must match the password in kargo/values.yaml (passwordHash field). KARGO_PW_HASH='$2b$10$juNdw96VeP/7oP3.RYPnwuUo2lk/eheAqkUqbwh16a1UH17olxyWC' kubectl get secret kargo-api -n kargo -o json | \ python3 -c " import sys, json, base64 d = json.load(sys.stdin) d['data']['ADMIN_ACCOUNT_PASSWORD_HASH'] = base64.b64encode(b'${KARGO_PW_HASH}').decode() for k in ['managedFields','resourceVersion','uid','creationTimestamp']: d['metadata'].pop(k, None) json.dump(d, sys.stdout) " | kubectl replace -f - > /dev/null 2>&1 || true kubectl delete pod -n kargo -l app.kubernetes.io/component=api --ignore-not-found > /dev/null 2>&1 echo " ✓ Kargo admin password configured (admin / St0nksKarg0!)" kubectl apply -f kargo/project.yaml kubectl apply -f kargo/project-config.yaml kubectl apply -f kargo/warehouse.yaml kubectl apply -f kargo/market-hours-check.yaml kubectl apply -f kargo/stages/beta.yaml kubectl apply -f kargo/stages/paper.yaml kubectl apply -f kargo/stages/live.yaml echo " ✓ Kargo project, warehouse, and stages applied" echo "" echo "=== Pipeline Infrastructure Install Complete ===" echo "" echo "Endpoints:" echo " Harbor: https://registry.celestium.life" echo " Woodpecker CI: https://stonks-ci.celestium.life" echo " ArgoCD: https://stonks-argocd.celestium.life" echo " Kargo: https://stonks-kargo.celestium.life"