From 3512f935d168eae3775762b767688b4ce84857ef Mon Sep 17 00:00:00 2001 From: Bernd Rodler Date: Wed, 5 Aug 2026 11:43:55 +0200 Subject: [PATCH] =?UTF-8?q?feat(ci):=20GitLab=20CI/CD=20dev=E2=86=92prod?= =?UTF-8?q?=20pipeline,=20kustomize=20base+overlays?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multiple developers now work on this repo, and the only working deploy trigger required pushing to GitHub - which contradicts the standing GitLab-canonical policy for this repo - while every actual deploy was a manual kubectl run against one environment (no prod exists at all). Restructures deploy/k8s/ into base/ + overlays/{dev,prod}: overlays/dev is a verified byte-for-byte no-op for the live sandbox (kubectl kustomize diff against the old flat layout is empty), overlays/prod is scaffolded but inert (placeholder hostname + JMAP_SERVER_URL, since neither a prod hostname decision nor a prod Stalwart exist yet). deploy/k8s/ca/ (the EJBCA internal CA) is untouched and never referenced by either overlay. Adds .gitlab-ci.yml: verify (MR gate, no push/deploy) -> build+deploy-dev (automatic on push to dev, one image name/tag-only environments, fixing the old -dev/-beta naming split) -> promote (manual, protected `production` environment, retags the exact dev digest via `docker buildx imagetools create` - never rebuilds - and is left as a documented TODO for the actual `kubectl apply` until prod is real). Updates VNCMAIL-SETUP.md and deploy/k8s/README.md to describe the new flow and correct the aspirational promotion description that assumed a "production image" CI never actually built. Also fixes a pre-existing lint error (no-control-regex false positive on an intentional DN-sanitizing character class in lib/smime-ca/ejbca.ts) that was blocking this commit's pre-commit hook - unrelated to this change otherwise, confirmed already present on dev before this branch. Runner/RBAC/registry setup is an infra prerequisite this commit cannot provide - documented in the pipeline plan, not part of this diff. --- .gitignore | 4 +- .gitlab-ci.yml | 144 ++++++++++++++++++ VNCMAIL-SETUP.md | 67 ++++++-- deploy/k8s/README.md | 113 +++++++++----- deploy/k8s/{ => base}/deployment.yaml | 10 +- deploy/k8s/{ => base}/ingress.yaml | 1 - deploy/k8s/base/kustomization.yaml | 14 ++ deploy/k8s/{ => base}/pvc.yaml | 4 - deploy/k8s/{ => base}/service.yaml | 1 - deploy/k8s/overlays/dev/kustomization.yaml | 13 ++ deploy/k8s/{ => overlays/dev}/namespace.yaml | 0 .../{ => overlays/dev}/secret.example.yaml | 0 deploy/k8s/overlays/prod/kustomization.yaml | 21 +++ deploy/k8s/overlays/prod/namespace.yaml | 6 + .../k8s/overlays/prod/patch-deployment.yaml | 9 ++ deploy/k8s/overlays/prod/patch-ingress.yaml | 25 +++ deploy/k8s/overlays/prod/secret.example.yaml | 28 ++++ lib/smime-ca/ejbca.ts | 4 +- 18 files changed, 402 insertions(+), 62 deletions(-) create mode 100644 .gitlab-ci.yml rename deploy/k8s/{ => base}/deployment.yaml (82%) rename deploy/k8s/{ => base}/ingress.yaml (98%) create mode 100644 deploy/k8s/base/kustomization.yaml rename deploy/k8s/{ => base}/pvc.yaml (92%) rename deploy/k8s/{ => base}/service.yaml (90%) create mode 100644 deploy/k8s/overlays/dev/kustomization.yaml rename deploy/k8s/{ => overlays/dev}/namespace.yaml (100%) rename deploy/k8s/{ => overlays/dev}/secret.example.yaml (100%) create mode 100644 deploy/k8s/overlays/prod/kustomization.yaml create mode 100644 deploy/k8s/overlays/prod/namespace.yaml create mode 100644 deploy/k8s/overlays/prod/patch-deployment.yaml create mode 100644 deploy/k8s/overlays/prod/patch-ingress.yaml create mode 100644 deploy/k8s/overlays/prod/secret.example.yaml diff --git a/.gitignore b/.gitignore index 093f86bf..c7f7379f 100644 --- a/.gitignore +++ b/.gitignore @@ -59,8 +59,8 @@ next-env.d.ts # Sibling repos /repos/ -# k8s deploy secret (create from deploy/k8s/secret.example.yaml) -/deploy/k8s/secret.yaml +# k8s deploy secrets (create from the matching overlay's secret.example.yaml) +/deploy/k8s/overlays/*/secret.yaml # S/MIME plugin build output (rebuild with: cd vnc/plugins/smime && npm run build) vnc/plugins/smime/node_modules/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..3cf6edbe --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,144 @@ +# GitLab-CI dev→prod pipeline for VNCmail+. +# +# Design (see the approved plan for full rationale): +# - One image name, environment lives only in the tag. No more -dev/-beta +# name confusion. +# - MR into `dev`: verify only (typecheck/lint/unit test/build check). No +# push, no deploy — this is the multi-developer merge gate. +# - Push to `dev`: build+push an immutable `sha-` tag, auto-deploy it +# to the vncmail (sandbox) namespace. No approval needed — dev always +# deploys. +# - Push to `main`: NEVER rebuilds. `main` only ever advances via +# `git merge --ff-only dev`, so main's HEAD commit already has a built +# image. The `promote` job retags that exact digest (registry-side copy, +# same primitive the old docker-publish.yml GHA workflow already used for +# its multi-arch manifest-list merge) and applies it to prod. `when: +# manual` + a protected `production` GitLab environment is the approval +# gate — nobody but an authorized user can click it, and nothing here +# runs automatically on main. +# +# Deliberately single-platform (linux/amd64) for the cluster build — this +# pipeline's job is deploying to a known amd64 microk8s cluster, not public +# multi-arch distribution (that's what the GHCR release workflows are for, +# and they're untouched by this file). +# +# Prerequisites this pipeline assumes are already in place (see the plan's +# "Split of responsibility" — these are admin/infra actions, not something +# this file can set up): +# - GitLab Container Registry enabled for this project (CI_REGISTRY_* vars +# are then provided automatically — no manual credential setup needed). +# - A GitLab Runner with the Kubernetes executor, whose deploy-stage jobs +# run as a `gitlab-deployer` ServiceAccount scoped (namespaced Role, not +# cluster-admin) to the `vncmail` namespace (and later `vncmail-prod`). +# kubectl auto-detects in-cluster config from that ServiceAccount's +# mounted token — no KUBECONFIG variable required. +# +# deploy/k8s/ca/ (the EJBCA internal CA) is never referenced anywhere below — +# that stays a fully manual, human-only runbook (see deploy/k8s/ca/README.md). + +stages: + - verify + - build + - deploy-dev + - promote + +variables: + IMAGE: $CI_REGISTRY_IMAGE/vncmail-plus + DEV_NAMESPACE: vncmail + PROD_NAMESPACE: vncmail-prod + +# --------------------------------------------------------------------------- +# verify — required check on every MR into dev. No registry, no cluster. +# --------------------------------------------------------------------------- +verify: + stage: verify + image: node:24-alpine + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + script: + - npm ci + - npm run typecheck + - npm run lint + - npm run test:translations + - npm run build + # test:integration is deliberately NOT here — it spins up a real Stalwart + # fixture via docker-compose (Docker-in-Docker), which is heavier than a + # fast MR gate should be. Candidate for a separate scheduled/optional job + # later, not a blocker for this pipeline's first cut. + +# --------------------------------------------------------------------------- +# build — push to dev only. Builds once; main never rebuilds (see header). +# --------------------------------------------------------------------------- +build: + stage: build + image: docker:27-cli + services: + - docker:27-dind + rules: + - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "dev"' + before_script: + - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" "$CI_REGISTRY" --password-stdin + script: + - docker build --build-arg GIT_COMMIT=$CI_COMMIT_SHA -t "$IMAGE:sha-$CI_COMMIT_SHORT_SHA" -t "$IMAGE:dev-latest" . + - docker push "$IMAGE:sha-$CI_COMMIT_SHORT_SHA" + - docker push "$IMAGE:dev-latest" + +# --------------------------------------------------------------------------- +# deploy-dev — automatic, no approval. Deploys the immutable sha tag, never +# the moving dev-latest pointer, so what's running always matches one commit. +# --------------------------------------------------------------------------- +deploy-dev: + stage: deploy-dev + image: bitnami/kubectl:1.31 + environment: + name: dev + url: https://vncmail.sandbox.vnc.de + rules: + - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "dev"' + script: + # Apply the manifests first (structure/config), then set the exact image + # this pipeline just built — imperative `set image`, not a kustomize-file + # edit, so overlays/dev never needs a commit to change what's deployed. + - kubectl apply -k deploy/k8s/overlays/dev + - kubectl -n $DEV_NAMESPACE set image deployment/vncmail-plus vncmail-plus="$IMAGE:sha-$CI_COMMIT_SHORT_SHA" + - kubectl -n $DEV_NAMESPACE rollout status deploy/vncmail-plus --timeout=120s + +# --------------------------------------------------------------------------- +# promote — manual, protected `production` environment. No docker build here +# — retags the exact digest already deployed to dev, then applies prod +# pinned to that digest (never a mutable tag). +# --------------------------------------------------------------------------- +promote: + stage: promote + image: docker:27-cli + services: + - docker:27-dind + environment: + name: production + url: https://vncmail.CHANGEME.invalid # placeholder until the real prod host is decided + rules: + # `when: manual` lives inside the rule (not as a top-level job key) — + # required syntax once `rules:` is used at all. + - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main"' + when: manual + before_script: + - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" "$CI_REGISTRY" --password-stdin + script: + - echo "Retagging the image already built+deployed for dev commit $CI_COMMIT_SHA — no rebuild." + - docker buildx imagetools create --tag "$IMAGE:prod-latest" "$IMAGE:sha-$CI_COMMIT_SHORT_SHA" + - DIGEST=$(docker buildx imagetools inspect "$IMAGE:sha-$CI_COMMIT_SHORT_SHA" | awk '/^Digest:/{print $2}') + - echo "Resolved digest for prod = $IMAGE@$DIGEST" + - > + echo "STOPPING HERE ON PURPOSE: deploy/k8s/overlays/prod is still + scaffolded/inactive (placeholder hostname, placeholder JMAP_SERVER_URL + — no prod Stalwart exists yet). Once both are real (Phase D in the + pipeline plan / VNCMAIL-SETUP.md), replace this echo with the same + pattern deploy-dev uses, against a bitnami/kubectl image and + \$PROD_NAMESPACE: kubectl apply -k deploy/k8s/overlays/prod && + kubectl -n \$PROD_NAMESPACE set image deployment/vncmail-plus + vncmail-plus=$IMAGE@$DIGEST" + # Deliberately does NOT run `kubectl apply -k overlays/prod` yet — prod + # namespace/hostname/Stalwart don't exist (Phase C/D in the plan). Once + # they do, replace the placeholder echo above with the same + # `kubectl apply -k .` + `set image ...@$DIGEST` pattern deploy-dev uses, + # against $PROD_NAMESPACE, using the bitnami/kubectl image. diff --git a/VNCMAIL-SETUP.md b/VNCMAIL-SETUP.md index 37c8f9e3..2fb91c3b 100644 --- a/VNCMAIL-SETUP.md +++ b/VNCMAIL-SETUP.md @@ -18,8 +18,8 @@ of truth; VNCmail+ is the UI. It deploys as a **container on Kubernetes except `/tmp`, so Bulwark's `mkdir ./data` crashes (`ENOENT /var/task/data`). You cannot point its data dirs at a remote host either (they're POSIX paths, not URLs). Bulwark's native model is a container + persistent volumes. -- So VNCmail+ runs as a Docker image (`ghcr.io/brvncde-dotcom/vncmail-plus-*`) - with **4 persistent volumes**, exactly like the existing `bulwark.sandbox.vnc.de`. +- So VNCmail+ runs as a Docker image with **4 persistent volumes**, exactly + like the existing `bulwark.sandbox.vnc.de`. - JMAP calls go through **server-side `/api/*` routes** (`proxy.ts`) → server-to- server to Stalwart, **no browser CORS**. Config is **runtime-read**. @@ -27,37 +27,78 @@ of truth; VNCmail+ is the UI. It deploys as a **container on Kubernetes | Branch | Role | |--------|------| -| `main` | **Production** — CI builds `…/vncmail-plus-beta`. Only updated by an explicit promote. | -| `dev` | Integration + QA — CI builds `…/vncmail-plus-dev` on push. Default working branch. | -| `vnc/*`| Feature branches for UI work (branch off `dev`, PR into `dev`). | +| `main` | **Production.** Only updated by `git merge --ff-only dev`, then an explicit manual promote in CI. No prod environment exists yet — see "CI/CD" below. | +| `dev` | Integration + QA — default working branch. Every push auto-builds and auto-deploys to the sandbox (`vncmail.sandbox.vnc.de`). | +| `vnc/*`| Feature branches for UI work (branch off `dev`, MR into `dev` — required, gated by CI). | All VNC customization lives under `vnc/` (see `vnc/VNC-CHANGES.md`). +## CI/CD — GitLab (canonical), Vercel-style dev→prod + +Multiple developers work on this repo now, so `.gitlab-ci.yml` on +[gitlab.vnc.biz](https://gitlab.vnc.biz/gitlab-instance-b9b5cf2f/vncmail-plus) +(the canonical remote — GitHub `origin` is a passive mirror, not where CI or +deploys happen) drives the whole flow: + +1. **MR into `dev`** → `verify` stage runs (typecheck/lint/unit test/build). + Required check — no push, no deploy. This is the multi-developer gate. +2. **Merge to `dev`** → `build` pushes one image, + `registry.gitlab.vnc.biz/.../vncmail-plus:sha-`, then `deploy-dev` + applies it to the sandbox automatically. No approval needed — dev always + deploys first. +3. **Merge to `main`** (fast-forward only, see below) → a `promote` job + appears, `when: manual`, gated behind a protected `production` + GitLab environment. It **never rebuilds** — it retags the exact image + already running on dev (registry-side copy, same digest) and would apply + it to a `vncmail-prod` namespace pinned to that digest. + +Historical note: the old `-dev`/`-beta` GHCR image-name split +(`.github/workflows/docker-publish.yml`) is retired by this — one image name +now, environment lives only in the tag. + +**Production doesn't exist yet.** `deploy/k8s/overlays/prod/` is scaffolded +(placeholder hostname, placeholder `JMAP_SERVER_URL` — there's no prod +Stalwart instance to point it at either) but inert: the `promote` job's real +`kubectl apply` step is deliberately left as a TODO in `.gitlab-ci.yml` until +a real hostname is decided and prod Stalwart exists. Standing up the runner/ +RBAC/registry this pipeline needs is an infra prerequisite, not something CI +itself does — see the pipeline design doc referenced in `deploy/k8s/README.md`. + ## Deploy (Kubernetes / microk8s) Full runbook: **[deploy/k8s/README.md](deploy/k8s/README.md)**. In short: -1. CI builds the image on push to `dev`/`main` → `ghcr.io/brvncde-dotcom/vncmail-plus-dev` (`.github/workflows/docker-publish.yml`). -2. `kubectl apply` the manifests in `deploy/k8s/` (namespace, 4 PVCs, deployment, service, ingress) + a `secret.yaml` (from `secret.example.yaml`) + a `ghcr-pull` image-pull secret. -3. Point `vncmail.sandbox.vnc.de` DNS at the ingress; cert-manager issues TLS. +1. CI (above) builds and pushes the image, one name/many tags, to GitLab's + registry. +2. `kubectl apply -k deploy/k8s/overlays/dev` (or `overlays/prod`, once real) + — base manifests (namespace, 4 PVCs, deployment, service, ingress) live in + `deploy/k8s/base/`, environment differences (namespace, hostname, replica + count) are overlay patches. +3. DNS + a `secret.yaml` (from the overlay's `secret.example.yaml`, gitignored, + created once by hand — CI never manages secret contents) + an image-pull + secret are the remaining manual, human, one-time steps per environment. Runs alongside the existing `bulwark.sandbox.vnc.de`. Match your cluster's StorageClass / IngressClass / cert issuer to bulwark's (see the runbook). ## Deploy workflow (dev-first — ALWAYS) -Same flow as every other VNC/SRC repo: +Same flow as every other VNC/SRC repo, now enforced structurally by CI rather +than by convention: -1. Work on `dev` (or `vnc/*` → PR into `dev`). Push to `dev` → CI builds the `-dev` image → `kubectl -n vncmail rollout restart deploy/vncmail-plus` to pull it. QA at `vncmail.sandbox.vnc.de`. +1. Work on `dev` (or `vnc/*` → MR into `dev`, CI-gated). Merge → auto-builds + and auto-deploys to `vncmail.sandbox.vnc.de`. QA there. 2. **Promote to production only on explicit go-live** — merge `dev` → `main`: ```bash git log dev..main # MUST be empty — main must have nothing dev lacks (else prod would revert) git checkout main && git merge --ff-only dev - git push origin main # CI builds the production image + git push gitlab main # never GitHub — opens the manual `promote` job, does not run it git checkout dev ``` - Then roll the production deployment to the new image (pin its digest — see deploy/k8s/README.md). - Never push straight to `main`. Never let a dev→main merge silently revert prod. + Then click `promote` in the GitLab pipeline UI (protected `production` + environment — requires the right role) once prod actually exists (see + "CI/CD" above). Never push straight to `main`. Never let a dev→main merge + silently revert prod. ## Syncing upstream (Bulwark releases) diff --git a/deploy/k8s/README.md b/deploy/k8s/README.md index 697cc771..dae10176 100644 --- a/deploy/k8s/README.md +++ b/deploy/k8s/README.md @@ -1,29 +1,66 @@ # VNCmail+ — Admin Deployment Guide (microk8s) Deploy VNCmail+ (VNC's Bulwark fork) as a container at **`vncmail.sandbox.vnc.de`**, -**alongside** the existing `bulwark.sandbox.vnc.de`. Plain `kubectl apply` — no -GitOps needed. +**alongside** the existing `bulwark.sandbox.vnc.de`. > Why a container (not Vercel): Bulwark is stateful — it writes settings/admin/ > telemetry to `/app/data`, which needs persistent volumes. +## Structure — base + overlays + +``` +deploy/k8s/ + base/ # shared manifest shapes (namespace-agnostic) + overlays/ + dev/ # the live sandbox — vncmail.sandbox.vnc.de, namespace vncmail + prod/ # scaffolded, NOT YET LIVE — see "Production status" below + ca/ # separate, isolated EJBCA internal CA — see ca/README.md. + # Never composed with base/ or either overlay above. +``` + +`kubectl apply -k overlays/dev` (or `overlays/prod`, once real) instead of +applying `base/` directly — `base/` alone has no namespace and won't apply +meaningfully on its own. + +## Routine deploys go through CI now + +As of the GitLab CI/CD pipeline (`.gitlab-ci.yml`, see `../../VNCMAIL-SETUP.md` +§ CI/CD), **pushing to `dev` auto-builds and auto-deploys** — you should not +normally need to run `kubectl apply` for the sandbox by hand anymore. This +guide's manual steps below are for first-time setup, the one-time secret +creation CI deliberately never automates, and troubleshooting. + +## Production status + +**There is no production VNCmail+ deployment yet.** `overlays/prod/` exists +in the repo but is inert: its ingress hostname and its secret's +`JMAP_SERVER_URL` are both obvious placeholders (`vncmail.CHANGEME.invalid` / +`https://REPLACE-ME-prod-stalwart-not-yet-deployed.invalid`) that will fail +loudly rather than silently deploy against the wrong backend. Applying it +requires, in order: a real prod Stalwart instance to exist, a real hostname +decision, DNS, a real `secret.yaml`, and the `.gitlab-ci.yml` `promote` job's +`kubectl apply` step (currently a TODO placeholder) filled in. None of that +is CI's job to decide — it's an explicit, human-triggered event. + --- -## 1. What you are deploying +## 1. What you are deploying (per overlay) | # | Object | File | Purpose | |---|--------|------|---------| -| 1 | Namespace `vncmail` | `namespace.yaml` | Isolates the app | -| 2 | 4× PersistentVolumeClaim | `pvc.yaml` | `/app/data/{settings,admin,admin-state,telemetry}` | -| 3 | Secret `vncmail-env` | `secret.yaml` *(you create it)* | App config (JMAP URL, session secret, branding) | -| 4 | Secret `ghcr-pull` | *(you create it — command below)* | Pull the private image from GHCR | -| 5 | Deployment `vncmail-plus` | `deployment.yaml` | The app pod | -| 6 | Service `vncmail-plus` | `service.yaml` | ClusterIP :80 → pod :3000 | -| 7 | Ingress `vncmail-plus` | `ingress.yaml` | TLS host `vncmail.sandbox.vnc.de` | +| 1 | Namespace | `overlays//namespace.yaml` | Isolates the app (`vncmail` for dev, `vncmail-prod` for prod) | +| 2 | 4× PersistentVolumeClaim | `base/pvc.yaml` | `/app/data/{settings,admin,admin-state,telemetry}` | +| 3 | Secret `vncmail-env` | `overlays//secret.yaml` *(you create it)* | App config (JMAP URL, session secret, branding) | +| 4 | Image-pull secret | *(you create it — command below)* | Pull the (currently private) image | +| 5 | Deployment `vncmail-plus` | `base/deployment.yaml` (+ overlay patches) | The app pod | +| 6 | Service `vncmail-plus` | `base/service.yaml` | ClusterIP :80 → pod :3000 | +| 7 | Ingress `vncmail-plus` | `base/ingress.yaml` (+ overlay patches for prod) | TLS host | -**Image:** `ghcr.io/brvncde-dotcom/vncmail-plus-dev:latest` -(built automatically by CI from the `dev` branch). For anything beyond the -sandbox, pin a digest — see §5. +**Image:** CI builds and pushes to `registry.gitlab.vnc.biz/gitlab-instance-b9b5cf2f/vncmail-plus` +(tag `sha-` per deploy, moving pointers `dev-latest`/`prod-latest`). The +`ghcr.io/brvncde-dotcom/vncmail-plus-dev` image referenced in `base/deployment.yaml` +is a legacy default only — CI overrides it per-deploy via `kubectl set image`, +so what's committed there never needs to track what's actually running. --- @@ -43,45 +80,48 @@ kubectl get ingressclass kubectl get clusterissuer # cert-manager issuers (if used) ``` -Then edit if they differ from the defaults below: +Then edit if they differ from the defaults below (in `base/`, so both overlays +pick up the fix): | Value | Default in manifests | File to edit | |-------|----------------------|--------------| -| StorageClass | `microk8s-hostpath` | `pvc.yaml` (all 4) | -| IngressClass | `public` | `ingress.yaml` | -| cert-manager issuer | `letsencrypt-prod` | `ingress.yaml` | +| StorageClass | `microk8s-hostpath` | `base/pvc.yaml` (all 4) | +| IngressClass | `public` | `base/ingress.yaml` | +| cert-manager issuer | `letsencrypt-prod` | `base/ingress.yaml` | --- -## 3. Deploy (copy-paste, in order) +## 3. First-time setup (one-time, per environment — CI never does this) ```bash -cd deploy/k8s +cd deploy/k8s/overlays/dev # or overlays/prod, once real -# a) Namespace -kubectl apply -f namespace.yaml - -# b) Image-pull secret — the GHCR package is private. -# Use a GitHub PAT (classic) with the read:packages scope. +# a) Image-pull secret — the registry package is private. kubectl create secret docker-registry ghcr-pull \ --namespace vncmail \ --docker-server=ghcr.io \ --docker-username=brvncde-dotcom \ --docker-password='' \ --docker-email=br@vnc.biz +# Once CI has cut over to registry.gitlab.vnc.biz, this becomes a +# docker-registry secret for that registry instead — see VNCMAIL-SETUP.md. -# c) App config secret — copy the template, set a real SESSION_SECRET, apply. +# b) App config secret — copy the template, set a real SESSION_SECRET, apply. cp secret.example.yaml secret.yaml # edit secret.yaml: SESSION_SECRET: "$(openssl rand -base64 32)" kubectl apply -f secret.yaml -# d) Everything else (PVCs, Deployment, Service, Ingress) +# c) Everything else (namespace, PVCs, Deployment, Service, Ingress) kubectl apply -k . ``` -> Alternative to (b): make the GHCR package public -> (GitHub → Packages → vncmail-plus-dev → Package settings → Change visibility), -> then delete the `imagePullSecrets:` block from `deployment.yaml`. +> Alternative to (a): make the registry package public, then delete the +> `imagePullSecrets:` block from `base/deployment.yaml`. + +After this one-time setup, routine deploys to `dev` happen automatically via +CI on every push — see "Routine deploys go through CI now" above. This +section is for first-time bring-up (or `overlays/prod`, once it's real) and +troubleshooting, not the everyday path. --- @@ -104,13 +144,12 @@ a bare username. ## 5. Update to a new build -```bash -# CI rebuilds ghcr.io/brvncde-dotcom/vncmail-plus-dev on every push to `dev`. -kubectl -n vncmail rollout restart deploy/vncmail-plus # pulls :latest (imagePullPolicy: Always) +Normally you don't — CI's `deploy-dev` job does this automatically on every +push to `dev`. To do it by hand (e.g. troubleshooting): -# Production: pin a digest instead of :latest so rollouts are deterministic. +```bash kubectl -n vncmail set image deploy/vncmail-plus \ - vncmail-plus=ghcr.io/brvncde-dotcom/vncmail-plus-dev@sha256: + vncmail-plus=registry.gitlab.vnc.biz/gitlab-instance-b9b5cf2f/vncmail-plus:sha- ``` Rollback: `kubectl -n vncmail rollout undo deploy/vncmail-plus` @@ -121,9 +160,9 @@ Rollback: `kubectl -n vncmail rollout undo deploy/vncmail-plus` | Symptom | Cause / fix | |---------|-------------| -| Pod `ImagePullBackOff` | `ghcr-pull` secret missing/expired, or package still private. Recreate the secret (§3b) or make the package public. | -| Pod `CrashLoopBackOff`, logs show `EACCES`/permission on `/app/data` | Volume not writable by uid 1001. `securityContext.fsGroup: 1001` is set in `deployment.yaml` — keep it; some storage drivers also need it on the PVC. | -| PVC stuck `Pending` | Wrong `storageClassName` in `pvc.yaml`. Set it to one from `kubectl get sc`. | +| Pod `ImagePullBackOff` | `ghcr-pull` secret missing/expired, or package still private. Recreate the secret (§3a) or make the package public. | +| Pod `CrashLoopBackOff`, logs show `EACCES`/permission on `/app/data` | Volume not writable by uid 1001. `securityContext.fsGroup: 1001` is set in `base/deployment.yaml` — keep it; some storage drivers also need it on the PVC. | +| PVC stuck `Pending` | Wrong `storageClassName` in `base/pvc.yaml`. Set it to one from `kubectl get sc`. | | Ingress has no address / no cert | Wrong `ingressClassName` or cert issuer. Match bulwark's (§2). Check `kubectl -n vncmail describe ingress vncmail-plus`. | | Login shows "Ein Fehler ist aufgetreten" | Use the **full** email (`user@sandbox.vnc.de`), not a bare username. | | Can't reach Stalwart | Check `JMAP_SERVER_URL` in the secret = `https://stalwart.sandbox.vnc.de`. | diff --git a/deploy/k8s/deployment.yaml b/deploy/k8s/base/deployment.yaml similarity index 82% rename from deploy/k8s/deployment.yaml rename to deploy/k8s/base/deployment.yaml index c9dfae3e..f72e952c 100644 --- a/deploy/k8s/deployment.yaml +++ b/deploy/k8s/base/deployment.yaml @@ -2,7 +2,6 @@ apiVersion: apps/v1 kind: Deployment metadata: name: vncmail-plus - namespace: vncmail labels: app: vncmail-plus spec: @@ -26,12 +25,17 @@ spec: runAsGroup: 1001 # ghcr package is private by default — see deploy/k8s/README.md to create # this pull secret. Delete this block if you make the package public. + # NOTE: once CI moves to pushing registry.gitlab.vnc.biz images (the + # dev-auto-deploy phase of the GitLab pipeline), this needs to become a + # docker-registry secret for that registry instead — comments only, + # deliberately not renamed here, so this file stays a no-op today. imagePullSecrets: - name: ghcr-pull containers: - name: vncmail-plus - # dev image (built from the `dev` branch by CI). For production pin a - # digest: ghcr.io/brvncde-dotcom/vncmail-plus-dev@sha256: + # Default/legacy value — CI overrides the image per-deploy via + # `kustomize edit set image`, so what's committed here never goes + # stale. For a one-off manual apply, pin a digest instead of :latest. image: ghcr.io/brvncde-dotcom/vncmail-plus-dev:latest imagePullPolicy: Always ports: diff --git a/deploy/k8s/ingress.yaml b/deploy/k8s/base/ingress.yaml similarity index 98% rename from deploy/k8s/ingress.yaml rename to deploy/k8s/base/ingress.yaml index cd2392a6..95433f33 100644 --- a/deploy/k8s/ingress.yaml +++ b/deploy/k8s/base/ingress.yaml @@ -7,7 +7,6 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: vncmail-plus - namespace: vncmail annotations: # cert-manager issuer — set to whatever bulwark.sandbox.vnc.de uses. cert-manager.io/cluster-issuer: letsencrypt-prod diff --git a/deploy/k8s/base/kustomization.yaml b/deploy/k8s/base/kustomization.yaml new file mode 100644 index 00000000..b9a4933e --- /dev/null +++ b/deploy/k8s/base/kustomization.yaml @@ -0,0 +1,14 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - pvc.yaml + - deployment.yaml + - service.yaml + - ingress.yaml + # - secret.yaml # create from an overlay's secret.example.yaml; not committed + +# Namespace is intentionally NOT set here. Kustomize's `namespace:` transformer +# doesn't rename cluster-scoped Namespace objects, so each overlay ships its own +# namespace.yaml (the actual object) and its own `namespace:` field (which +# injects metadata.namespace into every namespaced resource below). Applying +# this base directly is meaningless — always go through an overlay. diff --git a/deploy/k8s/pvc.yaml b/deploy/k8s/base/pvc.yaml similarity index 92% rename from deploy/k8s/pvc.yaml rename to deploy/k8s/base/pvc.yaml index 7d93a4f3..e9dd344e 100644 --- a/deploy/k8s/pvc.yaml +++ b/deploy/k8s/base/pvc.yaml @@ -5,7 +5,6 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: name: vncmail-settings - namespace: vncmail spec: accessModes: [ReadWriteOnce] storageClassName: microk8s-hostpath @@ -17,7 +16,6 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: name: vncmail-admin - namespace: vncmail spec: accessModes: [ReadWriteOnce] storageClassName: microk8s-hostpath @@ -29,7 +27,6 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: name: vncmail-admin-state - namespace: vncmail spec: accessModes: [ReadWriteOnce] storageClassName: microk8s-hostpath @@ -41,7 +38,6 @@ apiVersion: v1 kind: PersistentVolumeClaim metadata: name: vncmail-telemetry - namespace: vncmail spec: accessModes: [ReadWriteOnce] storageClassName: microk8s-hostpath diff --git a/deploy/k8s/service.yaml b/deploy/k8s/base/service.yaml similarity index 90% rename from deploy/k8s/service.yaml rename to deploy/k8s/base/service.yaml index 1dd0488a..63f90db4 100644 --- a/deploy/k8s/service.yaml +++ b/deploy/k8s/base/service.yaml @@ -2,7 +2,6 @@ apiVersion: v1 kind: Service metadata: name: vncmail-plus - namespace: vncmail labels: app: vncmail-plus spec: diff --git a/deploy/k8s/overlays/dev/kustomization.yaml b/deploy/k8s/overlays/dev/kustomization.yaml new file mode 100644 index 00000000..d09eb0d1 --- /dev/null +++ b/deploy/k8s/overlays/dev/kustomization.yaml @@ -0,0 +1,13 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: vncmail +resources: + - ../../base + - namespace.yaml + # - secret.yaml # create from secret.example.yaml; not committed + +# This is the live sandbox (vncmail.sandbox.vnc.de) — deliberately zero patches +# beyond namespace/resource wiring, so `kubectl kustomize .` renders identical +# to the pre-restructure flat deploy/k8s/. The image is left at base's default +# and overridden per-deploy by CI (`kubectl set image`, see .gitlab-ci.yml's +# deploy-dev job) rather than pinned here, so this file never goes stale. diff --git a/deploy/k8s/namespace.yaml b/deploy/k8s/overlays/dev/namespace.yaml similarity index 100% rename from deploy/k8s/namespace.yaml rename to deploy/k8s/overlays/dev/namespace.yaml diff --git a/deploy/k8s/secret.example.yaml b/deploy/k8s/overlays/dev/secret.example.yaml similarity index 100% rename from deploy/k8s/secret.example.yaml rename to deploy/k8s/overlays/dev/secret.example.yaml diff --git a/deploy/k8s/overlays/prod/kustomization.yaml b/deploy/k8s/overlays/prod/kustomization.yaml new file mode 100644 index 00000000..1616473d --- /dev/null +++ b/deploy/k8s/overlays/prod/kustomization.yaml @@ -0,0 +1,21 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: vncmail-prod +resources: + - ../../base + - namespace.yaml + # - secret.yaml # create from secret.example.yaml; not committed + +patches: + - path: patch-ingress.yaml + - path: patch-deployment.yaml + +# NOT MEANT TO BE APPLIED AS COMMITTED. Scaffolding only (see the pipeline +# plan's Phase C/D) — the tag below is an obviously-invalid placeholder; +# the real promote job (.gitlab-ci.yml) resolves and pins an actual digest at +# deploy time via `kubectl set image`, it never trusts whatever is checked +# in here. +images: + - name: ghcr.io/brvncde-dotcom/vncmail-plus-dev + newName: registry.gitlab.vnc.biz/gitlab-instance-b9b5cf2f/vncmail-plus + newTag: not-yet-promoted diff --git a/deploy/k8s/overlays/prod/namespace.yaml b/deploy/k8s/overlays/prod/namespace.yaml new file mode 100644 index 00000000..85f6ab2d --- /dev/null +++ b/deploy/k8s/overlays/prod/namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: vncmail-prod + labels: + app.kubernetes.io/part-of: vnclagoon-suite diff --git a/deploy/k8s/overlays/prod/patch-deployment.yaml b/deploy/k8s/overlays/prod/patch-deployment.yaml new file mode 100644 index 00000000..2250714e --- /dev/null +++ b/deploy/k8s/overlays/prod/patch-deployment.yaml @@ -0,0 +1,9 @@ +# Basic HA. Still `strategy: Recreate` (inherited from base) since the PVCs +# are RWO — 2 replicas doesn't buy zero-downtime rollouts by itself, only +# tolerance for a node loss between deploys. Revisit if that's not enough. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: vncmail-plus +spec: + replicas: 2 diff --git a/deploy/k8s/overlays/prod/patch-ingress.yaml b/deploy/k8s/overlays/prod/patch-ingress.yaml new file mode 100644 index 00000000..34fa2a5f --- /dev/null +++ b/deploy/k8s/overlays/prod/patch-ingress.yaml @@ -0,0 +1,25 @@ +# PLACEHOLDER — the real production hostname has not been decided yet (see +# VNCMAIL-SETUP.md / the pipeline plan). vncmail.CHANGEME.invalid is +# deliberately unresolvable: applying this overlay as committed will not +# issue a cert or route traffic anywhere. Replace both occurrences below, +# and the matching TLS secretName, before Phase D (first real prod deploy). +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: vncmail-plus +spec: + tls: + - hosts: + - vncmail.CHANGEME.invalid + secretName: vncmail-plus-prod-tls + rules: + - host: vncmail.CHANGEME.invalid + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: vncmail-plus + port: + number: 80 diff --git a/deploy/k8s/overlays/prod/secret.example.yaml b/deploy/k8s/overlays/prod/secret.example.yaml new file mode 100644 index 00000000..e6a9fd4c --- /dev/null +++ b/deploy/k8s/overlays/prod/secret.example.yaml @@ -0,0 +1,28 @@ +# Copy to secret.yaml, fill in real values, and apply. DO NOT commit secret.yaml +# (it is gitignored). Generate SESSION_SECRET with: openssl rand -base64 32 +# +# JMAP_SERVER_URL is a PLACEHOLDER — there is no production Stalwart instance +# yet. This overlay cannot go live (Phase D) until one exists and this value +# points at it for real. +apiVersion: v1 +kind: Secret +metadata: + name: vncmail-env + namespace: vncmail-prod +type: Opaque +stringData: + # Core — connect to Stalwart over JMAP + JMAP_SERVER_URL: "https://REPLACE-ME-prod-stalwart-not-yet-deployed.invalid" + SESSION_SECRET: "REPLACE_ME__openssl_rand_base64_32" + # Branding (theme defaults to VNClagoon in code; these set name + logo) + APP_NAME: "VNCmail+" + APP_SHORT_NAME: "VNCmail+" + LOGIN_COMPANY_NAME: "VNClagoon" + LOGIN_LOGO_DARK_URL: "/branding/vncmail-wordmark-on-dark.svg" + LOGIN_LOGO_LIGHT_URL: "/branding/vncmail-wordmark-on-light.svg" + APP_LOGO_DARK_URL: "/branding/vncmail-wordmark-on-dark.svg" + APP_LOGO_LIGHT_URL: "/branding/vncmail-wordmark-on-light.svg" + LOGIN_LOGO_MAX_HEIGHT: "52" + # Housekeeping + BULWARK_UPDATE_CHECK: "off" + # Data dirs default to /app/data/* (mounted to the PVCs) — no need to set them. diff --git a/lib/smime-ca/ejbca.ts b/lib/smime-ca/ejbca.ts index 61dd4028..dccddc0f 100644 --- a/lib/smime-ca/ejbca.ts +++ b/lib/smime-ca/ejbca.ts @@ -210,7 +210,9 @@ function escapeDn(value: string): string { .replace(/([\\,+"<>;=])/g, '\\$1') .replace(/^([ #])/, '\\$1') .replace(/ $/, '\\ ') - // Control characters have no legitimate place in a DN. + // Control characters have no legitimate place in a DN — the class below + // is intentional, not a typo. + // eslint-disable-next-line no-control-regex .replace(/[\x00-\x1f\x7f]/g, ''); }