feat(tracktor): add Tracktor with its own volume and pocket-id login #288
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/tracktor"
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?
What was wrong
The car logbook of the house is LubeLogger, and the owner wants Tracktor instead: the cluster has no
place for it. Nothing is migrated — the instance starts on an empty volume, LubeLogger stays where it
is, and the two run side by side until the records are moved by hand. The project warns in its own
README that it is under active development and not stable for production; that is said here so this
PR is not read as a promise of a stable service. Overlooked details: the application has no OIDC of
its own (a local user database with sessions), and it has no server database at all (SQLite).
What this fixes
kubernetes/apps/tracktor/{namespace.yaml,kustomization.yaml}— a namespace of its own(
prune: disabled: the namespace outlives an application directory removed from git) plus thesopscomponent as in every other namespace. There is no database Kustomization: the projectkeeps every record in SQLite, so the CNPG cluster of LubeLogger, Mealie or Tandoor has no
counterpart here.
tracktor/ks.yaml— the application Kustomization:postBuildsubstitution of${SECRET_DOMAIN},waitwith a health check on the HelmRelease, anddependsOnontracktor-config(the pod reads the client Secret the operator creates from the CR there; thegenerators of the application are pulled in behind that Kustomization).
tracktor/app/helmrelease.yaml—app-template(the project publishes no chart of its own), imageghcr.io/javedh-dev/tracktor:2.1.0@sha256:edb43955…66292138:Dockerfilesets noUSER), but the only uid it defines is 1000 — thenodeaccount ofnode:22-alpine— and every path the process writes lies under/data(
DB_PATH,UPLOADS_DIR,LOG_DIRare set by the image itself), so the pod runs as 1000 withreadOnlyRootFilesystem.APP_SECRETfrom a Secret born in a generator of ESO: without a value the application refuses tostore or read the credentials of a notification provider (
requireAppSecretinsrc/server/services/crypto.service.tsof the tag), and the reminders are part of what theinstance is installed for — the key is therefore created with the deployment, not on the day the
first provider is configured.
HTTP_MODE: https(the session cookie gets thesecureflag under TLS) andCORS_ORIGINSsetto the one origin of the instance instead of the default
*(getCorsOrigininsrc/server/middlewares/cors.ts).httpGet /api/healthas readiness and as liveness. The path is the one the middlewareleaves without a session (
BYPASS_PATHSinsrc/server/middlewares/auth.ts) and it is served bya static handler, while every request still awaits the initialization of the database
(
handleInitinsrc/hooks.server.tsruns the migrations) — a passing probe also says that themigrations ran and the volume is writable. The neighbouring applications carry no liveness
because their
/healthanswers from the database; this one touches no database once theinitialization is done, and the single replica has nothing to take over a hung process.
BODY_SIZE_LIMITstaysInfinityas in the image, and the memory limit is what actually caps anupload: adapter-node collects the request body in memory (
get_raw_bodyin@sveltejs/kit), notin a file.
/data,/tmpis anemptyDir(the root filesystem is read-only).tracktor/app/pvc.yaml— 10Gi onproxmox-local-zfs: one volume for the database, the uploadedphotographs and documents and the logs of the application, a separate object so that it survives
the recreation of the release.
tracktor/app/httproute.yaml—tracktor.${SECRET_DOMAIN}through theenvoy-internalgateway:the wildcard SAN of the certificate of the gateway already covers the name.
tracktor/app/backend.yaml,tracktor/app/securitypolicy.yaml,tracktor/config/**,tracktor/secrets/**— the login: OIDC at the gateway through pocket-id (aBackendssoof thisnamespace pointing at the provider in-cluster, and a
SecurityPolicyon the route), the client CRthe operator turns into the Secret
tracktor-oidc-client, and two values born in generators of ESO(
CreatedOnce,immutable,Orphan): the client secret andAPP_SECRET. Tracktor has no OIDC,so its own username/password login stays as the emergency door behind the policy.
vpa/**— the object for the recommender,updateMode: "Off"as in the other namespaces.homepage/homepage/app/configmap.yaml— the entry of the new service in the dashboard.Verified
flate test all -p ./kubernetes/flux/cluster—222 passed, includingKustomization tracktor/{tracktor,tracktor-config,tracktor-secrets,tracktor-vpa}andHelmRelease tracktor/tracktor;flate build ksrenders the namespace directory, and the new Kustomizationappears in the output with its path and its
dependsOn.values.schema.jsonofapp-template5.2.1 (the chart theOCIRepository points at, layer digest
sha256:1cf49aeb…): zero violations.helm template, v4.3.0):ServiceAccount,Serviceand
Deploymentare all namedtracktor, so thebackendRefof the HTTPRoute and thetargetRefof the VPA point at objects that really appear; the pod mounts the claim at
/dataand anemptyDirat/tmp, and the environment, the probes and the security context are what themanifest declares.
SecurityPolicyagainst the CRD of the INSTALLED Envoy Gateway(
gateway.envoyproxy.io_securitypolicies.yamlof thegateway-helmchart 1.9.2, the tag of ourOCIRepository): zero violations.
clientIDRef/clientSecretexpect the keysclient-idandclient-secretin that Secret, which is exactly what the client CR renames them to.PocketIDOIDCClientagainst the CRD of the installed operator (chart 0.14.3): zero violations;its callback address equals
redirectURLof the policy to the letter.Backendagainst the CRD of the same chart: zero violations.ghcr.io/javedh-dev/tracktor:2.1.0(noUSER,WorkingDir /app,CMD node build, the threepaths under
/data,RUNTIME=docker) and its build history (docker-entrypoint.shbelongs tonode:22-alpine, uid 1000 is itsnodeaccount). The tag is pinned by the index digestsha256:edb43955…of that manifest.and their defaults (
docs/environment.md), the initialization and the migrations(
src/server/db/init.tswithhandleInit), the open path and the session guard, CORS, the refusalwithout
APP_SECRET, the upload route writing intoUPLOADS_DIR, and the body handling of theadapter.
tracktor.grachevko.ruanswers NXDOMAIN today, while the neighbouring internal names(
lubelogger,vikunja) resolve to192.168.40.11: thek8s-gatewayanswers the names thatexist in the cluster, so the record is not a step of this PR — the check in the section below is
what confirms it.
After the merge
then shows its registration page while no user exists (
docs/authentication.md) — the accountcreated there is also the emergency door behind the policy.
tracktor.grachevko.ruanswers with192.168.40.11, the address of the internalgateway. If it stays unknown, the record is a step of its own (the neighbouring internal names
carry one).
the first thing a policy like that can break silently.
Rollback
git revert— the namespace, the volume and the secrets are pruned with it; oncerecords live on the volume, take a copy of it first (the revert deletes the PVC).
SecurityPolicy: the route keeps serving the local loginof the application. A mismatch between the callback address of the client CR and the
redirectURLof the policy is the other candidate — the provider refuses an address it does not know.
## What was wrong The car logbook of the house is LubeLogger, and the owner wants Tracktor instead: the cluster has no place for it. Nothing is migrated — the instance starts on an empty volume, LubeLogger stays where it is, and the two run side by side until the records are moved by hand. The project warns in its own README that it is under active development and not stable for production; that is said here so this PR is not read as a promise of a stable service. Overlooked details: the application has no OIDC of its own (a local user database with sessions), and it has no server database at all (SQLite). ## What this fixes - `kubernetes/apps/tracktor/{namespace.yaml,kustomization.yaml}` — a namespace of its own (`prune: disabled`: the namespace outlives an application directory removed from git) plus the `sops` component as in every other namespace. There is no database Kustomization: the project keeps every record in SQLite, so the CNPG cluster of LubeLogger, Mealie or Tandoor has no counterpart here. - `tracktor/ks.yaml` — the application Kustomization: `postBuild` substitution of `${SECRET_DOMAIN}`, `wait` with a health check on the HelmRelease, and `dependsOn` on `tracktor-config` (the pod reads the client Secret the operator creates from the CR there; the generators of the application are pulled in behind that Kustomization). - `tracktor/app/helmrelease.yaml` — `app-template` (the project publishes no chart of its own), image `ghcr.io/javedh-dev/tracktor:2.1.0@sha256:edb43955…66292138`: - the image starts as root (`Dockerfile` sets no `USER`), but the only uid it defines is 1000 — the `node` account of `node:22-alpine` — and every path the process writes lies under `/data` (`DB_PATH`, `UPLOADS_DIR`, `LOG_DIR` are set by the image itself), so the pod runs as 1000 with `readOnlyRootFilesystem`. - `APP_SECRET` from a Secret born in a generator of ESO: without a value the application refuses to store or read the credentials of a notification provider (`requireAppSecret` in `src/server/services/crypto.service.ts` of the tag), and the reminders are part of what the instance is installed for — the key is therefore created with the deployment, not on the day the first provider is configured. - `HTTP_MODE: https` (the session cookie gets the `secure` flag under TLS) and `CORS_ORIGINS` set to the one origin of the instance instead of the default `*` (`getCorsOrigin` in `src/server/middlewares/cors.ts`). - probes: `httpGet /api/health` as readiness and as liveness. The path is the one the middleware leaves without a session (`BYPASS_PATHS` in `src/server/middlewares/auth.ts`) and it is served by a static handler, while every request still awaits the initialization of the database (`handleInit` in `src/hooks.server.ts` runs the migrations) — a passing probe also says that the migrations ran and the volume is writable. The neighbouring applications carry no liveness because their `/health` answers from the database; this one touches no database once the initialization is done, and the single replica has nothing to take over a hung process. - `BODY_SIZE_LIMIT` stays `Infinity` as in the image, and the memory limit is what actually caps an upload: adapter-node collects the request body in memory (`get_raw_body` in `@sveltejs/kit`), not in a file. - the claim is mounted at `/data`, `/tmp` is an `emptyDir` (the root filesystem is read-only). - `tracktor/app/pvc.yaml` — 10Gi on `proxmox-local-zfs`: one volume for the database, the uploaded photographs and documents and the logs of the application, a separate object so that it survives the recreation of the release. - `tracktor/app/httproute.yaml` — `tracktor.${SECRET_DOMAIN}` through the `envoy-internal` gateway: the wildcard SAN of the certificate of the gateway already covers the name. - `tracktor/app/backend.yaml`, `tracktor/app/securitypolicy.yaml`, `tracktor/config/**`, `tracktor/secrets/**` — the login: OIDC at the gateway through pocket-id (a `Backend` `sso` of this namespace pointing at the provider in-cluster, and a `SecurityPolicy` on the route), the client CR the operator turns into the Secret `tracktor-oidc-client`, and two values born in generators of ESO (`CreatedOnce`, `immutable`, `Orphan`): the client secret and `APP_SECRET`. Tracktor has no OIDC, so its own username/password login stays as the emergency door behind the policy. - `vpa/**` — the object for the recommender, `updateMode: "Off"` as in the other namespaces. - `homepage/homepage/app/configmap.yaml` — the entry of the new service in the dashboard. ## Verified - `flate test all -p ./kubernetes/flux/cluster` — `222 passed`, including `Kustomization tracktor/{tracktor,tracktor-config,tracktor-secrets,tracktor-vpa}` and `HelmRelease tracktor/tracktor`; `flate build ks` renders the namespace directory, and the new Kustomization appears in the output with its path and its `dependsOn`. - The values of the HelmRelease against `values.schema.json` of `app-template` 5.2.1 (the chart the OCIRepository points at, layer digest `sha256:1cf49aeb…`): zero violations. - A full render of the chart with these values (`helm template`, v4.3.0): `ServiceAccount`, `Service` and `Deployment` are all named `tracktor`, so the `backendRef` of the HTTPRoute and the `targetRef` of the VPA point at objects that really appear; the pod mounts the claim at `/data` and an `emptyDir` at `/tmp`, and the environment, the probes and the security context are what the manifest declares. - The `SecurityPolicy` against the CRD of the INSTALLED Envoy Gateway (`gateway.envoyproxy.io_securitypolicies.yaml` of the `gateway-helm` chart 1.9.2, the tag of our OCIRepository): zero violations. `clientIDRef`/`clientSecret` expect the keys `client-id` and `client-secret` in that Secret, which is exactly what the client CR renames them to. - The `PocketIDOIDCClient` against the CRD of the installed operator (chart 0.14.3): zero violations; its callback address equals `redirectURL` of the policy to the letter. - The `Backend` against the CRD of the same chart: zero violations. - The facts about the image are read from the registry and not from memory: the config blob of `ghcr.io/javedh-dev/tracktor:2.1.0` (no `USER`, `WorkingDir /app`, `CMD node build`, the three paths under `/data`, `RUNTIME=docker`) and its build history (`docker-entrypoint.sh` belongs to `node:22-alpine`, uid 1000 is its `node` account). The tag is pinned by the index digest `sha256:edb43955…` of that manifest. - The facts about the application are read from the sources of tag 2.1.0: the environment variables and their defaults (`docs/environment.md`), the initialization and the migrations (`src/server/db/init.ts` with `handleInit`), the open path and the session guard, CORS, the refusal without `APP_SECRET`, the upload route writing into `UPLOADS_DIR`, and the body handling of the adapter. - `tracktor.grachevko.ru` answers NXDOMAIN today, while the neighbouring internal names (`lubelogger`, `vikunja`) resolve to `192.168.40.11`: the `k8s-gateway` answers the names that exist in the cluster, so the record is not a step of this PR — the check in the section below is what confirms it. ## After the merge 1. Flux creates the namespace and the release; the first visit passes pocket-id, and the instance then shows its registration page while no user exists (`docs/authentication.md`) — the account created there is also the emergency door behind the policy. 2. Check the name: `tracktor.grachevko.ru` answers with `192.168.40.11`, the address of the internal gateway. If it stays unknown, the record is a step of its own (the neighbouring internal names carry one). 3. The websockets of the UI behind the OIDC filter: nothing in the sources suggests them, but this is the first thing a policy like that can break silently. 4. LubeLogger is not touched by this PR: it keeps serving under its own name. ## Rollback - The whole PR: `git revert` — the namespace, the volume and the secrets are pruned with it; once records live on the volume, take a copy of it first (the revert deletes the PVC). - If the login does not come up, remove the `SecurityPolicy`: the route keeps serving the local login of the application. A mismatch between the callback address of the client CR and the `redirectURL` of the policy is the other candidate — the provider refuses an address it does not know.Rendered diff of the HelmReleases in this pull request: