diff --git a/Dockerfile.ocp b/Dockerfile.ocp new file mode 100644 index 0000000..63aa557 --- /dev/null +++ b/Dockerfile.ocp @@ -0,0 +1,58 @@ +FROM python:3.11-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + VIRTUAL_ENV=/opt/venv \ + PATH="/opt/venv/bin:$PATH" \ + DEBIAN_FRONTEND=noninteractive \ + PORT=8000 + +# OS deps for repo building (rpm/createrepo-c for RPM; reprepro for DEB) + DB client libs +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential git ca-certificates curl \ + libpq-dev pkg-config \ + reprepro dpkg-dev gnupg rpm createrepo-c postgresql-client \ + && rm -rf /var/lib/apt/lists/* + +ARG SETUPTOOLS_VERSION=74.1.3 +RUN python -m venv "$VIRTUAL_ENV" +RUN "$VIRTUAL_ENV/bin/python" -m pip install --no-cache-dir --upgrade \ + pip \ + "setuptools==${SETUPTOOLS_VERSION}" \ + wheel +RUN "$VIRTUAL_ENV/bin/python" -c "import pkg_resources; print('pkg_resources OK')" +RUN chgrp -R 0 "$VIRTUAL_ENV" && chmod -R g+rwX "$VIRTUAL_ENV" + +RUN "$VIRTUAL_ENV/bin/pip" install --no-cache-dir \ + "gunicorn<20.1.0" \ + "pecan" \ + "sqlalchemy==1.3.0" \ + "psycopg2-binary==2.9.9" \ + "pecan-notario" \ + "celery<=6.2.5" \ + "alembic" \ + "python-statsd" \ + "requests" + +# Install Chacra from upstream main +RUN "$VIRTUAL_ENV/bin/pip" install --no-cache-dir \ + "git+https://github.com/ceph/chacra.git@main#egg=chacra" + +# Vendor the repo’s Alembic migrations into the image at /alembic +RUN git clone --depth 1 https://github.com/ceph/chacra.git /tmp/chacra-src +RUN cp -r /tmp/chacra-src/alembic /alembic +RUN rm -rf /tmp/chacra-src +RUN chgrp -R 0 /alembic && chmod -R g+rwX /alembic + +# OpenShift-friendly dirs (group-writable for arbitrary UID) +RUN mkdir -p /srv/chacra/log /srv/chacra/run /data/binaries /data/repos /etc/chacra \ + && chgrp -R 0 /srv/chacra /data /etc/chacra \ + && chmod -R g+rwX /srv/chacra /data /etc/chacra + +# Add Debian repo tooling for Chacra repo builds +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + apt-utils dpkg-dev gnupg && \ + rm -rf /var/lib/apt/lists/* + +EXPOSE 8000 diff --git a/docker/entrypoint-api.sh b/docker/entrypoint-api.sh new file mode 100644 index 0000000..403c083 --- /dev/null +++ b/docker/entrypoint-api.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env sh +set -ex +/opt/venv/bin/python - <<'PY' +try: + import pkg_resources +except Exception: + import sys, subprocess + subprocess.check_call([sys.executable, "-m", "pip", "install", "--no-cache-dir", "setuptools"]) +PY +exec /opt/venv/bin/celery -A chacra.asynch beat --loglevel=INFO +❯ cat entrypoint-api.sh +#!/usr/bin/env bash +set -euo pipefail + +export ALEMBIC_CONFIG=/etc/chacra/alembic.ini + +# Wait for Postgres +until pg_isready -h "${CHACRA_DB_HOST}" -p "${CHACRA_DB_PORT}" -U "${CHACRA_DB_USER}"; do + echo "Waiting for Postgres..." + sleep 2 +done + +# DB migrations + seed (idempotent) +alembic upgrade head || true +pecan populate /etc/chacra/prod.py || true + +# Serve API +exec gunicorn --workers="${GUNICORN_WORKERS:-4}" --timeout=1200 \ + --bind 0.0.0.0:8000 'pecan:make_app("/etc/chacra/prod.py")' diff --git a/docker/entrypoint-beat.sh b/docker/entrypoint-beat.sh new file mode 100644 index 0000000..0f48dd8 --- /dev/null +++ b/docker/entrypoint-beat.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env sh +set -ex +/opt/venv/bin/python - <<'PY' +try: + import pkg_resources +except Exception: + import sys, subprocess + subprocess.check_call([sys.executable, "-m", "pip", "install", "--no-cache-dir", "setuptools"]) +PY +exec /opt/venv/bin/celery -A chacra.asynch beat --loglevel=INFO diff --git a/docker/entrypoint-celery.sh b/docker/entrypoint-celery.sh new file mode 100644 index 0000000..ba10af4 --- /dev/null +++ b/docker/entrypoint-celery.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env sh +set -ex +/opt/venv/bin/python - <<'PY' +try: + import pkg_resources +except Exception: + import sys, subprocess + subprocess.check_call([sys.executable, "-m", "pip", "install", "--no-cache-dir", "setuptools"]) +PY +# Use module path suitable for installed package +exec /opt/venv/bin/celery -A chacra.asynch worker \ + --loglevel=INFO -Q poll_repos,celery,build_repos --hostname=worker@%h diff --git a/openshift/README.md b/openshift/README.md new file mode 100644 index 0000000..a54d7be --- /dev/null +++ b/openshift/README.md @@ -0,0 +1,81 @@ +# Chacra on OpenShift — Quick Start + +This guide helps you build and deploy **Chacra** 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 Chacra. + +1. (One‑time) Create the namespace +``` +oc apply -f openshift/deploy/namespace.yaml +``` +2. Build pipeline (ImageStream + BuildConfigs) +``` +oc -n chacra apply -f openshift/build/ +``` +3. (a) Build from upstream Git +``` +oc -n chacra start-build bc/chacra-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 chacra start-build bc/chacra-binary --from-dir=. --follow +``` +4. Deploy infra and app configs + +Apply app configuration, secrets, and infra components (Postgres, RabbitMQ, PVC): +``` +oc -n chacra apply -f openshift/deploy/configmap.yaml +oc -n chacra apply -f openshift/deploy/alembic-configmap.yaml +oc -n chacra apply -f openshift/deploy/secret.yaml +oc -n chacra apply -f openshift/deploy/chacra-callbacks-secret.yaml +oc -n chacra apply -f openshift/serviceaccount.yaml +oc -n chacra apply -f openshift/deploy/postgres.yaml +oc -n chacra apply -f openshift/deploy/rabbitmq.yaml +oc -n chacra apply -f openshift/deploy/postgres-pvc.yaml +oc -n chacra apply -f openshift/deploy/postgres-svc.yaml +oc -n chacra apply -f openshift/deploy/chacra-data-rwx-pvc.yaml +``` +5. Run DB migrations +```bash +# Run once per brand‑new database +oc -n chacra apply -f openshift/deploy/db-bootstrap-job.yaml + +# Run only when a new release adds Alembic revisions +oc -n chacra apply -f openshift/deploy/db-migration-job.yaml +``` +6. Deploy Chacra API, Celery and Beat +``` +oc -n chacra apply -f openshift/deploy/deployment.yaml +oc -n chacra apply -f openshift/deploy/service.yaml +oc -n chacra apply -f openshift/deploy/route.yaml +``` +7. Verify the rollout + +Wait for the Chacra API, Celery and Beat deployments to be ready +``` +oc -n chacra rollout status deploy/chacra-api +oc -n chacra rollout status deploy/chacra-celery +oc -n chacra rollout status deploy/chacra-beat +``` +Get the public host +``` +oc -n chacra get route chacra -o jsonpath='{.spec.host}{"\n"}' +``` +Open the URL in your browser: +``` +https:/// +``` diff --git a/openshift/deploy/alembic-configmap.yaml b/openshift/deploy/alembic-configmap.yaml new file mode 100644 index 0000000..8129b72 --- /dev/null +++ b/openshift/deploy/alembic-configmap.yaml @@ -0,0 +1,54 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: chacra-alembic + namespace: chacra + labels: + app: chacra +data: + alembic.ini: | + [alembic] + # Tell Alembic where the migration scripts live in the container + script_location = /alembic + + # (Optional) logging sections can be omitted; the above is sufficient + # sqlalchemy.url is provided via '-x dburl=...' at runtime + + + sqlalchemy.url = sqlite:///dev.db + + + # Logging configuration + [loggers] + keys = root,sqlalchemy,alembic + + [handlers] + keys = console + + [formatters] + keys = generic + + [logger_root] + level = WARN + handlers = console + qualname = + + [logger_sqlalchemy] + level = WARN + handlers = + qualname = sqlalchemy.engine + + [logger_alembic] + level = INFO + handlers = + qualname = alembic + + [handler_console] + class = StreamHandler + args = (sys.stderr,) + level = NOTSET + formatter = generic + + [formatter_generic] + format = %(levelname)-5.5s [%(name)s] %(message)s + datefmt = %H:%M:%S diff --git a/openshift/deploy/chacra-callbacks-secret.yaml b/openshift/deploy/chacra-callbacks-secret.yaml new file mode 100644 index 0000000..12eb546 --- /dev/null +++ b/openshift/deploy/chacra-callbacks-secret.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: Secret +metadata: + name: chacra-callbacks + namespace: chacra +type: Opaque +stringData: + # This is the exact Python module Chacra imports at runtime + prod_callbacks.py: | + # --- Chacra -> Shaman integration (callbacks & health pings) --- + # Shaman API base (we will post to /api/... and /api/nodes/...) + callback_url = "https://shaman-shaman.apps.pok.os.sepia.ceph.com/api" + + # Basic auth that Shaman expects for POST/DELETE (must match Shaman config) + callback_user = "admin" # TODO: set same user configured in Shaman + callback_key = "secret" # TODO: set same key configured in Shaman + + # Verify TLS of the Shaman Route (set False only if using a self-signed test cert) + callback_verify_ssl = True + + # Enable periodic health pings to Shaman's node registry + health_ping = True + health_ping_url = "https://shaman-shaman.apps.pok.os.sepia.ceph.com/api/nodes/" + + # Name to register this Chacra node under in Shaman + # Tip: choose something meaningful; the in-cluster FQDN is fine too. + hostname = "chacra-chacra.apps.pok.os.sepia.ceph.com" diff --git a/openshift/deploy/chacra-data-rwx-pvc.yaml b/openshift/deploy/chacra-data-rwx-pvc.yaml new file mode 100644 index 0000000..c9bea2b --- /dev/null +++ b/openshift/deploy/chacra-data-rwx-pvc.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: chacra-shared-rwx + namespace: chacra + labels: + app: chacra +spec: + storageClassName: cephfs-rwx + accessModes: + - ReadWriteMany + resources: + requests: + storage: 200Gi diff --git a/openshift/deploy/chacra-secret-bootstrap-job.yaml b/openshift/deploy/chacra-secret-bootstrap-job.yaml new file mode 100644 index 0000000..ba8e142 --- /dev/null +++ b/openshift/deploy/chacra-secret-bootstrap-job.yaml @@ -0,0 +1,121 @@ +# openshift/deploy/chacra-secret-bootstrap-job.yaml +apiVersion: batch/v1 +kind: Job +metadata: + name: chacra-secret-bootstrap + namespace: chacra + labels: { app: chacra, job: secret-bootstrap } +spec: + backoffLimit: 0 + ttlSecondsAfterFinished: 3600 + template: + metadata: + labels: { app: chacra, job: secret-bootstrap } + spec: + serviceAccountName: chacra-bootstrap + restartPolicy: Never + containers: + - name: bootstrap + image: registry.access.redhat.com/ubi9/python-311:latest + imagePullPolicy: IfNotPresent + env: + - { name: NAMESPACE, value: "chacra" } + - { name: SECRET_NAME, value: "chacra-secrets" } + - { name: DEPLOYMENT, value: "chacra-api" } + - { name: KEY_NAME, value: "SECRET_KEY" } + - { name: USER_NAME, value: "CHACRA_API_USER" } + command: ["/bin/sh","-lc"] + args: + - | + set -euo pipefail + python - <<'PY' + import os, json, base64, hashlib, time, ssl, urllib.request + + NS = os.environ["NAMESPACE"] + S = os.environ["SECRET_NAME"] + DEP = os.environ["DEPLOYMENT"] + KEYN = os.environ["KEY_NAME"] + USERN= os.environ["USER_NAME"] + + host = os.environ.get("KUBERNETES_SERVICE_HOST") + port = os.environ.get("KUBERNETES_SERVICE_PORT", "443") + api = f"https://{host}:{port}" + + token_path = "/var/run/secrets/kubernetes.io/serviceaccount/token" + ca_path = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" + with open(token_path,"r") as f: token = f.read().strip() + ctx = ssl.create_default_context(cafile=ca_path) + + def req(method, url, data=None, ctype=None): + r = urllib.request.Request(url, data=data, method=method) + r.add_header("Authorization", f"Bearer {token}") + if ctype: + r.add_header("Content-Type", ctype) + return urllib.request.urlopen(r, context=ctx) + + # 1) GET secret + import json as _json + try: + resp = req("GET", f"{api}/api/v1/namespaces/{NS}/secrets/{S}") + secret = _json.loads(resp.read()) + except urllib.error.HTTPError as e: + if e.code == 404: + secret = {"apiVersion":"v1","kind":"Secret","metadata":{"name":S},"type":"Opaque","data":{}} + # create empty secret + data = json.dumps(secret).encode() + req("POST", f"{api}/api/v1/namespaces/{NS}/secrets", data, "application/json") + # re-get for resourceVersion + resp = req("GET", f"{api}/api/v1/namespaces/{NS}/secrets/{S}") + secret = _json.loads(resp.read()) + else: + raise + + data = secret.get("data", {}) + + # decode helper + def b64get(key): + val = data.get(key) + if not val: return "" + return base64.b64decode(val).decode() + + cur_key = b64get(KEYN) + cur_user = b64get(USERN) or "admin" + + if not cur_key: + # generate new key + import secrets + new_key = secrets.token_urlsafe(48) + patch = { + "data": { + KEYN: base64.b64encode(new_key.encode()).decode(), + USERN: base64.b64encode(cur_user.encode()).decode() + } + } + mpatch = json.dumps(patch).encode() + req("PATCH", + f"{api}/api/v1/namespaces/{NS}/secrets/{S}", + mpatch, + "application/merge-patch+json") + cur_key = new_key + + # 2) Patch deployment template annotation to trigger rollout + to_hash = (cur_user + ":" + cur_key).encode() + h = hashlib.sha256(to_hash).hexdigest() + dpatch = { + "spec": { + "template": { + "metadata": { + "annotations": { + "credentials-hash": h + } + } + } + } + } + ddata = json.dumps(dpatch).encode() + req("PATCH", + f"{api}/apis/apps/v1/namespaces/{NS}/deployments/{DEP}", + ddata, + "application/merge-patch+json") + print("Secret ensured and deployment patched for rollout.") + PY diff --git a/openshift/deploy/configmap.yaml b/openshift/deploy/configmap.yaml new file mode 100644 index 0000000..7c6f097 --- /dev/null +++ b/openshift/deploy/configmap.yaml @@ -0,0 +1,48 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: chacra-config + labels: { app: chacra } +data: + config.py: | + import os + STORAGE_ROOT = os.environ.get("STORAGE_ROOT", "/data") + polling_cycle = int(os.environ.get("POLLING_CYCLE", "60")) + sqlalchemy = { + "url": os.environ.get("DATABASE_URL"), + "echo": False, + "pool_recycle": 3600, + } + celery_broker_url = os.environ.get("CELERY_BROKER_URL") + app = { + "root": "chacra.controllers.root.RootController", + "modules": ["chacra"], + "debug": False, + } + server = { "host": "0.0.0.0", "port": int(os.environ.get("PORT", "8000")) } + + # --- required by Chacra for uploads and repo building --- + binary_root = os.path.join(STORAGE_ROOT, "binaries") + distributions_root = os.path.join(STORAGE_ROOT, "repos") # Debian repos live here + repos_root = os.path.join(STORAGE_ROOT, "repos") # used by code checks/tasks + + + # ADD: API credentials required for POST/DELETE + api_user = os.environ.get("CHACRA_API_USER") + api_key = os.environ.get("CHACRA_API_KEY") + + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: chacra-wsgi + labels: { app: chacra } +data: + # This module will be imported as "wsgi_wrapper" + # and Gunicorn will run 'wsgi_wrapper:application'. + # + # It deploys the Pecan app using our config map at /etc/chacra/config.py + wsgi_wrapper.py: | + from pecan.deploy import deploy + application = deploy("/etc/chacra/config.py") diff --git a/openshift/deploy/db-bootstrap-job.yaml b/openshift/deploy/db-bootstrap-job.yaml new file mode 100644 index 0000000..f5ca184 --- /dev/null +++ b/openshift/deploy/db-bootstrap-job.yaml @@ -0,0 +1,60 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: chacra-db-bootstrap + namespace: chacra + labels: { app: chacra, job: db-bootstrap } +spec: + backoffLimit: 0 + ttlSecondsAfterFinished: 86400 + template: + metadata: + labels: { app: chacra, job: db-bootstrap } + spec: + restartPolicy: Never + securityContext: + runAsNonRoot: true + seccompProfile: { type: RuntimeDefault } + containers: + - name: bootstrap + image: image-registry.openshift-image-registry.svc:5000/chacra/chacra:latest + imagePullPolicy: Always + command: ["/bin/sh","-lc"] + args: + - | + set -euo pipefail + echo "==> Creating schema via SQLAlchemy models, then Alembic stamp head ..." + /opt/venv/bin/python - <<'PY' + import os + from sqlalchemy import create_engine + # 1) create all tables from Chacra models + from chacra import models # models.Base.metadata is expected in the package + url = os.environ["DATABASE_URL"] + engine = create_engine(url) + models.Base.metadata.create_all(engine) # creates e.g. projects, repos, etc. + # 2) alembic stamp head + from alembic.config import Config + from alembic import command + cfg = Config("/alembic/alembic.ini") # points to script_location = /alembic + cfg.set_main_option("sqlalchemy.url", url) + command.stamp(cfg, "head") + print("==> Schema created and alembic stamped at head.") + PY + env: + - name: DATABASE_URL + valueFrom: + secretKeyRef: { name: chacra-secrets, key: DATABASE_URL } + volumeMounts: + - { name: alembic-ini, mountPath: /alembic/alembic.ini, subPath: alembic.ini } + resources: + requests: { cpu: 50m, memory: 128Mi } + limits: { cpu: "1", memory: 512Mi } + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: { drop: ["ALL"] } + volumes: + - name: alembic-ini + configMap: + name: chacra-alembic + items: [ { key: alembic.ini, path: alembic.ini } ] diff --git a/openshift/deploy/db-migration-job.yaml b/openshift/deploy/db-migration-job.yaml new file mode 100644 index 0000000..0184cce --- /dev/null +++ b/openshift/deploy/db-migration-job.yaml @@ -0,0 +1,66 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: chacra-db-migrate + namespace: chacra + labels: + app: chacra + job: db-migrate +spec: + backoffLimit: 0 + ttlSecondsAfterFinished: 86400 + template: + metadata: + labels: + app: chacra + job: db-migrate + spec: + restartPolicy: Never + securityContext: + runAsNonRoot: true + seccompProfile: { type: RuntimeDefault } + containers: + - name: alembic + image: image-registry.openshift-image-registry.svc:5000/chacra/chacra:latest + imagePullPolicy: Always + command: ["/bin/sh","-lc"] + args: + - | + set -euo pipefail + echo "==> Running Alembic upgrade head (programmatic sqlalchemy.url; stdin, no writes) ..." + /opt/venv/bin/python - <<'PY' + import os + from alembic.config import Config + from alembic import command + cfg = Config("/alembic/alembic.ini") # script_location = /alembic + cfg.set_main_option("sqlalchemy.url", os.environ["DATABASE_URL"]) + command.upgrade(cfg, "head") + PY + echo "==> Alembic migration completed." + env: + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: chacra-secrets + key: DATABASE_URL + volumeMounts: + - name: alembic-ini + mountPath: /alembic/alembic.ini + subPath: alembic.ini + resources: + requests: { cpu: 50m, memory: 128Mi } + limits: { cpu: "1", memory: 512Mi } + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: { drop: ["ALL"] } + volumes: + - name: alembic-ini + configMap: + # This CM must contain an 'alembic.ini' key with: + # [alembic] + # script_location = /alembic + name: chacra-alembic + items: + - key: alembic.ini + path: alembic.ini diff --git a/openshift/deploy/deployment.yaml b/openshift/deploy/deployment.yaml new file mode 100644 index 0000000..29632e3 --- /dev/null +++ b/openshift/deploy/deployment.yaml @@ -0,0 +1,351 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: chacra-api + namespace: chacra + labels: { app: chacra, tier: api } + annotations: + image.openshift.io/triggers: | + [ + { + "from": { "kind": "ImageStreamTag", "name": "chacra:latest" }, + "fieldPath": "spec.template.spec.containers[?(@.name==\"api\")].image" + } + ] +spec: + replicas: 1 + selector: + matchLabels: { app: chacra, tier: api } + template: + metadata: + labels: { app: chacra, tier: api } + spec: + securityContext: + runAsNonRoot: true + seccompProfile: { type: RuntimeDefault } + initContainers: + - name: init-volumes + image: registry.access.redhat.com/ubi9/ubi-minimal + imagePullPolicy: IfNotPresent + command: ["/bin/sh","-lc"] + args: + - > + set -e; + mkdir -p /mnt/data/binaries /mnt/data/repos; + echo "[init] ensured /mnt/data/binaries and /mnt/data/repos exist"; + exit 0 + volumeMounts: + - { name: data, mountPath: /mnt/data } + containers: + - name: api + image: chacra:latest + imagePullPolicy: IfNotPresent + command: ["/bin/sh","-lc"] + args: + - > + exec /opt/venv/bin/gunicorn + --bind 0.0.0.0:${PORT:-8000} + --workers ${GUNICORN_WORKERS:-4} + --timeout ${GUNICORN_TIMEOUT:-120} + --log-level ${LOG_LEVEL:-info} + 'wsgi_wrapper:application' + ports: [ { name: http, containerPort: 8000 } ] + env: + # <<< UPDATED: prepend /etc/chacra-callbacks to PYTHONPATH + - { name: PYTHONPATH, value: "/etc/chacra-callbacks:/opt/chacra:${PYTHONPATH}" } + - { name: PORT, value: "8000" } + - { name: LOG_LEVEL, value: "INFO" } + - { name: STORAGE_ROOT, value: "/data" } + - { name: PECAN_CONFIG, value: "/etc/chacra/config.py" } + - { name: POLLING_CYCLE, value: "60" } + - name: SECRET_KEY + valueFrom: { secretKeyRef: { name: chacra-secrets, key: SECRET_KEY } } + - name: DATABASE_URL + valueFrom: { secretKeyRef: { name: chacra-secrets, key: DATABASE_URL } } + - name: CELERY_BROKER_URL + valueFrom: { secretKeyRef: { name: chacra-secrets, key: CELERY_BROKER_URL } } + - name: CELERY_RESULT_BACKEND + valueFrom: { secretKeyRef: { name: chacra-secrets, key: CELERY_RESULT_BACKEND } } + - { name: MIGRATE_ON_START, value: "false" } + - name: CHACRA_API_USER + valueFrom: { secretKeyRef: { name: chacra-secrets, key: CHACRA_API_USER } } + - name: CHACRA_API_KEY + valueFrom: { secretKeyRef: { name: chacra-secrets, key: CHACRA_API_KEY } } + + volumeMounts: + - { name: data, mountPath: /data } + - { name: config, mountPath: /etc/chacra } + - { name: wsgi, mountPath: /opt/chacra } + # <<< ADDED: mount the callbacks secret as a single file + - name: callbacks + mountPath: /etc/chacra-callbacks/prod_callbacks.py + subPath: prod_callbacks.py + readOnly: true + readinessProbe: + httpGet: { path: "/", port: 8000 } + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 6 + livenessProbe: + httpGet: { path: "/", port: 8000 } + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 3 + failureThreshold: 3 + resources: + requests: { cpu: 100m, memory: 256Mi } + limits: { cpu: "1", memory: 1Gi } + securityContext: + allowPrivilegeEscalation: false + capabilities: { drop: ["ALL"] } + volumes: + - name: data + persistentVolumeClaim: { claimName: chacra-shared-rwx } # RWX claim + - name: config + configMap: { name: chacra-config } + - name: wsgi + configMap: + name: chacra-wsgi + items: [ { key: wsgi_wrapper.py, path: wsgi_wrapper.py } ] + # <<< ADDED: secret holding prod_callbacks.py + - name: callbacks + secret: + secretName: chacra-callbacks + items: + - key: prod_callbacks.py + path: prod_callbacks.py + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: chacra-beat + namespace: chacra + labels: { app: chacra, tier: beat } + annotations: + image.openshift.io/triggers: | + [ + { + "from": { "kind": "ImageStreamTag", "name": "chacra:latest" }, + "fieldPath": "spec.template.spec.containers[?(@.name==\"beat\")].image" + } + ] +spec: + replicas: 1 + selector: + matchLabels: { app: chacra, tier: beat } + template: + metadata: + labels: { app: chacra, tier: beat } + spec: + securityContext: + runAsNonRoot: true + seccompProfile: { type: RuntimeDefault } + initContainers: + - name: init-volumes + image: registry.access.redhat.com/ubi9/ubi-minimal + imagePullPolicy: IfNotPresent + command: ["/bin/sh","-lc"] + args: + - > + set -e; + mkdir -p /mnt/data/binaries /mnt/data/repos; + echo "[init] ensured /mnt/data/binaries and /mnt/data/repos exist"; + exit 0 + volumeMounts: + - { name: data, mountPath: /mnt/data } + containers: + - name: beat + image: chacra:latest + imagePullPolicy: IfNotPresent + command: ["/bin/sh","-lc"] + args: + - > + exec /opt/venv/bin/celery -A chacra.asynch + beat + --loglevel ${LOG_LEVEL:-INFO} + --schedule /srv/chacra/run/celerybeat-schedule + --pidfile /srv/chacra/run/celery-beat.pid + env: + - { name: LOG_LEVEL, value: "INFO" } + - { name: STORAGE_ROOT, value: "/data" } + - { name: PECAN_CONFIG, value: "/etc/chacra/config.py" } + - { name: POLLING_CYCLE, value: "60" } + # <<< UPDATED: prepend /etc/chacra-callbacks to PYTHONPATH + - { name: PYTHONPATH, value: "/etc/chacra-callbacks:/opt/chacra:${PYTHONPATH}" } + - name: DATABASE_URL + valueFrom: { secretKeyRef: { name: chacra-secrets, key: DATABASE_URL } } + - name: CELERY_BROKER_URL + valueFrom: { secretKeyRef: { name: chacra-secrets, key: CELERY_BROKER_URL } } + - name: CELERY_RESULT_BACKEND + valueFrom: { secretKeyRef: { name: chacra-secrets, key: CELERY_RESULT_BACKEND } } + - name: CHACRA_API_USER + valueFrom: { secretKeyRef: { name: chacra-secrets, key: CHACRA_API_USER } } + - name: CHACRA_API_KEY + valueFrom: { secretKeyRef: { name: chacra-secrets, key: CHACRA_API_KEY } } + volumeMounts: + - { name: data, mountPath: /data } + - { name: config, mountPath: /etc/chacra } + - { name: run, mountPath: /srv/chacra/run } + - { name: log, mountPath: /srv/chacra/log } + # <<< ADDED: mount the callbacks secret + - name: callbacks + mountPath: /etc/chacra-callbacks/prod_callbacks.py + subPath: prod_callbacks.py + readOnly: true + livenessProbe: + exec: + command: [ /bin/sh, -lc, 'test -s /srv/chacra/run/celery-beat.pid && kill -0 $(cat /srv/chacra/run/celery-beat.pid)' ] + initialDelaySeconds: 20 + periodSeconds: 15 + timeoutSeconds: 5 + failureThreshold: 6 + readinessProbe: + exec: + command: [ /bin/sh, -lc, 'test -s /srv/chacra/run/celery-beat.pid && kill -0 $(cat /srv/chacra/run/celery-beat.pid)' ] + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + resources: + requests: { cpu: 50m, memory: 128Mi } + limits: { cpu: 500m, memory: 512Mi } + securityContext: + allowPrivilegeEscalation: false + capabilities: { drop: ["ALL"] } + volumes: + - name: data + persistentVolumeClaim: { claimName: chacra-shared-rwx } # RWX claim + - name: config + configMap: { name: chacra-config } + - name: run + emptyDir: {} + - name: log + emptyDir: {} + # <<< ADDED: secret holding prod_callbacks.py + - name: callbacks + secret: + secretName: chacra-callbacks + items: + - key: prod_callbacks.py + path: prod_callbacks.py + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: chacra-celery + namespace: chacra + labels: { app: chacra, tier: worker } + annotations: + image.openshift.io/triggers: | + [ + { + "from": { "kind": "ImageStreamTag", "name": "chacra:latest" }, + "fieldPath": "spec.template.spec.containers[?(@.name==\"worker\")].image" + } + ] +spec: + replicas: 1 + selector: + matchLabels: { app: chacra, tier: worker } + template: + metadata: + labels: { app: chacra, tier: worker } + spec: + securityContext: + runAsNonRoot: true + seccompProfile: { type: RuntimeDefault } + initContainers: + - name: init-volumes + image: registry.access.redhat.com/ubi9/ubi-minimal + imagePullPolicy: IfNotPresent + command: ["/bin/sh","-lc"] + args: + - > + set -e; + mkdir -p /mnt/data/binaries /mnt/data/repos; + echo "[init] ensured /mnt/data/binaries and /mnt/data/repos exist"; + exit 0 + volumeMounts: + - { name: data, mountPath: /mnt/data } + containers: + - name: worker + image: chacra:latest + imagePullPolicy: IfNotPresent + command: ["/bin/sh","-lc"] + args: + - > + exec /opt/venv/bin/celery -A chacra.asynch + worker + --loglevel ${LOG_LEVEL:-INFO} + --concurrency ${CELERY_CONCURRENCY:-2} + -Q ${CELERY_QUEUES:-default} + --pidfile /srv/chacra/run/celery-worker.pid + --logfile /srv/chacra/log/celery-worker.log + env: + - { name: LOG_LEVEL, value: "INFO" } + - { name: STORAGE_ROOT, value: "/data" } + - { name: PECAN_CONFIG, value: "/etc/chacra/config.py" } + - { name: POLLING_CYCLE, value: "60" } + # <<< UPDATED: prepend /etc/chacra-callbacks to PYTHONPATH + - { name: PYTHONPATH, value: "/etc/chacra-callbacks:/opt/chacra:${PYTHONPATH}" } + - name: DATABASE_URL + valueFrom: { secretKeyRef: { name: chacra-secrets, key: DATABASE_URL } } + - name: CELERY_BROKER_URL + valueFrom: { secretKeyRef: { name: chacra-secrets, key: CELERY_BROKER_URL } } + - name: CELERY_RESULT_BACKEND + valueFrom: { secretKeyRef: { name: chacra-secrets, key: CELERY_RESULT_BACKEND } } + - { name: CELERY_CONCURRENCY, value: "2" } + - { name: CELERY_QUEUES, value: "build_repos,poll_repos,celery,default" } + - name: CHACRA_API_USER + valueFrom: { secretKeyRef: { name: chacra-secrets, key: CHACRA_API_USER } } + - name: CHACRA_API_KEY + valueFrom: { secretKeyRef: { name: chacra-secrets, key: CHACRA_API_KEY } } + volumeMounts: + - { name: data, mountPath: /data } + - { name: config, mountPath: /etc/chacra } + - { name: run, mountPath: /srv/chacra/run } + - { name: log, mountPath: /srv/chacra/log } + # <<< ADDED: mount the callbacks secret + - name: callbacks + mountPath: /etc/chacra-callbacks/prod_callbacks.py + subPath: prod_callbacks.py + readOnly: true + livenessProbe: + exec: + command: [ /bin/sh, -lc, 'test -s /srv/chacra/run/celery-worker.pid && kill -0 $(cat /srv/chacra/run/celery-worker.pid)' ] + initialDelaySeconds: 20 + periodSeconds: 15 + timeoutSeconds: 5 + failureThreshold: 6 + readinessProbe: + exec: + command: [ /bin/sh, -lc, 'test -s /srv/chacra/run/celery-worker.pid && kill -0 $(cat /srv/chacra/run/celery-worker.pid)' ] + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + resources: + requests: { cpu: 100m, memory: 256Mi } + limits: { cpu: "1", memory: 1Gi } + securityContext: + allowPrivilegeEscalation: false + capabilities: { drop: ["ALL"] } + volumes: + - name: data + persistentVolumeClaim: { claimName: chacra-shared-rwx } # RWX claim + - name: config + configMap: { name: chacra-config } + - name: run + emptyDir: {} + - name: log + emptyDir: {} + # <<< ADDED: secret holding prod_callbacks.py + - name: callbacks + secret: + secretName: chacra-callbacks + items: + - key: prod_callbacks.py + path: prod_callbacks.py diff --git a/openshift/deploy/namespace.yaml b/openshift/deploy/namespace.yaml new file mode 100644 index 0000000..7cd1cd7 --- /dev/null +++ b/openshift/deploy/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: chacra diff --git a/openshift/deploy/postgres-pvc.yaml b/openshift/deploy/postgres-pvc.yaml new file mode 100644 index 0000000..8b24f95 --- /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: chacra +spec: + accessModes: ["ReadWriteOnce"] + storageClassName: cephfs-rwx # or omit this line to use the default + resources: + requests: + storage: 50Gi diff --git a/openshift/deploy/postgres-svc.yaml b/openshift/deploy/postgres-svc.yaml new file mode 100644 index 0000000..6786d01 --- /dev/null +++ b/openshift/deploy/postgres-svc.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: postgresql + labels: + app: postgres +spec: + selector: + app: postgres + ports: + - name: pg + port: 5432 + targetPort: pg + type: ClusterIP diff --git a/openshift/deploy/postgres.yaml b/openshift/deploy/postgres.yaml new file mode 100644 index 0000000..37e0a13 --- /dev/null +++ b/openshift/deploy/postgres.yaml @@ -0,0 +1,58 @@ +# openshift/deploy/postgres.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postgres + namespace: chacra + 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: chacra-secrets, key: CHACRA_DB_USER } } + - name: POSTGRESQL_PASSWORD + valueFrom: { secretKeyRef: { name: chacra-secrets, key: CHACRA_DB_PASS } } + - name: POSTGRESQL_DATABASE + valueFrom: { secretKeyRef: { name: chacra-secrets, key: CHACRA_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: chacra + 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..e6d36a4 --- /dev/null +++ b/openshift/deploy/rabbitmq.yaml @@ -0,0 +1,43 @@ +apiVersion: v1 +kind: Service +metadata: + name: rabbitmq + labels: { app: chacra, tier: mq } +spec: + ports: + - name: amqp + port: 5672 + targetPort: 5672 + - name: http + port: 15672 + targetPort: 15672 + selector: { app: chacra, tier: mq } +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: rabbitmq + labels: { app: chacra, tier: mq } +spec: + replicas: 1 + selector: + matchLabels: { app: chacra, tier: mq } + template: + metadata: + labels: { app: chacra, tier: mq } + spec: + containers: + - name: rabbitmq + image: rabbitmq:3-management + imagePullPolicy: IfNotPresent + env: + - name: RABBITMQ_DEFAULT_USER + value: "chacra" # align with CELERY_BROKER_URL + - name: RABBITMQ_DEFAULT_PASS + value: "chacra" + ports: + - containerPort: 5672 + - containerPort: 15672 + resources: + requests: { cpu: 50m, memory: 128Mi } + limits: { cpu: 500m, memory: 512Mi } diff --git a/openshift/deploy/route.yaml b/openshift/deploy/route.yaml new file mode 100644 index 0000000..891676f --- /dev/null +++ b/openshift/deploy/route.yaml @@ -0,0 +1,15 @@ +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: chacra + labels: { app: chacra } +spec: + to: + kind: Service + name: chacra + weight: 100 + port: + targetPort: http + tls: + termination: edge + # host: chacra. # optional diff --git a/openshift/deploy/secret.yaml b/openshift/deploy/secret.yaml new file mode 100644 index 0000000..2e32ad6 --- /dev/null +++ b/openshift/deploy/secret.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Secret +metadata: + name: chacra-secrets + labels: { app: chacra } +type: Opaque +stringData: + # Leave SECRET_KEY empty (will be auto-generated by the job) + SECRET_KEY: "" + + DATABASE_URL: "postgresql://chacra:chacra@postgres:5432/chacra" # TODO change + CELERY_BROKER_URL: "pyamqp://chacra:chacra@rabbitmq:5672//" # TODO change + CELERY_RESULT_BACKEND: "" # e.g., redis://, db+postgresql://, or leave empty + + RABBITMQ_URL: "amqp://chacra:chacra@rabbitmq:5672/%2f" + CHACRA_DB_USER: "chacra" + CHACRA_DB_PASS: "chacra" + CHACRA_DB_NAME: "chacra" + CHACRA_API_USER: "admin" + CHACRA_API_KEY: "secret" diff --git a/openshift/deploy/service.yaml b/openshift/deploy/service.yaml new file mode 100644 index 0000000..b970469 --- /dev/null +++ b/openshift/deploy/service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: chacra + labels: { app: chacra } +spec: + selector: { app: chacra, tier: api } + ports: + - name: http + port: 80 + targetPort: 8000 diff --git a/openshift/serviceaccount.yaml b/openshift/serviceaccount.yaml new file mode 100644 index 0000000..4323244 --- /dev/null +++ b/openshift/serviceaccount.yaml @@ -0,0 +1,33 @@ +# openshift/deploy/sa-rbac-bootstrap.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: chacra-bootstrap + namespace: chacra +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: chacra-bootstrap-secrets-role + namespace: chacra +rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get","create","update","patch"] + - apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["get","patch","update"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: chacra-bootstrap-secrets-rb + namespace: chacra +subjects: + - kind: ServiceAccount + name: chacra-bootstrap + namespace: chacra +roleRef: + kind: Role + name: chacra-bootstrap-secrets-role + apiGroup: rbac.authorization.k8s.io