5 Commits

Author SHA1 Message Date
Niklas Ye dc39e3a5d3 Move the database to Postgres, before teams need the schema
CI / chart (pull_request) Successful in 1s
CI / security (pull_request) Successful in 17s
CI / test (pull_request) Successful in 2m5s
First step of #1, and it goes first for one reason: #4 adds a team_id to
nearly every table, and doing that twice -- once for SQLite, once for
Postgres -- is work nobody gets paid for. The teams migrations now only
have to be written against one database.

The ten SQLite migrations are replaced by a single Postgres baseline
rather than ported one by one. They were incremental in a way that has
no value on a fresh install: 004 adds columns 008 drops again, and 008's
backfill rewrites data a Postgres database never had. The history stays
in git; the schema they add up to is now 001_baseline.sql.

Timestamps stay BIGINT unix seconds and are NOT converted to timestamptz.
Everything in Go already speaks epochs, so converting would have been a
second, larger change riding along inside this one. It is worth doing on
its own. The JSON columns did move to jsonb, because #4 will want to
filter and index on labels.

Most of the port is mechanical -- 170 placeholders from ? to $1 -- but
four things needed more than a search and replace:

  * Dynamically built WHERE clauses cannot keep their numbering straight
    by hand, so they hand out placeholders through sqlArgs instead. A
    filter can now be added or reordered without renumbering anything.

  * SUM(resolved_at IS NULL) was SQLite counting a boolean as 0 or 1.
    Postgres has no sum(boolean), and this was breaking every dead man's
    switch -- silently, since the sweeper only logs. Now COUNT(*) FILTER.

  * unixepoch() became FLOOR(EXTRACT(EPOCH FROM now()))::bigint. The
    FLOOR is load-bearing: a bare cast rounds half up, so a row written
    at .6 of a second claimed a timestamp a second in the future and
    disagreed with the time.Now().Unix() the Go side stamps.

  * The unique-violation check matched SQLite's error text. It matches
    SQLSTATE 23505 now, so a renamed constraint cannot turn a 409 back
    into a 500.

Tests need a real Postgres, because there is no in-memory Postgres the
way there was an in-memory SQLite. Each test gets its own schema on a
shared server -- cheaper than a database each, and still isolated.
TERDUT_TEST_DSN says where it is; `make test-db` starts one locally and
ci.yaml runs one as a service container. An unset DSN fails the suite
rather than skipping it: a run that quietly tests nothing is worse than
one that does not run.

TestMigration_BackfillCarriesAckAndComments is deleted along with the
migrations it replayed. What it protected -- an upgrade not losing
acknowledgements and comments -- now belongs to scripts/sqlite-to-postgres.go,
which is build-tagged so the SQLite driver stays out of the server
binary. Both are meant to be deleted once this install has migrated.

The chart loses the PVC, the data volume and the python backup sidecar,
and requires database.dsnSecret.name: it provisions no database and
cannot guess where the credentials live, so a render without it is meant
to fail. Backups move to where Postgres actually runs. The other half of
that -- the postgresql CR, the k8up pg_dump annotation and the network
policy -- is a change to the wrapper chart in Ryuvia/charts and is not in
here.

Verified rather than assumed: the gate is green with -race against
Postgres 17, govulncheck and gitleaks are clean, and the migration script
was run end to end against a SQLite database built at the old schema and
seeded in every table. Ids survive, so incidents keep their numbers and
every foreign key still points where it did; the identity sequences are
moved past the copied ids, and a webhook after the migration opened
incident 12 rather than colliding at 1.
2026-09-20 10:44:12 +02:00
Niklas Ye 79f5db2636 Scan the source and the working tree too, not just the image
CI / chart (push) Successful in 0s
CI / security (push) Successful in 19s
CI / test (push) Successful in 25s
The image scan added yesterday reads the built artifact. It cannot see a
vulnerable dependency the binary never calls into, and it cannot see a
credential in a file that never reaches the image — this one is FROM scratch
and contains a single binary, so almost nothing in the repo is in it. Those
are two different questions and they need two different tools, which is why
riksdata and rd-web have run govulncheck and gitleaks all along.

Both run on every push and pull request rather than only on a tag, since
neither needs anything published.

Checked by hand before wiring in, as with the image scan. govulncheck
reports no vulnerabilities the code can reach, and gitleaks finds nothing in
the tree.

What govulncheck does report is worth writing down, because it is the
argument for having it. It found three advisories in chi and reports none of
them, all three being IP spoofing in middleware.RealIP, which router.go does
not use — it uses Logger and Recoverer. The analysis is symbol-level rather
than dependency-level, so adding middleware.RealIP would turn this red on
the next push. That is precisely when someone should be made to look, and it
is a plausible thing to reach for here, since the API sits behind a gateway
and real client addresses are exactly what RealIP is for. The fourth finding
is an integer overflow in golang.org/x/sys/windows, which a linux/scratch
image will not be calling.

Both gates were checked for the failure direction as well. gitleaks exits 1
on a private key block. Worth knowing when testing it: it allowlists
well-known example credentials, so the AWS key from Amazon's own
documentation does not trip it and proves nothing.

Neither reads git history. gitleaks runs with --no-git, which scans the
working tree, so it stops a secret on the way in and says nothing about what
is already committed.

Claude-Session: https://claude.ai/code/session_01S7R4gWTz5wh5xCY4nCSJjN
2026-09-02 10:11:13 +02:00
Niklas Ye 84146fc903 Scan the published image for known vulnerabilities
CI / chart (push) Successful in 1s
CI / test (push) Successful in 25s
terdut-server was the only one of the three release-managed repos with no
image scanning at all. riksdata and rd-web have had a scan-image job since
they were set up; everything published here up to and including v0.9.3 went
out without a CVE check.

It scans the pushed image rather than a locally built one, for the same
reason the siblings do: trivy cannot read a local image on this runner,
since Talos has no docker socket and the dind sidecar shares no filesystem
with the job. So it runs after image rather than gating it, and a red scan
unpublishes nothing. What it means is narrower and worth stating plainly: do
not bump the wrapper chart in Ryuvia/charts to that version.

Checked before wiring it in rather than after. v0.9.3 scans clean at
HIGH,CRITICAL with unfixed findings ignored, so this does not turn the
pipeline red on arrival, and the same command exits 1 on an image that does
have findings — a gate that cannot fail is not a gate.

One platform is scanned, not both. The image is FROM scratch, so there are
no OS packages and trivy sees a single target: the Go binary and its module
graph. linux/amd64 and linux/arm64 are that same module set built for a
different GOARCH, so a finding in one is a finding in both. On an image with
a base layer that reasoning would not hold.

Still no govulncheck and no gitleaks here, which riksdata and rd-web run in
a separate CI job. This is the only security scanning terdut-server has.

Claude-Session: https://claude.ai/code/session_01S7R4gWTz5wh5xCY4nCSJjN
2026-09-02 10:04:51 +02:00
Niklas Ye c6f1fe317e Correct the docs that said this repo has no publishing targets
CI / chart (push) Successful in 0s
CI / test (push) Successful in 25s
Both CLAUDE.md and README.md claimed there were deliberately no build or
push targets because the workflow owned publishing. That stopped being true
in 69fcc24, which moved publishing onto the Makefile so release.yaml could
call it — the docs described the arrangement that change replaced.

The claim was wrong in its reasoning too, not just out of date. It was
written on the assumption that riksdata and rd-web duplicated their
pipelines by having those targets. They never did: their workflows call
make and always have, which is what makes a green gate locally and a green
pipeline the same code instead of two descriptions of it. This repo was the
exception, for the single day it had a Makefile that nothing called.

Claude-Session: https://claude.ai/code/session_01S7R4gWTz5wh5xCY4nCSJjN
2026-09-01 22:44:22 +02:00
Niklas Ye 5f9c202d65 Beskriv hur ett släpp går till, och vad som är särskilt här
CI / test (push) Successful in 6s
Repot kom in under den gemensamma släppprocessen i 1081260 utan att någon
dokumentation sa det. README hade Kubernetes och Development men ingenting om
vägen från en commit till något som rullar i klustret, och till skillnad från
riksdata och rd-web fanns ingen CLAUDE.md alls.

Två saker om just det här repot är värda att stå skrivna, eftersom båda
avviker från syskonrepona och båda upptäcktes mitt i ett släpp:

Pipelinen har ingen bildskanning. riksdata och rd-web kör trivy efter
publiceringen; .gitea/workflows/release.yaml här har test, binaries, image och
chart och inget mer. Ett grönt släpp är alltså inget belägg för att bilden är
fri från kända sårbarheter, och en släppnot får inte antyda det.

Wrapperchartets values.yaml har två tag:-rader, appbilden och
python-sidovagnen för säkerhetskopiering, så chart-bump behöver --image för
att veta vilken som flyttas. Utan den vägrar den, vilket är rätt.

Samtidigt: varför det inte finns några build- eller push-mål, varför chartet
publiceras enbart från taggen (766f439), och varför make test kör -race när CI
inte gör det. Helm-exemplet pekade fortfarande på 0.9.0 och går till 0.9.2.

Claude-Session: https://claude.ai/code/session_01S7R4gWTz5wh5xCY4nCSJjN
2026-09-01 22:09:15 +02:00