Plane Enterprise {{ .Chart.Version }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }} App version: {{ .Values.planeVersion }} URL: {{ if or .Values.ssl.tls_secret_name (and .Values.ssl.createIssuer .Values.ssl.generateCerts) }}https{{ else }}http{{ end }}://{{ .Values.license.licenseDomain }} Admin setup: {{ if or .Values.ssl.tls_secret_name (and .Values.ssl.createIssuer .Values.ssl.generateCerts) }}https{{ else }}http{{ end }}://{{ .Values.license.licenseDomain }}/god-mode/ {{- $warnings := list }} {{- /* Credentials still living in values.yaml. */}} {{- if and (empty .Values.external_secrets.app_keys_existingSecret) (not .Values.env.requireExplicitSecrets) }} {{- $publicDefaults := list }} {{- if or (empty .Values.env.secret_key) (eq .Values.env.secret_key "60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5") }}{{ $publicDefaults = append $publicDefaults "SECRET_KEY" }}{{ end }} {{- if or (empty .Values.env.live_server_secret_key) (eq .Values.env.live_server_secret_key "htbqvBJAgpm9bzvf3r4urJer0ENReatceh") }}{{ $publicDefaults = append $publicDefaults "LIVE_SERVER_SECRET_KEY" }}{{ end }} {{- if or (empty .Values.env.silo_envs.aes_secret_key) (eq .Values.env.silo_envs.aes_secret_key "dsOdt7YrvxsTIFJ37pOaEVvLxN8KGBCr") }}{{ $publicDefaults = append $publicDefaults "AES_SECRET_KEY" }}{{ end }} {{- if or (empty .Values.env.pi_envs.internal_secret) (eq .Values.env.pi_envs.internal_secret "tyfvfqvBJAgpm9bzvf3r4urJer0Ehfdubk") }}{{ $publicDefaults = append $publicDefaults "PI_INTERNAL_SECRET" }}{{ end }} {{- if and .Values.services.silo.enabled (or (empty .Values.env.silo_envs.hmac_secret_key) (eq .Values.env.silo_envs.hmac_secret_key "gzb7MRLr0FoN129NyWARZEs84P9LzQ")) }}{{ $publicDefaults = append $publicDefaults "SILO_HMAC_SECRET_KEY" }}{{ end }} {{- if and .Values.services.silo.enabled (or (empty .Values.env.silo_envs.cursor_webhook_secret) (eq .Values.env.silo_envs.cursor_webhook_secret "TTqazTcoBajYKzIAeIKFZeTX9czAoUsG")) }}{{ $publicDefaults = append $publicDefaults "CURSOR_WEBHOOK_SECRET" }}{{ end }} {{- if $publicDefaults }} {{- $warnings = append $warnings (printf "Using the chart's PUBLIC example values for: %s.\n These are published in the chart and are not secret. Supply your own through\n external_secrets.app_keys_existingSecret, and set env.requireExplicitSecrets=true\n to make a missing value fail the render instead of falling back." (join ", " $publicDefaults)) }} {{- end }} {{- end }} {{- /* The marketplace OAuth apps' redirect URIs are built from license.licenseDomain, not from env.web_url. When the two disagree the callback the app advertises is not the host the browser is on, and every integration's OAuth round-trip fails the redirect check -- with nothing in the logs naming the cause. */}} {{- if and .Values.env.web_url (ne .Values.env.web_url (printf "http://%s" .Values.license.licenseDomain)) (ne .Values.env.web_url (printf "https://%s" .Values.license.licenseDomain)) }} {{- $warnings = append $warnings (printf "env.web_url (%s) does not match license.licenseDomain (%s).\n Integration redirect URIs are generated from licenseDomain, so they will point at\n a host your users are not on and OAuth will fail the redirect check. Set both to\n the host users actually reach." .Values.env.web_url .Values.license.licenseDomain) }} {{- end }} {{- /* Externalized credentials with no way to pick up a rotation. */}} {{- if or .Values.external_secrets.database.secretName .Values.external_secrets.rabbitmq.secretName .Values.external_secrets.redis.secretName .Values.external_secrets.app_keys_existingSecret }} {{- $warnings = append $warnings "Credentials come from an external Secret. Whatever refreshes that Secret does not\n restart anything, so pods keep the values they started with and a rotated\n credential does not take effect until something rolls them. Install Stakater\n Reloader and annotate the workloads that read it:\n services..annotations:\n reloader.stakater.com/auto: \"true\"" }} {{- end }} {{- /* live's export queue has no parts path at all, on any app version. live reads only AMQP_URL — as a full URL, or as a bare host:port paired with AMAZONMQ_SECRET_ARN (apps/live/src/env.ts, lib/amqp-secrets.ts). It has Redis parts; it has no broker parts. So when the RabbitMQ mirror is set, this chart correctly stops rendering a DSN into live-secrets and live is then left with no broker. Not fatal — live boots and only exports fail, lazily — but it fails quietly, which is worse. */}} {{- if and .Values.external_secrets.rabbitmq.secretName (empty .Values.external_secrets.live_env_existingSecret) }} {{- $warnings = append $warnings "The RabbitMQ mirror reaches live only from the app release that added discrete\n broker parts to it. Before that, live read only AMQP_URL, so with\n external_secrets.rabbitmq.secretName set its export queue is unconfigured — live still\n serves collaborative editing and only exports fail, which is easy to miss. On an older\n planeVersion, supply external_secrets.live_env_existingSecret with a templated AMQP_URL,\n or keep the broker credential in services.rabbitmq.external_rabbitmq_url." }} {{- end }} {{- /* Version gate. A deployment pinned below the app release that carries a given piece of discrete-parts support gets the warnings that follow. Both app changes — the API's REDIS_* support and silo/live/pi's — are expected in v3.2.0, hence the identical bounds. They are kept as separate variables because they come from separate app PRs: if one lands in a later release than the other, bump only that bound. planeVersion is operator-supplied and need not be semver ("stable", a git sha), so an unparsable value warns rather than failing the render — semverCompare would error on it. */}} {{- $version := trimPrefix "v" (.Values.planeVersion | toString) }} {{/* Anchored on purpose. A prefix-only match accepts "3.2.0.1" and "3.2.0foo", which then reach semverCompare and abort the render with a parse error instead of falling through to the warning below. Four-part vendor versions are the realistic case. */}} {{- $parsable := regexMatch "^[0-9]+\\.[0-9]+\\.[0-9]+(-[0-9A-Za-z.-]+)?(\\+[0-9A-Za-z.-]+)?$" $version }} {{- $preRedisParts := or (not $parsable) (semverCompare "< 3.2.0" $version) }} {{- $preServiceParts := or (not $parsable) (semverCompare "< 3.2.0" $version) }} {{- /* Services that cannot consume discrete credential parts on this app version. */}} {{- $externalInfra := or .Values.external_secrets.database.secretName .Values.external_secrets.rabbitmq.secretName .Values.external_secrets.redis.secretName }} {{- if and $externalInfra $preServiceParts }} {{- $urlServices := list }} {{- if and .Values.services.silo.enabled (empty .Values.external_secrets.silo_env_existingSecret) }}{{ $urlServices = append $urlServices "silo" }}{{ end }} {{- if and .Values.services.pi.enabled (empty .Values.external_secrets.pi_api_env_existingSecret) }}{{ $urlServices = append $urlServices "pi" }}{{ end }} {{- if and .Values.external_secrets.redis.secretName (empty .Values.external_secrets.live_env_existingSecret) }}{{ $urlServices = append $urlServices "live" }}{{ end }} {{- if $urlServices }} {{- $warnings = append $warnings (printf "These services read discrete credential parts only from planeVersion v3.2.0\n onward, and this release runs %s: %s.\n On an older image they will start without working connection details. Either\n upgrade, or supply their DSN through the matching *_existingSecret with an ESO\n template block — see examples/external-secrets/." .Values.planeVersion (join ", " $urlServices)) }} {{- end }} {{- end }} {{- /* A composed URL would shadow the discrete parts. */}} {{- if and .Values.external_secrets.app_env_existingSecret $externalInfra }} {{- $warnings = append $warnings (printf "Both external_secrets.app_env_existingSecret (%s) and the discrete credential\n parts are set. Make sure that Secret does NOT contain DATABASE_URL, AMQP_URL or\n REDIS_URL: the application prefers a URL when one is present, so a stale URL there\n would silently shadow the rotated credential." .Values.external_secrets.app_env_existingSecret) }} {{- end }} {{- /* Redis parts need app support. */}} {{- if and .Values.external_secrets.redis.secretName $preRedisParts }} {{- $warnings = append $warnings (printf "external_secrets.redis requires discrete REDIS_HOST/PORT/PASSWORD/SSL support in\n the application, added in planeVersion v3.2.0. This release runs %s — upgrade, or\n keep supplying REDIS_URL instead." .Values.planeVersion) }} {{- end }} {{- /* Duplicate shared keys across two Secrets. The chart suppresses its own copies when app_keys_existingSecret is set, but it cannot see inside an operator-supplied Secret. app-keys is mounted last, so it wins and the duplicate is ignored; but the duplicate silently becomes the live value again if app_keys_existingSecret is ever unset. */}} {{- if .Values.external_secrets.app_keys_existingSecret }} {{- $legacyGroups := list }} {{- with .Values.external_secrets.app_env_existingSecret }}{{ $legacyGroups = append $legacyGroups (printf "app_env (%s)" .) }}{{ end }} {{- with .Values.external_secrets.silo_env_existingSecret }}{{ $legacyGroups = append $legacyGroups (printf "silo_env (%s)" .) }}{{ end }} {{- with .Values.external_secrets.live_env_existingSecret }}{{ $legacyGroups = append $legacyGroups (printf "live_env (%s)" .) }}{{ end }} {{- with .Values.external_secrets.pi_api_env_existingSecret }}{{ $legacyGroups = append $legacyGroups (printf "pi_api_env (%s)" .) }}{{ end }} {{- with .Values.external_secrets.runner_env_existingSecret }}{{ $legacyGroups = append $legacyGroups (printf "runner_env (%s)" .) }}{{ end }} {{- if $legacyGroups }} {{- $warnings = append $warnings (printf "app_keys_existingSecret is set alongside: %s.\n Those Secrets must NOT contain SECRET_KEY, AES_SECRET_KEY, AES_SALT,\n LIVE_SERVER_SECRET_KEY, PI_INTERNAL_SECRET, SILO_HMAC_SECRET_KEY,\n CURSOR_WEBHOOK_SECRET or RUNNER_HMAC_SECRET_KEY. app_keys wins today, so a duplicate\n is ignored; but unsetting app_keys_existingSecret later would make that stale copy\n live, which surfaces as auth failures or undecryptable data. Remove the duplicates." (join ", " $legacyGroups)) }} {{- end }} {{- end }} {{- /* Runner signing key (plane.runnerHmacResolve). While app_keys_existingSecret is set the chart renders no RUNNER_HMAC_SECRET_KEY of its own and cannot look inside that Secret; one built from the key list of an older README would lack it, and the runner then exits at startup on images after v4.0.1. */}} {{- $runnerHmacNote := "" }} {{- if and .Values.services.runner.enabled (empty .Values.external_secrets.runner_env_existingSecret) }} {{- if .Values.external_secrets.app_keys_existingSecret }} {{- $warnings = append $warnings (printf "The runner is enabled and its signing key comes from app_keys_existingSecret (%s).\n Make sure that Secret carries RUNNER_HMAC_SECRET_KEY (generate one with\n `openssl rand -hex 32`). The chart cannot check it, and runner images after v4.0.1\n exit at startup without it." .Values.external_secrets.app_keys_existingSecret) }} {{- else }} {{- include "plane.runnerHmacResolve" . }} {{- if eq .Values._planeRunnerHmac.source "none" }} {{- $warnings = append $warnings (printf "RUNNER_HMAC_SECRET_KEY is empty: this render could not read namespace %s, so the\n chart could neither find the runner's existing key nor keep a new one. Runner images\n after v4.0.1 exit at startup without it. If --create-namespace just created the\n namespace, run `helm upgrade` once more and the chart generates the key (or create the\n namespace before installing next time). If you render\n with helm template or a GitOps tool, set env.runner_envs.hmac_secret_key\n (`openssl rand -hex 32`)." .Release.Namespace) }} {{- else if eq .Values._planeRunnerHmac.source "generated" }} {{- $runnerHmacNote = printf "No runner signing key (RUNNER_HMAC_SECRET_KEY) was set or found, so this render\ngenerated one. An install or upgrade stores it in Secret %s-runner-secrets\n(a dry run stores nothing), and later upgrades keep it. To use your own, set\nenv.runner_envs.hmac_secret_key." .Release.Name }} {{- end }} {{- end }} {{- end }} {{- /* silo-secrets is mounted on the API workloads (they verify silo's HMAC) after app_env, so a Secret supplied as silo_env decides their DATABASE_URL/REDIS_URL/AMQP_URL too. Warn rather than reorder: reordering would change which value existing installs use. */}} {{- if and .Values.services.silo.enabled .Values.external_secrets.silo_env_existingSecret }} {{- $warnings = append $warnings (printf "external_secrets.silo_env_existingSecret (%s) is mounted on the API workloads\n after their own Secret, so its DATABASE_URL, REDIS_URL and AMQP_URL are the values\n the API uses, and so is its AES_SECRET_KEY unless app_keys_existingSecret carries\n that key (it is mounted last). Keep them identical to the API's, or leave the URLs\n empty when you use external_secrets.database/redis/rabbitmq." .Values.external_secrets.silo_env_existingSecret) }} {{- end }} {{- /* pgdb/rabbitmq_existingSecret only reach the bundled servers; the apps' URLs are still built from values.yaml. */}} {{- if and .Values.services.postgres.local_setup .Values.external_secrets.pgdb_existingSecret (empty .Values.external_secrets.app_env_existingSecret) }} {{- $warnings = append $warnings (printf "external_secrets.pgdb_existingSecret (%s) sets the bundled Postgres credentials\n only. The applications still connect with env.pgdb_username/env.pgdb_password, so\n the two must match, or supply the URLs through app_env/silo_env/pi_api_env." .Values.external_secrets.pgdb_existingSecret) }} {{- end }} {{- if and .Values.services.rabbitmq.local_setup .Values.external_secrets.rabbitmq_existingSecret (empty .Values.external_secrets.app_env_existingSecret) }} {{- $warnings = append $warnings (printf "external_secrets.rabbitmq_existingSecret (%s) sets the bundled RabbitMQ credentials\n only. The applications still connect with services.rabbitmq.default_user/\n default_password, so the two must match, or supply the URLs through\n app_env/silo_env/live_env/pi_api_env." .Values.external_secrets.rabbitmq_existingSecret) }} {{- end }} {{- /* Secrets that env rotation cannot reach. */}} {{- if or .Values.external_secrets.app_keys_existingSecret $externalInfra }} {{- $warnings = append $warnings "The SMTP password, OAuth client secrets, LLM_API_KEY and LDAP_BIND_PASSWORD are\n managed in the god-mode admin UI and stored in the database, not read from the\n environment. Rotating them through a Secret has no effect; change them in god-mode." }} {{- end }} {{- if ne (.Values.env.skip_env_var | default "1" | toString) "1" }} {{- $warnings = append $warnings "env.skip_env_var is no longer a chart value. Instance settings are managed in\n god-mode and the API ignores '0', but worker, beat and the consumers read the\n environment while it is set, so they can disagree with the API. Save those settings\n in god-mode, then remove env.skip_env_var." }} {{- end }} {{- /* Static object-storage keys where a cloud identity would do. */}} {{- if and .Values.env.aws_access_key (not (include "plane.minioEnabled" .)) (not .Values.serviceAccount.annotations) (not .Values.serviceAccount.cloudIdentity) }} {{- $warnings = append $warnings "Object storage is configured with a static AWS access key. Prefer a cloud identity:\n annotate the ServiceAccount (serviceAccount.annotations) for IRSA / EKS Pod Identity\n / GKE or Azure Workload Identity and clear env.aws_access_key and\n env.aws_secret_access_key — the chart then omits those env vars so the SDK's default\n credential chain picks up the pod's role." }} {{- end }} {{- /* An attached cloud identity reaches every container on the ServiceAccount, so it changes behaviour beyond object storage. The chart cannot detect the association — it is an EKS API object keyed on cluster + namespace + service account — hence the declared flag. */}} {{- $hasIdentity := or .Values.serviceAccount.cloudIdentity .Values.serviceAccount.annotations }} {{- if $hasIdentity }} {{- /* A static key beats the identity in the SDK credential chain. */}} {{- if and .Values.env.aws_access_key (not (include "plane.minioEnabled" .)) }} {{- $warnings = append $warnings "A cloud identity is configured, but env.aws_access_key is also set. Environment\n credentials take precedence over the identity in every AWS SDK's chain, so the\n static key is what will be used. Clear env.aws_access_key and\n env.aws_secret_access_key to let the identity apply." }} {{- end }} {{- if .Values.services.pi.ai_providers.embedding_model.aws_access_key }} {{- $warnings = append $warnings "A cloud identity is configured, but\n services.pi.ai_providers.embedding_model.aws_access_key is also set. Plane AI falls\n back to the identity when the BR_AWS_* keys are absent, so clear them to use it." }} {{- end }} {{- /* The one that is genuinely surprising: an identity attached for S3 silently changes how OpenSearch is authenticated, because both services read "AWS credentials are present" as "sign with SigV4". */}} {{- if and .Values.env.opensearch_remote_url (not (include "plane.externalOpensearch" .)) (not .Values.env.opensearch_remote_username) }} {{- $warnings = append $warnings "A cloud identity is configured and the remote OpenSearch has no credentials, so the\n API and Plane AI will authenticate to it with SigV4 — an identity injects AWS\n credentials into every container, and both read that as a request to sign.\n That is correct for Amazon OpenSearch Service and wrong for any other OpenSearch.\n Either set external_secrets.opensearch.secretName, or set OPENSEARCH_AUTH_MODE=basic\n through extraEnv (needs planeVersion v3.2.0 or newer)." }} {{- end }} {{- end }} {{/* Keyless storage on an upgrade: the chart omits AWS_ACCESS_KEY_ID so boto3 finds the pod identity, but Helm cannot delete a key it has stopped rendering (manifests write stringData, live Secrets store data), so a release that previously ran bundled MinIO keeps its root credentials in the Secret — and a present access key wins the credential chain. Fires only when this is a keyless external-storage configuration, which is exactly when the leftover is fatal rather than cosmetic. */}} {{- if and (not .Values.services.minio.local_setup) (ne (.Values.env.storage_provider | default "S3" | upper) "GCS") (not .Values.env.aws_access_key) (empty .Values.external_secrets.doc_store_existingSecret) }} {{- $warnings = append $warnings (printf "Storage is keyless (no access key), which relies on AWS_ACCESS_KEY_ID being ABSENT.\n If this release EVER ran with services.minio.local_setup=true, its MinIO root\n credentials are still in %s-doc-store-secrets: Helm cannot remove a key it no longer\n renders, and a present access key is found first in boto3's chain, so every S3 call\n fails with InvalidClientTokenId while the config looks correct. Once:\n kubectl delete secret %s-doc-store-secrets -n %s && helm upgrade ...\n kubectl rollout restart deploy -n %s -l app.kubernetes.io/instance=%s\n The restart matters — envFrom is read once at container start. Fresh installs and\n releases already on chart 3.6.0+ are unaffected." .Release.Name .Release.Name .Release.Namespace .Release.Namespace .Release.Name) }} {{- end }} {{- if $warnings }} ──────────────────────────────────────────────────────────────────────────────── {{ len $warnings }} configuration {{ if eq (len $warnings) 1 }}warning{{ else }}warnings{{ end }}: {{ range $i, $w := $warnings }} {{ add1 $i }}. {{ $w }} {{ end }} See the "Keeping credentials out of values.yaml" section of the chart README. ──────────────────────────────────────────────────────────────────────────────── {{- end }} {{- /* Redis Sentinel (bring your own). Set $sentinelMinVersion to the first Plane release whose images read REDIS_SENTINELS once it ships; until then the note names the work instead. An image without Sentinel support finds REDIS_URL empty and has no Redis. */}} {{- if include "plane.redisSentinel" . }} {{- $sentinelMinVersion := "" }} {{- if and $sentinelMinVersion $parsable (semverCompare (printf "< %s" $sentinelMinVersion) $version) }} WARNING: Redis Sentinel mode needs planeVersion v{{ $sentinelMinVersion }} or later; this release runs {{ .Values.planeVersion }}. Older images ignore REDIS_SENTINELS and, with REDIS_URL left empty, start without Redis. {{- end }} Redis Sentinel mode: services follow master "{{ .Values.env.redis_sentinel_master }}" via {{ .Values.env.redis_sentinels }}. - Needs {{ if $sentinelMinVersion }}planeVersion v{{ $sentinelMinVersion }} or later{{ else }}a planeVersion that includes INFRA-1336 (Redis Sentinel support){{ end }}, and an MCP server image with Sentinel support if services.mcp_server is on. - notify-keyspace-events must include E and x in the config file of EVERY data node, not only the current primary. Without it silo's webhook-driven syncs stop after a failover. - Plane AI (pi) uses Sentinel for its Celery broker, so it needs an image with Sentinel support too. With pi_api_env_existingSecret, set CELERY_BROKER_URL to sentinel:// in that Secret; a fixed Redis URL there won't follow a failover. {{- end }} {{- with $runnerHmacNote }} Note: {{ . }} {{- end }} {{- if .Values.global.forceRedeploy }} Note: global.forceRedeploy is on, so every workload restarts on every `helm upgrade`. Turn it off to have upgrades roll only the workloads whose configuration changed. {{- end }}