From 86c60cb53c56fce7acf48da3a9f7079ec7110003 Mon Sep 17 00:00:00 2001 From: Jitendra-Sahu Date: Thu, 5 Mar 2026 17:49:36 +0530 Subject: [PATCH 1/6] openshift: add build/deploy manifests, migration job, static route - BuildConfigs/ImageStream for images - Postgres/RabbitMQ + PVC - Alembic migration job - Shaman Deployment/Service/Route - Static server + /static Route Signed-off-by: Jitendra-Sahu Comments updated Signed-off-by: Jitendra-Sahu --- Dockerfile.ocp | 37 ++++++++++ docker/entrypoint.sh | 51 ++++++++++++++ openshift/deploy/configmap.yaml | 64 +++++++++++++++++ openshift/deploy/deployment.yaml | 102 +++++++++++++++++++++++++++ openshift/deploy/migration-job.yaml | 70 +++++++++++++++++++ openshift/deploy/namespace.yaml | 4 ++ openshift/deploy/postgres-pvc.yaml | 12 ++++ openshift/deploy/postgres.yaml | 58 ++++++++++++++++ openshift/deploy/rabbitmq.yaml | 49 +++++++++++++ openshift/deploy/route.yaml | 11 +++ openshift/deploy/secret.yaml | 37 ++++++++++ openshift/deploy/service.yaml | 9 +++ openshift/deploy/shaman-static.yaml | 104 ++++++++++++++++++++++++++++ 13 files changed, 608 insertions(+) create mode 100644 Dockerfile.ocp create mode 100644 docker/entrypoint.sh create mode 100644 openshift/deploy/configmap.yaml create mode 100644 openshift/deploy/deployment.yaml create mode 100644 openshift/deploy/migration-job.yaml create mode 100644 openshift/deploy/namespace.yaml create mode 100644 openshift/deploy/postgres-pvc.yaml create mode 100644 openshift/deploy/postgres.yaml create mode 100644 openshift/deploy/rabbitmq.yaml create mode 100644 openshift/deploy/route.yaml create mode 100644 openshift/deploy/secret.yaml create mode 100644 openshift/deploy/service.yaml create mode 100644 openshift/deploy/shaman-static.yaml diff --git a/Dockerfile.ocp b/Dockerfile.ocp new file mode 100644 index 0000000..ed3e884 --- /dev/null +++ b/Dockerfile.ocp @@ -0,0 +1,37 @@ +# Dockerfile.ocp +# OpenShift-friendly image (arbitrary UID) for Shaman API + +FROM python:3.9-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + APP_HOME=/opt/app \ + PATH="/opt/app/.local/bin:${PATH}" + +# Build deps (psycopg2, etc.) +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential gcc libpq-dev curl ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR ${APP_HOME} + +# Layer cache for Python deps +COPY requirements.txt ./ +RUN pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir -r requirements.txt + +# App bits +COPY shaman/ shaman/ +COPY public/ public/ +COPY alembic/ alembic/ +COPY alembic.ini alembic.ini +COPY config/ config + +# Entrypoint that generates config/prod.py from config/dev.py at runtime +COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh \ + && chgrp -R 0 ${APP_HOME} && chmod -R g=u ${APP_HOME} + +EXPOSE 8080 +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] +CMD ["pecan", "serve", "config/prod.py"] diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..fe9d3fe --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Build a production config by importing config/dev.py via path and overriding +# only what we need with environment variables from OpenShift. + + +cd "${APP_HOME:-/opt/app}" || true +mkdir -p config + +cat > config/prod.py <<'PY' +import os, pathlib, importlib.util + +# Load config/dev.py by file path (no package import needed) +_cfg_path = pathlib.Path(__file__).with_name('dev.py') +_spec = importlib.util.spec_from_file_location("dev_cfg", _cfg_path) +base = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(base) + + +server = dict(getattr(base, 'server', {})) +if not server: + server = {'host': '0.0.0.0', 'port': '8080'} # default if dev.py doesn’t define it + +app = dict(getattr(base, 'app', {})) +app['debug'] = False + +# Runtime DB config used by the app +sqlalchemy = { + 'url' : os.getenv('DATABASE_URL', 'postgresql+psycopg2://shaman:shaman@postgres:5432/shaman'), + 'echo' : False, +} + +# API auth and optional GitHub secret (shaman/auth.py checks api_user/api_key) +api_user = os.getenv('SHAMAN_API_USER', getattr(base, 'api_user', '')) +api_key = os.getenv('SHAMAN_API_KEY', getattr(base, 'api_key', '')) +_gs = os.getenv('GITHUB_SECRET', '') +github_secret = _gs.encode('utf-8') if _gs else getattr(base, 'github_secret', None) + +# Health behavior & chacra TLS verify +health_check_retries = int(os.getenv('HEALTH_CHECK_RETRIES', str(getattr(base, 'health_check_retries', 3)))) +chacra_verify_ssl = os.getenv('CHACRA_VERIFY_SSL', str(getattr(base, 'chacra_verify_ssl', 'false'))).lower() == 'true' + +# RabbitMQ (pika) URL; define both names to satisfy any code path +_bus = os.getenv('RABBITMQ_URL', '') +bus_url = _bus or getattr(base, 'bus_url', '') +rabbitmq_url = _bus or getattr(base, 'rabbitmq_url', '') +PY + +# Hand over to Pecan with the generated config +exec pecan serve "${APP_HOME}/config/prod.py" diff --git a/openshift/deploy/configmap.yaml b/openshift/deploy/configmap.yaml new file mode 100644 index 0000000..7698d22 --- /dev/null +++ b/openshift/deploy/configmap.yaml @@ -0,0 +1,64 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: shaman-config + namespace: shaman +data: + HEALTH_CHECK_RETRIES: "3" + CHACRA_VERIFY_SSL: "false" + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: shaman-run-config + namespace: shaman +data: + run.py: | + import os + from pecan import hooks + + class DBSessionHook(hooks.PecanHook): + def before(self, state): + from shaman import models + models.start() + def after(self, state): + from shaman import models + models.clear() + def on_error(self, state, exc): + from shaman import models + try: + models.rollback() + finally: + models.clear() + + # Pecan reads host/port from this module (used by `pecan serve`) + server = {'host': '0.0.0.0', 'port': 8080} + + app = { + 'root': 'shaman.controllers.root.RootController', + 'modules': ['shaman'], + 'debug': False, + 'hooks': [DBSessionHook()], + + # === UI wiring === + # These paths match your image layout inside the Shaman container + 'template_path': '/opt/app/shaman/templates', + 'static_root': '/opt/app/public', + # Note: Pecan's config supports 'static_root' and 'template_path'. + # It doesn't have a 'static_url' key; static is conventionally served under /public/*. + } + + # DB bindings – Shaman expects both R/W and R/O; add 'sqlalchemy' too for safety + _url = os.environ['DATABASE_URL'] + sqlalchemy = {'url': _url} + sqlalchemy_w = {'url': _url} + sqlalchemy_ro = {'url': _url} + + # Shaman API auth for POST/DELETE + api_user = os.environ.get('SHAMAN_API_USER', 'admin') + api_key = os.environ['SHAMAN_API_KEY'] + + # Optional settings + health_check_retries = int(os.environ.get('HEALTH_CHECK_RETRIES', '3')) + chacra_verify_ssl = os.environ.get('CHACRA_VERIFY_SSL', 'false').lower() == 'true' diff --git a/openshift/deploy/deployment.yaml b/openshift/deploy/deployment.yaml new file mode 100644 index 0000000..0407189 --- /dev/null +++ b/openshift/deploy/deployment.yaml @@ -0,0 +1,102 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: shaman + namespace: shaman +spec: + replicas: 1 + selector: + matchLabels: { app: shaman } + template: + metadata: + labels: { app: shaman } + spec: + volumes: + - name: run-config + configMap: + name: shaman-run-config + items: + - key: run.py + path: run.py + containers: + - name: shaman + image: image-registry.openshift-image-registry.svc:5000/shaman/shaman:latest + imagePullPolicy: Always + command: ["/bin/bash","-lc"] + args: + - | + set -euo pipefail + APP_HOME=/opt/app + cd "${APP_HOME}" + export PYTHONPATH="${APP_HOME}:${PYTHONPATH:-}" + mkdir -p config + + # Write a proper Python config (no repr; import dev.py by absolute path) + cat > /opt/app/config/prod.py <<'PY' + import os, importlib.util + + _spec = importlib.util.spec_from_file_location("dev_cfg", "/opt/app/config/dev.py") + base = importlib.util.module_from_spec(_spec) + _spec.loader.exec_module(base) + + # Server: host/port belong in config + server = getattr(base, "server", {"host": "0.0.0.0", "port": "8080"}) + if not isinstance(server, dict): + server = {"host": "0.0.0.0", "port": "8080"} + + # App: reuse base but force debug=False; keep hooks intact + app = dict(getattr(base, "app", {})) + app["debug"] = False + + # Primary SQLAlchemy binding from env + sqlalchemy = { + "url": os.getenv( + "DATABASE_URL", + "postgresql+psycopg2://shaman:shaman@postgres:5432/shaman", + ), + "echo": False, + } + + # Shaman expects R/W and R/O aliases as well: + sqlalchemy_w = sqlalchemy + sqlalchemy_ro = sqlalchemy + + # Other secrets/urls + api_user = os.getenv("SHAMAN_API_USER", getattr(base, "api_user", "")) + api_key = os.getenv("SHAMAN_API_KEY", getattr(base, "api_key", "")) + _gs = os.getenv("GITHUB_SECRET", "") + github_secret = _gs.encode("utf-8") if _gs else getattr(base, "github_secret", None) + bus_url = os.getenv("RABBITMQ_URL", getattr(base, "bus_url", "")) + rabbitmq_url = os.getenv("RABBITMQ_URL", getattr(base, "rabbitmq_url", "")) + + # Optional knobs + health_check_retries = int(os.getenv("HEALTH_CHECK_RETRIES", str(getattr(base, "health_check_retries", 3)))) + chacra_verify_ssl = os.getenv("CHACRA_VERIFY_SSL", str(getattr(base, "chacra_verify_ssl", "false"))).lower() == "true" + PY + + echo "[command] Wrote /opt/app/config/prod.py" + + # --- ADDED: prefer ConfigMap run.py if present; fallback to prod.py --- + if [ -f "config/run.py" ]; then + echo "[command] Found /opt/app/config/run.py (ConfigMap); using it" + exec pecan serve config/run.py + else + exec pecan serve config/prod.py + fi + envFrom: + - secretRef: { name: shaman-secrets } + - configMapRef: { name: shaman-config } + volumeMounts: + - name: run-config + mountPath: /opt/app/config/run.py + subPath: run.py + ports: + - { containerPort: 8080, name: http } + readinessProbe: + httpGet: { path: /api/, port: 8080 } + initialDelaySeconds: 10 + periodSeconds: 10 + livenessProbe: + httpGet: { path: /api/, port: 8080 } + initialDelaySeconds: 30 + periodSeconds: 15 diff --git a/openshift/deploy/migration-job.yaml b/openshift/deploy/migration-job.yaml new file mode 100644 index 0000000..1a3e419 --- /dev/null +++ b/openshift/deploy/migration-job.yaml @@ -0,0 +1,70 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: shaman-migrate + namespace: shaman +spec: + backoffLimit: 3 + template: + metadata: + labels: + app: shaman-migrate + spec: + restartPolicy: OnFailure + workingDir: /opt/app + containers: + - name: migrate + image: image-registry.openshift-image-registry.svc:5000/shaman/shaman:latest + command: ["/bin/bash","-lc"] + args: + - | + set -euo pipefail + echo "[migrate] CWD=$(pwd)" + export PYTHONPATH=/opt/app:${PYTHONPATH:-} + echo "[migrate] PYTHONPATH=${PYTHONPATH}" + + # 1) Wait for Postgres to be reachable using the same DATABASE_URL the app uses + python - <<'PY' + import os, time, sys + from sqlalchemy import create_engine + url = os.environ['DATABASE_URL'] + last = None + for i in range(30): + try: + create_engine(url, pool_pre_ping=True).connect().close() + print("[migrate] DB is reachable.") + break + except Exception as e: + last = e + print("[migrate] Waiting for DB...", i+1, "error:", e) + time.sleep(5) + else: + print("[migrate] ERROR: DB not reachable.", last); sys.exit(1) + PY + + # 2) Bootstrap schema from models if 'repos' table doesn't exist yet + python - <<'PY' + import os + from sqlalchemy import create_engine, inspect + from shaman import models + eng = create_engine(os.environ['DATABASE_URL']) + insp = inspect(eng) + tables = set(insp.get_table_names()) + if 'repos' not in tables: + print("[migrate] Bootstrapping schema via SQLAlchemy create_all()...") + models.Base.metadata.create_all(eng) + print("[migrate] Schema created.") + else: + print("[migrate] Base tables already present; skipping bootstrap.") + PY + + # 3) Point Alembic to Postgres (repo default is sqlite:///dev.db) + sed -ri "s#^sqlalchemy.url = .*#sqlalchemy.url = ${DATABASE_URL//#/\\#}#" alembic.ini + echo "[migrate] alembic.ini updated" + + # 4) Record current DB as up-to-date with code (no-op DDL) + alembic -c alembic.ini stamp head + echo "[migrate] Alembic stamped to head." + envFrom: + - secretRef: { name: shaman-secrets } + - configMapRef: { name: shaman-config } diff --git a/openshift/deploy/namespace.yaml b/openshift/deploy/namespace.yaml new file mode 100644 index 0000000..7dbab05 --- /dev/null +++ b/openshift/deploy/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: shaman diff --git a/openshift/deploy/postgres-pvc.yaml b/openshift/deploy/postgres-pvc.yaml new file mode 100644 index 0000000..8d42708 --- /dev/null +++ b/openshift/deploy/postgres-pvc.yaml @@ -0,0 +1,12 @@ +# openshift/deploy/postgres-pvc.yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: postgres-data + namespace: shaman +spec: + accessModes: ["ReadWriteOnce"] + storageClassName: cephfs-rwx # or omit this line to use the default + resources: + requests: + storage: 10Gi diff --git a/openshift/deploy/postgres.yaml b/openshift/deploy/postgres.yaml new file mode 100644 index 0000000..93c49ad --- /dev/null +++ b/openshift/deploy/postgres.yaml @@ -0,0 +1,58 @@ +# openshift/deploy/postgres.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres + namespace: shaman + labels: { app: postgres } +spec: + replicas: 1 + selector: { matchLabels: { app: postgres } } + template: + metadata: + labels: { app: postgres } + spec: + securityContext: + runAsNonRoot: true + containers: + - name: postgres + image: quay.io/sclorg/postgresql-16-c9s + imagePullPolicy: IfNotPresent + ports: [{ containerPort: 5432, name: pg }] + env: + - name: POSTGRESQL_USER + valueFrom: { secretKeyRef: { name: shaman-secrets, key: SHAMAN_DB_USER } } + - name: POSTGRESQL_PASSWORD + valueFrom: { secretKeyRef: { name: shaman-secrets, key: SHAMAN_DB_PASS } } + - name: POSTGRESQL_DATABASE + valueFrom: { secretKeyRef: { name: shaman-secrets, key: SHAMAN_DB_NAME } } + readinessProbe: + tcpSocket: { port: pg } + initialDelaySeconds: 10 + periodSeconds: 10 + livenessProbe: + tcpSocket: { port: pg } + initialDelaySeconds: 30 + periodSeconds: 15 + securityContext: + allowPrivilegeEscalation: false + capabilities: { drop: ["ALL"] } + seccompProfile: { type: RuntimeDefault } + volumeMounts: + - { name: pgdata, mountPath: /var/lib/pgsql/data } + volumes: + - name: pgdata + persistentVolumeClaim: + claimName: postgres-data +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: shaman + labels: { app: postgres } +spec: + selector: { app: postgres } + ports: + - { name: pg, port: 5432, targetPort: pg } + type: ClusterIP diff --git a/openshift/deploy/rabbitmq.yaml b/openshift/deploy/rabbitmq.yaml new file mode 100644 index 0000000..d31b0a8 --- /dev/null +++ b/openshift/deploy/rabbitmq.yaml @@ -0,0 +1,49 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: rabbitmq-data + namespace: shaman +spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 5Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: rabbitmq + namespace: shaman +spec: + replicas: 1 + selector: { matchLabels: { app: rabbitmq } } + template: + metadata: + labels: { app: rabbitmq } + spec: + containers: + - name: rabbitmq + image: docker.io/library/rabbitmq:3.12-management + env: + - { name: RABBITMQ_DEFAULT_USER, value: "shaman" } + - { name: RABBITMQ_DEFAULT_PASS, value: "shaman" } + ports: + - { containerPort: 5672, name: amqp } + - { containerPort: 15672, name: http } + volumeMounts: + - { name: data, mountPath: /var/lib/rabbitmq } + volumes: + - name: data + persistentVolumeClaim: + claimName: rabbitmq-data +--- +apiVersion: v1 +kind: Service +metadata: + name: rabbitmq + namespace: shaman +spec: + selector: { app: rabbitmq } + ports: + - { name: amqp, port: 5672, targetPort: 5672 } + - { name: http, port: 15672, targetPort: 15672 } diff --git a/openshift/deploy/route.yaml b/openshift/deploy/route.yaml new file mode 100644 index 0000000..9f3dc5c --- /dev/null +++ b/openshift/deploy/route.yaml @@ -0,0 +1,11 @@ +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: shaman + namespace: shaman +spec: + to: + kind: Service + name: shaman + tls: + termination: edge diff --git a/openshift/deploy/secret.yaml b/openshift/deploy/secret.yaml new file mode 100644 index 0000000..1970af7 --- /dev/null +++ b/openshift/deploy/secret.yaml @@ -0,0 +1,37 @@ +apiVersion: v1 +kind: Secret +metadata: + name: shaman-secrets + namespace: shaman +type: Opaque +stringData: + # ---------------------------- + # PostgreSQL (for SCLorg image) + # ---------------------------- + # These three keys are read by the postgres Deployment: + # POSTGRESQL_USER / POSTGRESQL_PASSWORD / POSTGRESQL_DATABASE + # The SCLorg entrypoint uses them to initialize the DB. + # Ref: SCLorg Postgres docs & Dockerfile comments. + # https://github.com/sclorg/postgresql-container (env vars) + # https://github.com/sclorg/postgresql-container/blob/master/16/Dockerfile.c9s + # (data dir is /var/lib/pgsql/data) + SHAMAN_DB_USER: "shaman" # <-- set to your desired DB username + SHAMAN_DB_PASS: "shaman" # <-- set a strong password + SHAMAN_DB_NAME: "shaman" # <-- your database name + + # ------------------------------------------ + # Shaman runtime (read by the app container) + # ------------------------------------------ + # SQLAlchemy URL must match the three values above + 'postgres' Service @ 5432 + # Using psycopg2 dialect: + DATABASE_URL: "postgresql+psycopg2://shaman:shaman@postgres:5432/shaman" + + # Shaman API Basic-Auth + SHAMAN_API_USER: "admin" + SHAMAN_API_KEY: "secret" + + # Message bus (RabbitMQ via pika) + RABBITMQ_URL: "amqp://shaman:shaman@rabbitmq:5672/%2f" + + # Optional: webhook secret used by shaman/auth.py (leave blank if unused) + GITHUB_SECRET: "" diff --git a/openshift/deploy/service.yaml b/openshift/deploy/service.yaml new file mode 100644 index 0000000..6e0aaa3 --- /dev/null +++ b/openshift/deploy/service.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Service +metadata: + name: shaman + namespace: shaman +spec: + selector: { app: shaman } + ports: + - { name: http, port: 8080, targetPort: 8080 } diff --git a/openshift/deploy/shaman-static.yaml b/openshift/deploy/shaman-static.yaml new file mode 100644 index 0000000..5c62783 --- /dev/null +++ b/openshift/deploy/shaman-static.yaml @@ -0,0 +1,104 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: shaman-static + namespace: shaman + labels: + app: shaman-static +spec: + replicas: 1 + selector: + matchLabels: + app: shaman-static + template: + metadata: + labels: + app: shaman-static + spec: + securityContext: + runAsNonRoot: true + volumes: + - name: static-content + emptyDir: {} + initContainers: + - name: copy-static + image: image-registry.openshift-image-registry.svc:5000/shaman/shaman:latest + command: ["/bin/bash", "-lc"] + args: + - | + set -euo pipefail + mkdir -p /work/public + cp -a /opt/app/public/* /work/public/ + volumeMounts: + - name: static-content + mountPath: /work + securityContext: + allowPrivilegeEscalation: false + capabilities: { drop: ["ALL"] } + seccompProfile: { type: RuntimeDefault } + containers: + - name: static-server + image: registry.access.redhat.com/ubi9/python-311 + command: ["/bin/bash","-lc"] + args: + - | + # Expose the same files under /static/* (symlink to /public/*) + ln -snf /srv/public /srv/static + # Serve /srv (docroot). Our assets are in /srv/public and /srv/static + exec python -m http.server 8080 --directory /srv + ports: + - name: http + containerPort: 8080 + volumeMounts: + - name: static-content + mountPath: /srv + readinessProbe: + httpGet: + path: /static/css/bootstrap.min.css + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 2 + livenessProbe: + httpGet: + path: / + port: 8080 + initialDelaySeconds: 15 + periodSeconds: 20 + timeoutSeconds: 2 + securityContext: + allowPrivilegeEscalation: false + capabilities: { drop: ["ALL"] } + seccompProfile: { type: RuntimeDefault } +--- +apiVersion: v1 +kind: Service +metadata: + name: shaman-static + namespace: shaman + labels: + app: shaman-static +spec: + selector: + app: shaman-static + ports: + - name: http + port: 8080 + targetPort: 8080 + +--- +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: shaman-static-public + namespace: shaman +spec: + host: shaman-shaman.apps.pok.os.sepia.ceph.com # ← same host as your Shaman route + path: /static + to: + kind: Service + name: shaman-static + weight: 100 + tls: + termination: edge + From 18172e9000c6123d0d099d646c0c089a1e0ec3c3 Mon Sep 17 00:00:00 2001 From: Jitendra-Sahu Date: Wed, 11 Mar 2026 16:00:12 +0530 Subject: [PATCH 2/6] Add README.md Signed-off-by: Jitendra-Sahu --- openshift/README.md | 87 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 openshift/README.md diff --git a/openshift/README.md b/openshift/README.md new file mode 100644 index 0000000..011a7bc --- /dev/null +++ b/openshift/README.md @@ -0,0 +1,87 @@ +# Shaman on OpenShift — Quick Start + +This guide helps you build and deploy **Shaman** on OpenShift using the provided manifests. + +--- + +## Prerequisites + +- OpenShift cluster access and `oc` CLI installed. +- Logged in to the right cluster: + ```bash + oc whoami + oc project + +Need sufficient permissions to create namespace/projects, routes, deployments, and PVCs. + +## All commands below assume the namespace is shaman. + +1. (One‑time) Create the namespace +``` +oc apply -f openshift/deploy/namespace.yaml +``` +2. Build pipeline (ImageStream + BuildConfigs) +``` +oc -n shaman apply -f openshift/build/ +``` +3. (a) Build from upstream Git +``` +oc -n shaman start-build bc/shaman-git --follow +``` +3. (b) OR build from your working tree (binary build) + +Use this when you want to build the image from your local repo state. +``` +oc -n shaman start-build bc/shaman-binary --from-dir=. --follow +``` +4. Deploy infra and app configs + +Apply app configuration, secrets, and infra components (Postgres, RabbitMQ, PVC): +``` +oc -n shaman apply -f openshift/deploy/configmap.yaml +oc -n shaman apply -f openshift/deploy/secret.yaml +oc -n shaman apply -f openshift/deploy/postgres.yaml +oc -n shaman apply -f openshift/deploy/rabbitmq.yaml +oc -n shaman apply -f openshift/deploy/postgres-pvc.yaml +``` +5. Run DB migrations (Alembic → Postgres) +``` +oc -n shaman apply -f openshift/deploy/migration-job.yaml +oc -n shaman wait --for=condition=complete job/shaman-migrate --timeout=180s +``` +6. Deploy Shaman API and static assets +``` +oc -n shaman apply -f openshift/deploy/deployment.yaml +oc -n shaman apply -f openshift/deploy/service.yaml +oc -n shaman apply -f openshift/deploy/route.yaml +oc -n shaman apply -f openshift/deploy/shaman-static.yaml +``` +The shaman-static.yaml serves CSS/JS over a path-based Route (e.g., /static). + +The Shaman API itself is exposed by the shaman Route. + +7. Verify the rollout + +Wait for the Shaman API deployment to be ready +``` +oc -n shaman rollout status deploy/shaman +``` +Get the public host +``` +oc -n shaman get route shaman -o jsonpath='{.spec.host}{"\n"}' +``` +Open the URL in your browser: +``` +https:/// +``` +--- +## Quick checks + +1. API root: +``` +curl -k -I "https:///api/" +``` +2. (If you used the static server) CSS/JS: +``` +curl -k -I "https:///static/css/bootstrap.min.css" +``` From d644c64b445739ebb6bd5b2063e7441f4115a275 Mon Sep 17 00:00:00 2001 From: Jitendra-Sahu Date: Fri, 13 Mar 2026 16:24:15 +0530 Subject: [PATCH 3/6] Add chacra nodes seeding job manifest file Signed-off-by: Jitendra-Sahu --- openshift/deploy/shaman-seed-nodes-db.yaml | 47 ++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 openshift/deploy/shaman-seed-nodes-db.yaml diff --git a/openshift/deploy/shaman-seed-nodes-db.yaml b/openshift/deploy/shaman-seed-nodes-db.yaml new file mode 100644 index 0000000..bf92a92 --- /dev/null +++ b/openshift/deploy/shaman-seed-nodes-db.yaml @@ -0,0 +1,47 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: shaman-seed-nodes-db + namespace: shaman +spec: + backoffLimit: 0 + ttlSecondsAfterFinished: 600 + template: + spec: + restartPolicy: Never + containers: + - name: seed-db + image: quay.io/sclorg/postgresql-16-c9s + env: + # DB connection via your existing secret & service + - name: PGHOST + value: "postgres" + - name: PGUSER + valueFrom: + secretKeyRef: + name: shaman-secrets + key: SHAMAN_DB_USER + - name: PGPASSWORD + valueFrom: + secretKeyRef: + name: shaman-secrets + key: SHAMAN_DB_PASS + - name: PGDATABASE + valueFrom: + secretKeyRef: + name: shaman-secrets + key: SHAMAN_DB_NAME + # The Chacra route to register in Shaman's nodes table + - name: NODE + value: "chacra-chacra.apps.pok.os.sepia.ceph.com" + command: ["/bin/sh","-c"] + args: + - | + set -euo pipefail + # Insert row iff missing (shell expands ${NODE} safely for hostnames) + psql -v ON_ERROR_STOP=1 -c " + INSERT INTO nodes (host, healthy, down_count, last_used, last_check) + SELECT '${NODE}', TRUE, 0, NOW(), NOW() + WHERE NOT EXISTS (SELECT 1 FROM nodes WHERE host = '${NODE}'); + " + echo "[seed-db] Seeded node (if missing): ${NODE}" From 5b6ed94724f3dad0c8d8518229a0232ae6716d23 Mon Sep 17 00:00:00 2001 From: Jitendra-Sahu Date: Fri, 27 Mar 2026 17:56:47 +0530 Subject: [PATCH 4/6] Update configmap file Signed-off-by: Jitendra-Sahu --- openshift/deploy/configmap.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openshift/deploy/configmap.yaml b/openshift/deploy/configmap.yaml index 7698d22..d8522a9 100644 --- a/openshift/deploy/configmap.yaml +++ b/openshift/deploy/configmap.yaml @@ -6,6 +6,10 @@ metadata: data: HEALTH_CHECK_RETRIES: "3" CHACRA_VERIFY_SSL: "false" + api_user: "admin" + api_key: "secret" + SHAMAN_API_USER: "admin" + SHAMAN_API_KEY: "secret" --- apiVersion: v1 From 9ffe42696465c63c5b8cd6223f860c13c8e54310 Mon Sep 17 00:00:00 2001 From: Jitendra Sahu Date: Tue, 14 Apr 2026 17:36:09 +0530 Subject: [PATCH 5/6] Add seeding instructions for Shaman deployment Added instructions to seed Shaman with Chacra nodes. --- openshift/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openshift/README.md b/openshift/README.md index 011a7bc..caf815f 100644 --- a/openshift/README.md +++ b/openshift/README.md @@ -66,6 +66,10 @@ Wait for the Shaman API deployment to be ready ``` oc -n shaman rollout status deploy/shaman ``` +8. Seed shaman with chacra nodes +``` +oc -n shaman apply -f openshift/deploy/shaman-seed-nodes-db.yaml +``` Get the public host ``` oc -n shaman get route shaman -o jsonpath='{.spec.host}{"\n"}' From 0ad75467cf59fd57c596b4ad6f64ad6b433685eb Mon Sep 17 00:00:00 2001 From: Jitendra-Sahu Date: Wed, 15 Apr 2026 13:39:37 +0530 Subject: [PATCH 6/6] Update secrets Signed-off-by: Jitendra-Sahu --- openshift/deploy/secret.yaml | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/openshift/deploy/secret.yaml b/openshift/deploy/secret.yaml index 1970af7..ba96262 100644 --- a/openshift/deploy/secret.yaml +++ b/openshift/deploy/secret.yaml @@ -5,32 +5,19 @@ metadata: namespace: shaman type: Opaque stringData: - # ---------------------------- - # PostgreSQL (for SCLorg image) - # ---------------------------- - # These three keys are read by the postgres Deployment: - # POSTGRESQL_USER / POSTGRESQL_PASSWORD / POSTGRESQL_DATABASE - # The SCLorg entrypoint uses them to initialize the DB. - # Ref: SCLorg Postgres docs & Dockerfile comments. - # https://github.com/sclorg/postgresql-container (env vars) - # https://github.com/sclorg/postgresql-container/blob/master/16/Dockerfile.c9s - # (data dir is /var/lib/pgsql/data) - SHAMAN_DB_USER: "shaman" # <-- set to your desired DB username - SHAMAN_DB_PASS: "shaman" # <-- set a strong password - SHAMAN_DB_NAME: "shaman" # <-- your database name + # PostgreSQL + SHAMAN_DB_USER: "shaman" + SHAMAN_DB_PASS: "shaman" + SHAMAN_DB_NAME: "shaman" - # ------------------------------------------ # Shaman runtime (read by the app container) - # ------------------------------------------ - # SQLAlchemy URL must match the three values above + 'postgres' Service @ 5432 - # Using psycopg2 dialect: DATABASE_URL: "postgresql+psycopg2://shaman:shaman@postgres:5432/shaman" # Shaman API Basic-Auth SHAMAN_API_USER: "admin" SHAMAN_API_KEY: "secret" - # Message bus (RabbitMQ via pika) + # RabbitMQ RABBITMQ_URL: "amqp://shaman:shaman@rabbitmq:5672/%2f" # Optional: webhook secret used by shaman/auth.py (leave blank if unused)