diff --git a/Dockerfile b/Dockerfile index ddbbfb8..1d31ca3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,7 @@ COPY --from=builder /app/.venv /app/.venv COPY --from=builder /app/src /app/src COPY alembic.ini ./ COPY migrations/ ./migrations/ +COPY scripts/ ./scripts/ COPY activity-definitions/ ./activity-definitions/ COPY event-types/ ./event-types/ COPY tasks/ ./tasks/ diff --git a/k8s/railiance/00-namespace.yaml b/k8s/railiance/00-namespace.yaml new file mode 100644 index 0000000..52cc531 --- /dev/null +++ b/k8s/railiance/00-namespace.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: activity-core + labels: + app.kubernetes.io/name: activity-core + app.kubernetes.io/part-of: custodian diff --git a/k8s/railiance/10-infrastructure.yaml b/k8s/railiance/10-infrastructure.yaml new file mode 100644 index 0000000..9878999 --- /dev/null +++ b/k8s/railiance/10-infrastructure.yaml @@ -0,0 +1,364 @@ +apiVersion: v1 +kind: Service +metadata: + name: actcore-app-db + namespace: activity-core + labels: + app.kubernetes.io/name: actcore-app-db + app.kubernetes.io/part-of: activity-core +spec: + selector: + app.kubernetes.io/name: actcore-app-db + ports: + - name: postgres + port: 5432 + targetPort: postgres +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: actcore-app-db + namespace: activity-core + labels: + app.kubernetes.io/name: actcore-app-db + app.kubernetes.io/part-of: activity-core +spec: + serviceName: actcore-app-db + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: actcore-app-db + template: + metadata: + labels: + app.kubernetes.io/name: actcore-app-db + app.kubernetes.io/part-of: activity-core + spec: + containers: + - name: postgres + image: postgres:16 + imagePullPolicy: IfNotPresent + ports: + - name: postgres + containerPort: 5432 + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: actcore-app-db-secret + key: username + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: actcore-app-db-secret + key: password + - name: POSTGRES_DB + valueFrom: + secretKeyRef: + name: actcore-app-db-secret + key: database + readinessProbe: + exec: + command: ["pg_isready", "-U", "actcore"] + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + exec: + command: ["pg_isready", "-U", "actcore"] + initialDelaySeconds: 30 + periodSeconds: 20 + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 5Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: actcore-temporal-db + namespace: activity-core + labels: + app.kubernetes.io/name: actcore-temporal-db + app.kubernetes.io/part-of: activity-core +spec: + selector: + app.kubernetes.io/name: actcore-temporal-db + ports: + - name: postgres + port: 5432 + targetPort: postgres +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: actcore-temporal-db + namespace: activity-core + labels: + app.kubernetes.io/name: actcore-temporal-db + app.kubernetes.io/part-of: activity-core +spec: + serviceName: actcore-temporal-db + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: actcore-temporal-db + template: + metadata: + labels: + app.kubernetes.io/name: actcore-temporal-db + app.kubernetes.io/part-of: activity-core + spec: + containers: + - name: postgres + image: postgres:16 + imagePullPolicy: IfNotPresent + ports: + - name: postgres + containerPort: 5432 + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: actcore-temporal-db-secret + key: username + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: actcore-temporal-db-secret + key: password + - name: POSTGRES_DB + valueFrom: + secretKeyRef: + name: actcore-temporal-db-secret + key: database + readinessProbe: + exec: + command: ["pg_isready", "-U", "temporal"] + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + exec: + command: ["pg_isready", "-U", "temporal"] + initialDelaySeconds: 30 + periodSeconds: 20 + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 8Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: actcore-nats + namespace: activity-core + labels: + app.kubernetes.io/name: actcore-nats + app.kubernetes.io/part-of: activity-core +spec: + selector: + app.kubernetes.io/name: actcore-nats + ports: + - name: client + port: 4222 + targetPort: client + - name: monitor + port: 8222 + targetPort: monitor +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: actcore-nats + namespace: activity-core + labels: + app.kubernetes.io/name: actcore-nats + app.kubernetes.io/part-of: activity-core +spec: + serviceName: actcore-nats + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: actcore-nats + template: + metadata: + labels: + app.kubernetes.io/name: actcore-nats + app.kubernetes.io/part-of: activity-core + spec: + containers: + - name: nats + image: nats:2.10-alpine + imagePullPolicy: IfNotPresent + args: ["-js", "-sd", "/data", "-m", "8222"] + ports: + - name: client + containerPort: 4222 + - name: monitor + containerPort: 8222 + readinessProbe: + httpGet: + path: /healthz + port: monitor + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /healthz + port: monitor + initialDelaySeconds: 30 + periodSeconds: 20 + volumeMounts: + - name: data + mountPath: /data + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 5Gi +--- +apiVersion: v1 +kind: Service +metadata: + name: actcore-temporal + namespace: activity-core + labels: + app.kubernetes.io/name: actcore-temporal + app.kubernetes.io/part-of: activity-core +spec: + selector: + app.kubernetes.io/name: actcore-temporal + ports: + - name: grpc + port: 7233 + targetPort: grpc +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: actcore-temporal + namespace: activity-core + labels: + app.kubernetes.io/name: actcore-temporal + app.kubernetes.io/part-of: activity-core +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: actcore-temporal + template: + metadata: + labels: + app.kubernetes.io/name: actcore-temporal + app.kubernetes.io/part-of: activity-core + spec: + containers: + - name: temporal + image: temporalio/auto-setup:1.29.1 + imagePullPolicy: IfNotPresent + ports: + - name: grpc + containerPort: 7233 + env: + - name: DB + value: postgres12 + - name: DB_PORT + value: "5432" + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: actcore-temporal-db-secret + key: username + - name: POSTGRES_PWD + valueFrom: + secretKeyRef: + name: actcore-temporal-db-secret + key: password + - name: POSTGRES_SEEDS + value: actcore-temporal-db + - name: DBNAME + value: temporal + - name: VISIBILITY_DBNAME + value: temporal_visibility + - name: ENABLE_ES + value: "false" + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: TEMPORAL_ADDRESS + value: "$(POD_IP):7233" + readinessProbe: + exec: + command: + - sh + - -c + - temporal operator cluster health --address "${POD_IP}:7233" + initialDelaySeconds: 45 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 12 +--- +apiVersion: v1 +kind: Service +metadata: + name: actcore-temporal-ui + namespace: activity-core + labels: + app.kubernetes.io/name: actcore-temporal-ui + app.kubernetes.io/part-of: activity-core +spec: + selector: + app.kubernetes.io/name: actcore-temporal-ui + ports: + - name: http + port: 8080 + targetPort: http +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: actcore-temporal-ui + namespace: activity-core + labels: + app.kubernetes.io/name: actcore-temporal-ui + app.kubernetes.io/part-of: activity-core +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: actcore-temporal-ui + template: + metadata: + labels: + app.kubernetes.io/name: actcore-temporal-ui + app.kubernetes.io/part-of: activity-core + spec: + containers: + - name: temporal-ui + image: temporalio/ui:latest + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8080 + env: + - name: TEMPORAL_ADDRESS + value: actcore-temporal:7233 + - name: TEMPORAL_CORS_ORIGINS + value: http://localhost:8080 diff --git a/k8s/railiance/20-runtime.yaml b/k8s/railiance/20-runtime.yaml new file mode 100644 index 0000000..6520f0b --- /dev/null +++ b/k8s/railiance/20-runtime.yaml @@ -0,0 +1,221 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: actcore-runtime-config + namespace: activity-core + labels: + app.kubernetes.io/name: activity-core + app.kubernetes.io/part-of: activity-core +data: + TEMPORAL_HOST: actcore-temporal:7233 + TEMPORAL_NAMESPACE: default + NATS_URL: nats://actcore-nats:4222 + STATE_HUB_URL: http://inter-hub.inter-hub.svc.cluster.local:8000 + REPO_SCOPING_URL: http://repo-scoping.repo-scoping.svc.cluster.local:8020 + ISSUE_CORE_URL: http://issue-core.issue-core.svc.cluster.local:8010 + ISSUE_SINK_TYPE: "null" + ACTIVITY_DEFINITION_DIRS: "" + PROMETHEUS_BIND_ADDR: 0.0.0.0:9090 + ACTIVITY_CURATOR_GATE: disabled +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: actcore-migrate + namespace: activity-core + labels: + app.kubernetes.io/name: actcore-migrate + app.kubernetes.io/part-of: activity-core +spec: + backoffLimit: 3 + template: + metadata: + labels: + app.kubernetes.io/name: actcore-migrate + app.kubernetes.io/part-of: activity-core + spec: + restartPolicy: OnFailure + containers: + - name: migrate + image: activity-core:railiance01-prod + imagePullPolicy: Never + command: ["python", "-m", "alembic", "upgrade", "head"] + envFrom: + - configMapRef: + name: actcore-runtime-config + - secretRef: + name: actcore-runtime-secret +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: actcore-sync + namespace: activity-core + labels: + app.kubernetes.io/name: actcore-sync + app.kubernetes.io/part-of: activity-core +spec: + backoffLimit: 3 + template: + metadata: + labels: + app.kubernetes.io/name: actcore-sync + app.kubernetes.io/part-of: activity-core + spec: + restartPolicy: OnFailure + containers: + - name: sync + image: activity-core:railiance01-prod + imagePullPolicy: Never + command: + - sh + - -c + - python scripts/sync_event_types.py && python -m activity_core.sync_activity_definitions + envFrom: + - configMapRef: + name: actcore-runtime-config + - secretRef: + name: actcore-runtime-secret +--- +apiVersion: v1 +kind: Service +metadata: + name: actcore-api + namespace: activity-core + labels: + app.kubernetes.io/name: actcore-api + app.kubernetes.io/part-of: activity-core +spec: + selector: + app.kubernetes.io/name: actcore-api + ports: + - name: http + port: 8010 + targetPort: http +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: actcore-api + namespace: activity-core + labels: + app.kubernetes.io/name: actcore-api + app.kubernetes.io/part-of: activity-core +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: actcore-api + template: + metadata: + labels: + app.kubernetes.io/name: actcore-api + app.kubernetes.io/part-of: activity-core + spec: + containers: + - name: api + image: activity-core:railiance01-prod + imagePullPolicy: Never + command: ["uvicorn", "activity_core.api:app", "--host", "0.0.0.0", "--port", "8010"] + ports: + - name: http + containerPort: 8010 + envFrom: + - configMapRef: + name: actcore-runtime-config + - secretRef: + name: actcore-runtime-secret + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 6 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 45 + periodSeconds: 20 + timeoutSeconds: 5 +--- +apiVersion: v1 +kind: Service +metadata: + name: actcore-worker-metrics + namespace: activity-core + labels: + app.kubernetes.io/name: actcore-worker + app.kubernetes.io/part-of: activity-core +spec: + selector: + app.kubernetes.io/name: actcore-worker + ports: + - name: metrics + port: 9090 + targetPort: metrics +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: actcore-worker + namespace: activity-core + labels: + app.kubernetes.io/name: actcore-worker + app.kubernetes.io/part-of: activity-core +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: actcore-worker + template: + metadata: + labels: + app.kubernetes.io/name: actcore-worker + app.kubernetes.io/part-of: activity-core + spec: + containers: + - name: worker + image: activity-core:railiance01-prod + imagePullPolicy: Never + command: ["python", "-m", "activity_core.worker"] + ports: + - name: metrics + containerPort: 9090 + envFrom: + - configMapRef: + name: actcore-runtime-config + - secretRef: + name: actcore-runtime-secret +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: actcore-event-router + namespace: activity-core + labels: + app.kubernetes.io/name: actcore-event-router + app.kubernetes.io/part-of: activity-core +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: actcore-event-router + template: + metadata: + labels: + app.kubernetes.io/name: actcore-event-router + app.kubernetes.io/part-of: activity-core + spec: + containers: + - name: event-router + image: activity-core:railiance01-prod + imagePullPolicy: Never + command: ["python", "-m", "activity_core.event_router"] + envFrom: + - configMapRef: + name: actcore-runtime-config + - secretRef: + name: actcore-runtime-secret diff --git a/k8s/railiance/README.md b/k8s/railiance/README.md new file mode 100644 index 0000000..d0ed4c4 --- /dev/null +++ b/k8s/railiance/README.md @@ -0,0 +1,56 @@ +# Railiance01 Kubernetes Deployment + +This bundle establishes activity-core as an internal production service on the +railiance01 K3s cluster. It keeps the unauthenticated API as a ClusterIP service; +publish it through an authenticated ingress only after choosing the final host +name and access policy. + +## Layout + +- `00-namespace.yaml`: namespace and shared labels +- `10-infrastructure.yaml`: PostgreSQL for app data, PostgreSQL for Temporal, + NATS JetStream, Temporal, and Temporal UI +- `20-runtime.yaml`: migrate/sync jobs plus API, worker, and event-router +- `bootstrap-secrets.sh`: idempotently creates generated Kubernetes secrets + +The runtime image tag is `activity-core:railiance01-prod` and is expected to be +loaded into the railiance01 K3s containerd image store. + +## Deploy + +```bash +docker build -t activity-core:railiance01-prod . +docker save -o /tmp/activity-core-railiance01-prod.tar activity-core:railiance01-prod +scp /tmp/activity-core-railiance01-prod.tar railiance01:/tmp/ +ssh railiance01 sudo k3s ctr images import /tmp/activity-core-railiance01-prod.tar +rsync -a k8s/railiance/ railiance01:activity-core/k8s/railiance/ + +ssh railiance01 +cd ~/activity-core +bash k8s/railiance/bootstrap-secrets.sh +kubectl apply -f k8s/railiance/10-infrastructure.yaml +kubectl -n activity-core wait --for=condition=ready pod -l app.kubernetes.io/name=actcore-app-db --timeout=180s +kubectl -n activity-core wait --for=condition=ready pod -l app.kubernetes.io/name=actcore-temporal-db --timeout=180s +kubectl -n activity-core wait --for=condition=ready pod -l app.kubernetes.io/name=actcore-nats --timeout=180s +kubectl -n activity-core rollout status deploy/actcore-temporal --timeout=300s + +kubectl -n activity-core delete job actcore-migrate --ignore-not-found +kubectl apply -f k8s/railiance/20-runtime.yaml +kubectl -n activity-core wait --for=condition=complete job/actcore-migrate --timeout=180s +kubectl -n activity-core rollout status deploy/actcore-api --timeout=180s +kubectl -n activity-core rollout status deploy/actcore-worker --timeout=180s +kubectl -n activity-core rollout status deploy/actcore-event-router --timeout=180s +kubectl -n activity-core delete job actcore-sync --ignore-not-found +kubectl apply -f k8s/railiance/20-runtime.yaml +kubectl -n activity-core wait --for=condition=complete job/actcore-sync --timeout=180s +``` + +## Verify + +```bash +kubectl -n activity-core exec deploy/actcore-api -- \ + python -c "import urllib.request; print(urllib.request.urlopen('http://localhost:8010/health').read().decode())" + +kubectl -n activity-core get pods +kubectl -n activity-core get svc +``` diff --git a/k8s/railiance/bootstrap-secrets.sh b/k8s/railiance/bootstrap-secrets.sh new file mode 100644 index 0000000..53bbad6 --- /dev/null +++ b/k8s/railiance/bootstrap-secrets.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +NS="${NS:-activity-core}" + +kubectl apply -f k8s/railiance/00-namespace.yaml + +secret_exists() { + kubectl -n "$NS" get secret "$1" >/dev/null 2>&1 +} + +random_password() { + openssl rand -base64 32 | tr -d '\n' +} + +if ! secret_exists actcore-app-db-secret; then + APP_DB_PASSWORD="$(random_password)" + kubectl -n "$NS" create secret generic actcore-app-db-secret \ + --from-literal=username=actcore \ + --from-literal=database=actcore \ + --from-literal=password="$APP_DB_PASSWORD" +else + APP_DB_PASSWORD="$(kubectl -n "$NS" get secret actcore-app-db-secret -o jsonpath='{.data.password}' | base64 -d)" +fi + +if ! secret_exists actcore-temporal-db-secret; then + kubectl -n "$NS" create secret generic actcore-temporal-db-secret \ + --from-literal=username=temporal \ + --from-literal=database=temporal \ + --from-literal=password="$(random_password)" +fi + +ACTCORE_DB_URL="postgresql+asyncpg://actcore:${APP_DB_PASSWORD}@actcore-app-db:5432/actcore" + +if ! secret_exists actcore-runtime-secret; then + kubectl -n "$NS" create secret generic actcore-runtime-secret \ + --from-literal=ACTCORE_DB_URL="$ACTCORE_DB_URL" \ + --from-literal=WEBHOOK_SECRET_GITEA="" \ + --from-literal=WEBHOOK_SECRET_GITHUB="" +fi diff --git a/src/activity_core/event_type_registry.py b/src/activity_core/event_type_registry.py index dc4b2ad..6faac69 100644 --- a/src/activity_core/event_type_registry.py +++ b/src/activity_core/event_type_registry.py @@ -183,7 +183,7 @@ async def sync_event_types(session_factory: Any) -> int: (type_id, version, publisher, governance, status, attribute_schema, raw_md, synced_at) VALUES (:type_id, :version, :publisher, :governance, :status, - :attribute_schema::jsonb, :raw_md, now()) + CAST(:attribute_schema AS jsonb), :raw_md, now()) ON CONFLICT (type_id) DO UPDATE SET version = EXCLUDED.version, publisher = EXCLUDED.publisher, diff --git a/workplans/ACTIVITY-WP-0005-railiance01-production-service.md b/workplans/ACTIVITY-WP-0005-railiance01-production-service.md new file mode 100644 index 0000000..e462a26 --- /dev/null +++ b/workplans/ACTIVITY-WP-0005-railiance01-production-service.md @@ -0,0 +1,58 @@ +--- +id: ACTIVITY-WP-0005 +type: workplan +title: "Railiance01 production service" +domain: custodian +repo: activity-core +status: finished +owner: codex +topic_slug: custodian +created: "2026-05-22" +updated: "2026-05-22" +--- + +# ACTIVITY-WP-0005 - Railiance01 Production Service + +## Review Railiance Runtime + +```task +id: ACTIVITY-WP-0005-T01 +status: done +priority: high +``` + +Confirm railiance01 access, operating system, container runtime, and cluster +shape before selecting the production deployment path. + +## Add Kubernetes Deployment Bundle + +```task +id: ACTIVITY-WP-0005-T02 +status: done +priority: high +``` + +Create a K3s-native deployment bundle for activity-core, including infrastructure, +runtime jobs, API, worker, event router, and generated Kubernetes secrets. + +## Build And Import Production Image + +```task +id: ACTIVITY-WP-0005-T03 +status: done +priority: high +``` + +Build the production image locally, transfer it to railiance01, and import it +into the K3s containerd image store. + +## Apply And Verify Service + +```task +id: ACTIVITY-WP-0005-T04 +status: done +priority: high +``` + +Apply the manifests on railiance01, run migrations and sync jobs, then verify +the API health endpoint and core pods.