feat(ci): GitLab CI/CD dev→prod pipeline, kustomize base+overlays

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.
This commit is contained in:
Bernd Rodler
2026-08-05 11:43:55 +02:00
parent 12908ab706
commit 3512f935d1
18 changed files with 402 additions and 62 deletions
+2 -2
View File
@@ -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/
+144
View File
@@ -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-<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.
+54 -13
View File
@@ -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-<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)
+76 -37
View File
@@ -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/<env>/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/<env>/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-<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='<GITHUB_PAT_read:packages>' \
--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:<digest>
vncmail-plus=registry.gitlab.vnc.biz/gitlab-instance-b9b5cf2f/vncmail-plus:sha-<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`. |
@@ -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:<digest>
# 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:
@@ -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
+14
View File
@@ -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.
@@ -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
@@ -2,7 +2,6 @@ apiVersion: v1
kind: Service
metadata:
name: vncmail-plus
namespace: vncmail
labels:
app: vncmail-plus
spec:
@@ -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.
@@ -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
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: vncmail-prod
labels:
app.kubernetes.io/part-of: vnclagoon-suite
@@ -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
@@ -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
@@ -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.
+3 -1
View File
@@ -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, '');
}