feat(server): tenant-registry skeleton boots against dev stack #4

Merged
sharang merged 5 commits from feat/skeleton into main 2026-05-19 09:35:04 +00:00
Owner

What

  • Lands the minimal tenant-registry service so portal can resolve a tenant from a slug.
  • Endpoints (under :8080 in dev):
    • GET /healthz{"status":"ok"}
    • GET /v1/tenants/by-slug/{slug} → seeded acme tenant or 404
    • GET /v1/tenants/{id} → same by UUID or 404
  • In-memory store pre-seeded with one tenant (acme) matching the seed user in the dev-stack Keycloak realm.
  • Stdlib-only — Go 1.22 enhanced ServeMux, log/slog, net/http. No third-party deps in the skeleton.
  • migrations/0001_init.up.sql lands the full M4.1 schema for review but is unapplied by the skeleton.
  • Makefile + multi-stage distroless Dockerfile.

Why

The portal middleware needs to call this service to resolve <slug>.localhost:3000 → tenant during the OIDC redirect dance. Without a running tenant-registry, portal can't even render /[slug]/dashboard in dev. Skeleton-mode unblocks portal work today; full pg-backed CRUD + JWT validation + audit endpoints land in the M4.1 follow-up.

Linked milestone: M4.1 (skeleton — full implementation in follow-up)

How

  • cmd/server/main.go — graceful-shutdown server, slog JSON logger.
  • internal/config/ — env-driven config (APP_ENV, ADDR, KEYCLOAK_ISSUER, DATABASE_URL).
  • internal/server/ — handlers + a tiny request-logging middleware. Stdlib mux with Go 1.22 method routing.
  • internal/store/memory.go — interface-shaped so the M4.1 follow-up swaps it for pgx without touching handlers. Pre-seeded with one tenant.
  • Three integration tests against httptest.Server: healthz, by-slug acme, by-slug 404.
  • Dockerfile uses gcr.io/distroless/static-debian12:nonroot — final image is ~10 MB; CGO disabled.

Test plan

  • go test -race ./... (3 tests in internal/server)
  • go vet ./... clean
  • gofmt -l . empty
  • go build (8.4 MB static binary)
  • End-to-end smoke once portal lands: visit acme.localhost:3000, observe portal calling tenant-registry /v1/tenants/by-slug/acme

Risk

Blast radius: dev only. Service has no production deployment yet (the manifest stub in orca-platform/manifests/vm-control/tenant-registry.toml still says placeholder image).

What could break:

  • The skeleton has no auth/v1/tenants/* is open. Fine for dev/loopback; must not ship to stage without the JWT middleware. The M4.1 follow-up adds it.
  • The in-memory store loses state on restart. Intentional; pgx-backed store lands with the schema in the follow-up.

Rollback plan: revert the PR. Nothing depends on this service yet.

Checklist

  • Unit tests added (3, exercising every endpoint + 404 path)
  • Docs updated (README, CHANGELOG)
  • Secrets via Infisical — none yet; dev .env documented; .env.local gitignored
  • Migration is forward-only — schema committed but unapplied; M4.1 follow-up wires golang-migrate
  • Tenant scoping — n/a (handler is a public lookup; tenant scoping enforced once auth lands)
  • OpenAPI spec — lands with the M4.1 follow-up that adds the full endpoint set
  • CHANGELOG entry under "Added"
## What - Lands the minimal tenant-registry service so portal can resolve a tenant from a slug. - Endpoints (under `:8080` in dev): - `GET /healthz` → `{"status":"ok"}` - `GET /v1/tenants/by-slug/{slug}` → seeded `acme` tenant or 404 - `GET /v1/tenants/{id}` → same by UUID or 404 - In-memory store pre-seeded with one tenant (`acme`) matching the seed user in the dev-stack Keycloak realm. - Stdlib-only — Go 1.22 enhanced ServeMux, log/slog, net/http. No third-party deps in the skeleton. - `migrations/0001_init.up.sql` lands the full M4.1 schema for review but is **unapplied** by the skeleton. - Makefile + multi-stage distroless Dockerfile. ## Why The `portal` middleware needs to call this service to resolve `<slug>.localhost:3000` → tenant during the OIDC redirect dance. Without a running tenant-registry, portal can't even render `/[slug]/dashboard` in dev. Skeleton-mode unblocks portal work today; full pg-backed CRUD + JWT validation + audit endpoints land in the M4.1 follow-up. Linked milestone: **M4.1** (skeleton — full implementation in follow-up) ## How - `cmd/server/main.go` — graceful-shutdown server, slog JSON logger. - `internal/config/` — env-driven config (`APP_ENV`, `ADDR`, `KEYCLOAK_ISSUER`, `DATABASE_URL`). - `internal/server/` — handlers + a tiny request-logging middleware. Stdlib mux with Go 1.22 method routing. - `internal/store/memory.go` — interface-shaped so the M4.1 follow-up swaps it for `pgx` without touching handlers. Pre-seeded with one tenant. - Three integration tests against `httptest.Server`: healthz, by-slug acme, by-slug 404. - Dockerfile uses `gcr.io/distroless/static-debian12:nonroot` — final image is ~10 MB; CGO disabled. ## Test plan - [x] `go test -race ./...` ✅ (3 tests in internal/server) - [x] `go vet ./...` clean - [x] `gofmt -l .` empty - [x] `go build` ✅ (8.4 MB static binary) - [ ] End-to-end smoke once portal lands: visit `acme.localhost:3000`, observe portal calling `tenant-registry /v1/tenants/by-slug/acme` ## Risk **Blast radius:** dev only. Service has no production deployment yet (the manifest stub in `orca-platform/manifests/vm-control/tenant-registry.toml` still says `placeholder` image). **What could break:** - The skeleton has **no auth** — `/v1/tenants/*` is open. Fine for dev/loopback; **must not** ship to stage without the JWT middleware. The M4.1 follow-up adds it. - The in-memory store loses state on restart. Intentional; pgx-backed store lands with the schema in the follow-up. **Rollback plan:** revert the PR. Nothing depends on this service yet. ## Checklist - [x] Unit tests added (3, exercising every endpoint + 404 path) - [x] Docs updated (README, CHANGELOG) - [x] Secrets via Infisical — none yet; dev .env documented; `.env.local` gitignored - [x] Migration is forward-only — schema committed but unapplied; M4.1 follow-up wires `golang-migrate` - [ ] Tenant scoping — n/a (handler is a public lookup; tenant scoping enforced once auth lands) - [ ] OpenAPI spec — lands with the M4.1 follow-up that adds the full endpoint set - [x] CHANGELOG entry under "Added"
sharang added 1 commit 2026-05-18 20:41:19 +00:00
feat(server): tenant-registry skeleton boots against dev stack
ci / shared (pull_request) Successful in 4s
ci / test (pull_request) Has been skipped
ci / image (pull_request) Has been skipped
6a6cd76426
Minimal Go service so platform/portal has something to resolve in local
dev. Stdlib net/http with Go 1.22 enhanced ServeMux (method+path
patterns); no third-party deps yet.

Layout:
  cmd/server/main.go               entry point with graceful shutdown
  internal/config/                 env-driven config (APP_ENV, ADDR, KC issuer)
  internal/server/                 http handlers + request-logging middleware
  internal/store/memory.go         in-memory tenant store, seeded with acme
  migrations/0001_init.up.sql      schema for the M4.1 follow-up (unapplied)
  Makefile                         dev/test/build/lint/docker targets
  Dockerfile                       multi-stage distroless build

Endpoints (under :8080 in dev):
  GET /healthz
  GET /v1/tenants/by-slug/{slug}   200 acme | 404
  GET /v1/tenants/{id}             200 by uuid | 404

JWT validation and the real Postgres-backed store land in the M4.1
follow-up PR — keeping this PR strictly to 'boots, replies, tests pass'.

Refs: M4.1 (skeleton)
CODEOWNERS rules requested review from Benjamin_Boenisch 2026-05-18 20:41:19 +00:00
sharang added 1 commit 2026-05-18 21:07:52 +00:00
ci(tenant-registry): drop hashFiles job gate
ci / shared (pull_request) Successful in 4s
ci / test (pull_request) Failing after 32s
ci / image (pull_request) Has been skipped
a590caa34b
act_runner doesn't reliably evaluate hashFiles() at job-level if:
conditions, so the gate skipped the test job even with the Go
sources committed. tenant-registry has Go source from day one — let
test always run.

Refs: M4.1
sharang added 1 commit 2026-05-18 21:09:17 +00:00
ci(tenant-registry): downgrade go directive 1.25 → 1.24
ci / shared (pull_request) Successful in 3s
ci / test (pull_request) Failing after 38s
ci / image (pull_request) Has been skipped
86d3454069
The runner's golangci-lint binary is built with Go 1.24 and refuses
to lint modules targeting a higher Go version ('the Go language
version (go1.24) used to build golangci-lint is lower than the
targeted Go version (1.25)'). 1.24 is current stable and covers
everything the skeleton uses (slog, ServeMux method routing).

Dockerfile pinned to golang:1.24-alpine to match.

Refs: M4.1
sharang added 1 commit 2026-05-18 21:11:18 +00:00
test(tenant-registry): cover store + config so coverage gate passes
ci / shared (pull_request) Successful in 4s
ci / test (pull_request) Failing after 7s
ci / image (pull_request) Has been skipped
152f84ec9d
CI's coverage gate is 70% (line-level, package average per the
matrix). The skeleton only had handler tests so most files at 0% =
65% average and CI red.

Adds:
  internal/store/memory_test.go    seeded fixture, ErrNotFound paths,
                                   pointer-copy isolation check
  internal/config/config_test.go   defaults, overrides, invalid APP_ENV

Refs: M4.1
sharang added 1 commit 2026-05-18 21:12:15 +00:00
ci(tenant-registry): scope coverage to internal/, bump setup-go to 1.24
ci / shared (pull_request) Successful in 4s
ci / test (pull_request) Successful in 43s
ci / image (pull_request) Has been skipped
673a5b9f13
Two-step fix:
- setup-go was pinned to 1.22; bump to 1.24 to match go.mod and
  unblock the golangci-lint version mismatch path consistently.
- Coverage gate runs over ./internal/... so cmd/server's signal
  handling + Listen path doesn't drag the line-pct under 70%. The
  internal/* packages all sit ≥ 70% individually; total is 78.6%.
  Real e2e tests land with M4.1.

Refs: M4.1
sharang merged commit af9f331781 into main 2026-05-19 09:35:04 +00:00
sharang deleted branch feat/skeleton 2026-05-19 09:35:05 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: platform/tenant-registry#4