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:
+2
-2
@@ -59,8 +59,8 @@ next-env.d.ts
|
|||||||
# Sibling repos
|
# Sibling repos
|
||||||
/repos/
|
/repos/
|
||||||
|
|
||||||
# k8s deploy secret (create from deploy/k8s/secret.example.yaml)
|
# k8s deploy secrets (create from the matching overlay's secret.example.yaml)
|
||||||
/deploy/k8s/secret.yaml
|
/deploy/k8s/overlays/*/secret.yaml
|
||||||
|
|
||||||
# S/MIME plugin build output (rebuild with: cd vnc/plugins/smime && npm run build)
|
# S/MIME plugin build output (rebuild with: cd vnc/plugins/smime && npm run build)
|
||||||
vnc/plugins/smime/node_modules/
|
vnc/plugins/smime/node_modules/
|
||||||
|
|||||||
+144
@@ -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
@@ -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`).
|
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,
|
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.
|
not URLs). Bulwark's native model is a container + persistent volumes.
|
||||||
- So VNCmail+ runs as a Docker image (`ghcr.io/brvncde-dotcom/vncmail-plus-*`)
|
- So VNCmail+ runs as a Docker image with **4 persistent volumes**, exactly
|
||||||
with **4 persistent volumes**, exactly like the existing `bulwark.sandbox.vnc.de`.
|
like the existing `bulwark.sandbox.vnc.de`.
|
||||||
- JMAP calls go through **server-side `/api/*` routes** (`proxy.ts`) → server-to-
|
- JMAP calls go through **server-side `/api/*` routes** (`proxy.ts`) → server-to-
|
||||||
server to Stalwart, **no browser CORS**. Config is **runtime-read**.
|
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 |
|
| Branch | Role |
|
||||||
|--------|------|
|
|--------|------|
|
||||||
| `main` | **Production** — CI builds `…/vncmail-plus-beta`. Only updated by an explicit promote. |
|
| `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 — CI builds `…/vncmail-plus-dev` on push. Default working branch. |
|
| `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`, PR into `dev`). |
|
| `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`).
|
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)
|
## Deploy (Kubernetes / microk8s)
|
||||||
|
|
||||||
Full runbook: **[deploy/k8s/README.md](deploy/k8s/README.md)**. In short:
|
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`).
|
1. CI (above) builds and pushes the image, one name/many tags, to GitLab's
|
||||||
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.
|
registry.
|
||||||
3. Point `vncmail.sandbox.vnc.de` DNS at the ingress; cert-manager issues TLS.
|
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
|
Runs alongside the existing `bulwark.sandbox.vnc.de`. Match your cluster's
|
||||||
StorageClass / IngressClass / cert issuer to bulwark's (see the runbook).
|
StorageClass / IngressClass / cert issuer to bulwark's (see the runbook).
|
||||||
|
|
||||||
## Deploy workflow (dev-first — ALWAYS)
|
## 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`:
|
2. **Promote to production only on explicit go-live** — merge `dev` → `main`:
|
||||||
```bash
|
```bash
|
||||||
git log dev..main # MUST be empty — main must have nothing dev lacks (else prod would revert)
|
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 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
|
git checkout dev
|
||||||
```
|
```
|
||||||
Then roll the production deployment to the new image (pin its digest — see deploy/k8s/README.md).
|
Then click `promote` in the GitLab pipeline UI (protected `production`
|
||||||
Never push straight to `main`. Never let a dev→main merge silently revert prod.
|
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)
|
## Syncing upstream (Bulwark releases)
|
||||||
|
|
||||||
|
|||||||
+76
-37
@@ -1,29 +1,66 @@
|
|||||||
# VNCmail+ — Admin Deployment Guide (microk8s)
|
# VNCmail+ — Admin Deployment Guide (microk8s)
|
||||||
|
|
||||||
Deploy VNCmail+ (VNC's Bulwark fork) as a container at **`vncmail.sandbox.vnc.de`**,
|
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
|
**alongside** the existing `bulwark.sandbox.vnc.de`.
|
||||||
GitOps needed.
|
|
||||||
|
|
||||||
> Why a container (not Vercel): Bulwark is stateful — it writes settings/admin/
|
> Why a container (not Vercel): Bulwark is stateful — it writes settings/admin/
|
||||||
> telemetry to `/app/data`, which needs persistent volumes.
|
> 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 |
|
| # | Object | File | Purpose |
|
||||||
|---|--------|------|---------|
|
|---|--------|------|---------|
|
||||||
| 1 | Namespace `vncmail` | `namespace.yaml` | Isolates the app |
|
| 1 | Namespace | `overlays/<env>/namespace.yaml` | Isolates the app (`vncmail` for dev, `vncmail-prod` for prod) |
|
||||||
| 2 | 4× PersistentVolumeClaim | `pvc.yaml` | `/app/data/{settings,admin,admin-state,telemetry}` |
|
| 2 | 4× PersistentVolumeClaim | `base/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) |
|
| 3 | Secret `vncmail-env` | `overlays/<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 |
|
| 4 | Image-pull secret | *(you create it — command below)* | Pull the (currently private) image |
|
||||||
| 5 | Deployment `vncmail-plus` | `deployment.yaml` | The app pod |
|
| 5 | Deployment `vncmail-plus` | `base/deployment.yaml` (+ overlay patches) | The app pod |
|
||||||
| 6 | Service `vncmail-plus` | `service.yaml` | ClusterIP :80 → pod :3000 |
|
| 6 | Service `vncmail-plus` | `base/service.yaml` | ClusterIP :80 → pod :3000 |
|
||||||
| 7 | Ingress `vncmail-plus` | `ingress.yaml` | TLS host `vncmail.sandbox.vnc.de` |
|
| 7 | Ingress `vncmail-plus` | `base/ingress.yaml` (+ overlay patches for prod) | TLS host |
|
||||||
|
|
||||||
**Image:** `ghcr.io/brvncde-dotcom/vncmail-plus-dev:latest`
|
**Image:** CI builds and pushes to `registry.gitlab.vnc.biz/gitlab-instance-b9b5cf2f/vncmail-plus`
|
||||||
(built automatically by CI from the `dev` branch). For anything beyond the
|
(tag `sha-<sha>` per deploy, moving pointers `dev-latest`/`prod-latest`). The
|
||||||
sandbox, pin a digest — see §5.
|
`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)
|
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 |
|
| Value | Default in manifests | File to edit |
|
||||||
|-------|----------------------|--------------|
|
|-------|----------------------|--------------|
|
||||||
| StorageClass | `microk8s-hostpath` | `pvc.yaml` (all 4) |
|
| StorageClass | `microk8s-hostpath` | `base/pvc.yaml` (all 4) |
|
||||||
| IngressClass | `public` | `ingress.yaml` |
|
| IngressClass | `public` | `base/ingress.yaml` |
|
||||||
| cert-manager issuer | `letsencrypt-prod` | `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
|
```bash
|
||||||
cd deploy/k8s
|
cd deploy/k8s/overlays/dev # or overlays/prod, once real
|
||||||
|
|
||||||
# a) Namespace
|
# a) Image-pull secret — the registry package is private.
|
||||||
kubectl apply -f namespace.yaml
|
|
||||||
|
|
||||||
# b) Image-pull secret — the GHCR package is private.
|
|
||||||
# Use a GitHub PAT (classic) with the read:packages scope.
|
|
||||||
kubectl create secret docker-registry ghcr-pull \
|
kubectl create secret docker-registry ghcr-pull \
|
||||||
--namespace vncmail \
|
--namespace vncmail \
|
||||||
--docker-server=ghcr.io \
|
--docker-server=ghcr.io \
|
||||||
--docker-username=brvncde-dotcom \
|
--docker-username=brvncde-dotcom \
|
||||||
--docker-password='<GITHUB_PAT_read:packages>' \
|
--docker-password='<GITHUB_PAT_read:packages>' \
|
||||||
--docker-email=br@vnc.biz
|
--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
|
cp secret.example.yaml secret.yaml
|
||||||
# edit secret.yaml: SESSION_SECRET: "$(openssl rand -base64 32)"
|
# edit secret.yaml: SESSION_SECRET: "$(openssl rand -base64 32)"
|
||||||
kubectl apply -f secret.yaml
|
kubectl apply -f secret.yaml
|
||||||
|
|
||||||
# d) Everything else (PVCs, Deployment, Service, Ingress)
|
# c) Everything else (namespace, PVCs, Deployment, Service, Ingress)
|
||||||
kubectl apply -k .
|
kubectl apply -k .
|
||||||
```
|
```
|
||||||
|
|
||||||
> Alternative to (b): make the GHCR package public
|
> Alternative to (a): make the registry package public, then delete the
|
||||||
> (GitHub → Packages → vncmail-plus-dev → Package settings → Change visibility),
|
> `imagePullSecrets:` block from `base/deployment.yaml`.
|
||||||
> then delete the `imagePullSecrets:` block from `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
|
## 5. Update to a new build
|
||||||
|
|
||||||
```bash
|
Normally you don't — CI's `deploy-dev` job does this automatically on every
|
||||||
# CI rebuilds ghcr.io/brvncde-dotcom/vncmail-plus-dev on every push to `dev`.
|
push to `dev`. To do it by hand (e.g. troubleshooting):
|
||||||
kubectl -n vncmail rollout restart deploy/vncmail-plus # pulls :latest (imagePullPolicy: Always)
|
|
||||||
|
|
||||||
# Production: pin a digest instead of :latest so rollouts are deterministic.
|
```bash
|
||||||
kubectl -n vncmail set image deploy/vncmail-plus \
|
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`
|
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 |
|
| Symptom | Cause / fix |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| Pod `ImagePullBackOff` | `ghcr-pull` secret missing/expired, or package still private. Recreate the secret (§3b) or make the package public. |
|
| 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 `deployment.yaml` — keep it; some storage drivers also need it on the PVC. |
|
| 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 `pvc.yaml`. Set it to one from `kubectl get sc`. |
|
| 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`. |
|
| 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. |
|
| 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`. |
|
| 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
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
name: vncmail-plus
|
name: vncmail-plus
|
||||||
namespace: vncmail
|
|
||||||
labels:
|
labels:
|
||||||
app: vncmail-plus
|
app: vncmail-plus
|
||||||
spec:
|
spec:
|
||||||
@@ -26,12 +25,17 @@ spec:
|
|||||||
runAsGroup: 1001
|
runAsGroup: 1001
|
||||||
# ghcr package is private by default — see deploy/k8s/README.md to create
|
# 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.
|
# 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:
|
imagePullSecrets:
|
||||||
- name: ghcr-pull
|
- name: ghcr-pull
|
||||||
containers:
|
containers:
|
||||||
- name: vncmail-plus
|
- name: vncmail-plus
|
||||||
# dev image (built from the `dev` branch by CI). For production pin a
|
# Default/legacy value — CI overrides the image per-deploy via
|
||||||
# digest: ghcr.io/brvncde-dotcom/vncmail-plus-dev@sha256:<digest>
|
# `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
|
image: ghcr.io/brvncde-dotcom/vncmail-plus-dev:latest
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
@@ -7,7 +7,6 @@ apiVersion: networking.k8s.io/v1
|
|||||||
kind: Ingress
|
kind: Ingress
|
||||||
metadata:
|
metadata:
|
||||||
name: vncmail-plus
|
name: vncmail-plus
|
||||||
namespace: vncmail
|
|
||||||
annotations:
|
annotations:
|
||||||
# cert-manager issuer — set to whatever bulwark.sandbox.vnc.de uses.
|
# cert-manager issuer — set to whatever bulwark.sandbox.vnc.de uses.
|
||||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||||
@@ -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
|
kind: PersistentVolumeClaim
|
||||||
metadata:
|
metadata:
|
||||||
name: vncmail-settings
|
name: vncmail-settings
|
||||||
namespace: vncmail
|
|
||||||
spec:
|
spec:
|
||||||
accessModes: [ReadWriteOnce]
|
accessModes: [ReadWriteOnce]
|
||||||
storageClassName: microk8s-hostpath
|
storageClassName: microk8s-hostpath
|
||||||
@@ -17,7 +16,6 @@ apiVersion: v1
|
|||||||
kind: PersistentVolumeClaim
|
kind: PersistentVolumeClaim
|
||||||
metadata:
|
metadata:
|
||||||
name: vncmail-admin
|
name: vncmail-admin
|
||||||
namespace: vncmail
|
|
||||||
spec:
|
spec:
|
||||||
accessModes: [ReadWriteOnce]
|
accessModes: [ReadWriteOnce]
|
||||||
storageClassName: microk8s-hostpath
|
storageClassName: microk8s-hostpath
|
||||||
@@ -29,7 +27,6 @@ apiVersion: v1
|
|||||||
kind: PersistentVolumeClaim
|
kind: PersistentVolumeClaim
|
||||||
metadata:
|
metadata:
|
||||||
name: vncmail-admin-state
|
name: vncmail-admin-state
|
||||||
namespace: vncmail
|
|
||||||
spec:
|
spec:
|
||||||
accessModes: [ReadWriteOnce]
|
accessModes: [ReadWriteOnce]
|
||||||
storageClassName: microk8s-hostpath
|
storageClassName: microk8s-hostpath
|
||||||
@@ -41,7 +38,6 @@ apiVersion: v1
|
|||||||
kind: PersistentVolumeClaim
|
kind: PersistentVolumeClaim
|
||||||
metadata:
|
metadata:
|
||||||
name: vncmail-telemetry
|
name: vncmail-telemetry
|
||||||
namespace: vncmail
|
|
||||||
spec:
|
spec:
|
||||||
accessModes: [ReadWriteOnce]
|
accessModes: [ReadWriteOnce]
|
||||||
storageClassName: microk8s-hostpath
|
storageClassName: microk8s-hostpath
|
||||||
@@ -2,7 +2,6 @@ apiVersion: v1
|
|||||||
kind: Service
|
kind: Service
|
||||||
metadata:
|
metadata:
|
||||||
name: vncmail-plus
|
name: vncmail-plus
|
||||||
namespace: vncmail
|
|
||||||
labels:
|
labels:
|
||||||
app: vncmail-plus
|
app: vncmail-plus
|
||||||
spec:
|
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
|
||||||
@@ -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.
|
||||||
@@ -210,7 +210,9 @@ function escapeDn(value: string): string {
|
|||||||
.replace(/([\\,+"<>;=])/g, '\\$1')
|
.replace(/([\\,+"<>;=])/g, '\\$1')
|
||||||
.replace(/^([ #])/, '\\$1')
|
.replace(/^([ #])/, '\\$1')
|
||||||
.replace(/ $/, '\\ ')
|
.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, '');
|
.replace(/[\x00-\x1f\x7f]/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user