<#1070 Pin service-config overrides per field> Pul...
# osctrl
g
#1070 Pin service-config overrides per field Pull request opened by javuto Pin service-config overrides per field, so one edit stops freezing the whole section Addresses #1066 (
osctrl-api
ignores
--port
,
SERVICE_PORT
and the config file). Problem A saved row in
service_configs
beat flags and environment variables, and the API stored the whole section on every save. Changing a log level in the Service Config page therefore also froze
Port
,
Listener
and
Host
at their values on the day of the save. From then on
--port 9002
lost on every boot, with no log line and nothing in the docs. The service-config API being disabled didn't help: stored rows keep applying after
--service-config-enabled
is switched off. What changed A row now records which fields an operator pinned (
ServiceConfig.Overrides
, an additive nullable column). Only pinned fields beat flags, environment variables and YAML. •
Resolve
applies only pinned fields. •
Seed
keeps every unpinned field of an existing row equal to the configuration the process started with. This matters because the page renders the stored value as the current setting; without it, a field that follows a flag would show a stale boot-1 value. • API:
PUT /api/v1/service-config/{service}/{section}
accepts
patch
(pin these fields) or
reset
(release these fields) in addition to the existing whole-section
value
. Exactly one form per request; mixing them is a 400. Unknown field names are a 400 that names the field. Field names match case-insensitively, as
encoding/json
does. Releasing the last pin returns the row to
source=yaml
. • UI: the page sends only the fields that changed, marks pinned fields, and offers per-field Release and a section Release all. A legacy row shows "all fields pinned". • "Write to disk" persists only pinned fields into the YAML file and clears the pins. This is why
Resolve
has to filter even though
Seed
already refreshes: persist resolves against a fresh YAML load with no
Seed
, and without the filter it would write the running process's flag values into the operator's file. • Visibility:
Resolve
now logs, at Info, each field a database row changes with both values (
Port (9002 -> 9000)
). Values are shown only for editable sections; credential sections get field names only. Compatibility A
source=db
row with an empty pin list means everything pinned. That is the only reading that can't silently drop an edit, since the original YAML is overwritten on the first save and the information needed to infer what was really edited no longer exists. So upgrading changes no existing deployment's behavior until someone releases fields; the startup log names each such row.
PUT {value}
keeps its old meaning (replace the section, pin everything), so scripts and automation are unaffected. During a rolling upgrade an older process treats any row as a whole-section override, which is the old behavior. For the reporter's database: after upgrading, use Release all (or release just
Port
) on the
service
section. Security and operational impact • Authorization is unchanged: admin-only, same editable-section gate. Credential-bearing sections remain non-patchable. • The audit line for a patch records field names, never values. A test fails if the handler is changed to leak them. • Schema: one additive nullable text column via
AutoMigrate
. No data migration. •
Seed
now rewrites an existing row's
value
when the process's configuration differs from what's stored (unpinned fields only; pinned fields and all-pinned rows are never touched). Rows that don't change aren't rewritten. Review notes • Translations: the 8 new strings were added to all 20 locales, because
tsc
enforces locale parity. *The 19 non-English translations are unreviewed drafts*; the pinned/release labels are security-relevant, so a native read is worth it, especially ar/fa/he/hi/ja/ko/zh. • Granularity is top-level fields. Editing one rate limit pins that whole limit, which matches how the page already edits it. •
configEndpoints
(an array section) can't be pinned per field and isn't refreshed on boot or patchable. • The pure pin parser lives in
frontend/src/features/service-config/pins.ts
, not the API client module, because tests mock that module wholesale and logic in it disappears with the mock. Validation •
go build ./...
,
go vet
,
gofmt
clean; `go test ./pkg/... ./cmd/... -count=1`: 0 failures. • Frontend:
tsc --noEmit
clean; `vitest run`: 433 passed (57 files). •
make openapi
regenerated;
make openapi-check
exits 0. The spec diff is only
overrides
on the row and `patch`/`reset` on the request. • Mutation-checked (each test fails when its guard is removed): • the
Resolve
pin filter, via two tests that don't depend on
Seed
(a test that goes through
Seed
passes either way, which is how I found it was unprotected), • the
Seed
refresh, • the credential-value guard on the log line, • the no-values guarantee on the audit line. • Three existing page tests asserted the old whole-section request body. They now assert the new contract and are stricter: unchanged fields must be absent from the request. • Not exercised: a real Postgres/MySQL upgrade from a populated
service_configs
table (tests use SQLite), and no screenshots are attached for the new pinned markers. jmpsec/osctrl