Skip to content

JITSU-48: add prod mode (published images) to the Helm chart - #1518

Open
sahiltyagi-jitsu wants to merge 35 commits into
chore/jitsu-48-rename-helm-chartfrom
feat/jitsu-48-prod-mode-scaffolding
Open

sahiltyagi-jitsu wants to merge 35 commits into
chore/jitsu-48-rename-helm-chartfrom
feat/jitsu-48-prod-mode-scaffolding

Conversation

@sahiltyagi-jitsu

@sahiltyagi-jitsu sahiltyagi-jitsu commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Implements JITSU-48 — one chart serving both self-hosted (prod / images) and dev (Minikube / source-build), selected by mode.

Stacked on #1517 (the chart rename), which is the base branch. Review that first; this PR retargets newjitsu automatically once it merges.

Requirements

Requirement Status
mode value, dev | prod ✅ any other value calls fail
prod — published jitsucom/* images, no init-container compilation verified running, 11/11 pods
dev — build from mounted source in init containers byte-identical render
per-service image override wins regardless of mode ✅ verified separately in both
Ingress — external hostnames, TLS, console + ingest routing ✅ new template (there was no Ingress before)
prod covers sidecar + functions-server via images ✅ both follow image.tag

How it works

values.yaml gains mode, chart-wide image.{registry,tag,pullPolicy}, and empty per-service images.<service> blocks.

jitsu.image holds the whole precedence rule in one place:

  1. images.<service>.repository — an explicit pin, honoured in both modes, so one service can run from an image while the rest build from source
  2. prod — {{ image.registry }}/<name>:<tag>
  3. dev — the base image the service builds against

Two mappings are passed explicitly rather than derived from the service name, because they are not one-to-one: profiles runs the rotor image with ROTOR_MODE=profiles, and the Go services run debian:bookworm-slim in dev because the golang image is their init container.

Image names were checked against .github/workflows/services.yaml rather than assumed — it 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.

In prod the init containers, command overrides, hostPath/cache volumes and both hook Jobs are gated off. install-job exists only to populate the node-cache PVC from the host checkout. db-push-job is unnecessary because the console image migrates itself — its entrypoint runs prisma db push --skip-generate unless UPDATE_DB is disabled, which is exactly how the reference docker-compose deployment 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>.type overrides.

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_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 — 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.yaml ships JWT_SECRET: dev-jwt-secret-change-in-production and SEED_USER_PASSWORD: changeme so 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-secrets via envFrom without optional: true, and that Secret was created by dev-deploy.sh — a dev bash script, not the chart. A fresh prod install failed with Error: secret "jitsu-secrets" not foundCreateContainerConfigError on 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 a lookup of the existing one, which was wrong — lookup is empty during helm template, --dry-run and 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-deps exposed the data layer publicly. All four datastores were type: LoadBalancer unconditionally, with no values switch. Harmless on Minikube (it is what minikube tunnel publishes); 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 to ClusterIP, with dev-deploy.sh passing LoadBalancer explicitly. 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 deploy has been broken since 14 Sep — helm-deps timing out with context deadline exceeded after 10 minutes while every dependency pod is 1/1 Running (release revisions 3-6 all failed). Cause: helm's readiness check for a type: LoadBalancer Service requires status.loadBalancer.ingress, which Minikube only assigns while minikube tunnel is running — and deploy starts the project mount, not the tunnel. Confirmed: all four Services sit at EXTERNAL-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 deploy now 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.sh the 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 — --wait is 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 lint and kubectl apply --dry-run=server pass in both modes.

Prod mode runs. Two commands into an empty namespace — helm install jitsu-deps ./helm-deps and helm 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 ClickHouse events_log tables, then passed its healthcheck.

Prod render contains zero occurrences of hostPath, initContainers, /build/, node-cache, /cache/workspace, the golang/node/debian base images, and LoadBalancer.

Review round two — five findings, all fixed

jitsu-code-review raised 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 lookup with a pre-install Job that creates jitsu-secrets in-cluster only when absent, so rendering stays pure and upgrades never rotate. Its Role can create secrets and get only jitsu-secrets. auth.token still 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_CONFIGURATION is set, which also creates demo connections, so SEED_USER_EMAIL/SEED_USER_PASSWORD alone did nothing. Added a post-install Job calling manage.js seed directly; idempotent via the existing profileCount === 0 guard.

Credentials were readable from the pod spec. JWT_SECRET and SEED_USER_PASSWORD were passed with --set env.console.*, landing in the Deployment, in helm 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_NAMESPACE defaults to "default" (operator/config.go:27) and was never set, so a release installed elsewhere created functions-server Deployments in default. Now from the downward API, as syncctl already did. Not prod-only — dev-deploy.sh:12 takes NAMESPACE too, 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.ingest made the host reachable but nothing told the console, so /api/app-config returned 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 lint passes in both modes. Prod contains zero occurrences of changeme, dev-jwt-secret, hostPath, initContainers or /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 5m number now comes from.

Fresh install, prod mode token-generator hook Complete in 24s, all 11 keys written; seed Job Completed; 9/9 Running
Site → Destination → Function → Connection, event lands rows in default.events carry an ac_marker column 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.
Service → Sync syncctl created real pods mongodb--discover-4466 and mongodb-…-read-e7a5, both SUCCESS. 5 rows in default.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-fs and both sync pods ran from published images.

Ingress — actually deployed this time

The first install ran --set mode=prod only, so the Ingress path had never been exercised — rendering it is not deploying it. Closed by installing ingress-nginx and using nip.io hostnames, so no DNS setup.

Routes both hosts console and ingest on separate hostnames
Console URLs derived from ingress NEXTAUTH_URL / JITSU_PUBLIC_URL / JITSU_INGEST_PUBLIC_URL, and they flip to https:// on their own when TLS is on
HTTP from the public internet console + ingest 200
HTTPS from the public internet both 200; HTTP 308-redirects to HTTPS
Certificate served ours, not nginx's fake fallback — which is the failure mode the README warns about
Events over HTTP and HTTPS ingress both landed in ClickHouse

Only 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:

  1. redpanda fsGroup86ab2f47c. PVC arrives owned by root, image runs as uid 101.
  2. --wait on the documented installbbc5883c5. A bare helm install exited 0 while five services were in CrashLoopBackOff.
  3. JWT_SECRET no longer shares the inter-service token9bf0705d7, P1 from review. In the auth.token path 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.jwtSecret is now separately required. Only ever affected the supplied-token path; the generated path already minted an independent secret, verified against the live install.
  4. The token-generator's create grant stated accurately99955486f, P2 from review. Kubernetes cannot apply resourceNames to create, 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 from 33ce48914.
  5. Cluster rights and RBAC documented33ce48914. Prerequisites listed Minikube and Helm; the chart creates 8 RBAC objects, two cluster-scoped, and GKE's roles/editor excludes RBAC entirely. Now also documents the default-StorageClass requirement, and what each Role grants and why — including that pods/exec on syncctl is effectively shell access to pods in scope, and that both ClusterRoles span every namespace.

Found, not fixed — the scope call for you:

  1. No resources anywhere, and no way to set them. Predicted in the previous version of this description; now confirmed and worse than expected — values.yaml has no resources key at all, so a client cannot set them without forking the chart. Every pod is BestEffort QoS, and a namespace with a LimitRange requiring requests rejects them outright. The same gap is in the sync pods syncctl generates. Autopilot hid it: the autopilot-default-resources-mutator lines in the install log are it patching this in real time.
  2. helm-deps has no storageClassName override. Now documented as a requirement, but a client on a cluster whose default StorageClass is wrong for them still cannot choose another without forking.
  3. Startup race. Services that read config from the console exit 1 rather than retrying (Cannot load cached repository. No CACHE_DIR is set.), four restarts each. Correction — this is not prod-specific. The commit message on bbc5883c5 says dev masks the race via its source-build init container. That is only true of a cold first install. Measured on Minikube afterwards: every dev-deploy.sh deploy replaces 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_DIR is never set anywhere. The chart already has the fix pattern in seed-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://clickhouse parsed to host http; the only symptom was dial tcp: lookup http … no such host in 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

sahiltyagi-jitsu and others added 7 commits September 15, 2026 07:33
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>

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread helm/templates/console.yaml Outdated
Comment thread helm/templates/ingress.yaml
Comment thread helm/templates/operator.yaml Outdated
`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>

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the production Helm path, including rendered dev/prod manifests and the image entrypoints.

Findings:

  • Production credentials supplied through env.console are rendered into the console Deployment and the Helm release record.
  • Client-side/GitOps renders regenerate the default inter-service token because lookup has no live Secret to reuse.

Comment thread helm/README.md
Comment thread helm/templates/secrets.yaml Outdated
sahiltyagi-jitsu and others added 3 commits September 16, 2026 12:45
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>

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread helm/README.md Outdated
Comment thread helm/templates/secrets.yaml Outdated
Comment thread helm/templates/seed-job.yaml
Comment thread helm/templates/_helpers.tpl Outdated
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>

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the Helm production/dev rendering paths, bootstrap hooks, and service configuration.

Finding:

  • The default prod token bootstrap hook is scheduled before the ServiceAccount and RBAC objects it requires.

Comment thread helm/templates/token-generator.yaml
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>

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread helm/README.md Outdated
Comment thread helm/templates/secrets.yaml
…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>

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the Helm production-mode manifests, credential/bootstrap hooks, ingress wiring, and rendered prod/dev charts.\n\nFinding:\n- Rotor is left without its inbound authentication variable in both generated and explicit-token production paths.

Comment thread helm/templates/token-generator.yaml
sahiltyagi-jitsu and others added 2 commits September 16, 2026 19:41
…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>

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the production Helm rendering, credential/bootstrap paths, and ingress configuration.

Findings:

  • The documented auth.token override 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.

Comment thread helm/templates/secrets.yaml
Comment thread helm/templates/token-generator.yaml Outdated

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the production-mode Helm rendering, bootstrap hooks, and published console image layout.\n\nFinding:\n- Upgrading an existing dev release to prod can leave the published console without its required credentials.

Comment thread helm/templates/token-generator.yaml

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the production-mode Helm rendering, bootstrap hooks, and published console image layout.

Finding:

  • Upgrading an existing dev release to prod can leave the published console without its required credentials.

Comment thread helm/templates/token-generator.yaml
…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>

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the production Helm-mode changes, including rendered dev/prod manifests, image/entrypoint selection, ingress URL wiring, secret bootstrapping, and hook ordering. No additional actionable findings beyond the existing review threads.

… 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>

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the Helm dev/prod rendering, release hooks, service wiring, and ingress configuration.

Finding:

  • The seed hook makes configurations with the console disabled fail after its five-minute wait.

Comment thread helm/templates/seed-job.yaml Outdated
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>

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the production/dev Helm renders, image entrypoints, bootstrap hooks, ingress validation, and the resolved review history.

Finding:

  • The documented production dependency install needs to wait for readiness before installing the main chart.

Comment thread helm/README.md Outdated
sahiltyagi-jitsu and others added 2 commits September 17, 2026 14:46
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>

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the production-mode templates, credential bootstrap hooks, ingress rendering, and dev/pinned-image render paths. One values-documentation issue is noted inline.

Comment thread helm/values.yaml Outdated
…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>

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

sahiltyagi-jitsu and others added 2 commits September 18, 2026 16:40
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>

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread helm/values.yaml
#
# `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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.yaml exposes no resources key at all, so a client cannot set requests or limits without forking the chart.
  • The prod render has no resources on any of the nine containers.
  • Every pod therefore runs BestEffort, and a namespace with a LimitRange that requires requests rejects them outright, exactly as described.
  • The same gap is in the sync pods syncctl generates, not just the chart. During the acceptance sync, Autopilot logged adjusted "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.

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread helm/templates/secrets.yaml Outdated
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 }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. lookup is empty during helm template, --dry-run and 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>

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the production Helm rendering, credential bootstrap/upgrade flow, ingress configuration, and dev compatibility. One RBAC-scope finding is included inline.

resourceNames: ["jitsu-secrets"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["create"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-generator in ns=jitsu, succeeded=1
  • secret/jitsu-secrets created in ns=jitsu with all 11 keys
  • nothing created in defaultsecrets "jitsu-secrets" not found there

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") }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the Helm prod/dev rendering, credential bootstrap paths, image-pinned dev behavior, and dependency exposure.

Finding: externally managed credential rotation does not trigger a coordinated workload rollout.

Comment thread helm/values.yaml
# 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant