SCRUM-212 — Config: schema get and replace¶
Plan ref: CFG-B2 (docs/11-admin-plane-plan.md). Stacked on SCRUM-211.
What changed¶
| Change | Where |
|---|---|
New package internal/schema: Compile (JSON Schema draft 2020-12, format asserted), Cache of compiled validators per (namespace, schema_version) behind an RWMutex, and Issues (flattens a schema error to {pointer, message} leaves) |
internal/schema/ |
GET /namespaces/{ns}/schema (viewer) → latest {namespace, schema_version, schema, created_by, created_at}; unknown ns → 404 |
internal/api/schemas.go, internal/store/schemas.go |
PUT /namespaces/{ns}/schema (admin): body is the schema itself; ≤1 MiB (413 body_too_large); invalid → 400 validation_failed naming the location; success → 200 new version + schema.replace audit row |
same |
Dependency github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 (and golang.org/x/text made direct) |
go.mod |
Security: the library's default loader is FileLoader, so a schema containing
"$ref":"file:///etc/passwd" would make Config read its own filesystem. Compile
installs a loader that refuses every URL; only the embedded meta-schemas resolve.
Duplicate keys and invalid UTF-8 are rejected first (encoding/json/jsontext).
Concurrency: replace locks the namespace row FOR UPDATE before computing
max(schema_version)+1, so concurrent replaces get consecutive versions instead of
a PK collision.
Error messages: the library's own Error() starts with a generic headline that
locates nothing. The handler reports up to three leaves, e.g.
schema is not a valid JSON Schema: at /type: got number, want array. Issues is
the same helper SCRUM-214's draft/validate will return as its errors array.
How to verify¶
cd services/config
go vet ./... && go test -race -count=1 ./...
export CONFIG_TEST_DATABASE_URL='postgres://USER:PASS@127.0.0.1:5433/config_test?sslmode=disable'
go test -race -count=1 -v -run 'Schema|Issues|Compile' ./internal/...
| Test | Proves |
|---|---|
schema.TestCompile* |
valid compiles; non-object, duplicate keys, bad schemas rejected; file:// and http:// $ref rejected without file contents in the error; format: email enforced; Cache returns the same pointer |
schema.TestIssuesLocateEachLeafFailure |
/type, /properties/damage/type, /minimum, and RFC 6901 escaping (a/b~c → a~1b~0c) |
store.TestLatestSchemaAndReplace |
new namespace is v1 {}; replace → v2 + one audit row |
store.TestReplaceSchemaIsSerialised |
10 concurrent replaces → versions 2..11, no error |
server.TestSchemaRoutesThroughTheLiveServer |
viewer GET 200, live_ops PUT 403, admin PUT 200 v2, {"type":12} → 400 naming at /type:, file:// ref → 400 without root:, unknown ns → 404 |
By hand (admin token in $T):
curl -s -X PUT -H "Authorization: Bearer $T" -d '{"type":"object","properties":{"damage":{"type":"integr"}}}' \
localhost:8080/api/admin/config/namespaces/ui.motd/schema
# 400 … "at /properties/damage/type: value must be one of 'array', 'boolean', …"
Results at time of writing¶
go vet,go test -race ./...against Postgres 16: pass (7 packages).
How it was built¶
DeepSeek run scoped (Landlock) to services/config (192 s, ~29k output tokens).
Claude: added the dependency beforehand (the sandbox cannot write the module
cache), reverted an unrelated go.work.sum churn from that step, and added
schema.Issues + its tests after finding the 400 message named no location.