go-tell/README.md

158 lines
7.1 KiB
Markdown
Raw Normal View History

2026-10-03 04:41:31 +02:00
# Go Tell
2026-10-03 04:41:31 +02:00
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
2026-09-20 17:20:34 +02:00
The backend targets Go 1.27.1 and uses a layered design (`internal/web`,
2026-10-03 04:41:31 +02:00
`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
```
2026-10-03 04:41:31 +02:00
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`.
| 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 |
## Authentication
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.
### Go Loose
Set `GO_LOOSE_CLIENT_ID` and `GO_LOOSE_CLIENT_SECRET` and the CMS grows a
browser login:
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.
2026-09-19 16:51:24 +02:00
2026-09-19 16:45:24 +02:00
## Content
2026-10-03 04:41:31 +02:00
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.
2026-09-19 16:45:24 +02:00
```sh
make backend # run the Go server
make frontend # run the React app
2026-09-20 17:20:34 +02:00
make run # build the frontend, then run the server
2026-10-03 04:41:31 +02:00
make check # gofmt, vet, and tests
make docker # build the all-in-one image
2026-09-20 17:20:34 +02:00
```
2026-10-03 04:41:31 +02:00
## Kubernetes deployment
2026-09-20 17:20:34 +02:00
2026-10-03 04:41:31 +02:00
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:
2026-09-20 17:20:34 +02:00
2026-10-03 04:41:31 +02:00
```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
```
2026-09-20 17:20:34 +02:00
2026-10-03 04:41:31 +02:00
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`:
2026-09-20 17:20:34 +02:00
```sh
make helm-deploy-production
```
2026-10-03 04:41:31 +02:00
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.