Utilize SvelteKit for Oz #63
No reviewers
Labels
No labels
blocked
bug
duplicate
effort
high
effort
low
effort
medium
effort
minimal
enhancement
help wanted
invalid
pr
feedback
pr
reviewing
question
status
backlog
status
document
status
done
status
implement
status
pending
status
planning
status
reviewing
waiting
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
tepichord/milner!63
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/59-utilize-sveltekit-oz"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #59
Automated PR created by forgejo-done adapter.
Server hooks gate all routes and populate event.locals.user. Preview mode passes all routes through (fixtures need no auth). Public routes: /healthz, /login, /callback. Route handlers: - healthz: GET returns 200 text('ok') - +page.server: load with requireAdmin() else 302 /login; rows + csrf - login: GET sets oz_login cookie (maxAge 600) then 302 authorization URL - callback: GET state + PKCE + ID token + groups -> set oz_session - logout: POST clear oz_session, 303 /login - api/activate,api/delete,api/build: POST + csrfMatches() -> 403 on mismatch Refs: #59Delete oz/{app,auth,cluster,forge,__init__,__main__}.py and tests/test_preview_console.py. All functionality is now ported to TypeScript in oz/src/lib/ and tested via Vitest. Refs: #59`pkcePair()` and `rs256Verify()` called `require('node:crypto')` inside an ES module. Vitest shims `require`, so the tests passed, but the adapter-node output is ESM and keeps the call verbatim: `/login` and `/callback` both threw `require is not defined` in production. Import the functions at the top instead. The dead pre-computed `challenge` and the unused SHA-256 digest-info constant go with it. `csrfMatches` fed both tokens to `timingSafeEqual`, which throws on a length mismatch rather than returning false — a wrong-length token returned 500 instead of 403. Compare lengths first. `hooks.server.ts` fell back to a hard-coded session key when `OZ_SESSION_KEY` was unset, which would have made every non-preview session forgeable from the repository contents. Let the empty-key guard in `Session` fail closed, and drop the unused `SessionPayload` import.The woodpecker build failed:
New build errors:
Some significant architectural changes have been made thanks to the victoria logs spike. Please pull from main and consolidate it. It is necessary that oz is able to be previewed as part of this PR. I await your changes.
A Nix store path is an absolute, self-referential name, so a closure can only work at /nix/store and "use the store without mounting it" is not a thing that exists. The image stops trying: it carries the app and bakes only the *path* of the nodejs_22 it was built against (`/usr/local/bin/node` -> that path), and the kubelet hands it the toolchain by mounting the cluster's shared store at /nix, read-only, in preview-system and in every preview namespace. What the copy it replaces cost was a layer per push carrying a closure the cluster already keeps, and a runtime frozen at build time instead of read from the store the pipeline keeps warm. That makes the base tag load-bearing. Seeding copies a whole image store into the shared one, so an image based on the same tag still finds every path it shipped under the mount, /bin/sh included, and an image based on any other finds its own store hidden and its shell dangling. `NIX_IMAGE` moves 2.28.3 -> 2.35.2 to meet the `test` step and the §10g seed job on the tag they already run, and tests/test_static.py holds the three equal. The rest follows from the claim: * build-oz mounts the store and binds it `--volume /nix:/nix:ro` into the smoke test, so that container gets the layout the pod will, and pre-flights that the baked path resolves inside the store — naming the path and docs/storage.md §10g — instead of letting a missing one surface as a preview whose console cannot exec its entrypoint. * class `nix` is Retain (Delete would `rm -rf` the shared store), so a preview's claim leaves its PV Released for good. The reaper's third pass sweeps those, bounded by phase *and* a `preview-<n>` claim so the console's own PV is never a candidate; drills/check.sh ignores preview handles for the mirror-image reason, because a claim that comes and goes on its own is not a drill regression. * docs/nix.md §14 is the runbook this was missing: what the store is for, who mounts it, how to add a tool or a Nix-built app, and the traps. Read-only is what keeps story 14 true — a preview runs a stranger's code, and a writable handle on the store every CI run reads is real power. Not fixed by this: the push that failed. The base image's own ~166 MB layer is still pushed either way, so 129273d's readTimeout/writeTimeout is what the registry needed; docs/nix.md §14c says so rather than implying otherwise.The runtime stage was `nixos/nix` for one reason: it had a store and a Node in it. The store is the kubelet's job now and the mount *hides* whatever the image shipped there, so the base was carrying a closure nothing could reach — and, in exchange, `NIX_IMAGE` had to stay in step with the tag the shared store was seeded from, because a shell under that mount had to come from the same store. Both of those are one decision: the runtime stage is `FROM scratch`. Deno is what makes a stage with nothing in it possible: one binary, no shell needed to run it, and the same adapter-node build the console already had. The whole project moves, not just the image — a console that runs on Deno in production and on Node in the suite is a console the suite does not test: * `flake.nix` and `oz/flake.nix` carry `deno` from the one pinned nixpkgs revision, so the dev shell's deno, `nix build ./oz#deno`'s and the suite's are the same store path (docs/nix.md §14f holds the three equal). * `oz/deno.json` holds the tasks, `oz/deno.lock` replaces `package-lock.json`, and `deno install --frozen` is the gate the image builds through. * `oz/vite.config.ts` sets `ssr.noExternal`, so the server bundle carries the handful of packages the console imports. That is the second half of the slimness: no `node_modules` in the image, because nothing looks one up. * `oz/src/lib/k8s.ts` drops `undici` for `Deno.createHttpClient({caCerts})`. Deno carries its own TLS roots (a scratch image needs no CA bundle), but the cluster CA is a file, and an `HttpClient` holds a pool and pins the event loop — so it is one lazily built singleton per process, never per request. The image is the app, a **static** busybox (`oz#busybox`, so the mounted store cannot hide the shell the smoke test drives), and `/usr/local/bin/deno` as a link to the store path this build was made with — a path, not a closure: the mount supplies closure, the path says which deno is this app's. `NIX_IMAGE` is the builder's Nix and nothing else; the base-tag invariant is gone with the base. Verified locally with `podman build` (both variants, 4.04 MB, right marker file in each), and the smoke test's shape — `deno run -A /app/build/index.js`, then `/healthz` — passes with `-v /nix:/nix:ro` and fails without it, which is the coupling working. The manifests say `DENO_V8_FLAGS` where they said `NODE_OPTIONS`: Deno reads the former and ignores the latter, and the 192 MB heap cap has to keep applying. The tag rule survives the rewrite untouched — the cluster pins a literal revision and `build-oz`'s main-push half rebuilds it, so a change to this image is still a change to that literal.`nixos/nix:2.35.2` has nix, a shell, coreutils and curl — and no `sed`. Both image steps read the pinned tag out of a manifest, and the pull request's number out of the forge, through `ci/*.sh` shared with the tag guards, L2 and L3, which run in images that *do* have one. So every run of this change set since the images became derivations died the same way, and never reached the registry: ci/oz-image.sh: line 36: sed: command not found ci/oz-image.sh: line 38: sed: command not found Exit 127, before an image was built: nothing was pushed, so the tag L3 deploys never appeared, and the last line of the log was about sed rather than about the registry the run never reached. The timeout fix in `infrastructure/zot` was necessary and was not this. The tool comes from the flake — `packages.gnused`, the same bargain `skopeo` already makes ("the steps build `.#skopeo` alone rather than entering this shell, because everything else in it would have to be realised first") — and each step puts it on its own PATH. `tests/test_static.py` holds the invariant up: a `nixos/nix` step that runs one of those scripts has to build the output and put it on PATH, so the next script that reaches for a tool the image does not carry fails the suite instead of the pipeline. docs/nix.md §14e is the third case it belongs to, beside "a tool for the suite" and "a tool an image needs at runtime".The `test` step's first command was `deno install --frozen && deno task test`. That install is a hundred-odd lookups at `registry.npmjs.org` fired at once, and this cluster's DNS chain answers a burst that size with REFUSED or SERVFAIL often enough to fail the step: error: Failed caching npm package '@sveltejs/vite-plugin-svelte@5.1.1' ... dns error: failed to lookup address information: Temporary failure in name resolution That is a step with no network in its job failing for the network's reasons, on the same commit whose earlier run passed it. `oz#test` is a derivation over the `deps` fixed-output derivation instead: `node_modules` comes from the store, no install runs, and a suite that passed on a tree is a store hit the next time that tree is gated. It is symmetric with `imageCheck` — a check is a derivation — and the console's build now reaches the network in exactly one place (`deps`, fetched once per lockfile) instead of once per run. The sandbox is what makes this checkable rather than hopeful: the derivation builds with no network at all and the suite's 87 tests pass in it, against the same dependency tree the image is built from. docs/nix.md §15c carries the reason; tests/test_static.py asserts that the step builds `./oz#test` and that nothing in it installs the console's dependencies.`NIX="nix --extra-experimental-features 'nix-command flakes'"` followed by `$NIX build …` is not the command it looks like. A variable's value is not re-parsed, so those quotes are characters in the value: nix read `'nix-command` as an unknown experimental feature and `flakes'` as the subcommand it had been told to run. The step dies eight seconds in, with exit 1, before it builds anything: warning: unknown experimental feature ''nix-command' error: 'flakes'' is not a recognised command It has been in both image steps since the images became derivations, and it was never reached: they died at `sed` first, and every `$NIX` call comes after that. The `test` step spells the flags out in each command, where the shell *does* parse the quotes, which is why it never had this problem. The flag is repeated instead — `--extra-experimental-features nix-command --extra-experimental-features flakes` — which needs no quoting at all. Checked in the image the step runs: `$NIX eval`, `$NIX flake metadata ./oz` and `$NIX build` all parse, where the old assignment reproduced the exact CI error. docs/nix.md §14d carries the trap, and a new test fails any step holding those flags in a variable with a quote inside the value.Both `oz#imageCheck` and `#routerCheck` linked the extracted image's `/app` at the filesystem root, so the entrypoint could be started through the path the image's `Cmd` spells. That needs a writable `/`: true inside the Nix sandbox, false in this cluster, where `nixos/nix` runs with `sandbox = false` and drops builds to an unprivileged `nixbld` user. The first pipeline that got as far as the check died on it, twenty-five seconds in: ln: failed to create symbolic link '/app': Permission denied A check that needs privileges to run is a check that stops running. Both checks now start the entrypoint from their extracted root, and assert what the image itself spells out of the image's own config file — the `Cmd` a runtime reads, and for the router the `PYTHONPATH` that goes with it. That is a stronger assertion than the one it replaces, which exercised a path the check created for itself rather than the path the image carries. Consequently the console's check no longer probes `/`. It is where the variants differ for a reader, but the app reads its marker at the absolute `/app/PREVIEW-BUILD`: run from anywhere else, both variants behave as the real console, so the check could not put them in the states that differ. Which variant these bytes are is decided by the marker assertions against the image's own layers. That the image and the app name one path is now asserted of both sides in `tests/test_static.py`, and that the predicate reads a marker which *is* there is a unit test — the two together are what the `/` probe was standing in for. Verified by building both real derivations with `--option sandbox false`, which is this cluster's configuration: they pass, `HOME=/homeless-shelter` and all (deno degrades to in-memory caches and says so, as it will in a pod with a read-only root filesystem).