go-doc/data/docs/go-places-developer.md

169 lines
8.6 KiB
Markdown
Raw Permalink Normal View History

2026-10-07 15:05:48 +02:00
<!-- go-doc:generated:begin source=go-places:DEVELOPER.md signature=0fcd11f2310a851002924fdb59e5ad85 generated=2026-10-05 18:14 UTC -->
Imported from DEVELOPER.md in the go-places repository.
---
# 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.
```bash
make build
```
* `make build-api`: compiles the service to `bin/go-places`.
* `make build-loose`: **not available here.** `go-loose` is a separate repository, so this target reports that clearly instead of failing obscurely.
### 2. Running Locally
```bash
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
```bash
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.
```bash
make validate
```
This runs `helm lint` against `charts/api` (go-places) and `charts/go-loose`.
### 5. Container Image
```bash
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.
```bash
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:
```bash
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-path` StorageClass does not allow volume expansion, so editing `persistence.size` makes the API server reject the resized claim. `helm upgrade` then aborts part-way and leaves the release in `STATUS=failed` (the running pod and its data are unaffected); recover with `helm rollback` or by upgrading again without the size change. Grow the storage or add an expandable StorageClass first.
- **`local-path` is 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 is `Retain`, 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
1. **Develop:** make code changes in `api/` or `web/`.
2. **Test:** `make build && make check`.
3. **Run:** `make run`, then `cd web && npm run dev`.
4. **Validate:** `make validate`.
5. **Deploy:** `make deploy`.
## 🖥️ Web front-end
`web/` is a Vite + React client for the REST routes above, not for GraphQL.
```bash
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.
```bash
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_URL` the 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.graphql` documents the domain structs. It is maintained by hand, so the executable schema in `api/internal/transport/schema.go` stays the source of truth and a drift check is still worth adding.
- `go-loose` is 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.
<!-- go-doc:generated:end -->