From 51530595db4461829e3eaedf97989ed8c63da56a Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Thu, 25 Jun 2026 17:45:12 +0200 Subject: [PATCH] feat: k3s exp --- .github/workflows/deploy-k8s.yml | 203 ++++++++++++++++++ .github/workflows/on_main_2.yml | 19 ++ .github/workflows/on_pr.yml | 49 +++++ infra/.sops.yaml | 1 + infra/k8s/base/configmaps/caddy-config.yaml | 67 ++++++ infra/k8s/base/deployments/core.yaml | 72 +++++++ infra/k8s/base/deployments/jobs.yaml | 43 ++++ infra/k8s/base/deployments/site-builder.yaml | 46 ++++ infra/k8s/base/jobs/minio-init.yaml | 45 ++++ infra/k8s/base/kustomization.yaml | 17 ++ infra/k8s/base/services/core.yaml | 13 ++ infra/k8s/base/services/minio.yaml | 17 ++ infra/k8s/base/services/postgres.yaml | 14 ++ infra/k8s/base/services/site-builder.yaml | 13 ++ infra/k8s/base/services/valkey.yaml | 14 ++ infra/k8s/base/statefulsets/minio.yaml | 54 +++++ infra/k8s/base/statefulsets/postgres.yaml | 68 ++++++ infra/k8s/base/statefulsets/valkey.yaml | 34 +++ infra/k8s/overlays/preview/kustomization.yaml | 39 ++++ .../overlays/preview/patches/core-env.yaml | 44 ++++ .../overlays/preview/patches/jobs-env.yaml | 20 ++ .../overlays/preview/patches/minio-env.yaml | 12 ++ .../preview/patches/site-builder-env.yaml | 22 ++ .../k8s/overlays/preview/resources/caddy.yaml | 48 +++++ .../overlays/preview/resources/inbucket.yaml | 50 +++++ .../overlays/preview/resources/ingress.yaml | 30 +++ .../preview/resources/mock-notify.yaml | 45 ++++ infra/k8s/overlays/sandbox/kustomization.yaml | 39 ++++ .../overlays/sandbox/patches/core-env.yaml | 44 ++++ .../overlays/sandbox/patches/jobs-env.yaml | 20 ++ .../overlays/sandbox/patches/minio-env.yaml | 12 ++ .../sandbox/patches/site-builder-env.yaml | 22 ++ .../k8s/overlays/sandbox/resources/caddy.yaml | 51 +++++ .../overlays/sandbox/resources/inbucket.yaml | 53 +++++ .../overlays/sandbox/resources/ingress.yaml | 30 +++ .../sandbox/resources/mock-notify.yaml | 48 +++++ infra/k8s/scripts/deploy-preview.sh | 47 ++++ infra/k8s/scripts/deploy-sandbox.sh | 49 +++++ infra/k8s/scripts/teardown-preview.sh | 14 ++ 39 files changed, 1528 insertions(+) create mode 100644 .github/workflows/deploy-k8s.yml create mode 100644 infra/k8s/base/configmaps/caddy-config.yaml create mode 100644 infra/k8s/base/deployments/core.yaml create mode 100644 infra/k8s/base/deployments/jobs.yaml create mode 100644 infra/k8s/base/deployments/site-builder.yaml create mode 100644 infra/k8s/base/jobs/minio-init.yaml create mode 100644 infra/k8s/base/kustomization.yaml create mode 100644 infra/k8s/base/services/core.yaml create mode 100644 infra/k8s/base/services/minio.yaml create mode 100644 infra/k8s/base/services/postgres.yaml create mode 100644 infra/k8s/base/services/site-builder.yaml create mode 100644 infra/k8s/base/services/valkey.yaml create mode 100644 infra/k8s/base/statefulsets/minio.yaml create mode 100644 infra/k8s/base/statefulsets/postgres.yaml create mode 100644 infra/k8s/base/statefulsets/valkey.yaml create mode 100644 infra/k8s/overlays/preview/kustomization.yaml create mode 100644 infra/k8s/overlays/preview/patches/core-env.yaml create mode 100644 infra/k8s/overlays/preview/patches/jobs-env.yaml create mode 100644 infra/k8s/overlays/preview/patches/minio-env.yaml create mode 100644 infra/k8s/overlays/preview/patches/site-builder-env.yaml create mode 100644 infra/k8s/overlays/preview/resources/caddy.yaml create mode 100644 infra/k8s/overlays/preview/resources/inbucket.yaml create mode 100644 infra/k8s/overlays/preview/resources/ingress.yaml create mode 100644 infra/k8s/overlays/preview/resources/mock-notify.yaml create mode 100644 infra/k8s/overlays/sandbox/kustomization.yaml create mode 100644 infra/k8s/overlays/sandbox/patches/core-env.yaml create mode 100644 infra/k8s/overlays/sandbox/patches/jobs-env.yaml create mode 100644 infra/k8s/overlays/sandbox/patches/minio-env.yaml create mode 100644 infra/k8s/overlays/sandbox/patches/site-builder-env.yaml create mode 100644 infra/k8s/overlays/sandbox/resources/caddy.yaml create mode 100644 infra/k8s/overlays/sandbox/resources/inbucket.yaml create mode 100644 infra/k8s/overlays/sandbox/resources/ingress.yaml create mode 100644 infra/k8s/overlays/sandbox/resources/mock-notify.yaml create mode 100755 infra/k8s/scripts/deploy-preview.sh create mode 100755 infra/k8s/scripts/deploy-sandbox.sh create mode 100755 infra/k8s/scripts/teardown-preview.sh diff --git a/.github/workflows/deploy-k8s.yml b/.github/workflows/deploy-k8s.yml new file mode 100644 index 000000000..d3126a71b --- /dev/null +++ b/.github/workflows/deploy-k8s.yml @@ -0,0 +1,203 @@ +name: Deploy to K3s + +on: + workflow_call: + inputs: + action: + required: true + type: string + description: "'deploy' or 'teardown'" + image_tag: + required: false + type: string + description: "image tag to deploy" + namespace: + required: true + type: string + description: "k8s namespace (e.g. pubstar-sandbox, preview-pr-42)" + hostname: + required: true + type: string + description: "public hostname for this deployment" + env_file: + required: true + type: string + description: "sops-encrypted env file to decrypt (relative to infra/)" + overlay: + required: true + type: string + description: "kustomize overlay to use (sandbox or preview)" + pr_number: + required: false + type: string + description: "PR number (only for preview deployments)" + secrets: + SSH_PRIVATE_KEY: + required: true + SSH_USER: + required: true + SSH_HOST: + required: true + GHCR_USER: + required: true + GHCR_TOKEN: + required: true + +permissions: + contents: read + +jobs: + deploy: + if: inputs.action == 'deploy' + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + + - name: Start SSH agent + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Add known hosts + run: | + mkdir -p ~/.ssh + ssh-keyscan -H "${{ secrets.SSH_HOST }}" >> ~/.ssh/known_hosts + + - name: Deploy to K3s + env: + SSH_USER: ${{ secrets.SSH_USER }} + SSH_HOST: ${{ secrets.SSH_HOST }} + REPO: ${{ github.repository }} + BRANCH: ${{ github.head_ref || github.ref_name }} + GHCR_USER: ${{ secrets.GHCR_USER }} + GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} + IMAGE_TAG: ${{ inputs.image_tag }} + NAMESPACE: ${{ inputs.namespace }} + DEPLOY_HOST: ${{ inputs.hostname }} + ENV_FILE: ${{ inputs.env_file }} + OVERLAY: ${{ inputs.overlay }} + PR_NUMBER: ${{ inputs.pr_number }} + run: | + ssh "${SSH_USER}@${SSH_HOST}" \ + "env GHCR_USER='${GHCR_USER}' GHCR_TOKEN='${GHCR_TOKEN}' IMAGE_TAG='${IMAGE_TAG}' NAMESPACE='${NAMESPACE}' ENV_FILE='${ENV_FILE}' OVERLAY='${OVERLAY}' PR_NUMBER='${PR_NUMBER}' bash -s -- '${REPO}' '${BRANCH}'" <<'EOS' + set -euo pipefail + + REPO="${1:?missing repo}" + BRANCH="${2:-main}" + + : "${IMAGE_TAG:?missing IMAGE_TAG}" + : "${GHCR_USER:?missing GHCR_USER}" + : "${GHCR_TOKEN:?missing GHCR_TOKEN}" + : "${NAMESPACE:?missing NAMESPACE}" + : "${ENV_FILE:?missing ENV_FILE}" + : "${OVERLAY:?missing OVERLAY}" + + REPO_NAME="${REPO##*/}" + APP_DIR="/srv/k3s/${REPO_NAME}" + REPO_SSH="git@github.com:${REPO}.git" + + ssh-keyscan -H github.com >> ~/.ssh/known_hosts 2>/dev/null + chmod 600 ~/.ssh/known_hosts + + if [[ ! -d "${APP_DIR}/.git" ]]; then + mkdir -p "${APP_DIR}" + git clone --branch "${BRANCH}" "${REPO_SSH}" "${APP_DIR}" + fi + + cd "${APP_DIR}" + git fetch --prune --tags origin + git checkout --detach "origin/${BRANCH}" + + cd infra + umask 077 + sops -d --input-type dotenv --output-type dotenv "$ENV_FILE" > ".env.k8s" + + # create namespace + kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - + + # label previews for cleanup tracking + if [[ -n "${PR_NUMBER:-}" ]]; then + kubectl label namespace "$NAMESPACE" \ + pubstar.io/preview=true \ + pubstar.io/pr-number="$PR_NUMBER" \ + --overwrite + fi + + # GHCR pull secret + kubectl create secret docker-registry ghcr \ + --docker-server=ghcr.io \ + --docker-username="$GHCR_USER" \ + --docker-password="$GHCR_TOKEN" \ + --namespace="$NAMESPACE" \ + --dry-run=client -o yaml | kubectl apply -f - + + # app secrets from decrypted env + kubectl create secret generic pubstar-env \ + --from-env-file=".env.k8s" \ + --namespace="$NAMESPACE" \ + --dry-run=client -o yaml | kubectl apply -f - + + rm -f ".env.k8s" + + # build and apply manifests + cd k8s/overlays/"$OVERLAY" + + if [[ "$OVERLAY" == "preview" ]]; then + kustomize build . \ + | sed "s/__PR_NUMBER__/${PR_NUMBER}/g" \ + | sed "s/newTag: IMAGE_TAG/newTag: ${IMAGE_TAG}/g" \ + | sed "s/image: \(ghcr\.io\/knowledgefutures\/[^:]*\):IMAGE_TAG/image: \1:${IMAGE_TAG}/g" \ + | kubectl apply -n "$NAMESPACE" -f - + else + kustomize edit set image \ + "ghcr.io/knowledgefutures/platform:${IMAGE_TAG}" \ + "ghcr.io/knowledgefutures/platform-jobs:${IMAGE_TAG}" \ + "ghcr.io/knowledgefutures/platform-site-builder:${IMAGE_TAG}" \ + "ghcr.io/knowledgefutures/mock-coar-notify-server:${IMAGE_TAG}" + kustomize build . | kubectl apply -n "$NAMESPACE" -f - + git checkout -- kustomization.yaml 2>/dev/null || true + fi + + echo "waiting for core deployment rollout..." + kubectl rollout status deployment/core -n "$NAMESPACE" --timeout=600s + + echo "" + kubectl get all -n "$NAMESPACE" + EOS + + teardown: + if: inputs.action == 'teardown' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Start SSH agent + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Add known hosts + run: | + mkdir -p ~/.ssh + ssh-keyscan -H "${{ secrets.SSH_HOST }}" >> ~/.ssh/known_hosts + + - name: Teardown namespace + env: + SSH_USER: ${{ secrets.SSH_USER }} + SSH_HOST: ${{ secrets.SSH_HOST }} + NAMESPACE: ${{ inputs.namespace }} + run: | + ssh "${SSH_USER}@${SSH_HOST}" \ + "env NAMESPACE='${NAMESPACE}' bash -s" <<'EOS' + set -euo pipefail + : "${NAMESPACE:?missing NAMESPACE}" + + echo "tearing down namespace $NAMESPACE" + + if kubectl get namespace "$NAMESPACE" &>/dev/null; then + kubectl delete namespace "$NAMESPACE" + echo "namespace $NAMESPACE deleted" + else + echo "namespace $NAMESPACE not found, nothing to tear down" + fi + EOS diff --git a/.github/workflows/on_main_2.yml b/.github/workflows/on_main_2.yml index 1d04d2d89..9d3d9de30 100644 --- a/.github/workflows/on_main_2.yml +++ b/.github/workflows/on_main_2.yml @@ -59,6 +59,25 @@ jobs: GHCR_USER: ${{ secrets.GHCR_USER }} GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} + deploy-sandbox-k3s: + needs: build-all + permissions: + contents: read + uses: ./.github/workflows/deploy-k8s.yml + with: + action: deploy + image_tag: ${{ github.sha }} + namespace: pubstar-sandbox + hostname: sandbox.k3s.pubstar.org + env_file: .env.sandbox.enc + overlay: sandbox + secrets: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + SSH_USER: ${{ secrets.SSH_USER }} + SSH_HOST: ${{ secrets.SSH_HOST_K3S }} + GHCR_USER: ${{ secrets.GHCR_USER }} + GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} + deploy-docs: permissions: contents: write diff --git a/.github/workflows/on_pr.yml b/.github/workflows/on_pr.yml index 28268361f..dd0938d68 100644 --- a/.github/workflows/on_pr.yml +++ b/.github/workflows/on_pr.yml @@ -165,6 +165,55 @@ jobs: GHCR_USER: ${{ secrets.GHCR_USER }} GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} + deploy-preview-k3s: + if: >- + always() && + contains(github.event.pull_request.labels.*.name, 'preview') && + github.event.action != 'closed' && + github.event.action != 'unlabeled' && + needs.skip_build_sha.result == 'success' && + (needs.build-all.result == 'success' || + (needs.build-all.result == 'skipped' && needs.skip_build_sha.outputs.last-successful-build-sha != '')) + uses: ./.github/workflows/deploy-k8s.yml + needs: + - skip_build_sha + - build-all + permissions: + contents: read + with: + action: deploy + image_tag: ${{ needs.skip_build_sha.outputs.last-successful-build-sha || github.event.pull_request.head.sha }} + namespace: preview-pr-${{ github.event.pull_request.number }} + hostname: pr-${{ github.event.pull_request.number }}.k3s.pubstar.org + env_file: .env.preview.enc + overlay: preview + pr_number: ${{ github.event.pull_request.number }} + secrets: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + SSH_USER: ${{ secrets.SSH_USER }} + SSH_HOST: ${{ secrets.SSH_HOST_K3S }} + GHCR_USER: ${{ secrets.GHCR_USER }} + GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} + + close-preview-k3s: + if: (github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'preview')) || (github.event.action == 'unlabeled' && github.event.label.name == 'preview') + uses: ./.github/workflows/deploy-k8s.yml + permissions: + contents: read + with: + action: teardown + namespace: preview-pr-${{ github.event.pull_request.number }} + hostname: pr-${{ github.event.pull_request.number }}.k3s.pubstar.org + env_file: .env.preview.enc + overlay: preview + pr_number: ${{ github.event.pull_request.number }} + secrets: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + SSH_USER: ${{ secrets.SSH_USER }} + SSH_HOST: ${{ secrets.SSH_HOST_K3S }} + GHCR_USER: ${{ secrets.GHCR_USER }} + GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }} + deploy-docs-preview: permissions: contents: write diff --git a/infra/.sops.yaml b/infra/.sops.yaml index 155163bdb..bf2e67e62 100644 --- a/infra/.sops.yaml +++ b/infra/.sops.yaml @@ -16,3 +16,4 @@ creation_rules: - age1pgxk292zq30wafwg03gge7hu5dlu3h7yfldp2y8kqekfaljjky7s752uwy - age15fsv503cl9ajfg9fphw45sfq0ytlptnr8wtlt48asytz4ev6rfks2pwt5s - age14dssfufn8n0n7lcvyxxp4rq7zuf5pv0ek4zl9h7atsy85ceynfvqa0qz59 + - age19pjnf2dqg0sd3j4v8sfvlpytk0jg4v0230j5np8qku55y67r6a5qjs6rmn # k3s-exp diff --git a/infra/k8s/base/configmaps/caddy-config.yaml b/infra/k8s/base/configmaps/caddy-config.yaml new file mode 100644 index 000000000..14dd69648 --- /dev/null +++ b/infra/k8s/base/configmaps/caddy-config.yaml @@ -0,0 +1,67 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: caddy-config + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: caddy +data: + Caddyfile: | + { + admin off + } + + :80 { + encode gzip + + @assetsHost header_regexp assets Host ^(?:pr-\d+-assets|sandbox-assets)\. + handle @assetsHost { + reverse_proxy minio:9000 + } + + handle_path /assets* { + reverse_proxy minio:9000 + } + + handle_path /assets-ui* { + reverse_proxy minio:9001 + } + + handle_path /site-builder* { + reverse_proxy site-builder:4000 + } + + handle_path /sites/* { + @pathWithSlash path_regexp dir (.+)/$ + handle @pathWithSlash { + redir {re.dir.1} permanent + } + + rewrite * /pubstar-assets/sites{uri} + + reverse_proxy minio:9000 { + @error status 403 404 + handle_response @error { + rewrite * {uri}/index.html + reverse_proxy minio:9000 { + @nestedError status 404 + handle_response @nestedError { + respond "Not found" 404 + } + } + } + } + } + + handle /emails* { + reverse_proxy inbucket:9000 + } + + handle /mock-notify* { + reverse_proxy mock-notify:3000 + } + + handle { + reverse_proxy core:3000 + } + } diff --git a/infra/k8s/base/deployments/core.yaml b/infra/k8s/base/deployments/core.yaml new file mode 100644 index 000000000..577c772a3 --- /dev/null +++ b/infra/k8s/base/deployments/core.yaml @@ -0,0 +1,72 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: core + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: core +spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + app.kubernetes.io/component: core + template: + metadata: + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: core + spec: + imagePullSecrets: + - name: ghcr + initContainers: + - name: wait-for-db + image: postgres:17-alpine + command: ["sh", "-c", "until pg_isready -h db -p 5432; do sleep 2; done"] + containers: + - name: core + image: ghcr.io/knowledgefutures/platform:IMAGE_TAG + ports: + - containerPort: 3000 + envFrom: + - secretRef: + name: pubstar-env + env: + - name: HOSTNAME + value: "0.0.0.0" + - name: NODE_ENV + value: production + - name: PORT + value: "3000" + - name: SITE_BUILDER_ENDPOINT + value: http://site-builder:4000 + - name: VALKEY_HOST + value: valkey + - name: VALKEY_PORT + value: "6379" + readinessProbe: + httpGet: + path: /api/health + port: 3000 + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 6 + livenessProbe: + httpGet: + path: /api/health + port: 3000 + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 3 + failureThreshold: 3 + resources: + requests: + cpu: 250m + memory: 512Mi + limits: + memory: 2Gi diff --git a/infra/k8s/base/deployments/jobs.yaml b/infra/k8s/base/deployments/jobs.yaml new file mode 100644 index 000000000..f8c09d5d1 --- /dev/null +++ b/infra/k8s/base/deployments/jobs.yaml @@ -0,0 +1,43 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: jobs + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: jobs +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/component: jobs + template: + metadata: + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: jobs + spec: + imagePullSecrets: + - name: ghcr + initContainers: + - name: wait-for-db + image: postgres:17-alpine + command: ["sh", "-c", "until pg_isready -h db -p 5432; do sleep 2; done"] + containers: + - name: jobs + image: ghcr.io/knowledgefutures/platform-jobs:IMAGE_TAG + envFrom: + - secretRef: + name: pubstar-env + env: + - name: NODE_ENV + value: production + - name: PUBSTAR_URL + value: http://core:3000 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + memory: 1Gi diff --git a/infra/k8s/base/deployments/site-builder.yaml b/infra/k8s/base/deployments/site-builder.yaml new file mode 100644 index 000000000..b7d299616 --- /dev/null +++ b/infra/k8s/base/deployments/site-builder.yaml @@ -0,0 +1,46 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: site-builder + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: site-builder +spec: + replicas: 1 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + app.kubernetes.io/component: site-builder + template: + metadata: + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: site-builder + spec: + imagePullSecrets: + - name: ghcr + containers: + - name: site-builder + image: ghcr.io/knowledgefutures/platform-site-builder:IMAGE_TAG + ports: + - containerPort: 4000 + envFrom: + - secretRef: + name: pubstar-env + env: + - name: NODE_ENV + value: production + - name: PORT + value: "4000" + - name: PUBSTAR_URL + value: http://core:3000 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + memory: 1Gi diff --git a/infra/k8s/base/jobs/minio-init.yaml b/infra/k8s/base/jobs/minio-init.yaml new file mode 100644 index 000000000..bbbc83018 --- /dev/null +++ b/infra/k8s/base/jobs/minio-init.yaml @@ -0,0 +1,45 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: minio-init + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: minio-init +spec: + backoffLimit: 5 + template: + metadata: + labels: + app.kubernetes.io/component: minio-init + spec: + restartPolicy: OnFailure + initContainers: + - name: wait-for-minio + image: busybox:1.36 + command: + - sh + - -c + - "until wget -q -O- http://minio:9000/minio/health/ready; do sleep 2; done" + containers: + - name: mc + image: minio/mc:latest + command: ["/bin/sh", "-c"] + args: + - | + /usr/bin/mc alias set myminio http://minio:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" + /usr/bin/mc mb --ignore-existing myminio/"$S3_BUCKET_NAME" + /usr/bin/mc anonymous set download myminio/"$S3_BUCKET_NAME" + /usr/bin/mc admin user add myminio "$S3_ACCESS_KEY" "$S3_SECRET_KEY" + /usr/bin/mc admin policy attach myminio readwrite --user "$S3_ACCESS_KEY" + if [ -n "$S3_BACKUP_BUCKET" ] && [ -n "$S3_BACKUP_ACCESS_KEY" ] && [ -n "$S3_BACKUP_SECRET_KEY" ]; then + /usr/bin/mc mb --ignore-existing myminio/"$S3_BACKUP_BUCKET" + /usr/bin/mc anonymous set none myminio/"$S3_BACKUP_BUCKET" + /usr/bin/mc admin user add myminio "$S3_BACKUP_ACCESS_KEY" "$S3_BACKUP_SECRET_KEY" + /usr/bin/mc admin policy attach myminio readwrite --user "$S3_BACKUP_ACCESS_KEY" + fi + echo "clearing assets and backup buckets for clean state..." + /usr/bin/mc rm --recursive --force myminio/"$S3_BUCKET_NAME"/ || true + /usr/bin/mc rm --recursive --force myminio/"$S3_BACKUP_BUCKET"/ || true + envFrom: + - secretRef: + name: pubstar-env diff --git a/infra/k8s/base/kustomization.yaml b/infra/k8s/base/kustomization.yaml new file mode 100644 index 000000000..464456b8e --- /dev/null +++ b/infra/k8s/base/kustomization.yaml @@ -0,0 +1,17 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - deployments/core.yaml + - deployments/jobs.yaml + - deployments/site-builder.yaml + - statefulsets/postgres.yaml + - statefulsets/valkey.yaml + - statefulsets/minio.yaml + - services/core.yaml + - services/site-builder.yaml + - services/postgres.yaml + - services/valkey.yaml + - services/minio.yaml + - jobs/minio-init.yaml + - configmaps/caddy-config.yaml diff --git a/infra/k8s/base/services/core.yaml b/infra/k8s/base/services/core.yaml new file mode 100644 index 000000000..d6aca7fd9 --- /dev/null +++ b/infra/k8s/base/services/core.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: core + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: core +spec: + selector: + app.kubernetes.io/component: core + ports: + - port: 3000 + targetPort: 3000 diff --git a/infra/k8s/base/services/minio.yaml b/infra/k8s/base/services/minio.yaml new file mode 100644 index 000000000..2e67b60d3 --- /dev/null +++ b/infra/k8s/base/services/minio.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: minio + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: minio +spec: + selector: + app.kubernetes.io/component: minio + ports: + - port: 9000 + targetPort: 9000 + name: api + - port: 9001 + targetPort: 9001 + name: console diff --git a/infra/k8s/base/services/postgres.yaml b/infra/k8s/base/services/postgres.yaml new file mode 100644 index 000000000..bb2ad5d60 --- /dev/null +++ b/infra/k8s/base/services/postgres.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: db + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: db +spec: + selector: + app.kubernetes.io/component: db + ports: + - port: 5432 + targetPort: 5432 + clusterIP: None diff --git a/infra/k8s/base/services/site-builder.yaml b/infra/k8s/base/services/site-builder.yaml new file mode 100644 index 000000000..8963d4ad6 --- /dev/null +++ b/infra/k8s/base/services/site-builder.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: site-builder + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: site-builder +spec: + selector: + app.kubernetes.io/component: site-builder + ports: + - port: 4000 + targetPort: 4000 diff --git a/infra/k8s/base/services/valkey.yaml b/infra/k8s/base/services/valkey.yaml new file mode 100644 index 000000000..7f4c9fe07 --- /dev/null +++ b/infra/k8s/base/services/valkey.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: valkey + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: valkey +spec: + selector: + app.kubernetes.io/component: valkey + ports: + - port: 6379 + targetPort: 6379 + clusterIP: None diff --git a/infra/k8s/base/statefulsets/minio.yaml b/infra/k8s/base/statefulsets/minio.yaml new file mode 100644 index 000000000..bc272b6bf --- /dev/null +++ b/infra/k8s/base/statefulsets/minio.yaml @@ -0,0 +1,54 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: minio + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: minio +spec: + serviceName: minio + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/component: minio + template: + metadata: + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: minio + spec: + containers: + - name: minio + image: minio/minio:latest + args: ["server", "--console-address", ":9001", "/data"] + ports: + - containerPort: 9000 + name: api + - containerPort: 9001 + name: console + envFrom: + - secretRef: + name: pubstar-env + volumeMounts: + - name: minio-data + mountPath: /data + readinessProbe: + httpGet: + path: /minio/health/ready + port: 9000 + periodSeconds: 10 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + memory: 1Gi + volumeClaimTemplates: + - metadata: + name: minio-data + spec: + accessModes: ["ReadWriteOnce"] + storageClassName: local-path + resources: + requests: + storage: 10Gi diff --git a/infra/k8s/base/statefulsets/postgres.yaml b/infra/k8s/base/statefulsets/postgres.yaml new file mode 100644 index 000000000..e4f6f2871 --- /dev/null +++ b/infra/k8s/base/statefulsets/postgres.yaml @@ -0,0 +1,68 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: db + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: db +spec: + serviceName: db + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/component: db + template: + metadata: + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: db + spec: + containers: + - name: postgres + image: postgres:17 + args: + - -c + - shared_buffers=512MB + - -c + - effective_cache_size=1GB + - -c + - work_mem=16MB + - -c + - maintenance_work_mem=128MB + - -c + - max_connections=200 + ports: + - containerPort: 5432 + envFrom: + - secretRef: + name: pubstar-env + volumeMounts: + - name: pgdata + mountPath: /var/lib/postgresql/data + - name: shm + mountPath: /dev/shm + readinessProbe: + exec: + command: ["pg_isready", "-U", "postgres"] + periodSeconds: 5 + timeoutSeconds: 3 + resources: + requests: + cpu: 250m + memory: 1Gi + limits: + memory: 2Gi + volumes: + - name: shm + emptyDir: + medium: Memory + sizeLimit: 512Mi + volumeClaimTemplates: + - metadata: + name: pgdata + spec: + accessModes: ["ReadWriteOnce"] + storageClassName: local-path + resources: + requests: + storage: 20Gi diff --git a/infra/k8s/base/statefulsets/valkey.yaml b/infra/k8s/base/statefulsets/valkey.yaml new file mode 100644 index 000000000..966f3e083 --- /dev/null +++ b/infra/k8s/base/statefulsets/valkey.yaml @@ -0,0 +1,34 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: valkey + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: valkey +spec: + serviceName: valkey + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/component: valkey + template: + metadata: + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: valkey + spec: + containers: + - name: valkey + image: valkey/valkey:8-alpine + ports: + - containerPort: 6379 + readinessProbe: + exec: + command: ["valkey-cli", "ping"] + periodSeconds: 5 + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + memory: 256Mi diff --git a/infra/k8s/overlays/preview/kustomization.yaml b/infra/k8s/overlays/preview/kustomization.yaml new file mode 100644 index 000000000..4dc1c0a29 --- /dev/null +++ b/infra/k8s/overlays/preview/kustomization.yaml @@ -0,0 +1,39 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# Namespace is set dynamically by the deploy script: preview-pr- + +resources: + - ../../base + - resources/inbucket.yaml + - resources/mock-notify.yaml + - resources/caddy.yaml + - resources/ingress.yaml + +patches: + - path: patches/core-env.yaml + target: + kind: Deployment + name: core + - path: patches/jobs-env.yaml + target: + kind: Deployment + name: jobs + - path: patches/site-builder-env.yaml + target: + kind: Deployment + name: site-builder + - path: patches/minio-env.yaml + target: + kind: StatefulSet + name: minio + +images: + - name: ghcr.io/knowledgefutures/platform + newTag: IMAGE_TAG + - name: ghcr.io/knowledgefutures/platform-jobs + newTag: IMAGE_TAG + - name: ghcr.io/knowledgefutures/platform-site-builder + newTag: IMAGE_TAG + - name: ghcr.io/knowledgefutures/mock-coar-notify-server + newTag: IMAGE_TAG diff --git a/infra/k8s/overlays/preview/patches/core-env.yaml b/infra/k8s/overlays/preview/patches/core-env.yaml new file mode 100644 index 000000000..806db450e --- /dev/null +++ b/infra/k8s/overlays/preview/patches/core-env.yaml @@ -0,0 +1,44 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: core +spec: + template: + spec: + containers: + - name: core + env: + - name: HOSTNAME + value: "0.0.0.0" + - name: NODE_ENV + value: production + - name: PORT + value: "3000" + - name: SITE_BUILDER_ENDPOINT + value: http://site-builder:4000 + - name: VALKEY_HOST + value: valkey + - name: VALKEY_PORT + value: "6379" + - name: PUBSTAR_URL + value: https://pr-__PR_NUMBER__.k3s.pubstar.org + - name: PUBSTAR_HOSTNAME + value: pr-__PR_NUMBER__.k3s.pubstar.org + - name: S3_ENDPOINT + value: https://pr-__PR_NUMBER__-assets.k3s.pubstar.org + - name: S3_BACKUP_ENDPOINT + value: https://pr-__PR_NUMBER__-assets.k3s.pubstar.org + - name: S3_PUBLIC_ENDPOINT + value: https://pr-__PR_NUMBER__-assets.k3s.pubstar.org + - name: S3_BACKUP_REGION + value: us-east-1 + - name: S3_BACKUP_KEY_PREFIX + value: pg-backups + - name: DB_RESET + value: "true" + - name: DB_SEED + value: "true" + - name: MOCK_NOTIFY_INBOX_URL + value: http://mock-notify:3000/mock-notify/api/inbox + - name: FLAGS + value: "uploads:on,invites:on,disabled-actions:http,http-allowed-domains:localhost+127.0.0.1+.pubstar.org+.k3s.pubstar.org,show-test-only-tools:on" diff --git a/infra/k8s/overlays/preview/patches/jobs-env.yaml b/infra/k8s/overlays/preview/patches/jobs-env.yaml new file mode 100644 index 000000000..50e253dc6 --- /dev/null +++ b/infra/k8s/overlays/preview/patches/jobs-env.yaml @@ -0,0 +1,20 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: jobs +spec: + template: + spec: + containers: + - name: jobs + env: + - name: NODE_ENV + value: production + - name: PUBSTAR_URL + value: http://core:3000 + - name: S3_BACKUP_REGION + value: us-east-1 + - name: S3_BACKUP_ENDPOINT + value: https://pr-__PR_NUMBER__-assets.k3s.pubstar.org + - name: S3_BACKUP_KEY_PREFIX + value: pg-backups diff --git a/infra/k8s/overlays/preview/patches/minio-env.yaml b/infra/k8s/overlays/preview/patches/minio-env.yaml new file mode 100644 index 000000000..35028035c --- /dev/null +++ b/infra/k8s/overlays/preview/patches/minio-env.yaml @@ -0,0 +1,12 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: minio +spec: + template: + spec: + containers: + - name: minio + env: + - name: MINIO_BROWSER_REDIRECT_URL + value: https://pr-__PR_NUMBER__.k3s.pubstar.org/assets-ui diff --git a/infra/k8s/overlays/preview/patches/site-builder-env.yaml b/infra/k8s/overlays/preview/patches/site-builder-env.yaml new file mode 100644 index 000000000..72362481c --- /dev/null +++ b/infra/k8s/overlays/preview/patches/site-builder-env.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: site-builder +spec: + template: + spec: + containers: + - name: site-builder + env: + - name: NODE_ENV + value: production + - name: PORT + value: "4000" + - name: PUBSTAR_URL + value: http://core:3000 + - name: SITES_BASE_URL + value: https://pr-__PR_NUMBER__.k3s.pubstar.org/sites + - name: S3_ENDPOINT + value: https://pr-__PR_NUMBER__-assets.k3s.pubstar.org + - name: S3_PUBLIC_ENDPOINT + value: https://pr-__PR_NUMBER__-assets.k3s.pubstar.org diff --git a/infra/k8s/overlays/preview/resources/caddy.yaml b/infra/k8s/overlays/preview/resources/caddy.yaml new file mode 100644 index 000000000..09c685c82 --- /dev/null +++ b/infra/k8s/overlays/preview/resources/caddy.yaml @@ -0,0 +1,48 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: caddy + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: caddy +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/component: caddy + template: + metadata: + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: caddy + spec: + containers: + - name: caddy + image: caddy:2 + ports: + - containerPort: 80 + volumeMounts: + - name: caddy-config + mountPath: /etc/caddy/Caddyfile + subPath: Caddyfile + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + memory: 128Mi + volumes: + - name: caddy-config + configMap: + name: caddy-config +--- +apiVersion: v1 +kind: Service +metadata: + name: caddy +spec: + selector: + app.kubernetes.io/component: caddy + ports: + - port: 80 + targetPort: 80 diff --git a/infra/k8s/overlays/preview/resources/inbucket.yaml b/infra/k8s/overlays/preview/resources/inbucket.yaml new file mode 100644 index 000000000..7304dc6e5 --- /dev/null +++ b/infra/k8s/overlays/preview/resources/inbucket.yaml @@ -0,0 +1,50 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: inbucket + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: inbucket +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/component: inbucket + template: + metadata: + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: inbucket + spec: + containers: + - name: inbucket + image: inbucket/inbucket:latest + ports: + - containerPort: 9000 + name: web + - containerPort: 2500 + name: smtp + env: + - name: INBUCKET_WEB_BASEPATH + value: /emails + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + memory: 128Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: inbucket +spec: + selector: + app.kubernetes.io/component: inbucket + ports: + - port: 9000 + targetPort: 9000 + name: web + - port: 2500 + targetPort: 2500 + name: smtp diff --git a/infra/k8s/overlays/preview/resources/ingress.yaml b/infra/k8s/overlays/preview/resources/ingress.yaml new file mode 100644 index 000000000..1f2542f9b --- /dev/null +++ b/infra/k8s/overlays/preview/resources/ingress.yaml @@ -0,0 +1,30 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: pubstar-ingress + labels: + app.kubernetes.io/name: pubstar + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure +spec: + rules: + - host: pr-__PR_NUMBER__.k3s.pubstar.org + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: caddy + port: + number: 80 + - host: pr-__PR_NUMBER__-assets.k3s.pubstar.org + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: caddy + port: + number: 80 diff --git a/infra/k8s/overlays/preview/resources/mock-notify.yaml b/infra/k8s/overlays/preview/resources/mock-notify.yaml new file mode 100644 index 000000000..d6adb97a4 --- /dev/null +++ b/infra/k8s/overlays/preview/resources/mock-notify.yaml @@ -0,0 +1,45 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mock-notify + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: mock-notify +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/component: mock-notify + template: + metadata: + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: mock-notify + spec: + imagePullSecrets: + - name: ghcr + containers: + - name: mock-notify + image: ghcr.io/knowledgefutures/mock-coar-notify-server:IMAGE_TAG + ports: + - containerPort: 3000 + env: + - name: PUBSTAR_URL + value: https://pr-__PR_NUMBER__.k3s.pubstar.org + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + memory: 256Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: mock-notify +spec: + selector: + app.kubernetes.io/component: mock-notify + ports: + - port: 3000 + targetPort: 3000 diff --git a/infra/k8s/overlays/sandbox/kustomization.yaml b/infra/k8s/overlays/sandbox/kustomization.yaml new file mode 100644 index 000000000..b2d961c85 --- /dev/null +++ b/infra/k8s/overlays/sandbox/kustomization.yaml @@ -0,0 +1,39 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: pubstar-sandbox + +resources: + - ../../base + - resources/inbucket.yaml + - resources/mock-notify.yaml + - resources/caddy.yaml + - resources/ingress.yaml + +patches: + - path: patches/core-env.yaml + target: + kind: Deployment + name: core + - path: patches/jobs-env.yaml + target: + kind: Deployment + name: jobs + - path: patches/site-builder-env.yaml + target: + kind: Deployment + name: site-builder + - path: patches/minio-env.yaml + target: + kind: StatefulSet + name: minio + +images: + - name: ghcr.io/knowledgefutures/platform + newTag: IMAGE_TAG + - name: ghcr.io/knowledgefutures/platform-jobs + newTag: IMAGE_TAG + - name: ghcr.io/knowledgefutures/platform-site-builder + newTag: IMAGE_TAG + - name: ghcr.io/knowledgefutures/mock-coar-notify-server + newTag: IMAGE_TAG diff --git a/infra/k8s/overlays/sandbox/patches/core-env.yaml b/infra/k8s/overlays/sandbox/patches/core-env.yaml new file mode 100644 index 000000000..9d2ba5f74 --- /dev/null +++ b/infra/k8s/overlays/sandbox/patches/core-env.yaml @@ -0,0 +1,44 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: core +spec: + template: + spec: + containers: + - name: core + env: + - name: HOSTNAME + value: "0.0.0.0" + - name: NODE_ENV + value: production + - name: PORT + value: "3000" + - name: SITE_BUILDER_ENDPOINT + value: http://site-builder:4000 + - name: VALKEY_HOST + value: valkey + - name: VALKEY_PORT + value: "6379" + - name: PUBSTAR_URL + value: https://sandbox.k3s.pubstar.org + - name: PUBSTAR_HOSTNAME + value: sandbox.k3s.pubstar.org + - name: S3_ENDPOINT + value: https://sandbox-assets.k3s.pubstar.org + - name: S3_BACKUP_ENDPOINT + value: https://sandbox-assets.k3s.pubstar.org + - name: S3_PUBLIC_ENDPOINT + value: https://sandbox-assets.k3s.pubstar.org + - name: S3_BACKUP_REGION + value: us-east-1 + - name: S3_BACKUP_KEY_PREFIX + value: pg-backups + - name: DB_RESET + value: "true" + - name: DB_SEED + value: "true" + - name: MOCK_NOTIFY_INBOX_URL + value: http://mock-notify:3000/mock-notify/api/inbox + - name: FLAGS + value: "uploads:on,invites:on,disabled-actions:http,http-allowed-domains:localhost+127.0.0.1+.pubstar.org+.k3s.pubstar.org,show-test-only-tools:on" diff --git a/infra/k8s/overlays/sandbox/patches/jobs-env.yaml b/infra/k8s/overlays/sandbox/patches/jobs-env.yaml new file mode 100644 index 000000000..fb8bcb360 --- /dev/null +++ b/infra/k8s/overlays/sandbox/patches/jobs-env.yaml @@ -0,0 +1,20 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: jobs +spec: + template: + spec: + containers: + - name: jobs + env: + - name: NODE_ENV + value: production + - name: PUBSTAR_URL + value: http://core:3000 + - name: S3_BACKUP_REGION + value: us-east-1 + - name: S3_BACKUP_ENDPOINT + value: https://sandbox-assets.k3s.pubstar.org + - name: S3_BACKUP_KEY_PREFIX + value: pg-backups diff --git a/infra/k8s/overlays/sandbox/patches/minio-env.yaml b/infra/k8s/overlays/sandbox/patches/minio-env.yaml new file mode 100644 index 000000000..3ef8ac066 --- /dev/null +++ b/infra/k8s/overlays/sandbox/patches/minio-env.yaml @@ -0,0 +1,12 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: minio +spec: + template: + spec: + containers: + - name: minio + env: + - name: MINIO_BROWSER_REDIRECT_URL + value: https://sandbox.k3s.pubstar.org/assets-ui diff --git a/infra/k8s/overlays/sandbox/patches/site-builder-env.yaml b/infra/k8s/overlays/sandbox/patches/site-builder-env.yaml new file mode 100644 index 000000000..c6040b6ab --- /dev/null +++ b/infra/k8s/overlays/sandbox/patches/site-builder-env.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: site-builder +spec: + template: + spec: + containers: + - name: site-builder + env: + - name: NODE_ENV + value: production + - name: PORT + value: "4000" + - name: PUBSTAR_URL + value: http://core:3000 + - name: SITES_BASE_URL + value: https://sandbox.k3s.pubstar.org/sites + - name: S3_ENDPOINT + value: https://sandbox-assets.k3s.pubstar.org + - name: S3_PUBLIC_ENDPOINT + value: https://sandbox-assets.k3s.pubstar.org diff --git a/infra/k8s/overlays/sandbox/resources/caddy.yaml b/infra/k8s/overlays/sandbox/resources/caddy.yaml new file mode 100644 index 000000000..e482985a0 --- /dev/null +++ b/infra/k8s/overlays/sandbox/resources/caddy.yaml @@ -0,0 +1,51 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: caddy + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: caddy +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/component: caddy + template: + metadata: + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: caddy + spec: + containers: + - name: caddy + image: caddy:2 + ports: + - containerPort: 80 + volumeMounts: + - name: caddy-config + mountPath: /etc/caddy/Caddyfile + subPath: Caddyfile + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + memory: 128Mi + volumes: + - name: caddy-config + configMap: + name: caddy-config +--- +apiVersion: v1 +kind: Service +metadata: + name: caddy + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: caddy +spec: + selector: + app.kubernetes.io/component: caddy + ports: + - port: 80 + targetPort: 80 diff --git a/infra/k8s/overlays/sandbox/resources/inbucket.yaml b/infra/k8s/overlays/sandbox/resources/inbucket.yaml new file mode 100644 index 000000000..bbae87300 --- /dev/null +++ b/infra/k8s/overlays/sandbox/resources/inbucket.yaml @@ -0,0 +1,53 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: inbucket + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: inbucket +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/component: inbucket + template: + metadata: + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: inbucket + spec: + containers: + - name: inbucket + image: inbucket/inbucket:latest + ports: + - containerPort: 9000 + name: web + - containerPort: 2500 + name: smtp + env: + - name: INBUCKET_WEB_BASEPATH + value: /emails + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + memory: 128Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: inbucket + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: inbucket +spec: + selector: + app.kubernetes.io/component: inbucket + ports: + - port: 9000 + targetPort: 9000 + name: web + - port: 2500 + targetPort: 2500 + name: smtp diff --git a/infra/k8s/overlays/sandbox/resources/ingress.yaml b/infra/k8s/overlays/sandbox/resources/ingress.yaml new file mode 100644 index 000000000..d7712df12 --- /dev/null +++ b/infra/k8s/overlays/sandbox/resources/ingress.yaml @@ -0,0 +1,30 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: pubstar-ingress + labels: + app.kubernetes.io/name: pubstar + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure +spec: + rules: + - host: sandbox.k3s.pubstar.org + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: caddy + port: + number: 80 + - host: sandbox-assets.k3s.pubstar.org + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: caddy + port: + number: 80 diff --git a/infra/k8s/overlays/sandbox/resources/mock-notify.yaml b/infra/k8s/overlays/sandbox/resources/mock-notify.yaml new file mode 100644 index 000000000..4260a2311 --- /dev/null +++ b/infra/k8s/overlays/sandbox/resources/mock-notify.yaml @@ -0,0 +1,48 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mock-notify + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: mock-notify +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/component: mock-notify + template: + metadata: + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: mock-notify + spec: + imagePullSecrets: + - name: ghcr + containers: + - name: mock-notify + image: ghcr.io/knowledgefutures/mock-coar-notify-server:IMAGE_TAG + ports: + - containerPort: 3000 + env: + - name: PUBSTAR_URL + value: https://sandbox.k3s.pubstar.org + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + memory: 256Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: mock-notify + labels: + app.kubernetes.io/name: pubstar + app.kubernetes.io/component: mock-notify +spec: + selector: + app.kubernetes.io/component: mock-notify + ports: + - port: 3000 + targetPort: 3000 diff --git a/infra/k8s/scripts/deploy-preview.sh b/infra/k8s/scripts/deploy-preview.sh new file mode 100755 index 000000000..f80d53b77 --- /dev/null +++ b/infra/k8s/scripts/deploy-preview.sh @@ -0,0 +1,47 @@ +#!/bin/bash +set -euo pipefail + +PR_NUMBER="${1:?missing PR_NUMBER}" +IMAGE_TAG="${2:?missing IMAGE_TAG}" +ENV_FILE="${3:?missing ENV_FILE (path to decrypted .env file)}" + +NAMESPACE="preview-pr-${PR_NUMBER}" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +K8S_DIR="$(dirname "$SCRIPT_DIR")" + +echo "deploying preview for PR #${PR_NUMBER} (image: ${IMAGE_TAG}) to namespace ${NAMESPACE}" + +# create namespace +kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - +kubectl label namespace "$NAMESPACE" \ + pubstar.io/preview=true \ + pubstar.io/pr-number="$PR_NUMBER" \ + --overwrite + +# create GHCR pull secret (idempotent) +if [ -n "${GHCR_USER:-}" ] && [ -n "${GHCR_TOKEN:-}" ]; then + kubectl create secret docker-registry ghcr \ + --docker-server=ghcr.io \ + --docker-username="$GHCR_USER" \ + --docker-password="$GHCR_TOKEN" \ + --namespace="$NAMESPACE" \ + --dry-run=client -o yaml | kubectl apply -f - +fi + +# create app secret from decrypted env file +kubectl create secret generic pubstar-env \ + --from-env-file="$ENV_FILE" \ + --namespace="$NAMESPACE" \ + --dry-run=client -o yaml | kubectl apply -f - + +# build manifests, substitute placeholders, apply +kustomize build "${K8S_DIR}/overlays/preview" \ + | sed "s/__PR_NUMBER__/${PR_NUMBER}/g" \ + | sed "s/IMAGE_TAG/${IMAGE_TAG}/g" \ + | kubectl apply -n "$NAMESPACE" -f - + +echo "waiting for core deployment rollout..." +kubectl rollout status deployment/core -n "$NAMESPACE" --timeout=600s + +echo "preview deployed: https://pr-${PR_NUMBER}.k3s.pubstar.org" +kubectl get all -n "$NAMESPACE" diff --git a/infra/k8s/scripts/deploy-sandbox.sh b/infra/k8s/scripts/deploy-sandbox.sh new file mode 100755 index 000000000..8d99a872c --- /dev/null +++ b/infra/k8s/scripts/deploy-sandbox.sh @@ -0,0 +1,49 @@ +#!/bin/bash +set -euo pipefail + +IMAGE_TAG="${1:?missing IMAGE_TAG}" +ENV_FILE="${2:?missing ENV_FILE (path to decrypted .env file)}" + +NAMESPACE="pubstar-sandbox" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +K8S_DIR="$(dirname "$SCRIPT_DIR")" + +echo "deploying sandbox (image: ${IMAGE_TAG}) to namespace ${NAMESPACE}" + +# create namespace +kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - + +# create GHCR pull secret (idempotent) +if [ -n "${GHCR_USER:-}" ] && [ -n "${GHCR_TOKEN:-}" ]; then + kubectl create secret docker-registry ghcr \ + --docker-server=ghcr.io \ + --docker-username="$GHCR_USER" \ + --docker-password="$GHCR_TOKEN" \ + --namespace="$NAMESPACE" \ + --dry-run=client -o yaml | kubectl apply -f - +fi + +# create app secret from decrypted env file +kubectl create secret generic pubstar-env \ + --from-env-file="$ENV_FILE" \ + --namespace="$NAMESPACE" \ + --dry-run=client -o yaml | kubectl apply -f - + +# build manifests, substitute image tag, apply +cd "${K8S_DIR}/overlays/sandbox" +kustomize edit set image \ + "ghcr.io/knowledgefutures/platform:${IMAGE_TAG}" \ + "ghcr.io/knowledgefutures/platform-jobs:${IMAGE_TAG}" \ + "ghcr.io/knowledgefutures/platform-site-builder:${IMAGE_TAG}" \ + "ghcr.io/knowledgefutures/mock-coar-notify-server:${IMAGE_TAG}" + +kustomize build . | kubectl apply -n "$NAMESPACE" -f - + +# reset kustomization.yaml image tags back to placeholder +git checkout -- kustomization.yaml 2>/dev/null || true + +echo "waiting for core deployment rollout..." +kubectl rollout status deployment/core -n "$NAMESPACE" --timeout=600s + +echo "sandbox deployed: https://sandbox.k3s.pubstar.org" +kubectl get all -n "$NAMESPACE" diff --git a/infra/k8s/scripts/teardown-preview.sh b/infra/k8s/scripts/teardown-preview.sh new file mode 100755 index 000000000..dffde75ff --- /dev/null +++ b/infra/k8s/scripts/teardown-preview.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -euo pipefail + +PR_NUMBER="${1:?missing PR_NUMBER}" +NAMESPACE="preview-pr-${PR_NUMBER}" + +echo "tearing down preview for PR #${PR_NUMBER} (namespace: ${NAMESPACE})" + +if kubectl get namespace "$NAMESPACE" &>/dev/null; then + kubectl delete namespace "$NAMESPACE" + echo "namespace ${NAMESPACE} deleted" +else + echo "namespace ${NAMESPACE} not found, nothing to tear down" +fi