2026-10-03 04:41:31 +02:00
|
|
|
# Go Tell
|
2026-09-19 14:17:27 +02:00
|
|
|
|
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.
|
2026-09-19 14:17:27 +02:00
|
|
|
|
|
|
|
|
## 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`.
|
2026-09-19 14:17:27 +02:00
|
|
|
|
|
|
|
|
```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.
|