Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions Dockerfile.ocp
Original file line number Diff line number Diff line change
@@ -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"]
51 changes: 51 additions & 0 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -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"
91 changes: 91 additions & 0 deletions openshift/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# 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
```
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"}'
```
Open the URL in your browser:
```
https://<printed-host>/
```
---
## Quick checks

1. API root:
```
curl -k -I "https://<printed-host>/api/"
```
2. (If you used the static server) CSS/JS:
```
curl -k -I "https://<printed-host>/static/css/bootstrap.min.css"
```
68 changes: 68 additions & 0 deletions openshift/deploy/configmap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: shaman-config
namespace: shaman
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
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'
102 changes: 102 additions & 0 deletions openshift/deploy/deployment.yaml
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading