diff --git a/.github/workflows/build-images.yaml b/.github/workflows/build-images.yaml index 3817539..eb452c1 100644 --- a/.github/workflows/build-images.yaml +++ b/.github/workflows/build-images.yaml @@ -133,13 +133,15 @@ jobs: if echo "$PLATFORMS" | grep -q 'amd64'; then MATRIX=$(echo "$MATRIX" | jq -c '.include += [ {"component":"backend","dockerfile_dir":"./backend","platform":"linux/amd64","platform_pair":"linux-amd64","runner":"ubuntu-latest"}, - {"component":"frontend","dockerfile_dir":"./frontend","platform":"linux/amd64","platform_pair":"linux-amd64","runner":"ubuntu-latest"} + {"component":"frontend","dockerfile_dir":"./frontend","platform":"linux/amd64","platform_pair":"linux-amd64","runner":"ubuntu-latest"}, + {"component":"temporal-worker","dockerfile_dir":"./backend","dockerfile":"Dockerfile.temporal-worker","platform":"linux/amd64","platform_pair":"linux-amd64","runner":"ubuntu-latest"} ]') fi if echo "$PLATFORMS" | grep -q 'arm64'; then MATRIX=$(echo "$MATRIX" | jq -c '.include += [ {"component":"backend","dockerfile_dir":"./backend","platform":"linux/arm64","platform_pair":"linux-arm64","runner":"ubuntu-24.04-arm"}, - {"component":"frontend","dockerfile_dir":"./frontend","platform":"linux/arm64","platform_pair":"linux-arm64","runner":"ubuntu-24.04-arm"} + {"component":"frontend","dockerfile_dir":"./frontend","platform":"linux/arm64","platform_pair":"linux-arm64","runner":"ubuntu-24.04-arm"}, + {"component":"temporal-worker","dockerfile_dir":"./backend","dockerfile":"Dockerfile.temporal-worker","platform":"linux/arm64","platform_pair":"linux-arm64","runner":"ubuntu-24.04-arm"} ]') fi # Use delimiter so JSON is not mangled by GITHUB_OUTPUT parsing @@ -194,7 +196,7 @@ jobs: uses: docker/build-push-action@v6 with: context: . - file: ${{ matrix.dockerfile_dir }}/Dockerfile + file: ${{ matrix.dockerfile_dir }}/${{ matrix.dockerfile || 'Dockerfile' }} platforms: ${{ matrix.platform }} labels: | org.opencontainers.image.revision=${{ github.sha }} @@ -288,3 +290,22 @@ jobs: --annotation "index:org.opencontainers.image.description=SOBA Next.js frontend for web form builder" \ -t ${IMAGE}:latest ${DIGESTS} fi + + - name: Merge temporal-worker + run: | + SHORT_SHA=$(git rev-parse --short HEAD) + IMAGE="ghcr.io/${{ github.repository }}/temporal-worker" + IMAGE_VERSION="${{ needs.prepare.outputs.image_version }}" + DIGESTS="" + for f in $(find digests -type f -name 'temporal-worker-*' 2>/dev/null); do + DIGESTS="${DIGESTS} ${IMAGE}@$(cat $f)" + done + [ -n "$DIGESTS" ] || { echo "No temporal-worker digests"; exit 1; } + docker buildx imagetools create \ + --annotation "index:org.opencontainers.image.description=SOBA Temporal worker for workflow execution" \ + -t ${IMAGE}:sha-${SHORT_SHA} -t ${IMAGE}:${IMAGE_VERSION} ${DIGESTS} + if [ "$IMAGE_VERSION" = "main" ]; then + docker buildx imagetools create \ + --annotation "index:org.opencontainers.image.description=SOBA Temporal worker for workflow execution" \ + -t ${IMAGE}:latest ${DIGESTS} + fi diff --git a/.github/workflows/pr_close.yaml b/.github/workflows/pr_close.yaml index a89d504..c5f0121 100644 --- a/.github/workflows/pr_close.yaml +++ b/.github/workflows/pr_close.yaml @@ -73,6 +73,17 @@ jobs: insecure_skip_tls_verify: true namespace: ${{ secrets.OC_NAMESPACE }} + - name: Delete Temporal PR namespace + env: + NAMESPACE: ${{ secrets.OC_NAMESPACE }} + PR_NUM: ${{ needs.set-vars.outputs.PR_NUM }} + run: | + TEMPORAL_NS="soba-pr-${PR_NUM}" + if oc get deployment temporal-admintools -n "${NAMESPACE}" &>/dev/null; then + oc exec deployment/temporal-admintools -n "${NAMESPACE}" -- \ + temporal operator namespace delete -n "${TEMPORAL_NS}" --yes 2>/dev/null || true + fi + - name: Uninstall SOBA and delete orphaned resources env: RELEASE: ${{ needs.set-vars.outputs.RELEASE }} diff --git a/.github/workflows/pr_open.yaml b/.github/workflows/pr_open.yaml index 4fc234a..17d6cb2 100644 --- a/.github/workflows/pr_open.yaml +++ b/.github/workflows/pr_open.yaml @@ -109,6 +109,36 @@ jobs: insecure_skip_tls_verify: true namespace: ${{ secrets.OC_NAMESPACE }} + - name: Deploy Shared Temporal (idempotent) + env: + NAMESPACE: ${{ secrets.OC_NAMESPACE }} + TEMPORAL_CHART_VERSION: ${{ vars.TEMPORAL_CHART_VERSION || '0.74.0' }} + run: | + helm upgrade --install temporal temporal \ + --repo https://go.temporal.io/helm-charts \ + --version "${TEMPORAL_CHART_VERSION}" \ + --namespace "${NAMESPACE}" \ + --timeout 900s \ + -f deployments/helm/temporal/values.yaml + + echo "Waiting for schema jobs..." + oc wait --for=condition=complete job -l app.kubernetes.io/instance=temporal \ + -n "${NAMESPACE}" --timeout=600s || true + + echo "Waiting for Temporal frontend..." + oc rollout status deployment/temporal-frontend \ + -n "${NAMESPACE}" --timeout=300s + + - name: Ensure Temporal PR namespace exists + env: + NAMESPACE: ${{ secrets.OC_NAMESPACE }} + PR_NUM: ${{ needs.set-vars.outputs.PR_NUM }} + run: | + TEMPORAL_NS="soba-pr-${PR_NUM}" + oc exec deployment/temporal-admintools -n "${NAMESPACE}" -- \ + sh -c "temporal operator namespace describe -n ${TEMPORAL_NS} 2>/dev/null || \ + temporal operator namespace create -n ${TEMPORAL_NS} --retention 3d" + - name: Get DB config from Crunchy secret id: db env: @@ -133,6 +163,7 @@ jobs: TAG: sha-${{ needs.build.outputs.short_sha }} DOMAIN: ${{ vars.OC_DOMAIN }} DATABASE_URI: ${{ steps.db.outputs.uri }} + PR_NUM: ${{ needs.set-vars.outputs.PR_NUM }} run: | REPO_LC=$(echo "$GITHUB_REPOSITORY" | tr '[:upper:]' '[:lower:]') # Escape single quotes for YAML: ' -> '' @@ -147,6 +178,10 @@ jobs: image: repository: "ghcr.io/${REPO_LC}/frontend" tag: "${TAG}" + temporalWorker: + image: + repository: "ghcr.io/${REPO_LC}/temporal-worker" + tag: "${TAG}" global: domain: "${DOMAIN}" database: @@ -155,6 +190,9 @@ jobs: admin: password: "${{ secrets.FORMIO_ADMIN_PASSWORD }}" jwtSecret: "${{ secrets.FORMIO_JWT_SECRET }}" + temporal: + address: "temporal-frontend.${NAMESPACE}.svc.cluster.local:7233" + namespace: "soba-pr-${PR_NUM}" OVERRIDE helm upgrade --install "${RELEASE}" ./deployments/helm/soba \ @@ -186,6 +224,9 @@ jobs: echo "Waiting for mongodb rollout..." oc rollout status statefulset/${RELEASE}-mongodb -n "${NAMESPACE}" --timeout=300s + echo "Waiting for temporal-worker rollout..." + oc rollout status deployment/${RELEASE}-temporal-worker -n "${NAMESPACE}" --timeout=300s + - name: Release Comment on PR uses: marocchino/sticky-pull-request-comment@v2 if: success() diff --git a/backend/Dockerfile.temporal-worker b/backend/Dockerfile.temporal-worker new file mode 100644 index 0000000..93d3242 --- /dev/null +++ b/backend/Dockerfile.temporal-worker @@ -0,0 +1,25 @@ +FROM node:lts-slim + +# enable pnpm via corepack and ensure workspace installs work +RUN corepack enable pnpm + +WORKDIR /app + +# copy root workspace config + backend package file +COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./ +COPY backend/package.json backend/ + +# install only backend deps (hoisted into root node_modules) +RUN pnpm install --filter ./backend... + +# copy entire repo and build backend +COPY . . +RUN pnpm --filter ./backend run build + +# OpenShift / arbitrary UID: run non-root; g=u lets assigned user (often gid 0) read /app +RUN chgrp -R 0 /app && chmod -R g=u /app +ENV HOME=/tmp +ENV XDG_CACHE_HOME=/tmp/.cache +USER 1001 + +CMD ["node", "backend/dist/temporal-worker.js"] diff --git a/deployments/helm/action-crunchy/values.yml b/deployments/helm/action-crunchy/values.yml index d692777..37ff1b7 100644 --- a/deployments/helm/action-crunchy/values.yml +++ b/deployments/helm/action-crunchy/values.yml @@ -83,3 +83,8 @@ crunchy: databases: - '{{ .Values.global.config.dbName }}' - 'postgres' + - name: 'temporal' + databases: + - 'temporal' + - 'temporal_visibility' + options: "SUPERUSER CREATEDB CREATEROLE" diff --git a/deployments/helm/soba/templates/temporal/configmap.yaml b/deployments/helm/soba/templates/temporal/configmap.yaml new file mode 100644 index 0000000..975d157 --- /dev/null +++ b/deployments/helm/soba/templates/temporal/configmap.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "soba.fullname" . }}-temporal + labels: + {{- include "soba.labels" (dict "root" . "component" "temporal") | nindent 4 }} +data: + TEMPORAL_ALLOWED: {{ .Values.temporal.allowed | default "false" | quote }} + TEMPORAL_ADDRESS: {{ .Values.temporal.address | default "" | quote }} + TEMPORAL_NAMESPACE: {{ .Values.temporal.namespace | default "default" | quote }} + TEMPORAL_TASK_QUEUE: {{ .Values.temporal.taskQueue | default "soba" | quote }} diff --git a/deployments/helm/soba/templates/temporal/deployment-worker.yaml b/deployments/helm/soba/templates/temporal/deployment-worker.yaml new file mode 100644 index 0000000..9f3bdf4 --- /dev/null +++ b/deployments/helm/soba/templates/temporal/deployment-worker.yaml @@ -0,0 +1,50 @@ +{{- $tw := .Values.temporalWorker | default dict }} +{{- $twEnabled := false }} +{{- if hasKey $tw "enabled" }}{{ $twEnabled = index $tw "enabled" }}{{ end }} +{{- if $twEnabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "soba.fullname" . }}-temporal-worker + labels: + {{- include "soba.labels" (dict "root" . "component" "temporal-worker") | nindent 4 }} +spec: + replicas: {{ .Values.temporalWorker.replicas }} + selector: + matchLabels: + {{- include "soba.selectorLabels" (dict "root" . "component" "temporal-worker") | nindent 6 }} + template: + metadata: + annotations: + checksum/config-temporal: {{ include (print $.Template.BasePath "/temporal/configmap.yaml") . | sha256sum }} + checksum/config-app: {{ include (print $.Template.BasePath "/backend/configmap-app.yaml") . | sha256sum }} + checksum/config-formio: {{ include (print $.Template.BasePath "/backend/configmap-formio.yaml") . | sha256sum }} + checksum/config-sso: {{ include (print $.Template.BasePath "/backend/configmap-sso.yaml") . | sha256sum }} + checksum/config-ratelimit: {{ include (print $.Template.BasePath "/backend/configmap-ratelimit.yaml") . | sha256sum }} + checksum/secret-db: {{ include (print $.Template.BasePath "/secrets/db-secret.yaml") . | sha256sum }} + checksum/secret-formio: {{ include (print $.Template.BasePath "/secrets/formio-secret.yaml") . | sha256sum }} + labels: + {{- include "soba.selectorLabels" (dict "root" . "component" "temporal-worker") | nindent 8 }} + spec: + containers: + - name: temporal-worker + image: "{{ .Values.temporalWorker.image.repository }}:{{ .Values.temporalWorker.image.tag }}" + imagePullPolicy: {{ .Values.temporalWorker.image.pullPolicy }} + envFrom: + - configMapRef: + name: {{ include "soba.fullname" . }}-temporal + - configMapRef: + name: {{ include "soba.fullname" . }}-backend-app + - configMapRef: + name: {{ include "soba.fullname" . }}-backend-formio + - configMapRef: + name: {{ include "soba.fullname" . }}-backend-sso + - configMapRef: + name: {{ include "soba.fullname" . }}-backend-ratelimit + - secretRef: + name: {{ include "soba.fullname" . }}-db + - secretRef: + name: {{ include "soba.fullname" . }}-formio + resources: + {{- toYaml .Values.temporalWorker.resources | nindent 12 }} +{{- end }} diff --git a/deployments/helm/soba/values-dev.yaml b/deployments/helm/soba/values-dev.yaml index 2dcbedf..04490ad 100644 --- a/deployments/helm/soba/values-dev.yaml +++ b/deployments/helm/soba/values-dev.yaml @@ -51,3 +51,17 @@ outboxWorker: limits: cpu: 100m memory: 256Mi + +temporal: + allowed: "true" + address: "" + namespace: "soba-dev" + taskQueue: "soba" + +temporalWorker: + enabled: true + image: + repository: ghcr.io/bcgov/soba/temporal-worker + tag: latest + pullPolicy: IfNotPresent + replicas: 1 diff --git a/deployments/helm/soba/values-pr.yaml b/deployments/helm/soba/values-pr.yaml index 9707684..063c099 100644 --- a/deployments/helm/soba/values-pr.yaml +++ b/deployments/helm/soba/values-pr.yaml @@ -77,3 +77,24 @@ outboxWorker: limits: cpu: 100m memory: 256Mi + +temporal: + allowed: "true" + address: "" + namespace: "default" + taskQueue: "soba" + +temporalWorker: + enabled: true + image: + repository: ghcr.io/bcgov/soba/temporal-worker + tag: latest + pullPolicy: IfNotPresent + replicas: 1 + resources: + requests: + cpu: 25m + memory: 128Mi + limits: + cpu: 100m + memory: 512Mi diff --git a/deployments/helm/soba/values.yaml b/deployments/helm/soba/values.yaml index bcb438a..a6a7202 100644 --- a/deployments/helm/soba/values.yaml +++ b/deployments/helm/soba/values.yaml @@ -227,3 +227,26 @@ mongodb: persistence: size: 1Gi storageClassName: "" + +# -- Temporal (shared server, env vars for backend + worker) ------------------ +temporal: + allowed: "false" + address: "" + namespace: "default" + taskQueue: "soba" + +# -- Temporal worker (dedicated image with glibc for @temporalio/core-bridge) -- +temporalWorker: + enabled: false + image: + repository: ghcr.io/bcgov/soba/temporal-worker + tag: latest + pullPolicy: IfNotPresent + replicas: 1 + resources: + requests: + cpu: 25m + memory: 128Mi + limits: + cpu: 100m + memory: 512Mi diff --git a/deployments/helm/temporal/values.yaml b/deployments/helm/temporal/values.yaml new file mode 100644 index 0000000..2af94cb --- /dev/null +++ b/deployments/helm/temporal/values.yaml @@ -0,0 +1,85 @@ +server: + image: + repository: temporalio/server + tag: 1.30.3 + configMapsToMount: "sprig" + setConfigFilePath: true + securityContext: + fsGroup: null + runAsUser: null + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + config: + persistence: + defaultStore: default + visibilityStore: visibility + default: + driver: "sql" + sql: + driver: "postgres12" + host: pg-soba-crunchy-primary + port: 5432 + database: temporal + user: temporal + existingSecret: pg-soba-crunchy-pguser-temporal + maxConns: 20 + maxIdleConns: 20 + maxConnLifetime: "1h" + visibility: + driver: "sql" + sql: + driver: "postgres12" + host: pg-soba-crunchy-primary + port: 5432 + database: temporal_visibility + user: temporal + existingSecret: pg-soba-crunchy-pguser-temporal + maxConns: 20 + maxIdleConns: 20 + maxConnLifetime: "1h" + +cassandra: + enabled: false +mysql: + enabled: false +elasticsearch: + enabled: false +prometheus: + enabled: false +grafana: + enabled: false + +schema: + createDatabase: + enabled: false + setup: + enabled: true + backoffLimit: 100 + update: + enabled: true + backoffLimit: 100 + +web: + enabled: true + resources: + requests: + cpu: 15m + memory: 48Mi + limits: + cpu: 100m + memory: 128Mi + +admintools: + enabled: true + resources: + requests: + cpu: 10m + memory: 48Mi + limits: + cpu: 50m + memory: 128Mi