8.4 KiB
Developer Workflow Guide for Go Places Project
This document outlines the standard operating procedure for building, testing, and deploying go-places, the service in this repository. It authenticates callers against go-loose, a separate SSO service that is not part of this repository.
🚀 Core Dependencies & Structure
The system relies on the following structure:
- Root:
/home/dnoulet/go/go-places/ - API:
api/— the Go module root (module go-places-api) - Dependency:
go-loose— the SSO/authentication service. It lives in a separate repository and is not present here; only its Helm chart (charts/go-loose/) is kept in this repository. - Web:
web/— React front-end - Infrastructure: all Kubernetes deployment definitions are in
charts/
🛠️ Local Development & Building (The Makefile)
All development tasks should begin by referencing the Makefile in the project root.
1. Building Local Binaries (For testing/local runs)
This simulates the container image build process locally.
make build
make build-api: compiles the service tobin/go-places.make build-loose: not available here.go-looseis a separate repository, so this target reports that clearly instead of failing obscurely.
2. Running Locally
make run
Starts the API with AUTH_MODE=dev on :8080, backed by a local SQLite file and ./data/media. Open http://localhost:8080/graphql in a browser for a small query console.
3. Testing
make test # go test ./...
make race # go test -race ./...
make vet # go vet ./...
make fmt # go fmt ./...
make check # everything CI runs: fmt, vet, race, web typecheck, helm lint
4. Validating Infrastructure
Before attempting deployment, always lint the Helm charts to ensure all YAML templates are valid.
make validate
This runs helm lint against charts/api (go-places) and charts/go-loose.
5. Container Image
docker build -t go-places:latest .
The build context is the repository root, because the Dockerfile copies api/. The container runs unprivileged and exposes the API on port 8080.
☁️ Deployment to Cluster (Helm/Rancher)
The production deployment strategy leverages Kubernetes via Helm, managed by Rancher.
make deploy
This runs helm upgrade --install for the go-places chart (from charts/api, whose internal name is go-places) and then the go-loose chart, both into the dev namespace.
📡 HTTP Surface
| Route | Auth | Purpose |
|---|---|---|
GET /healthz |
no | Liveness probe. |
GET /readyz |
no | Readiness probe; pings the database. |
POST /graphql |
bearer | Queries and mutations. This is the only route that executes a query. |
GET /graphql |
no | Browser query console. A ?query= parameter is rejected, because this route is unauthenticated. |
POST /places/{placeId}/images |
bearer | Multipart picture upload (image field, optional caption). |
POST /upload |
bearer | Same upload, with placeId given as a form field. |
GET /media/{key} |
no | Serves a stored picture. |
GET /api/places |
bearer | REST list, ENABLE_REST=true. |
GET /api/places/{placeId} |
bearer | REST read, including relatedPlaces and each picture's linkedImages. |
POST /api/places |
bearer | REST create. |
GET /api/places/{placeId}/images |
bearer | REST picture list. |
Health probes and /media/ are the only unauthenticated data routes, and neither
carries user data: a kubelet cannot present a token, and an <img> tag cannot
attach one. Everything else requires a bearer token.
Every setting is read from the environment by internal/config; see api/internal/config/config.go for the full list and its defaults.
Authentication. With AUTH_MODE=dev, the token valid_token identifies the user dev. With AUTH_MODE=jwt, an HS256 token issued by go-loose must carry sub and exp, and may carry username and email. The algorithm is pinned to HS256, so a token cannot select none or an asymmetric algorithm.
Uploads. Analysis runs inline: the picture is stored, decoded, and turned into badges before the response is written. A picture whose bytes cannot be decoded is still stored, with analysisStatus: failed and the reason in analysisError.
Linking. Image.linkedImages and Place.relatedPlaces are computed on read from the pictures' own coordinates (EXIF GPS when present, otherwise the place's coordinate). Nothing is persisted, so links always reflect the current set of pictures.
💾 Storage and durability
SQLite and the uploaded pictures share a single volume, mounted at /app/data (persistence.mountPath). The chart requests a ReadWriteOnce claim and forces a Recreate strategy, because SQLite permits one writer and a rolling update would briefly run two pods against the same database file. replicaCount must stay at 1.
The volume is what makes the travel log survive a pod being rescheduled. To confirm:
kubectl delete pod -n dev -l app.kubernetes.io/name=go-places
A place and its pictures are still there afterwards. The image runs as uid 10001, and securityContext.fsGroup is set to the same value so the non-root process can create the database file inside the volume.
Two limits worth knowing:
- The claim cannot be resized. The cluster's
local-pathStorageClass does not allow volume expansion, so editingpersistence.sizemakes the API server reject the resized claim.helm upgradethen aborts part-way and leaves the release inSTATUS=failed(the running pod and its data are unaffected); recover withhelm rollbackor by upgrading again without the size change. Grow the storage or add an expandable StorageClass first. local-pathis node-local. Data lives on the node's own disk under/var/lib/rancher/k3s/storage, so it survives pod replacement but not loss of that node. It also means the service is pinned to one node. Its reclaim policy isRetain, so deleting the claim does not destroy the data directory.
Set persistence.enabled=false to fall back to an emptyDir. That is useful for a throwaway demo and wrong for anything real: the database and every picture are lost on each pod replacement.
📚 Workflow Summary
- Develop: make code changes in
api/orweb/. - Test:
make build && make check. - Run:
make run, thencd web && npm run dev. - Validate:
make validate. - Deploy:
make deploy.
🖥️ Web front-end
web/ is a Vite + React client for the REST routes above, not for GraphQL.
cd web
npm install
npm run dev # http://localhost:5173
The dev server proxies /api, /media, /healthz and /graphql to the API, so
the browser only ever makes same-origin requests and CORS never enters the
picture. It expects the API on http://localhost:8080; override with
VITE_PROXY_TARGET.
The client has no login flow: identity belongs to go-loose, so it asks for a
token and keeps it in localStorage. VITE_API_TOKEN bakes one into the bundle
at build time, and VITE_API_BASE points the client at an API on another origin.
A build without either asks for a token, which is what you want in production —
a token in a public bundle is public.
cd web
npm run build # -> web/dist, a static bundle
npx tsc --noEmit # type check
web/dist is not served by the API image and charts/api does not deploy it.
See the front-end section of INFRASTRUCTURE.md for the two ways to publish it.
🚫 Known Gaps / TODOs
- There is no Docker Compose setup, so RabbitMQ and a remote inference endpoint are not exercised locally by default. Without
AMQP_URLthe service logs events instead. - Object recognition is heuristic. Detecting an arbitrary subject ("a dog") needs the remote analyzer at
ANALYZER_URL/v1/analyze; the local analyzer only produces scene and property badges such as Water, Sky, Vivid and Nature. shared-interfaces/schema/models.graphqldocuments the domain structs. It is maintained by hand, so the executable schema inapi/internal/transport/schema.gostays the source of truth and a drift check is still worth adding.go-looseis consumed purely as an HTTP/SSO dependency; there is no in-process client to it.- Only comments can be read over REST. Adding one, or deleting a place or picture from the UI, needs a GraphQL call or new REST routes.
This guide assumes familiarity with Go, Docker, and Kubernetes concepts.