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
|
||||
/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
@@ -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`).
|
||||
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
@@ -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
|
||||
@@ -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
|
||||
@@ -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(/^([ #])/, '\\$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, '');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user