# 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](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`, `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.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://./connect/authorize`. 2. Go Loose authenticates the user and redirects to `https:///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. ```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 ``` ## 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: ```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-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: ```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.