Pilot's deploy repository: its Helm chart and per-stage configuration. The app is
pvginkel/PilotApp.
Two stages, each an Argo CD Application on the prd cluster that renders chart/ with
config/<stage>/: pilot-dev at https://pilot-dev.home, internal, with its own Keycloak client,
database (pilot_dev_db) and secrets, on kubecoder-dev's fleet, follows main; and pilot-prd,
production at https://pilot.ginbov.nl, public, on the operator's KubeCoder, follows the prd
branch. Every green PilotApp build pins :dev-<n> into config/dev/ and :prd-<n> into
config/prd/ in one commit on main, so dev rolls to it and production waits for a promotion: the
job PilotApp/Promote-PRD (Jenkinsfile.promote), started by hand, retags pilot, pilot-ui and
pilot-sync from dev-<n> to prd-<n>, fast-forwards prd to a commit on main, its tip unless
the run names another, moves pilot-sync:prd-latest, which production's store environment runs, to
that build's sidecar, and tags that commit release-<m>. A rollback is a revert on main,
promoted. prd's Terraform state owns the repository's webhook (manage_webhook), dev's none.
The pod runs the backend, Pilot's MCP server and the UI's nginx. The MCP server is the backend
image's second entry point (python -m pilot_mcp), the container mcp, calling the backend over
the pod's loopback, behind a Service of its own, pilot-mcp, at the stage's mcp.host in
config/<stage>/values.yaml: pilot-dev-mcp.home or pilot-mcp.home, intranet only on both
stages, since the session's bearer is its whole authentication. The backend hands a work item's
session https://<mcp.host>/mcp (MCP_SERVER_URL), and a brain session the brain's tools under
it, …/mcp/store.
Each stage's store is a KubeCoder environment on its fleet whose pilot-sync sidecar the
backend admits: store.environmentId in config/<stage>/values.yaml sets STORE_ENVIRONMENT_ID,
and the sidecar's bearer, SYNC_TOKEN, comes from the leaf eso/prd/pilot/<stage>/sync, property
token. The bearer's Secret is optional: a backend started while it is absent refuses every sync
call until its next start. The brain's sessions there run on brain in the same values: model,
reasoningEffort and chatCloseTimeout set the backend's BRAIN_MODEL, BRAIN_REASONING_EFFORT
and CHAT_CLOSE_TIMEOUT, each required, so that a stage without them fails its render, and
tests/render-chart.py pins them on both stages' backend and their absence from migrate.
The gate's slot, how many work items run at once, each in an environment of its own, is set in
work in the same values: parallelismMax across every project and projectParallelismMax in one
set the backend's WORK_PARALLELISM_MAX and PROJECT_PARALLELISM_MAX. Each is optional: one left
out is not set at all, and the backend's default stands, 3 across every project and in one project
the cross-project maximum. Production runs 5 and 3; the dev stage sets neither.
tests/render-chart.py pins each stage's backend on what its values set and migrate on neither.
The stage's VAPID key, which signs every web push the backend sends and whose public key the
browsers subscribe with, comes from the leaf eso/prd/pilot/<stage>/vapid, property
private_key, a P-256 key in PKCS#8 PEM: the ExternalSecret pilot-vapid fills the Secret
pilot-vapid, key private_key, with the leaf's PEM as it stands, through no template, and the
backend reads it as VAPID_PRIVATE_KEY. It is kept in OpenBao and never generated in the cluster,
since a new key ends every browser's subscription. The read is required, since a production backend
refuses to start without the key, and no other container, migrate among them, gets it;
tests/render-chart.py pins it on both stages: the backend alone holding it, off its stage's leaf,
and its Secret untemplated.
YouTrack's webhook token is production's alone, since the dev stage gets no tracker events: an
ExternalSecret fills the Secret pilot-youtrack-webhook-token, key token, from the leaf
eso/prd/pilot/prd/youtrack-webhook-token, property token, a copy of the instance's shared
webhook token that SecretRotator keeps. The backend reads it as YOUTRACK_WEBHOOK_TOKEN and compares
it with each delivery's X-YouTrack-Token on its tracker-events endpoint. The chart sets the variable
only where a stage's values carry the Secret, so the dev stage's backend refuses every delivery. The
Secret is optional: a backend started while it is absent refuses every delivery until its next start.
tests/render-chart.py pins it on production's backend and its absence from migrate and from the
dev stage.
The read-only role is production's alone: readonly_role in config/prd/terraform.tfvars has
terraform/main.tf make the login role pilot_prd_readonly on pilot_prd_db
(terraform/modules/postgres-readonly-role), with CONNECT on the database, USAGE on its schema
public, SELECT on every table and sequence in it, and default privileges that give it SELECT on
those pilot_prd_db, the role the migrations run as, creates later. It writes and creates nothing.
Its credentials go into the Secret pilot-db-readonly (keys database, username, password),
which the chart does not read: the operator's ad-hoc tools reach the database with it, through a
copy of its password in KubeCoder's prd catalog leaf eso/prd/kubecoder/prd/catalog, key
pilot-prd-db-readonly-password, which PilotApp's environment projects as
PILOTAPP_PRD_DB_PASSWORD. The hook holds no OpenBao credential, so the copy is made from the
Secret outside Terraform, from an environment that can write OpenBao, and no rotation renews it.
tests/terraform-test.py pins the role, its grants and its Secret on prd, and their absence from
dev.
Rotation keepers are the rotation_epoch map in each stage's config/<stage>/rotation.tfvars,
which terraform/main.tf passes to the credentials it mints: db re-mints the Postgres role's
password, and readonly, on prd, the read-only role's, after which the catalog's copy is written
again. SecretRotator's terraform kind writes it, one commit rotate <keeper> (secret-rotator)
per rotation, and a changed value re-mints its credential at the next sync. A keeper once set stays
in the file: removing it re-mints too.