deploy configuration

This commit is contained in:
Daniel Noulet 2026-10-03 04:41:31 +02:00
parent 9ba3c47d82
commit dac02c61c3
45 changed files with 1405 additions and 582 deletions

160
README.md
View file

@ -1,66 +1,158 @@
# CMS project
# Go Tell
This repository contains a Go backend and React frontend for the CMS.
Go Tell is a small content CMS: one Go binary that serves the API and embeds
the React frontend, backed by a JSON file. It is served from a single host,
`https://tell.dev`, and it uses [Go Loose](https://github.com/danyel/go-loose)
to decide who is allowed to edit.
## Backend
The backend targets Go 1.27.1 and uses a layered design (`internal/web`,
`service`, and `security`). Content is seeded in memory and can be mirrored to
a JSON file configured through `CMS_CONTENT_FILE`.
`internal/service`, `internal/security`, `internal/golooseauth`). Content is
seeded in memory and can be mirrored to a JSON file configured through
`CMS_CONTENT_FILE`.
```sh
go run ./cmd/server
```
Configuration is read from environment variables (see `.env`). The API includes `GET /health`, `GET /api/auth/session`, `GET /api/protected`, `GET /api/content`, `GET /api/content/categories`, `GET /api/content/badges`, and `GET|PUT /api/content/:slug`.
Configuration is read from environment variables (see `.env.example`). The API
exposes `GET /health`, `GET /api/auth/session`, `GET /api/protected`,
`GET /api/content`, `GET /api/content/categories`, `GET /api/content/badges`,
and `GET|PUT /api/content/:slug`.
## Single sign-on
| Variable | Default | Purpose |
| --- | --- | --- |
| `CMS_HTTP_ADDR` | `:8080` | Listen address |
| `CMS_PROFILE` | `demo` | `demo` enables the in-memory seed and the sudo terminal, anything else requires an authenticated editor |
| `CMS_CONTENT_FILE` | empty | Path of the JSON file that stores edits; empty keeps everything in memory |
| `CMS_POSTGRES_URL` | empty | Reserved for the PostgreSQL persistence adapter |
| `CMS_ALLOWED_ORIGINS` | `http://localhost:5173` | `Access-Control-Allow-Origin` value |
| `CMS_SSO_HEADER` | `X-SSO-Token` | Identity header set by the proxy; empty disables proxy identity |
| `CMS_SSO_TOKEN` | empty | Shared secret the proxy value must match |
| `CMS_SSO_COOKIE` | empty | Cookie carrying the same value when the proxy sets no header |
| `GO_LOOSE_TENANT` | `tell` | Go Loose tenant that owns the login |
| `GO_LOOSE_AUTH_DOMAIN` | `auth.dev` | Identity domain shared by all Go Loose tenants |
| `GO_LOOSE_APP_DOMAIN` | `tell.dev` | Public host of the CMS, which also determines the redirect URI |
| `GO_LOOSE_CLIENT_ID` | empty | Client id of the Go Loose application |
| `GO_LOOSE_CLIENT_SECRET` | empty | Client secret of the Go Loose application |
| `GO_LOOSE_CA_FILE` | empty | Extra CA bundle for an internally signed identity domain |
The CMS never signs anyone in and stores no users, admins, or sessions. An identity-aware proxy (oauth2-proxy, Authelia, Cloudflare Access, ...) authenticates the owner and forwards a header on every accepted request:
## Authentication
- **Header present** → the request is the owner and can edit content (`canEdit: true`).
- **Header absent** → the request is anonymous and content is read-only (`canEdit: false`).
Go Tell accepts an editor from three sources, in this order: the demo sudo
token, the identity-aware proxy header, and the Go Loose browser session.
Reading content is always anonymous; `PUT /api/content/:slug` answers `401`
without one of them.
Configure the header with `CMS_SSO_HEADER` (default `X-SSO-Token`). Set `CMS_SSO_TOKEN` to a shared secret to require an exact value; leave it empty to trust any non-empty forwarded value. `CMS_SSO_COOKIE` optionally names a cookie fallback.
### Go Loose
Because the backend trusts the forwarded header, it must only be reachable through the proxy. Bind it to the loopback interface or a private network, and make sure the proxy strips any client-supplied copy of that header.
Set `GO_LOOSE_CLIENT_ID` and `GO_LOOSE_CLIENT_SECRET` and the CMS grows a
browser login:
Anonymous visitors get a **Sign in with Google** button in the header. It redirects the browser to `VITE_SSO_LOGIN_URL` (frontend setting), where the provider authenticates the owner and returns to the page; the CMS itself never performs an OAuth exchange and holds no client secret. Leave the setting empty to hide the button when the proxy gates the whole app automatically.
1. `GET /api/auth/login` redirects to
`https://<GO_LOOSE_TENANT>.<GO_LOOSE_AUTH_DOMAIN>/connect/authorize`.
2. Go Loose authenticates the user and redirects to
`https://<GO_LOOSE_APP_DOMAIN>/api/auth/callback`, which exchanges the
authorization code and stores the session in an HTTP-only cookie.
3. `GET /api/auth/session` then reports `authenticated: true`, the editor name,
and the login and logout endpoints the frontend follows.
4. `GET|POST /api/auth/logout` revokes the session at the identity provider and
clears the cookie.
That redirect URI has to be registered on the Go Loose application, next to the
client id and secret. Without credentials the login routes answer `404` with
the variables to set, and the CMS falls back to the proxy identity.
The local platform application lives in the Go Loose tenant `tell`, so the login
origin is `https://tell.auth.dev` and the interview accounts are
`interview@tell.auth.dev` and `reviewer@tell.auth.dev`. The one-time client
secret is recorded in the platform's gitignored `secrets/credentials.md` and in
the gitignored `.env` of this repository.
### Proxy identity
Without Go Loose, an identity-aware proxy (oauth2-proxy, Authelia, Cloudflare
Access, ...) can authenticate the owner and forward a header on every accepted
request: the header present means the request can edit, the header absent means
read-only. Because the backend trusts that header, it must only be reachable
through the proxy, and the proxy must strip any client-supplied copy of it.
## Content
Content supports one reusable category per document plus any number of badges. The anonymous `GET /api/content` endpoint accepts `category=engineering` and repeated `badge=go&badge=docker` filters; selected badges are combined with AND semantics. `GET /api/content/categories` returns the category dropdown values. The frontend applies these filters immediately when the category changes or when a badge is entered.
Editing requires the SSO identity: `PUT /api/content/:slug` returns `401` without it. `GET /api/content/:slug` and the taxonomy endpoints are public.
Content supports one reusable category per document plus any number of badges.
The anonymous `GET /api/content` endpoint accepts `category=engineering` and
repeated `badge=go&badge=docker` filters; selected badges are combined with AND
semantics. `GET /api/content/categories` returns the category dropdown values.
The frontend applies these filters immediately when the category changes or when
a badge is entered.
```sh
make backend # run the Go server
make frontend # run the React app
make run # build the frontend, then run the server
make check # gofmt, vet, and tests
make docker # build the all-in-one image
```
## Rancher deployment
## Kubernetes deployment
The Helm chart at `deploy/helm/cms` provides two profiles:
- `values-development.yaml` runs the demo profile on NodePort `30081` with
ephemeral content.
- `values-production.yaml` runs the seeded demo application on NodePort
`31373` with a persistent content volume. Host Nginx proxies `cms.urpi.be`
to this port.
Validate or deploy either profile with:
The Helm chart at `deploy/helm/go-tell` deploys the single image to the k3s
cluster behind `ingress-nginx` and `cert-manager`, both of which have to be
present first:
```sh
make cluster-prepare # install ingress-nginx and cert-manager
make helm-lint # lint and render both profiles
make helm-deploy-production # namespace go-tell-production
make helm-deploy-development # namespace go-tell-development
```
Go Loose credentials are never written into a values file. The production target
reads `GO_LOOSE_CLIENT_ID`, `GO_LOOSE_CLIENT_SECRET`, and `GO_LOOSE_CA_SECRET`
from the environment, after sourcing the gitignored `.env`:
```sh
make helm-lint
make helm-deploy-development
make helm-deploy-production
```
The deploy workflow automatically deploys a successful `main` image build to
production and also supports manual development or production deployments.
Create GitHub environments named `development` and `production`. Each needs
the secrets `RANCHER_KUBE_CONFIG_BASE64`, `REGISTRY_USERNAME`, and
`REGISTRY_PASSWORD`. `CMS_SSO_TOKEN` is optional. Environment variables
`RANCHER_NAMESPACE`, `CMS_ALLOWED_ORIGINS`, `CMS_SSO_HEADER`, and
`CMS_SSO_COOKIE` can override the chart defaults.
They end up in the release secret and in the `--set-string` flags the target
builds. Override any of them on the command line when deploying from elsewhere:
```sh
make helm-deploy-production \
--set-string goLoose.clientID="$GO_LOOSE_CLIENT_ID" \
--set-string goLoose.clientSecret="$GO_LOOSE_CLIENT_SECRET" \
--set-string goLoose.ca.existingSecret=local-ca
```
`goLoose.ca.existingSecret` names the Secret holding the platform CA bundle, so
the pod trusts `tell.auth.dev`. It has to exist in the namespace before the
release starts:
```sh
kubectl -n go-tell-production create secret generic local-ca \
--from-file=ca.crt=~/sources/go/infrastructure/certs/local-ca.crt
```
`go-tell-production` is the profile that matters: one host, TLS from
cert-manager, and a persistent volume for the content file. `go-tell-development`
runs the demo profile on NodePort `30010` with ephemeral content and no Go
Loose login, which is what `make frontend` talks to.
The earlier dedicated-host deployment (`cms.urpi.be` through host Nginx on a
Rancher cluster) is retired: `deploy/nginx` and `deploy/rancher` are gone, and
every Makefile target talks to `~/.kube/config`.
## Continuous delivery
`build-test-push.yml` publishes `batty1039.startdedicated.net:5000/go-tell` on
every push to `main`. `deploy-rancher.yml` deploys a successful build to
production and can also be dispatched by hand for either profile. Create the
GitHub environments `development` and `production` with the secrets
`RANCHER_KUBE_CONFIG_BASE64`, `REGISTRY_USERNAME`, `REGISTRY_PASSWORD`,
`GO_LOOSE_CLIENT_ID`, `GO_LOOSE_CLIENT_SECRET`, and optionally
`CMS_SSO_TOKEN` and `LOCAL_CA_CERT` (the internal CA bundle mounted through
`goLoose.ca.existingSecret`). The variables `RANCHER_NAMESPACE`,
`CMS_ALLOWED_ORIGINS`, `CMS_SSO_HEADER`, and `CMS_SSO_COOKIE` override the
chart defaults.