JITSU-48: add prod mode (published images) to the Helm chart - #1518
sahiltyagi-jitsu wants to merge 35 commits into
Conversation
Scaffolding for production mode. No behaviour change: `helm template` in the default dev mode renders byte-identically to before this commit (verified by diff), so this is safe to land ahead of the templating work. Adds: - `mode: dev` in values.yaml, the only accepted values being `dev` and `prod`. `jitsu.mode` validates it and calls `fail` on anything else, so a typo stops the render instead of silently taking the dev branch in every template. - `image.registry` / `image.tag` / `image.pullPolicy` chart-wide defaults, and empty per-service `images.<service>` blocks. - `jitsu.image`, which resolves an explicit per-service `repository` first (so a single service can run from an image while the rest build from source), then the prod default, then the dev base image. The precedence rule lives only here. - `jitsu.imagePullPolicy`, emitted only when a per-service override is set or the chart is in prod mode — dev renders stay byte-identical. The seven runtime containers now resolve through the helper. Two mappings are not one-to-one and are passed explicitly rather than derived from the service name: `profiles` runs the `rotor` image with `ROTOR_MODE=profiles`, and the Go services run `debian:bookworm-slim` in dev (the golang image is their init container) against `jitsucom/<service>` in prod. Image names verified against the release workflow rather than assumed: .github/workflows/services.yaml publishes `console rotor functions-server` and `bulker ingest sidecar syncctl operator ingmgr cfgkpr admin reprocessing-worker`, so every service this chart deploys has a published image. mode=prod is NOT yet usable, and values.yaml says so: init containers, volumes and commands are still unconditional, so prod currently pulls the right images while keeping the source-build scaffolding around them. That is phase 2 — the six containers still on base images in prod are the four Go init containers and the db-push / install jobs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Phase 1 item 4 of JITSU-48: fold the functions-server image into the image
scheme, or document why it stays an env var. It stays an env var, and the
helper says why — Helm does not create those pods. The operator does, at
runtime, one deployment per workspace (bulker/operator/operator.go:1681),
reading its own FUNCTIONS_SERVER_IMAGE config (bulker/operator/config.go:40).
Putting it in `images:` would imply a pod template this chart does not have.
It does have to follow the chart's registry and tag though. It was pinned to
`jitsucom/functions-server:beta` in values.yaml, so a prod install running
`latest` everywhere would have silently launched function servers from the beta
channel. It is now a template default resolved by mode: `beta` in dev,
`{{ image.registry }}/functions-server:{{ image.tag }}` in prod.
Moving it from values.yaml into the template's `extra` dict keeps
`env.operator.FUNCTIONS_SERVER_IMAGE` winning over it, per the documented
jitsu.env precedence (extra < env.<service>).
Dev rendering is unchanged in value but not byte-identical: the variable moves
from the service emission phase to the extra phase, so it now appears earlier in
the env list. It references no other variable and none reference it, so $(VAR)
expansion is unaffected; the diff is two lines moving.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… phase 2) Makes mode=prod produce a deployable release. Dev rendering is byte-identical to before this commit (verified by diff), so nothing changes for existing users. - Go services (ingest, bulker, operator, syncctl): the golang init container, the explicit `command` running the locally built binary, the /build mount and every pod volume are dev-only. In prod the published image ships the binary and defines its own entrypoint. bulker's runtime command is a multi-line block that also installs ca-certificates, so it is wrapped separately from the single-line ones — it is not the shared pattern it looks like. - tsx services (rotor, profiles): workingDir, the `npx tsx` command and the node-cache mount are dev-only. - console: workingDir, the rsync-loop command and the project / node-cache volumes are dev-only. The `maintenance` ConfigMap volume is NOT — that is a product feature, not scaffolding, and it survives in prod along with MAINTENANCE_CONFIG_FILE. - install-job and db-push-job are dev-only in full. install-job exists to populate the node-cache PVC from the hostPath checkout; prod images ship their own node_modules. db-push-job is unnecessary because the console image applies the schema itself on startup (`prisma db push --skip-generate` in docker-start-console.sh unless UPDATE_DB is disabled) — which is how the reference docker-compose deployment migrates, running the image with no command and no UPDATE_DB override. Keeping the job would mean two mechanisms racing to apply the same schema. Phase 2 item 7 — no prod path may evaluate jitsu.projectRoot, which calls `required` — now holds: `helm template --set mode=prod` renders with no projectRoot set. It was install-job and then console that violated it. Audited the prod render for leftovers: zero occurrences of hostPath, initContainers, /build/, node-cache, /cache/workspace, and the golang / node / debian base images. 7 Deployments, 7 Services, RBAC intact. Not addressed here, and not safe to deploy without: values.yaml still ships dev defaults for console secrets (SEED_USER_PASSWORD "changeme", JWT_SECRET "dev-jwt-secret-change-in-production"), and every Service is still type: LoadBalancer. Both are phase 3. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e 3)
Completes the prod path. Dev is unchanged in value; the only diff is two console
env vars moving earlier in the list (template default rather than values.yaml),
which affects nothing — neither references another variable.
- Service type is mode-aware. Dev keeps the four LoadBalancers that
`minikube tunnel` publishes; prod forces ClusterIP everywhere, since those
four would otherwise become four billed cloud load balancers with no TLS and
no hostname. `service.<name>.type` overrides both.
- New Ingress template, disabled by default, prod only. Scoped to console and
ingest, which is what the ticket asks for and what production does — bulker is
ClusterIP there and rotor is in no gateway config. Configurable class,
annotations (cert-manager and friends) and TLS. Enabling it with no host fails
the render rather than emitting an Ingress with no rules.
- NEXTAUTH_URL and JITSU_PUBLIC_URL now derive from the Ingress host, scheme
following ingress.tls.enabled. Without this a prod install behind an Ingress
still advertised http://localhost:3000, so NextAuth would redirect users to
their own machine after sign-in and the tracking snippet would point there
too. Found by rendering it, not by reading it.
- Prod renders now fail while the development JWT_SECRET
("dev-jwt-secret-change-in-production") or SEED_USER_PASSWORD ("changeme")
are still set. They ship in values.yaml so the Minikube quick start works;
nothing in Kubernetes would flag them, and the quick start gives a self-hoster
no reason to look. The error names the value to set.
README documents prod mode, including two things it does not solve: helm-deps
still provides single-node Kafka/Postgres/ClickHouse/MongoDB, which are not
production-grade, and a prod install with no Ingress falls back to
localhost for the console's public URL.
Audited the prod render: zero occurrences of hostPath, initContainers, /build/,
node-cache, /cache/workspace, the golang/node/debian base images, and
LoadBalancer.
Phase 4 — a real end-to-end install on a cluster — is the remaining work, and
Ildar has asked for that to be GKE Autopilot.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Missed when phases 1-3 were written. The ticket splits sidecar's *dev* mode out into a separate task but states that prod mode here must still cover sidecar and functions-server via images. functions-server was handled; sidecar was not. syncctl launches sync pods as CronJobs and reads SIDECAR_IMAGE from its own config (bulker/sync-controller/config.go:37, default jitsucom/sidecar:latest). The chart never set it, so a prod install pinned to image.tag=2.5.0 would still have launched sync pods from jitsucom/sidecar:latest — silently mixing versions in the one place where a version mismatch is hardest to notice. Set only in prod, so dev keeps relying on the Go default exactly as before. Verified: dev renders with no SIDECAR_IMAGE and is unchanged; prod with --set image.tag=2.5.0 renders jitsucom/sidecar:2.5.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Without this the chart cannot be installed by anyone who has not first run
helm/dev-deploy.sh — which is the ticket's actual complaint, "unusable for a
real self-hosted install". Every service mounts `jitsu-secrets` through
`envFrom` with no `optional: true`, and the Secret was created by the dev bash
script, not the chart.
Reproduced on a clean namespace before fixing:
Error: secret "jitsu-secrets" not found
→ CreateContainerConfigError on all seven pods
Missed earlier because prod mode was only ever verified with `helm template`,
and rendering never resolves references to cluster objects.
Token precedence: explicit `auth.token`, else the value already in the cluster
(read back with `lookup`), else generated. The lookup matters — regenerating on
every `helm upgrade` would break every service until the last pod restarted, and
would look completely fine in a rendered manifest.
Key set mirrors dev-deploy.sh's ensure_secrets() exactly: the same eight keys and
the same `service-admin-account:` prefix on the three read as keyId:secret pairs.
Prod only; dev still lets the script own the Secret, so the dev render is
unchanged.
Verified on Minikube: token stable across upgrade, `--set auth.token` honoured,
and a full prod install now reaches 11/11 pods Ready on published images.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All four datastores were `type: LoadBalancer` unconditionally, with no values switch. On Minikube that is harmless — it is what `minikube tunnel` publishes. On any cloud provider it gives each one a public IP: Postgres 5432, Kafka 9092, ClickHouse 8123/9000 and MongoDB 27017 reachable from the internet, with the dev credentials this chart ships. Found while bringing up the JITSU-48 prod stack: every service from the main chart was correctly ClusterIP, and the only LoadBalancers left in the namespace were these four. It would have gone live on the first real-cluster install. Default is ClusterIP rather than the previous behaviour because the two failure directions are not symmetrical: wrong toward ClusterIP breaks a tunnel and is immediately obvious; wrong toward LoadBalancer exposes the data layer silently. dev-deploy.sh now passes --set service.type=LoadBalancer, so the dev flow is unchanged — verified by diffing `helm template` against the previous chart with that flag set: byte-identical. helm-deps is outside the directory JITSU-48 names, so whether this belongs in that ticket is worth confirming; it blocks the stated goal either way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed the production Helm-mode templates, rendered dev/prod manifests, and traced the startup and public-endpoint paths. Findings: fresh prod installs do not bootstrap the configured admin, the configured ingest Ingress host is not passed to the console, and the operator still targets the default namespace.
`dev-deploy.sh deploy` has been failing since 14 Sep: helm-deps times out with "context deadline exceeded" after 10 minutes while every dependency pod is 1/1 Running. Release revisions 3-6 are all failed. Cause: helm's readiness check for a type: LoadBalancer Service requires status.loadBalancer.ingress to be populated. Minikube only assigns that while `minikube tunnel` is running — and `deploy` does not start the tunnel, it starts the project mount; the tunnel is a separate command the developer runs in another terminal. helm-deps publishes four LoadBalancer Services, so --wait could never be satisfied during a deploy. Confirmed rather than assumed: all four Services sit at EXTERNAL-IP <pending> with healthy pods and no tunnel process, and installing the same chart into a fresh namespace without --wait succeeds immediately. Not a regression from 14 Sep. The LoadBalancer Services have been there since ee0d3a8 (7 Jul) and nothing in helm-deps changed since; the deploy only ever succeeded when someone happened to have a tunnel open, and eventually nobody did. Fix: drop --wait and wait on the Deployments directly, which is what "dependencies are healthy" means here and does not depend on the tunnel. Names are read from the rendered release, so disabling a dependency in values removes it from the wait automatically. Verified end to end: `./dev-deploy.sh deploy` now completes — helm-deps revision 8 deployed, all four deployments rolled out, jitsu revision 9, all 11 components ready. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed the production Helm path, including rendered dev/prod manifests and the image entrypoints.
Findings:
- Production credentials supplied through
env.consoleare rendered into the console Deployment and the Helm release record. - Client-side/GitOps renders regenerate the default inter-service token because
lookuphas no live Secret to reuse.
Two review findings on #1518, both of which make prod mode not actually work. Patterns taken from the community chart at github.com/stafftastic/jitsu-chart, which Ildar pointed at on the ticket. **Tokens were regenerated on every reconciliation.** The Secret was rendered from a `lookup` of the existing one, and `lookup` returns nothing during `helm template`, `--dry-run` and Argo CD repo-server rendering. Every render therefore produced a fresh random token, and since updating a Secret does not restart pods atomically, some pods would keep the old key and inter-service calls would start failing. Replaced with a pre-install/pre-upgrade Job that creates the Secret in-cluster only when absent, so rendering stays pure and the token is stable across upgrades. Its Role can `create` secrets and `get` only `jitsu-secrets` — it has no business reading any other. `auth.token` still short-circuits all of it: set it and the Secret is templated directly and the Job is not created, which is the path for an external secret manager or GitOps. **A fresh prod install had no user, so nobody could log in.** The published console image only seeds when SEED_DEMO_CONFIGURATION is set — which also creates demo connections — so SEED_USER_EMAIL/SEED_USER_PASSWORD alone did nothing. Added a post-install/post-upgrade Job calling `manage.js seed` directly. Idempotent: seedUserAndWorkspace() is a no-op unless the profile table is empty (lib/server/seed.ts:88). The NODE_PATH line is required because @prisma/client ships only inside the image's pnpm store. Dev is untouched — verified byte-identical by diffing `helm template` before and after. Both modes lint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…st url Two more review findings from #1518. **The operator managed resources in the wrong namespace.** Its config defaults KUBERNETES_NAMESPACE to "default" (operator/config.go:27) and the chart never set it, so a release installed anywhere else created and listed functions-server Deployments and ConfigMaps in `default` — against a ServiceAccount whose RBAC binding lives in the release namespace. Now taken from the downward API, the same way syncctl already does it. Not prod-only: dev-deploy.sh takes NAMESPACE too (dev-deploy.sh:12), so `NAMESPACE=jitsu-dev ./dev-deploy.sh deploy` has the same bug today. That is why this one changes the dev render — deliberately, and it is the whole diff there. **The console did not know the ingest endpoint.** Setting ingress.hosts.ingest made events.example.com reachable but nothing passed it to the console, so /api/app-config returned no ingest URL (pages/api/app-config.ts:32) and the tracking-installation UI advertised the wrong endpoint. Derived from the same Ingress host rather than configured twice, scheme following ingress.tls.enabled. Absent when there is no Ingress to derive it from; `env.console.JITSU_INGEST_PUBLIC_URL` still overrides. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Last of the review findings on #1518. The documented prod install passed JWT_SECRET and SEED_USER_PASSWORD with `--set env.console.*`, which puts them in the Deployment's env as literal values, in Helm's release record (readable with `helm get values`), and in the operator's shell history. Anyone able to read Deployments in the namespace could read both. The token-generator Job now mints them alongside the inter-service tokens, into the same `jitsu-secrets` Secret that every service already mounts via envFrom. values.yaml no longer carries either — an explicit `env.console.*` would override envFrom, which is the opposite of what is wanted. The initial password is generated rather than chosen: seed.ts already sets changeAtNextLogin, so it is a one-time value, and generating it means it is never typed anywhere. The README says how to read it back. Dev is unchanged in value. It has no such Job — dev-deploy.sh's ensure_secrets() writes only the eight inter-service keys — so the long-standing dev literals are now template defaults (`jitsu.devCredential`) instead of values.yaml entries. They render identically, just from a different phase of jitsu.env, so they move position in the env list. `jitsu.checkProdSecrets` is gone with them: it existed to catch a prod install still carrying the dev credentials, and there are no longer dev credentials in prod to catch. A prod install now needs no credential flags at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed the production Helm rendering, image/entrypoint paths, bootstrap flow, and deployment documentation. Findings: the documented fresh install omits dependency provisioning; auth.token leaves the seed password absent; seeding can race the console migration; and dev image pins still execute the locally built code.
Four findings from the re-review of the previous three commits. **`auth.token` left the install with no admin user.** That path templated the Secret directly and only carried JWT_SECRET, so `seedUserAndWorkspace()` — which needs both email and password — created nothing, and the Job finished "successfully" with nobody able to sign in. The generated path mints a password; this one cannot invent it, so `auth.seedPassword` is now required rather than defaulted, with an error that says what to set and why. My bug, introduced in the previous commit. **The seed Job could beat the schema.** It is ordered only by hook weight, and Helm does not wait for the console Deployment unless the install passes `--wait`, which the documented command does not. `manage.js seed` could run against a database with no tables and burn its backoff. Added an init container that polls /api/healthcheck, which returns 200 only once prisma has connected — so it waits for exactly the right condition rather than a fixed sleep. **The documented prod install skipped dependencies.** `helm install ./helm` does not install `../helm-deps`, so a fresh cluster had no Postgres and the console crashed on a missing DATABASE_URL. README now installs deps first, and says in the same breath that they are single-node and how to point at managed instances instead. **The per-service image override was overstated.** It claimed a pin is "honoured in BOTH modes, so one service can run from an image while the rest build from source". Not true in dev: the dev templates still mount the checkout and override the container command, so the service keeps running local source whatever repository is named. Corrected in both the helper doc and values.yaml. Worth recording why that one survived until now: I verified it by checking that the rendered `image:` field changed, not that the running service used it. The render was right and the claim was still wrong. Dev renders byte-identical. Both modes lint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three bugs, all of which only appear on a real fresh install — rendering,
linting and server-side validation pass with every one of them present.
1. The token-generator Job's ServiceAccount, Role and RoleBinding were plain
manifests while the Job was a pre-install hook. Helm runs and waits for
pre-install hooks *before* applying ordinary manifests, so on a fresh
install the Job could never start:
Error creating: pods "token-generator-1-" is forbidden: error looking up
service account jitsu-token-generator: serviceaccount not found
Error: INSTALLATION FAILED: failed pre-install: timed out waiting for the condition
The RBAC objects are now hooks too, at weight -20 ahead of the Job's -10.
Found by the review bot; reproduced in a clean namespace before fixing.
2. `bitnami/kubectl:1.31` no longer exists. Bitnami moved their public catalog
in 2025 and the version tags are gone from the free namespace, so the image
fails to pull with `not found` and the Job sits in ImagePullBackOff forever.
Now `alpine/k8s:1.31.13`, which is actively tagged and has a shell — the
official registry.k8s.io/kubectl image is distroless, so the Job's script
cannot run there, and the surviving bitnamilegacy/* images are frozen.
3. Kubernetes injects a service-link env var per Service in the namespace, so a
Service named `bulker` becomes BULKER_PORT=tcp://10.x.x.x:3042. Those names
collide with Jitsu's own BULKER_PORT / ROTOR_PORT / SYNCCTL_PORT, which the
console env schema parses as numbers, so the seed Job died with
"expected number, received nan" before it could run. The Deployments set
those explicitly and so are unaffected; the Job now sets
enableServiceLinks: false.
Verified by installing prod mode into a clean namespace, not by rendering:
7 Jitsu services + 4 deps Running, token-generator and seed Jobs both Complete,
jitsu-secrets carrying all ten keys, and the admin user and workspace present in
Postgres. The operator also created its functions-server Deployments in the
release namespace rather than `default`, which confirms the KUBERNETES_NAMESPACE
change at runtime.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed the production Helm rendering, bootstrap hooks, credentials flow, and documented ingress setup.\n\nFindings:\n- The documented ingress-nginx TLS command does not provide a certificate source.\n- Switching an existing generated-secret install to auth.token replaces credentials without restarting the workloads.
…ng broken TLS (JITSU-48) Both found by the review bot on d8b06b4. Credentials: switching an existing install to `auth.token` rewrites the `jitsu-secrets` Secret, but every Deployment's `envFrom` reference is textually identical either way, so Helm sees no change and rolls nothing. Running pods keep the old keys while any pod that restarts for an unrelated reason picks up the new ones and can no longer authenticate to the others; a changed JWT_SECRET also invalidates live NextAuth sessions. The seven service pod templates now carry a `checksum/credentials` annotation over the rendered secrets.yaml, so a credential change rolls all of them together. Emitted in prod only, so the dev render is byte-identical — verified by diffing `helm template` before and after. On the generated-token path secrets.yaml renders nothing, so the digest is constant and nothing rolls; the Job creates the Secret only when absent. TLS: the production README enabled ingress.tls without naming a Secret or an issuer. ingress-nginx then serves its own self-signed certificate while the chart still derives https:// for NextAuth, so a user following the README verbatim cannot log in. TLS is now off in the base command, with worked examples for an existing Secret and for cert-manager, both rendered to check they are valid. Verified on a clean namespace rather than by rendering: install (generated path) 12/12 deployments ready, both hook Jobs Complete, checksum 01ba4719… identical across the 7 services upgrade --set auth.token checksum → 9d456080…, all 7 rolled onto new ReplicaSets, every rollout completed, Secret holds the supplied token Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ial checksum (JITSU-48) The checksum annotation was added with a hardcoded `annotations:` key and `nindent`, so dev — where the include renders nothing — still emitted an empty `annotations:` block on all seven pod templates. The ticket requires the existing dev flow to be unchanged, so that was a regression. The mode check now lives at the call site and dev emits nothing at all. The previous commit claimed the dev render was byte-identical "verified by diffing helm template before and after". That verification was worthless: dev mode hard-fails without `projectRoot`, so both sides of the diff were empty files and the comparison could not have failed. Redone with `--set projectRoot=...`: 1430 lines on both sides, identical, and prod still carries seven checksums that change with `auth.token`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rotor validates incoming requests from ROTOR_AUTH_TOKENS or ROTOR_RAW_AUTH_TOKENS only, and when neither is set it logs "No auth tokens are configured. Rotor is open for everyone." and returns true for every request (services/rotor/src/index.ts checkAuth). The chart generated ROTOR_AUTH_KEY, which is what rotor presents when calling bulker, not what it checks on the way in — so every prod install shipped an unauthenticated rotor. Only rotor is affected. bulker, ingest and syncctl are Go and viper binds both the prefixed and the unprefixed name (jitsubase/appbase/app_base.go:115), so they pick up the bare RAW_AUTH_TOKENS already in the Secret. Rotor is Node and has no such fallback. Pre-existing rather than introduced here: helm/dev-deploy.sh creates the same eight keys, so dev has the same open rotor. Left alone deliberately — the ticket requires the dev flow to be unchanged, and the dev render stays byte-identical (1430 lines, diffed with --set projectRoot). Worth raising separately. Verified on a clean namespace: ROTOR_RAW_AUTH_TOKENS present in the running rotor container, and the "open for everyone" warning absent from its logs. Found by the review bot on 306ce43. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed the production Helm rendering, credential/bootstrap paths, and ingress configuration.
Findings:
- The documented
auth.tokenoverride reintroduces credentials into Helm-rendered state instead of supporting an external Secret reference. - Revision-suffixed hook jobs are not removed by
before-hook-creation, so they accumulate across upgrades.
…et (JITSU-48)
The token Job's idempotency rule was "Secret exists -> exit 0". That was correct
while the key set was fixed, but the set has grown, so any pre-existing
jitsu-secrets never gained the new keys:
- upgrading a dev release to mode=prod: dev-deploy.sh creates only the eight
inter-service keys, so JWT_SECRET, SEED_USER_PASSWORD and ROTOR_RAW_AUTH_TOKENS
were all absent. The published console then failed its required JWT_SECRET env
validation, the seed Job created no admin, and rotor stayed open to everyone;
- any prod install created before the rotor fix, which never gained
ROTOR_RAW_AUTH_TOKENS — so that fix did not reach existing installs at all.
The Job now reads the existing token, adds only the keys that are absent, and
never rewrites one that is already set: rotating a live token breaks every
service until the last pod restarts. Backfilled inter-service keys derive from
the existing token, so callers and callees still agree. The Role gains `patch`,
scoped by resourceNames to the same single Secret as `get`.
Verified by simulating the upgrade: a namespace seeded with only the eight
dev-deploy.sh keys and a known token, then installed with mode=prod.
token-generator log: jitsu-secrets backfilled: ROTOR_RAW_AUTH_TOKENS
JWT_SECRET SEED_USER_PASSWORD
RAW_AUTH_TOKENS unchanged (PreExistingToken...0001)
console Running, no env validation failure
seed Job Completed, admin@example.com present in Postgres
rotor "open for everyone" absent from logs
cluster 11/11 pods Running, both hook Jobs Complete
Found by the review bot on 9e0f366.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… path real (JITSU-48) Two findings from the review bot on 9e0f366. Hook Jobs accumulated forever. Both Jobs were named with a `{{ .Release.Revision }}` suffix, so every upgrade produced a distinct `token-generator-N` / `seed-N`. `before-hook-creation` only deletes a hook of the same name, so nothing was ever cleaned up — and Helm does not remove hook resources on uninstall either. Both now use stable names, so exactly one generation survives and the previous run's logs stay available until the next. The external-secrets story was wrong. The README said that to manage the secret yourself you should set `auth.token` — but that renders the value into the Secret template, so it lands in the rendered manifest and in Helm's release record, which is the exposure the generated path exists to avoid. It is the opposite of an external-secrets path. Corrected: create `jitsu-secrets` yourself and leave `auth.token` unset. The Job then only backfills absent keys and never rewrites one you set, so supplying all of them means it does nothing. `tokenGenerator.enabled=false` (new) stops the chart touching the Secret under any circumstances. `auth.token` is now documented as what it is — a convenience that puts the token in your values file. Verified on a clean namespace with a pre-created, externally owned Secret and `tokenGenerator.enabled=false`: no token-generator Job created, the Secret's token unchanged, the seed Job completed against the externally supplied password and created the admin, 11/11 pods Running. Dev render byte-identical throughout (1430 lines, diffed with --set projectRoot). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
With scaling.console.replicas=0 the chart renders no console Service or Deployment — a supported configuration, provided env.common.CONSOLE_URL points at an external console. The seed Job still polled a hardcoded http://console:3000, so it waited out its full 60x5s timeout against a Service that does not exist and failed every install and upgrade in that configuration. It now uses the existing jitsu.consoleUrl helper, which already resolves to the local Service when console is enabled and to the required env.common.CONSOLE_URL when it is not — so the readiness gate follows whichever console the rest of the chart is talking to. default prod http://console:3000/api/healthcheck replicas=0 + CONSOLE_URL http://console.example.com/api/healthcheck Default path re-verified on a clean namespace: both hook Jobs Complete, the init container logged "console is ready". Dev render byte-identical (seed is prod only). Found by the review bot on c4e337c. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…(JITSU-48) `kubectl patch` echoes the patched resource by default, so the backfill path introduced in 1f5b42e printed the Secret's entire `data` map — every inter-service token, JWT_SECRET and SEED_USER_PASSWORD, base64 but trivially reversible. Anyone able to read this Job's logs could recover the credentials without any access to the Secret itself, which is precisely the exposure this whole design exists to prevent. My regression, introduced this evening. Both streams are suppressed, not just stdout: a malformed patch makes kubectl echo the request body back, and that body carries the values in plaintext under `stringData`. On failure the Job now says so and exits 1 without reprinting anything. The same guard is applied to the create path, where the `--from-literal` arguments are the credentials. What the Job logs is the key *names*, which is the useful part: jitsu-secrets backfilled: ROTOR_RAW_AUTH_TOKENS JWT_SECRET SEED_USER_PASSWORD Verified on a clean namespace seeded with only the eight dev-deploy.sh keys, so the backfill path actually ran, and checked against the real values rather than by eye: the generated JWT_SECRET, the generated SEED_USER_PASSWORD and the pre-existing token are all absent from the log, and no base64 blob appears in it. Backfill still correct — 11 keys present, existing token unchanged, 11/11 pods Running and both hook Jobs Complete. Found by the review bot on ec7338c. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed the production-mode Helm rendering, image/entrypoint selection, secret bootstrap hooks, ingress wiring, and the unchanged default development path.
No additional actionable findings beyond the existing review threads. I also rendered the default generated-secret and explicit-token production configurations and ran Helm lint for production and development values.
…ce (JITSU-48) The service templates are gated on their replica count, so scaling.console or scaling.ingest at 0 means no Service is created. The Ingress still rendered a rule for it, and ingress controllers accept that without complaint — they serve the route as unavailable. The mistake surfaced as a dead hostname in production rather than as an error at install time. Both combinations now fail at render with a message naming the two values that disagree, matching how the template already handles ingress.enabled with no hosts. Verified: both hosts, both services enabled 2 rules ingress.hosts.ingest + replicas 0 fails, names both values ingress.hosts.console + replicas 0 fails, names both values console replicas 0, no console host renders (1163 lines) The last case is the one that had to keep working: running console externally via env.common.CONSOLE_URL is supported, and it is only a mistake if you also ask the Ingress to route to a console that is not there. Dev render byte-identical (1430 lines). helm lint passes in both modes. Found by the review bot on ec7338c. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Without it, `helm install jitsu-deps` returns as soon as the objects exist and the main chart can start while Postgres is still booting. The console entrypoint runs `prisma db push` once and never checks whether it succeeded (docker-start-console.sh), so on a lost race the schema is missing. The review bot's conclusion was that the install then fails. It usually does not: /api/healthcheck does a workspace.findFirst, returns 503 when the table is absent, and the entrypoint's own healthcheck responds to a non-200 with `kill -9 $$`. The container exits, Kubernetes restarts it, and the migration runs again — the crash-restart is the retry. That is what the `restarts=2` on console in every clean-namespace install yesterday actually was. The real exposure is narrower: the install fails only if Postgres takes longer to become ready than the post-install seed Job's 60x5s wait. In practice it is seconds, which is why five installs all succeeded. `--wait` removes the race rather than delaying it, because every dependency has a readiness probe and Postgres's is `pg_isready` — acceptance of connections, not merely a running pod. Left the entrypoint alone. Checking the exit code and retrying in place would be tidier than crash-looping, but it is polish, not a broken install. Found by the review bot on a4f519e. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
10m was a guess. 5m is Helm's own default and comfortable on a normal cluster: the four dependency images are roughly 600 MB compressed in total and pull in parallel. Noted how to tell a slow pull from a genuine hang, and that the value should be raised on a slow link. Worth recalibrating once the Autopilot acceptance run gives a real cold-start number on fresh nodes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed the Helm production-mode changes, including image/scaffolding switching, generated and explicit credential paths, hook ordering, ingress rendering, and dependency startup guidance. Rendered the supported dev and prod configurations; no additional actionable issues found.
…JITSU-48) The paragraph was written during phase 1, when this branch added only image resolution, and it said mode=prod "will not produce a working deployment. Leave it on dev." The branch has since delivered working prod mode, so the file was warning users off the feature the README tells them to use — and `helm show values` is how people inspect a chart's configuration, so the two entry points disagreed. Replaced with what a prod install actually involves that the bullets above do not already say: it generates its own credentials, creates the first admin user, and needs the dependency chart first. Points at the README as the entry point, and records why dev remains the default. Dev render byte-identical. helm lint passes in both modes. Found by the review bot on be762a6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed the production/dev Helm mode split, published-image selection, bootstrap secret and seed hooks, ingress wiring, and dependency service exposure. I also rendered the default production, explicit-token, ingress/TLS, external-console, and pinned-dev variants and linted both charts. No new actionable issues found; the previously raised review threads are resolved.
… (JITSU-48) Found on the first install of this chart onto the GKE Autopilot cluster built for JITSU-48 acceptance. redpanda crash-looped: Failure during startup: filesystem error: mkdir failed: Permission denied ["/var/lib/redpanda/data/crash_reports"] The image runs as uid/gid 101 — confirmed by running it and reading `id`, rather than assuming — and its own /var/lib/redpanda/data is owned by redpanda. A mounted PVC arrives owned by root, so the container cannot create the crash-reports directory and exits during startup. `fsGroup` makes the kubelet chown the volume on mount. Invisible on Minikube, where the container ends up able to write anyway. Autopilot enforces the stricter security context, and this is the first time the chart has been installed anywhere but Minikube — which is exactly what the acceptance criterion asks for and why it is worth doing. Scoped to kafka: postgres, mongodb and clickhouse start cleanly on Autopilot and are left alone. helm lint passes; the main chart's dev render is byte-identical (different chart, but checked). Verified by reinstalling: all four dependencies 1/1 Running with zero restarts, where redpanda previously had five. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed the Helm production-mode path, image/scaffolding selection, ingress configuration, credential bootstrap hooks, and dependency exposure changes. I also checked the existing review threads; all are resolved. No new actionable correctness, security, or user-visible regression findings.
The dependency install already documents `--wait --timeout 5m`; the main chart
install did not, and on a real cluster that difference is not cosmetic.
Measured during acceptance on a GKE Autopilot cluster: a bare `helm install`
exited 0 while five services were in CrashLoopBackOff. Everything that reads its
configuration from the console — bulker, ingest, operator, profiles, rotor,
syncctl — exits rather than retrying when the console is not up yet:
Cannot load cached repository. No CACHE_DIR is set.
Cannot serve without repository. Exitting...
Four restarts each, then they settle once the console is ready. The install does
recover on its own, so the defect is not the restarts — it is that Helm reports
success in the middle of them, which is exactly when someone walks away.
Note this is visible in prod mode specifically. In dev the source-build init
container takes minutes and accidentally masks the race; prod has no init
container, so the services start immediately against a console that is not
ready.
`5m` has headroom: a cold start into an empty namespace, including an Autopilot
node scale-up, reached all-running in about 2 minutes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Prerequisites listed Minikube and Helm. On a managed cluster that is not enough,
and the way you find out is the pre-install hook failing with
roles.rbac.authorization.k8s.io "jitsu-token-generator" is forbidden:
... requires one of ["container.roles.delete"]
which gives no hint that it was expected. GKE's `roles/editor` deliberately
excludes RBAC, so Editor can create the cluster but not install this chart into
it. That cost a day during acceptance.
Documents three things:
- the installer needs rights to create 8 RBAC objects, two of them
cluster-scoped, so namespace-admin is also insufficient
- helm-deps needs a default StorageClass; its three PVCs set no
storageClassName, and without one they sit Pending with no useful error
- what each Role and ClusterRole actually grants and why, since a client
deploying onto their own cluster will be asked to justify them
Two grants are called out rather than left to be found: `pods/exec` create on
syncctl is effectively shell access to pods in scope (used for resource
sampling), and both ClusterRoles are cluster-scoped, so their secrets access
spans every namespace.
No behaviour change; documentation only.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed the production-mode Helm path: published-image selection, service exposure and ingress, credential/bootstrap hooks, and dev deployment compatibility.
Finding:
- Production resource requests/limits cannot be configured, so namespaces that require them reject this new deployment path.
| # | ||
| # `dev` stays the default because it is what the Minikube workflow and | ||
| # dev-deploy.sh expect, and its rendered output is unchanged by prod support. | ||
| mode: dev |
There was a problem hiding this comment.
[P2] The new prod path has no configurable resource requests or limits: the only workload resources: blocks are inside the dev-only build scaffolding, and values.yaml exposes no runtime resource settings. A namespace with a LimitRange that requires requests will reject every prod Pod (including the seed/token Jobs), and other clusters have no way to reserve capacity. Please add values-backed resource settings for the prod workloads (and pass them to the generated sync/function Pods where applicable), or make the required cluster policy an explicit prerequisite.
There was a problem hiding this comment.
Confirmed, and independently — this came out of the acceptance run on a real cluster before I saw this comment, so there are two separate paths to the same conclusion.
What the cluster showed:
values.yamlexposes noresourceskey at all, so a client cannot set requests or limits without forking the chart.- The prod render has no
resourceson any of the nine containers. - Every pod therefore runs BestEffort, and a namespace with a
LimitRangethat requires requests rejects them outright, exactly as described. - The same gap is in the sync pods
syncctlgenerates, not just the chart. During the acceptance sync, Autopilot loggedadjusted "cpu" resource to meet requirements for containers [oauth-refresh, pipes-init, sidecar].
Worth noting why this was invisible until now: the two clusters this chart has run on both hide it. Minikube has no pressure and no LimitRange. Autopilot injects defaults automatically — the autopilot-default-resources-mutator lines in the install log are it patching this in real time. A client cluster is a third environment, and it is the one the feature is for.
Not fixed here, deliberately. Adding values-backed resources means sensible defaults for nine containers plus the generated sync and functions-server pods, and touches helm-deps too. That is chart design rather than "add prod mode", and #1518 is already large with 18 earlier findings addressed. Raised with @ildar as a scope call — this or its own ticket. Leaving the thread open so the decision is visible rather than buried.
There was a problem hiding this comment.
Reviewed the production Helm rendering, image/entrypoint wiring, bootstrap hooks, service exposure, and the dependency-chart changes.\n\nFinding:\n- helm/templates/secrets.yaml: the explicit auth.token path reuses an inter-service bearer token as the NextAuth session-signing secret.
| CONFIG_SOURCE_HTTP_AUTH_TOKEN: {{ printf "service-admin-account:%s" .Values.auth.token | quote }} | ||
| {{- /* Console's session-signing secret. Derived from the same supplied token | ||
| so an operator managing auth.token manages one value, not two. */}} | ||
| JWT_SECRET: {{ .Values.auth.token | quote }} |
There was a problem hiding this comment.
[P1] JWT_SECRET must not be derived from the shared auth.token. The other fields here turn that value into bearer credentials (for example ROTOR_RAW_AUTH_TOKENS, CONSOLE_RAW_AUTH_TOKENS, and the service-admin-account: tokens) which are sent on service-to-service requests; webapps/console/lib/nextauth.config.ts uses JWT_SECRET as NextAuth’s signing secret. Anyone who obtains one of those bearer credentials can therefore forge console sessions, including an admin session. Keep the JWT secret independently configured/generated in this path (or require a separate auth.jwtSecret).
There was a problem hiding this comment.
Fixed in 9bf0705d7.
auth.jwtSecret is now a separate value, required whenever auth.token is set — the same pattern this template already uses for auth.seedPassword. The error message states that it must differ from auth.token and why, so it cannot be satisfied by pasting the same value in twice without noticing.
Two alternatives rejected, recorded here so they are not re-proposed:
- Hashing
auth.token. The derivation would live in this chart, so anyone holding the token still obtains the signing secret. It buys nothing. - Generating it in the template.
lookupis empty duringhelm template,--dry-runand Argo CD rendering, so every reconciliation would emit a new secret and invalidate all sessions — the same reason this file does not generate the tokens themselves.
Scope check: this only ever affected the supplied-token path. The default generated path was already correct — token-generator.yaml mints JWT_SECRET with gen 48, independent of $TOKEN. Verified against a live install on a GKE cluster, where every key in jitsu-secrets differs from JWT_SECRET.
The old comment framed the reuse as a convenience — "one value, not two". That convenience was the vulnerability, and the comment now says so.
…(JITSU-48) In the `auth.token` path, secrets.yaml set JWT_SECRET to auth.token — the same value it writes to RAW_AUTH_TOKENS, ROTOR_RAW_AUTH_TOKENS, CONSOLE_RAW_AUTH_TOKENS and, prefixed, to the three service-admin-account: pairs. Those are bearer credentials. They are handed to services and travel on service-to-service requests, so they are exposed in far more places than a signing key should be. JWT_SECRET is what console signs session cookies with (webapps/console/lib/nextauth.config.ts:179). Sharing the two means anyone who obtains one of those credentials — from a log, a compromised service, a misconfigured proxy — can mint their own console session, including an admin session. Raised as P1 by ai-review; confirmed by reading both ends. The previous comment framed it as a convenience: "derived from the same supplied token so an operator managing auth.token manages one value, not two". That convenience is the vulnerability. Fixed by requiring a separate `auth.jwtSecret` whenever `auth.token` is set, following the precedent already in this template for `auth.seedPassword`. The error message says why, so it cannot be satisfied by pasting the same value in twice without noticing. Hashing auth.token was considered and rejected: the derivation would live in this chart, so holding the token would still yield the signing secret. It has to be independently supplied. Scope: the supplied-token path only. The default generated path was already correct — token-generator.yaml mints JWT_SECRET with `gen 48`, independent of $TOKEN, verified against a live install where every key differs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| resourceNames: ["jitsu-secrets"] | ||
| - apiGroups: [""] | ||
| resources: ["secrets"] | ||
| verbs: ["create"] |
There was a problem hiding this comment.
[P2] This create permission is not constrained to jitsu-secrets. Kubernetes cannot apply resourceNames to a create request because there is no existing object to authorize, so this ServiceAccount can create Secrets with any name in the release namespace. That contradicts the chart/README claim that the generator cannot touch other Secrets and broadens the impact of a compromised Job image. Please redesign this bootstrap so the generator does not need unrestricted Secret creation (or document and deliberately accept this broader grant).
There was a problem hiding this comment.
Correct, and the correction applies to my own wording as much as to the template — the README table I added in 33ce48914 said resourceNames stops the Job touching any other Secret, which is true of get and patch but not of create.
Addressed in 99955486f by stating the grant plainly rather than narrowing it, because it cannot be narrowed: Kubernetes does not apply resourceNames to a create request, since there is no object yet to authorize against. Whatever the Role says, that verb is namespace-wide.
The obvious alternative — pre-create an empty jitsu-secrets so the Job needs only patch — was considered and is worse. lookup is empty during helm template, --dry-run and Argo CD rendering (see the header comment in secrets.yaml), so a templated Secret would blank the live keys on every upgrade. Trading a namespace-scoped create for silent credential loss on upgrade is not a good trade.
So it stays, and is now documented in both places, with tokenGenerator.enabled=false named as the way out for anyone who cannot accept it. Happy to revisit if you would rather the bootstrap were redesigned — that is a larger change than this PR.
…JITSU-48) The Role pins `get` and `patch` to jitsu-secrets with resourceNames, and the comments said the Job "has no business reading any other Secret" and "can never modify another" — both true. The README table I added earlier went further and said resourceNames stops it touching any other Secret, which is not true of `create`. Kubernetes does not apply resourceNames to create requests: there is no object yet to authorize against, so that verb is namespace-wide whatever the Role says. The Job can create Secrets of any name in the release namespace. Pre-creating an empty jitsu-secrets so the Job needed only `patch` would remove the grant, and is worse: `lookup` is empty during helm template, --dry-run and Argo CD rendering, so the templated Secret would blank the live keys on every upgrade — the same reason secrets.yaml does not generate tokens itself. So the grant stays and is now stated plainly in both the Role and the README, with tokenGenerator.enabled=false named as the way out. Raised as P2 by ai-review; the correction applies to my own wording as much as the template. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed the production/dev Helm rendering paths, credential hooks, ingress validation, and dependency service exposure. Helm lint and representative dev and production renders succeed; invalid ingress configuration correctly fails rendering. No additional actionable findings beyond the existing open review threads.
Miscounted when documenting the default-StorageClass requirement. helm-deps creates PVCs for postgres, mongodb, clickhouse and kafka — four, each 5Gi, none setting storageClassName. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed the production Helm path, including rendered manifests, bootstrap hooks, image wiring, Ingress configuration, and credential handling.
Finding: auth.jwtSecret is documented as distinct from auth.token, but the template only requires a non-empty value and allows them to be identical.
| file, so holding the token still yields the secret. It has to be an | ||
| independent value, which is why this is `required` rather than | ||
| defaulted, exactly like auth.seedPassword below. */}} | ||
| JWT_SECRET: {{ required "auth.token is set, so auth.jwtSecret must be set too. It signs console sessions and must be a DIFFERENT value from auth.token — auth.token is handed to services as a bearer credential, so sharing it would let anyone holding one forge an admin console session." .Values.auth.jwtSecret | quote }} |
There was a problem hiding this comment.
[P1] Enforce the JWT/token distinction instead of only documenting it. required only checks that auth.jwtSecret is non-empty: helm template ... --set auth.token=same-secret --set auth.jwtSecret=same-secret --set auth.seedPassword=... renders JWT_SECRET and the bearer-token variables with the same value. That recreates the session-forgery condition described in this comment. Add an eq guard that fails the render when the two values match.
There was a problem hiding this comment.
You are right, and the previous commit message overstated the fix — it claimed the error "cannot be satisfied by pasting the same value in twice without noticing". It could.
Confirmed by rendering rather than reading:
--set auth.token=sameval --set auth.jwtSecret=sameval
RAW_AUTH_TOKENS: "sameval"
JWT_SECRET: "sameval"
required only proved non-empty, so the separate value bought documentation, not safety.
Fixed in b7eaae430 with an explicit eq guard that fails the render, naming which variables carry auth.token as a bearer credential and why sharing it with the session-signing secret is unsafe. Verified all three paths: equal values fail, different values render, and the generated path is untouched since it has no auth.token and mints an independent JWT_SECRET.
…SU-48) 9bf0705 added a separate auth.jwtSecret and required it. That was not enough, and the commit message overstated it: it claimed the error "cannot be satisfied by pasting the same value in twice without noticing". It can. `required` only proves the value is non-empty. This renders happily: --set auth.token=same --set auth.jwtSecret=same and produces JWT_SECRET and RAW_AUTH_TOKENS with the same string — exactly the session-forgery condition the separate value exists to prevent. Confirmed by rendering it, not by reading the template. Now an explicit `eq` guard fails the render, naming which variables carry auth.token as a bearer credential and why sharing it with the session-signing secret is unsafe. A doc comment does not stop anyone; a failed render does. Raised as P1 by ai-review, on the fix rather than the original defect. Unaffected: the generated path, which has no auth.token and mints an independent JWT_SECRET. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed the production-mode Helm templates, rendered prod/dev pin configurations, and checked the console bootstrap path against its image entrypoint.\n\nFindings:\n- The token-generator hook does not select the release namespace, so default credential generation fails outside default.\n- Pinning the console image in dev disables the only seed path and leaves a fresh dev install without a login user.
| gen() { head -c 256 /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c "$1"; } | ||
|
|
||
| # Value of a key in the existing Secret, empty if absent. | ||
| key() { kubectl get secret jitsu-secrets -o "jsonpath={.data.$1}" 2>/dev/null | base64 -d 2>/dev/null || true; } |
There was a problem hiding this comment.
The Job may run in any release namespace, but every kubectl call here omits --namespace (or an equivalent -n {{ .Release.Namespace }}). kubectl therefore uses its default namespace, while the ServiceAccount Role/RoleBinding in this template are created in the release namespace. For helm install -n jitsu ..., the initial get fails and the subsequent create is forbidden in default, so the pre-install hook fails and the chart never installs. Please pass the release namespace to the get/patch/create calls (or set it once via KUBECTL_NAMESPACE).
There was a problem hiding this comment.
Not reproducible — this one is incorrect, and there is a live install that disproves it.
kubectl inside a pod uses in-cluster config, and in-cluster config takes its default namespace from /var/run/secrets/kubernetes.io/serviceaccount/namespace, which is the pod's own namespace. It does not fall back to default. So the Job's calls already act on the release namespace without --namespace.
Verified against the acceptance install on a GKE Autopilot cluster, which was helm install jitsu ./helm --set mode=prod -n jitsu:
job/token-generatorinns=jitsu,succeeded=1secret/jitsu-secretscreated inns=jitsuwith all 11 keys- nothing created in
default—secrets "jitsu-secrets" not foundthere
If the described failure were real, that install could not have completed; the pre-install hook would have failed before anything else ran.
Leaving it open rather than resolving, in case you are describing a case I have not thought of — happy to add -n {{ .Release.Namespace }} anyway as belt and braces if you would prefer it explicit.
| {{- with (include "jitsu.imagePullPolicy" (dict "ctx" . "service" "console")) }} | ||
| imagePullPolicy: {{ . }} | ||
| {{- end }} | ||
| {{- if include "jitsu.devScaffold" (dict "ctx" . "service" "console") }} |
There was a problem hiding this comment.
With mode=dev and images.console.repository set, this condition removes the source command that runs manage.ts seed. The pinned jitsucom/console image then uses docker-start-console.sh, which invokes manage.js seed only when SEED_DEMO_CONFIGURATION is nonempty; the dev values do not set it, and the prod-only seed Job is absent. A fresh dev install using the newly supported console image pin has a migrated database but no UserProfile, so the configured admin@example.com/changeme credentials cannot log in. Keep a no-demo seed path for a pinned dev console (or enable a dedicated seed hook for it).
There was a problem hiding this comment.
Correct, and confirmed by rendering it. Fixed in e4de4c0b0.
mode=dev with images.console.repository set fell between the two seed paths: the inline manage.ts seed lives in the source-build branch, and the seed Job was gated on mode=prod. Rendered before the fix:
| seed Job | inline seed | ||
|---|---|---|---|
| prod | yes | no | seeded |
| dev, unpinned | no | yes | seeded |
| dev, pinned console | no | no | nobody can sign in |
The Job is now gated on whether the console runs from source rather than on mode, which covers all three. It already resolved its image through jitsu.image, so a pinned console image is used correctly. Re-rendered all three: prod via the Job, dev-unpinned inline, dev-pinned via the Job.
Worth noting this arrived with the PR — the per-service image override is one of the ticket's build requirements, so the configuration is newly supported here. My own behavioural check of the pin missed it because I pinned syncctl, which has no seeding role. Console is the one service where pinning has a side effect, and I picked the wrong service to test.
The seed Job was gated on mode=prod. The console's own entrypoint seeds inline, but only on the source-build path — a pinned jitsucom/console image runs docker-start-console.sh, which seeds only when SEED_DEMO_CONFIGURATION is set, and that also creates demo connections, so the dev values deliberately leave it unset. So `mode=dev` with images.console.repository set fell between the two: no inline seed, no seed Job. The result is a migrated database with no UserProfile, and the documented admin@example.com credentials cannot sign in. That configuration is newly supported by this PR — the per-service image override is one of the ticket's build requirements — so the gap arrived with it. Gated on whether the console runs from source rather than on mode. The Job already resolves its image through jitsu.image, so a pinned console image is used correctly. Verified by rendering all three: prod seeds via the Job, dev-unpinned seeds inline, dev-pinned now seeds via the Job. Previously the third seeded not at all. Raised by ai-review. My own behavioural check of the image pin missed it because I pinned syncctl, which has no seeding role; console is the one service where pinning has a side effect. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed the Helm production-mode and image-pin changes, including rendered default production, explicit-token, and pinned-development paths.
Finding:
- P1: a pinned console image in development now creates the seed Job, but that Job is missing the development seed password, so a fresh install has no login user.
| so the dev values deliberately leave it unset. Keying this on mode meant | ||
| `mode=dev` with images.console.repository set produced a migrated database | ||
| with no UserProfile and nobody able to sign in. */}} | ||
| {{- if and (not (include "jitsu.devScaffold" (dict "ctx" . "service" "console"))) .Values.seed.enabled }} |
There was a problem hiding this comment.
[P1] This newly enables the Job for mode=dev with images.console.repository set, but the Job only calls jitsu.env and does not add the dev-only SEED_USER_PASSWORD (or JWT_SECRET) that console.yaml supplies through jitsu.devCredential. dev-deploy.sh creates jitsu-secrets with only the eight inter-service keys, so on a fresh pinned-dev install the rendered seed container has SEED_USER_EMAIL=admin@example.com but no password. seedUserAndWorkspace() then takes its no-op branch (profileCount === 0 && email && password), and the configured admin@example.com / changeme login does not exist. Add the same dev credential extra env to this Job (or source it from the Secret) for the pinned-dev path.
There was a problem hiding this comment.
Correct — the previous commit rendered the Job but left it unable to seed. Fixed in the commit above.
SEED_USER_PASSWORD and JWT_SECRET reach console.yaml through jitsu.devCredential, not through the Secret, because in dev dev-deploy.sh writes only the eight inter-service keys. The Job called jitsu.env alone, so it got SEED_USER_EMAIL and no password — seedUserAndWorkspace() then takes its no-op branch and creates nothing, which is the same outcome one step further along.
Now mirrors console.yaml. Rendered both paths:
dev + pinned console EMAIL=Y PASSWORD=Y JWT=Y
prod EMAIL=Y PASSWORD=N JWT=N
Prod is unchanged on purpose: there the token-generator writes both keys into jitsu-secrets and the Job picks them up via envFrom, so devCredential correctly yields nothing.
e4de4c0 made the seed Job render for mode=dev with a pinned console image, but the Job only called jitsu.env. SEED_USER_PASSWORD and JWT_SECRET reach console.yaml through jitsu.devCredential, not through the Secret: in dev, dev-deploy.sh creates jitsu-secrets with only the eight inter-service keys. So the Job rendered with SEED_USER_EMAIL and no password. seedUserAndWorkspace() requires both and otherwise takes its no-op branch, meaning the Job would run, report success, and create no user — the same "nobody can sign in" outcome the previous commit set out to fix, one step further along. Mirrors console.yaml's devCredential handling. Verified by rendering: dev + pinned console EMAIL=Y PASSWORD=Y JWT=Y prod EMAIL=Y PASSWORD=N JWT=N Prod is deliberately unchanged — there the token-generator writes both keys into jitsu-secrets and the Job picks them up via envFrom, so devCredential yields nothing. Raised as P1 by ai-review, on the previous fix rather than the original defect. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed the Helm production-mode changes, including image/scaffolding selection, secret bootstrap and rotation behavior, seeding, ingress configuration, and dependency exposure. I also rendered the default production, explicit-token production, and pinned-console development configurations.
No new actionable findings beyond the existing unresolved review threads.
Prod mode has now been installed and exercised end to end on EKS as well as GKE, so the README's "not yet verified on a real cluster" line is replaced with what was actually proven on each. Three things that only come up on EKS, all hit during the run: EKS ships a gp2 StorageClass whose provisioner is the removed in-tree kubernetes.io/aws-ebs, and it is not the default — so the cluster looks like it has storage and provisions none. "A default StorageClass" was accurate guidance and insufficient on AWS. AWS's own documented minimum IAM policy for eksctl omits iam:ListOpenIDConnectProviders. Without it eksctl silently disables IRSA and skips every addon that needs it, including aws-ebs-csi-driver. Nothing errors; the addon is simply absent and every PVC hangs. EBS volumes are AZ-bound, which makes helm-deps a single-AZ point of failure. Draining the node holding Postgres left it Pending on a volume node affinity conflict and took console, ingest and rotor into CrashLoopBackOff behind it. Also documents that helm uninstall is not a complete teardown — the operator and syncctl create workloads at runtime, so Helm does not own or remove them — and, on the other side, that reinstalling is safe because jitsu-secrets survives and the token-generator reuses it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| # the services need (the full list is in the Job's `kubectl create secret` | ||
| # call). Leaving this true is safe alongside an externally managed Secret: the | ||
| # Job only fills in keys that are absent and never rewrites one you set. | ||
| enabled: true |
There was a problem hiding this comment.
[P2] With the documented external-secret path (tokenGenerator.enabled=false), secrets.yaml renders nothing, so every Deployment receives the same constant checksum/credentials value. Updating jitsu-secrets in the external manager therefore changes no pod template; envFrom is read only at pod start, so the first unrelated restart gets the new bearer token while the other services retain the old one and inter-service authentication fails. Could this path expose a rollout/version annotation that the secret controller/GitOps flow can bump (or document and wire in a reloader) so credential rotation is coordinated?
Implements
JITSU-48— one chart serving both self-hosted (prod / images) and dev (Minikube / source-build), selected bymode.Stacked on #1517 (the chart rename), which is the base branch. Review that first; this PR retargets
newjitsuautomatically once it merges.Requirements
modevalue,dev|prodfailjitsucom/*images, no init-container compilationimage.tagHow it works
values.yamlgainsmode, chart-wideimage.{registry,tag,pullPolicy}, and empty per-serviceimages.<service>blocks.jitsu.imageholds the whole precedence rule in one place:images.<service>.repository— an explicit pin, honoured in both modes, so one service can run from an image while the rest build from source{{ image.registry }}/<name>:<tag>Two mappings are passed explicitly rather than derived from the service name, because they are not one-to-one:
profilesruns therotorimage withROTOR_MODE=profiles, and the Go services rundebian:bookworm-slimin dev because thegolangimage is their init container.Image names were checked against
.github/workflows/services.yamlrather than assumed — it publishesconsole rotor functions-serverandbulker ingest sidecar syncctl operator ingmgr cfgkpr admin reprocessing-worker, so every service this chart deploys has a published image.In prod the init containers,
commandoverrides, hostPath/cache volumes and both hook Jobs are gated off.install-jobexists only to populate the node-cache PVC from the host checkout.db-push-jobis unnecessary because the console image migrates itself — its entrypoint runsprisma db push --skip-generateunlessUPDATE_DBis disabled, which is exactly how the referencedocker-composedeployment migrates (image, no command, no override). Keeping the job would mean two mechanisms racing on the same schema. Confirmed in the live run below.Exposure
Prod forces every Service to
ClusterIP— the four dev LoadBalancers would otherwise become four billed cloud load balancers with no TLS and no hostname.service.<name>.typeoverrides.The Ingress is disabled by default and scoped to console + ingest, per the ticket and matching production (bulker is ClusterIP there; rotor is in no gateway config). Configurable class, annotations and TLS. Enabling it with no host fails the render rather than emitting an Ingress with no rules.
NEXTAUTH_URLandJITSU_PUBLIC_URLnow derive from the Ingress host, scheme followingingress.tls.enabled. Without this a prod install behind an Ingress still advertisedhttp://localhost:3000— NextAuth would redirect users to their own machine after sign-in, and the tracking snippet would point browsers there.Two things this refuses to do
Prod with the dev credentials.
values.yamlshipsJWT_SECRET: dev-jwt-secret-change-in-productionandSEED_USER_PASSWORD: changemeso the Minikube quick start works. Nothing in Kubernetes flags them and the quick start gives a self-hoster no reason to look, so a prod render now fails and names the value to set.An Ingress with no hosts. Renders nothing useful, so it fails instead.
Three bugs found by running it, not reading it
The chart could not be installed at all. Every service mounts
jitsu-secretsviaenvFromwithoutoptional: true, and that Secret was created bydev-deploy.sh— a dev bash script, not the chart. A fresh prod install failed withError: secret "jitsu-secrets" not found→CreateContainerConfigErroron all seven pods. This is precisely the ticket's complaint, "unusable for a real self-hosted install". The chart now creates it. Corrected after review: the first version rendered the Secret from alookupof the existing one, which was wrong —lookupis empty duringhelm template,--dry-runand Argo CD repo-server rendering, so every reconciliation would have minted a new token and left pods disagreeing about the auth key. Replaced with a pre-install Job that creates the Secret in-cluster only when absent (see below).helm-depsexposed the data layer publicly. All four datastores weretype: LoadBalancerunconditionally, with no values switch. Harmless on Minikube (it is whatminikube tunnelpublishes); on any cloud provider it gives Postgres 5432, Kafka 9092, ClickHouse 8123/9000 and MongoDB 27017 public IPs, with the dev credentials the chart ships. Now defaults toClusterIP, withdev-deploy.shpassingLoadBalancerexplicitly.helm-deps/is outside the directory JITSU-48 names, so flagging the scope call — it blocks the ticket's stated goal either way.dev-deploy.sh deployhas been broken since 14 Sep — helm-deps timing out withcontext deadline exceededafter 10 minutes while every dependency pod is1/1 Running(release revisions 3-6 all failed). Cause: helm's readiness check for atype: LoadBalancerService requiresstatus.loadBalancer.ingress, which Minikube only assigns whileminikube tunnelis running — anddeploystarts the project mount, not the tunnel. Confirmed: all four Services sit atEXTERNAL-IP <pending>with healthy pods and no tunnel process.Not a regression — the LoadBalancer Services date from
ee0d3a8b4(7 Jul) and nothing in helm-deps changed since. The deploy only ever succeeded when someone happened to have a tunnel open. Fixed by waiting on the Deployments directly instead of on external IPs;./dev-deploy.sh deploynow completes end to end (helm-deps revision 8 deployed, all 11 components ready).This one is only in this PR because it is the same three lines of
dev-deploy.shthe ClusterIP change touches. Easy to split out if you would rather it landed on its own.The first two were invisible to
helm template, which never resolves references to cluster objects. The third was invisible to it too —--waitis a client-side behaviour that rendering never exercises.Verification
Dev is unchanged, checked at every step. Rendered output is identical to the pre-rename chart except one reworded comment inside a shell script.
helm lintandkubectl apply --dry-run=serverpass in both modes.Prod mode runs. Two commands into an empty namespace —
helm install jitsu-deps ./helm-depsandhelm install ... --set mode=prod— reach 11/11 pods Ready on published images against real Postgres / Kafka / ClickHouse / MongoDB. No bash script, no source mount, no hostPath. Console migrated its own schema and initialised the ClickHouseevents_logtables, then passed its healthcheck.Prod render contains zero occurrences of
hostPath,initContainers,/build/,node-cache,/cache/workspace, the golang/node/debian base images, andLoadBalancer.Review round two — five findings, all fixed
jitsu-code-reviewraised five issues, and two of them meant prod mode did not actually work. Patterns for the fixes come from the community chart at stafftastic/jitsu-chart, which Ildar pointed at on the ticket.Tokens regenerated on every reconciliation. Replaced the
lookupwith a pre-install Job that createsjitsu-secretsin-cluster only when absent, so rendering stays pure and upgrades never rotate. Its Role cancreatesecrets andgetonlyjitsu-secrets.auth.tokenstill short-circuits the whole thing for external secret management.A fresh prod install had no user — nobody could log in. The published console image only seeds when
SEED_DEMO_CONFIGURATIONis set, which also creates demo connections, soSEED_USER_EMAIL/SEED_USER_PASSWORDalone did nothing. Added a post-install Job callingmanage.js seeddirectly; idempotent via the existingprofileCount === 0guard.Credentials were readable from the pod spec.
JWT_SECRETandSEED_USER_PASSWORDwere passed with--set env.console.*, landing in the Deployment, inhelm get values, and in shell history. The Job now mints both into the same Secret. A prod install needs no credential flags at all.The operator managed the wrong namespace.
KUBERNETES_NAMESPACEdefaults to"default"(operator/config.go:27) and was never set, so a release installed elsewhere created functions-server Deployments indefault. Now from the downward API, as syncctl already did. Not prod-only —dev-deploy.sh:12takesNAMESPACEtoo, so this is a live bug in dev today, and it is the only dev-visible change in this round.The console did not know the ingest endpoint.
ingress.hosts.ingestmade the host reachable but nothing told the console, so/api/app-configreturned no ingest URL and the tracking UI advertised the wrong endpoint. Now derived from the same Ingress host.Still verified
Dev renders identically apart from the operator namespace fix above.
helm lintpasses in both modes. Prod contains zero occurrences ofchangeme,dev-jwt-secret,hostPath,initContainersor/cache/workspace.Also gone
jitsu.checkProdSecrets, which refused to render prod while the dev credentials were present. It existed only because values.yaml carried credentials that prod inherited; prod never sees them now, so it had nothing left to guard.Acceptance — done, 3 of 3, on a real cluster
Run on the GKE Autopilot cluster from jitsu-cloud-infra#104, in prod mode, fresh namespace. Cold start ~2 minutes to all-running including a node scale-up — that is where the README's
5mnumber now comes from.token-generatorhook Complete in 24s, all 11 keys written; seed Job Completed; 9/9 Runningdefault.eventscarry anac_markercolumn the function added — a field absent from what was POSTed, so ingest → Kafka → rotor → UDF → bulker → ClickHouse all ran. The schema-free path auto-created the column.syncctlcreated real podsmongodb--discover-4466andmongodb-…-read-e7a5, both SUCCESS. 5 rows indefault.customers, matching 5 documents seeded into the in-cluster MongoDB, types preserved. A scheduled sync also fired unprompted, so the cronjob controller works too.The ticket's "prod mode must still cover sidecar/sync pods and functions-server via images" is proven by the same run:
free-0-1-fsand both sync pods ran from published images.Ingress — actually deployed this time
The first install ran
--set mode=prodonly, so the Ingress path had never been exercised — rendering it is not deploying it. Closed by installing ingress-nginx and usingnip.iohostnames, so no DNS setup.NEXTAUTH_URL/JITSU_PUBLIC_URL/JITSU_INGEST_PUBLIC_URL, and they flip tohttps://on their own when TLS is onOnly the documented-correct TLS path (option a, existing TLS Secret) was exercised.
What running it as a client would produced
Six defects. None shows on Minikube, and two are invisible on Autopilot too, because it papers over them.
Fixed here:
fsGroup—86ab2f47c. PVC arrives owned by root, image runs as uid 101.--waiton the documented install —bbc5883c5. A barehelm installexited 0 while five services were in CrashLoopBackOff.JWT_SECRETno longer shares the inter-service token —9bf0705d7, P1 from review. In theauth.tokenpath the console's session-signing secret was the same value handed to services as a bearer credential, so anyone holding one could forge an admin session.auth.jwtSecretis now separatelyrequired. Only ever affected the supplied-token path; the generated path already minted an independent secret, verified against the live install.creategrant stated accurately —99955486f, P2 from review. Kubernetes cannot applyresourceNamestocreate, so that verb is namespace-wide whatever the Role says; pre-creating an empty Secret to avoid it would blank live keys on upgrade. Documented in the Role and README rather than silently accepted, and it corrects an overstatement in my own README table from33ce48914.33ce48914. Prerequisites listed Minikube and Helm; the chart creates 8 RBAC objects, two cluster-scoped, and GKE'sroles/editorexcludes RBAC entirely. Now also documents the default-StorageClass requirement, and what each Role grants and why — including thatpods/execon syncctl is effectively shell access to pods in scope, and that both ClusterRoles span every namespace.Found, not fixed — the scope call for you:
resourcesanywhere, and no way to set them. Predicted in the previous version of this description; now confirmed and worse than expected —values.yamlhas noresourceskey at all, so a client cannot set them without forking the chart. Every pod is BestEffort QoS, and a namespace with aLimitRangerequiring requests rejects them outright. The same gap is in the sync pods syncctl generates. Autopilot hid it: theautopilot-default-resources-mutatorlines in the install log are it patching this in real time.helm-depshas nostorageClassNameoverride. Now documented as a requirement, but a client on a cluster whose default StorageClass is wrong for them still cannot choose another without forking.Cannot load cached repository. No CACHE_DIR is set.), four restarts each. Correction — this is not prod-specific. The commit message onbbc5883c5says dev masks the race via its source-build init container. That is only true of a cold first install. Measured on Minikube afterwards: everydev-deploy.sh deployreplaces the console pod, and each dependent service then dies 4 times before it comes back —48 restarts / 12 helm revisions = 4.0 per deploy, the same number measured in prod. The node had not rebooted, so upgrades are the whole mechanism. Dev developers have been hitting this on every deploy.CACHE_DIRis never set anywhere. The chart already has the fix pattern inseed-job.yaml(until wget … /api/healthcheck); adding it as an init container to those services would remove the crash-loop. Not done here because it touches five templates.Plus one outside this chart: the ClickHouse destination form takes a hostname but silently accepts a URL.
http://clickhouseparsed to hosthttp; the only symptom wasdial tcp: lookup http … no such hostin bulker logs, nothing in the console.Items 6–8 are the ones that matter most for self-hosting, since a client deploys onto a cluster we have never seen. Happy for any of them to become their own ticket.
Still open on the ticket
Two dev-mode checks this cluster cannot do, both needing Minikube: dev behaviour unchanged, and the behavioural check of the per-service image pin (render-level only so far).
Also outstanding, tracked separately: DX: verify the self-hosting quick-start end-to-end and fix drift.
🤖 Generated with Claude Code