| .github/workflows | ||
| cmd/server | ||
| deploy | ||
| internal | ||
| migrations | ||
| ui | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| cert-manager.yaml | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| Makefile | ||
| README.md | ||
| server | ||
Go Tell
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
to decide who is allowed to edit.
Backend
The backend targets Go 1.27.1 and uses a layered design (internal/web,
internal/service, internal/security, internal/golooseauth). Content is
seeded in memory and can be mirrored to a JSON file configured through
CMS_CONTENT_FILE.
go run ./cmd/server
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:
GET /api/auth/loginredirects tohttps://<GO_LOOSE_TENANT>.<GO_LOOSE_AUTH_DOMAIN>/connect/authorize.- 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. GET /api/auth/sessionthen reportsauthenticated: true, the editor name, and the login and logout endpoints the frontend follows.GET|POST /api/auth/logoutrevokes 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.
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
Kubernetes deployment
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:
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:
make helm-deploy-production
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:
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:
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.