Repository navigation
fix(plane-enterprise): align integration-event-consumer with webhook-consumer - #375
Saurabhkmr98 wants to merge 43 commits into
Conversation
…rom class name (#255) * fix(plane-enterprise): decouple ingress controller type from class name The ingress templates used `ingress.ingressClass` to decide both the resource kind (standard Ingress vs Traefik IngressRoute) and the class name string. As a result `ingress.yaml` rendered only for the exact value `nginx` and `ingress-traefik.yaml` only for values prefixed `traefik` — any other class name (a Traefik alias, or `nginx-new`) matched neither template and the ingress was silently dropped. Introduce `ingress.controller` to select the resource kind, decoupled from the free-form class name. Only `traefik` selects the IngressRoute; any other value selects a standard Ingress. Empty falls back to the previous auto-detection from `ingressClass`, so existing installs are unaffected. Also fix a pre-existing latent bug in ingress.yaml where `len` of a nil `ingress_annotations` threw a hard render error; switched to `with`. Bumps chart 2.6.2 -> 2.6.3. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(plane-enterprise): drop Traefik-alias-on-standard-Ingress guidance The ingress.controller feature stays (it implements the F5/nginx-new request). Remove only the README/values examples about serving a Traefik-named class alias via a standard Ingress, which is no longer a needed use case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(plane-enterprise): align Middleware gate + expose controller in questions.yml - traefik-middleware.yaml: gate on plane.ingressController (matching ingress-traefik.yaml, licenseDomain condition included) instead of hasPrefix on ingressClass. Prevents a missing Middleware (and route admission failure) when controller=traefik with a non-traefik ingressClass, and avoids an orphan Middleware in the reverse case or when licenseDomain is unset. - questions.yml: add ingress.controller so the Rancher form can reach the new value, describe what ingressClass now does, and default ingress.ingressClass to "traefik" to match values.yaml (the authoritative chart default). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(plane-enterprise): make the widened Ingress fallback opt-in An upgrade-safety review of 3.5.2 -> 3.5.5 found the one case where this branch was not inert. With ingress.controller empty, the widened fallback in ingress.yaml rendered a <release>-ingress for any class outside {traefik*, nginx, openshift} -- alb, contour, istio, gce, nginx-new, openshift-default, a custom IngressClass name, an empty string. Those operators get no ingress from 3.5.2 and therefore run one of their own, so on upgrade they would get a second Ingress for the same host (a second ALB on the AWS LB controller), or a failed upgrade if theirs is already named <release>-ingress and Helm refuses to adopt it. The fallback is now reached only when an operator sets ingress.controller. With it empty the helper reproduces the pre-3.5.5 selection exactly, including its case sensitivity, so every existing values file renders byte-for-byte what it did before. The F5/nginx-new case the ticket asked for still works via ingress.controller. Also reverts the questions.yml ingress.ingressClass default to "nginx". Defaulting the Rancher form to "traefik" would fail a new install on any cluster without the Traefik CRDs; upgrades keep their stored answer either way, so the change bought consistency with values.yaml at the cost of new form installs. Verified: rendered output is identical to 3.5.2 across 22 ingressClass values plus ingress.enabled=false, an empty licenseDomain, and examples/values-openshift.yaml. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(plane-enterprise): add a values snippet for every ingress configuration Ten copy-pasteable `ingress` blocks, split by whether they need the new ingress.controller value: - Already supported, controller omitted: Traefik IngressRoute (the default), standard Ingress on ingress-nginx, OpenShift Routes, and ingress.enabled false for bring-your-own. - Newly possible with controller set: a class name that is not "nginx" (F5 NGINX, a second ingress-nginx install), AWS ALB, Contour/HAProxy/ Kong/Istio/Cilium/GCE, Traefik behind a non-traefik class name, OpenShift Routes behind a non-openshift class name, and OpenShift's ingress-to-route conversion. Each snippet states what it renders and carries the annotations that controller actually needs. All ten were rendered with `helm template` and the output checked against the documented result, annotations included. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(plane-enterprise): rename ingress.yaml to ingress-nginx.yaml Names the standard-Ingress template after the controller it is for, matching ingress-traefik.yaml and ingress-openshift.yaml, and scopes the docs to the three supported configurations: nginx, Traefik and OpenShift. - templates/ingress.yaml -> templates/ingress-nginx.yaml. Template filenames do not affect rendering; verified the output is unchanged. - README: drop the AWS ALB and Contour/HAProxy/Kong/Istio/Cilium/GCE snippets and renumber the rest, so only nginx/Traefik/OpenShift are documented. The remaining eight snippets were re-extracted from the committed README and re-rendered. - README/values.yaml/questions.yml: stop listing third-party controllers as ingress.controller options. The "any other value" behaviour is still documented, because that is what serves a non-"nginx" class name such as nginx-new, but it is no longer presented as a supported controller list. - The unrecognised-class warning is kept and reworded to "any class other than nginx, openshift or traefik*" rather than naming specific controllers, so the silent no-op stays documented. Verified: rendered output identical to 3.5.2 across 22 ingressClass values plus ingress.enabled=false, an empty licenseDomain and examples/values-openshift.yaml; resource ordering unchanged by the rename. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix formatting --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…305) * feat(plane-enterprise): make silo init container image configurable The silo deployment's two init containers (`wait-for-rabbitmq` and the S3-CA-gated `prepare-ca-bundle`) hardcoded `busybox`, forcing an unauthenticated Docker Hub pull. That blocks air-gapped installs and private-registry mirroring, and is exposed to Docker Hub rate limits. Add `services.silo.init_image`, following the `services.minio.init_image` convention from #298, and document it in the README table and questions.yml. Defaults to `busybox`, so rendered output is unchanged unless overridden. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(plane-enterprise): note command requirements for silo init_image Address CodeRabbit review on #305: a custom init image must provide the utilities the init scripts invoke (/bin/sh, grep, nslookup, sleep for wait-for-rabbitmq; /bin/sh, ls, cat, touch for prepare-ca-bundle), otherwise the affected init container fails and silo never starts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…306) * feat(plane-enterprise): make live init container image configurable The live deployment's `prepare-ca-bundle` init container hardcoded `busybox`, forcing an unauthenticated Docker Hub pull. This container only renders when a custom S3 CA is configured (`airgapped.enabled` with an S3 CA secret) -- so the one init container specific to air-gapped installs was itself unmirrorable. Add `services.live.init_image`, following the `services.minio.init_image` convention from #298, and document it in the README table and questions.yml. Defaults to `busybox`, so rendered output is unchanged unless overridden. `services.live_exporter` needs no equivalent -- it has no init containers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(plane-enterprise): note command requirements for live init_image Address CodeRabbit review on #306: a custom init image must provide the utilities the prepare-ca-bundle script invokes (/bin/sh, ls, cat, touch), otherwise the init container fails and live never starts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(plane-enterprise): bump iframely v1.2.0 -> v2.5.4
…h zero-downtime rotation (#278) * feat(plane-enterprise): consume credentials from external Secrets, with keyless cloud identity (3.6.2) Rebased onto master, which has since gained opt-in OpenTelemetry (#248) and the v3.1.1 release. The two touch the same regions of every workload — master adds OTel env, this branch adds credential env — so the resolution keeps both: one `env:` per container, one guard carrying both conditions, and OTel's envFrom entry back in the envFrom position where it belongs. Verified rather than eyeballed. Every workload template parses; the default, all-services, OTel and externalized-credentials renders all succeed; and both features coexist — the api container comes out with OTEL_SERVICE_NAME=api, POSTGRES_PASSWORD from the operator's Secret, and otel-vars alongside the credential Secrets in envFrom. Resolved environments are identical to pre-rebase for all 22 containers except APP_VERSION, which moves 3.1.0 -> 3.1.1 because that is master's release. hack/assert-secrets.py --no-dsn still passes. What this branch does, in the order it was built: - Credentials come from Secrets the operator owns, as discrete parts rather than a DSN, so a rotated password can actually reach the app. Postgres, RabbitMQ, Redis, OpenSearch and storage, plus whole-Secret and key-group hooks for the rest. - The same contract extended to silo, live and Plane AI, which each read a different subset. - live's AMQP_URL guarded so the RabbitMQ mirror is not silently inert for live. - live-exporter's ServiceAccount (it was the only workload hardcoding the release-scoped name), and a NOTES warning that the MQ mirror does not reach live's export queue. - Keyless S3: the chart omits AWS_ACCESS_KEY_ID rather than rendering it empty, because an empty credential is found first in boto3's chain and shadows the pod's identity. That Secret now renders base64 `data`, so a key the chart stops rendering is a deletion Helm can express — without which switching an existing release from MinIO to S3 fails as InvalidClientTokenId while the configuration looks correct. - Bedrock credentials, keyed (AWS_BEARER_TOKEN_BEDROCK) or keyless via the pod's identity, with the profile ARN and region outside the provider-key suppression group because they are identifiers. The chart version stays at 3.6.2, above master's 3.3.0; appVersion takes master's 3.1.1. Rebasing replayed as a single commit: master's OTel change collides with all eight of the original commits in the same few regions, and resolving the same conflict eight times invites exactly the silent mangling this diff is meant to avoid — two of the intermediate resolutions had already produced duplicate `env:` keys before being caught. The original commit messages are preserved in the PR history. * feat(plane-enterprise): give live and live-exporter the broker parts (3.6.3) The chart already blanked live's AMQP_URL so the RabbitMQ mirror's parts would take over — but it never injected any parts into live, and live-exporter had no credential helper at all. So the mirror reached every service except the two that own the export queue, and it failed in the quietest way available: live boots, serves collaborative editing, and only exports are dead. live now gets plane.rabbitmqCredsEnv alongside the Redis parts it already had. live-exporter gets both — it is the export worker, so the broker credential is the entire point for it, and Redis comes too because it shares live's RedisManager for job state. Paired with the app change that teaches apps/live to compose AMQP_URL from RABBITMQ_HOST/PORT/USER/PASSWORD/VHOST/SSL (makeplane/plane-ee#8747). Verified against the ext-secret-test values: both workloads resolve AMQP_URL empty with the five parts present, the credentials arriving by secretKeyRef from the operator's Secret, RABBITMQ_SSL=1 and port 5671 for Amazon MQ. The NOTES warning is reworded rather than deleted. It was stated as a permanent gap in live; it is actually a version constraint, and an operator on an older planeVersion still needs to hear it — with the specific note that only exports fail, because that is what makes it easy to miss. Additive as usual: with no external_secrets set, all 18 containers resolve an identical environment to 3.6.2. * feat(plane-enterprise): extraObjects, so an env's ESO wiring can live with its release (3.6.4) Everything a Plane environment needs in order to STOP holding credentials — an ESO SecretStore, the ExternalSecrets that produce the Secrets this chart reads by name — is not part of this chart and should not be. But it also has nowhere to live when a GitOps tool treats one directory as one unit: Fleet's dependsOn orders one bundle against another, so expressing "Secrets before workloads" forced a second bundle to exist purely to be depended on. extraObjects removes that. Rendered with toYaml and NOT tpl, deliberately. What people put here is usually an ExternalSecret whose target.template contains ESO's own {{ }} placeholders; tpl would try to evaluate those as Helm expressions and either fail or silently resolve them to empty. Helm does not template values, so this passes them through untouched — verified by rendering an ExternalSecret whose template references {{ .CLOUDFLARE_KEY }} and confirming it survives. Ordering is the caller's to choose with the standard Helm hook annotations, and the template's comment shows the shape, because it is the whole point: this chart's Secret references are optional: false, so a pod that starts before its Secret exists sits in CreateContainerConfigError until it appears. A pre-install/pre-upgrade hook with a negative weight is applied before the workloads. Inert when unset — extraObjects defaults to [] and the default render is unchanged. * fix(plane-enterprise): address the remaining CodeRabbit review on the secret contract Three findings from the review on #278, plus one declined. NOTES.txt — the SemVer probe was a prefix match, so "3.2.0.1" (a realistic four-part vendor version) was treated as parsable, reached semverCompare and aborted the render with "Invalid Semantic Version" instead of falling through to the compatibility warning it exists to print. Anchored, with optional prerelease and build metadata so real SemVer still compares. app-env.yaml — externalized credentials describe a remote backend, but nothing stopped them being paired with a bundled one. The chart would then hand the app the operator's credential and the in-cluster endpoint, which the bundled service never authenticates; for Redis it also blanks REDIS_URL, leaving an older image with no route at all. Now a render-time failure for postgres/redis/rabbitmq, matching what plane.externalOpensearch already did for its own backend. app-env.yaml — the read-replica guard proved credentials existed but not an endpoint, so replica-enabled plus a database Secret and no pgdb_read_replica_host rendered POSTGRES_READ_REPLICA_HOST="". The API deliberately does not fall back to the primary there, because that would send every "replica" read to the writer with no error, so an empty host is a broken replica rather than a slow one. The contract is now enforced and documented in values.yaml. Declined: removing plane.siloConnectorsSecretRef from the migrator. The migrator already mounts <release>-silo-secrets on master, and silo.yaml suppresses the connector keys from it when silo_connectors_existingSecret is set — so dropping the ref would give the migrator strictly fewer variables on the externalized path than on the default one, which is the divergence this PR exists to avoid. * chore(plane-enterprise): ship the whole secret contract as one 3.6.0 bump The branch had walked 3.6.1 through 3.6.5 as it grew. Nothing above 3.5.2 was ever published, so collapse it: one version for one PR, and the base64 `data` note in README/NOTES now points at the release that actually carries it. * fix(plane-enterprise): roll workloads when the OTEL config changes plane.configChecksum hashed every config-secret template except otel.yaml, so retuning observability.otel.* rewrote <release>-otel-vars / -otel-secrets but left the checksum untouched. envFrom is read once at container start, so the running pods kept the old exporter config until something else rolled them. Only reachable since this branch replaced the unconditional `timestamp` pod annotation with a content checksum -- on 3.5.2 every upgrade rolled everything, which hid it. Folded into the same 3.6.0 bump; nothing is released yet. docker-registry and cert-issuers stay out of the hash: neither is pod env, so hashing them would roll every workload for a change no container can observe. Also moves the extraEnv HTTP_PROXY example back under extraEnv, where it sat before extraObjects was inserted above it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: drop the hack/ verification scripts from the chart repo assert-secrets.py and resolve-env.py were written to check this branch's work -- that no rendered Secret still carries a credential, and that each container resolves the env it did before. Useful while building it, but they are not chart deliverables and they sat at the repo root of a published charts repo. Nothing references them: no README, NOTES.txt, or the rotation runbook. Both were added on this branch, so removing them leaves no trace in master's history. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(plane-enterprise): do not enforce the Reloader annotation on email Back to annotations: {}, so nothing is imposed on the workload. Operators who want the restart set reloader.enabled, or add the annotation themselves. Note this differs from master, where the annotation was a literal in email.deployment.yaml and so applied to every deployment. That literal sat above the plane.labelsAndAnnotations include, which emits its own annotations: key -- so any operator who set services.email_service.annotations turned it into a duplicate YAML key and silently lost the Reloader one. Routing everything through the helper fixes that: annotations now compose instead of clobbering. The trade-off is stated in values.yaml rather than left implicit -- this service mounts a cert-manager TLS Secret, so without the annotation it will not pick up a renewed certificate until something else restarts it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(plane-enterprise): let the key-group Secrets win in envFrom The same ordering bug review found in plane-ce, which this chart shares: the key-group includes were FIRST in envFrom on every workload that uses them. envFrom is later-source-wins, so an operator setting app_keys_existingSecret alongside app_env_existingSecret -- or silo_connectors_existingSecret alongside silo_env_existingSecret -- would have the whole-Secret hook override the key the group hook owns. That defeats the group hook's purpose. SECRET_KEY and LIVE_SERVER_SECRET_KEY are rendered into both the app and live Secrets and must agree; an override landing on the api but not on live leaves the two with different signing keys and live-server auth fails with nothing logged. SECRET_KEY also derives the key encrypting the instance-configuration rows. NOTES.txt already warned about this interaction at install time. A warning is weaker than correct ordering: it fires only if the operator reads it, and says nothing about which of the two Secrets wins. 17 envFrom blocks across 17 workloads. Two needed more than a straight move -- api carries both appKeysSecretRef and siloConnectorsSecretRef stacked together, and silo has two envFrom blocks with the include leading the second. Verified by rendering with both the group and whole-Secret hooks set and checking every container: 10 containers receive a group Secret and in all 10 it now sorts after every other source. The default render is unchanged, which it must be -- with no hook set the includes emit nothing, so order cannot matter. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(plane-enterprise): drop the reloader flag, keep the annotation route This chart is public, and a Reloader annotation is a deployment decision rather than something a chart should offer a switch for -- which workloads must restart when a credential rotates depends on the install, and the datastores generally should not. services.<name>.annotations already reaches the WORKLOAD resource, which is where Reloader looks, so nothing is lost: an operator sets services: api: annotations: reloader.stakater.com/auto: "true" and gets exactly what the flag did, for the workloads they choose. Removing the values key means NOTES.txt could no longer dereference .Values.reloader.enabled -- that would have been a nil-pointer failure on every install, not a silent no-op, so the warning is rewritten. It still fires whenever an external-Secret hook is set, and now names the annotation to add instead of a flag to flip. README, the ESO examples and the rotation runbook follow. email.deployment.yaml keeps taking its annotations from the helper. The literal it used to carry is still gone, which is what was asked for earlier: the chart imposes nothing. No default-render change -- the flag defaulted false, so 0 lines differ. Renders 44 documents, NOTES.txt renders under --dry-run with and without a hook set, a per-workload annotation lands on the workload and not the pod template, and lint is clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…PII scanner service (#314) * [INFRA-881] feat(plane-enterprise): add the Argus sensitive-data & PII scanner service (3.6.0) Argus (apps/argus in plane-ee) was deployable through the plane-cloud chart and, since INFRA-877 (plane-ee#9446), through every commercial self-hosted method except this chart. This ports it, opt-in and mirroring that contract: - templates/workloads/argus.deployment.yaml: headless Service + Deployment on port 8100. Pinned to replicas: 1 with strategy Recreate and no `replicas` value: export CSVs are served from the pod's own filesystem and the startup reapers are unscoped, so a second pod breaks in-flight work. Probes hit <base_path>/healthz (the only health route, mounted under the prefix). terminationGracePeriodSeconds 60 because Argus drains scan workers before its 10s HTTP drain. An emptyDir is mounted at ARGUS_DATA_DIR so exports stay writable under securityContext.enabled or an OpenShift-assigned UID. - templates/workloads/argus-migrator.job.yaml: one-shot `argus migrate` (args only; the image ENTRYPOINT is the binary). `argus start` deliberately does not migrate. - templates/config-secrets/argus-env.yaml: Secret (ARGUS_DATABASE_URL derived from services.postgres / env.pgdb_* like silo, ARGUS_CONTENT_DATABASE_URL from the read replica, ARGUS_FINGERPRINT_SECRET, ARGUS_INTERNAL_SECRET) with external_secrets.argus_env_existingSecret, plus the ConfigMap. Rendering fails with the reason when the fingerprint secret is under 32 characters and no existing Secret is named, instead of shipping a crash-looping pod. - /argus/ route (from env.argus_envs.base_path) on the nginx, Traefik and OpenShift ingress templates, forwarded without rewrite. - values.yaml (services.argus, env.argus_envs, argus_env_existingSecret), questions.yml ("Argus Setup" group, image, existing secret), README (Argus Deployment section, External Secrets and Custom Ingress Routes tables), Chart.yaml 3.5.8 -> 3.6.0. Off by default; no rendered output changes unless services.argus.enabled. Co-Authored-By: WOZCODE <contact@withwoz.com> Claude-Session: https://claude.ai/code/session_01Wu7g4mh2TaFo4Dcp9fYeKc * [INFRA-881] chore(plane-enterprise): bump Plane-EE to v3.2.0 and default the Argus internal secret - appVersion 3.1.4 -> 3.2.0; planeVersion v3.1.4 -> v3.2.0 in values, questions and README - env.argus_envs.internal_secret now ships a placeholder default, matching the other placeholder secrets in the chart (rotate per deployment) Co-Authored-By: WOZCODE <contact@withwoz.com> Claude-Session: https://claude.ai/code/session_013yaAavA8NMZ5jTziKPjM48 * Bump version from 3.6.0 to 3.7.0 --------- Co-authored-by: WOZCODE <contact@withwoz.com>
…ker Hub org (#315) MinIO removed the `minio` organisation from Docker Hub. Every deployment that pulls `minio/minio` or `minio/mc` now fails with: pull access denied for minio/minio, repository does not exist or may require 'docker login' The registry returns 401 (not 404) to anonymous clients, so the error reads like a credentials problem when the repository is simply gone. quay.io is MinIO's own second registry and still serves both images publicly (multi-arch: amd64, arm64, ppc64le). Repoint every default at quay.io, and pin an explicit RELEASE tag instead of `:latest` so a future upstream change to a floating tag cannot break deployments the same way: minio -> quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z mc -> quay.io/minio/mc:RELEASE.2025-08-13T08-35-41Z These are the digests `:latest` currently resolves to on quay. Note: the community MinIO build is effectively frozen upstream (last community push was 2025-09-07), so this unblocks deployments but does not change the longer-term need to steer self-hosted users toward external S3, with bundled MinIO as a dev/eval convenience. Claude-Session: https://claude.ai/code/session_01TB2aRDJ5BMx6m5c7jDzkqM Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…316) Version bump for the MinIO registry fix merged in #315, which changed the shipped image defaults in both charts: minio -> quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z mc -> quay.io/minio/mc:RELEASE.2025-08-13T08-35-41Z Patch bumps, matching how comparable image-default changes were versioned previously (iframely 3.5.7 -> 3.5.8, busybox/mc 1.6.2 -> 1.6.3). appVersion is unchanged in both charts - no application release is involved. Claude-Session: https://claude.ai/code/session_01TB2aRDJ5BMx6m5c7jDzkqM Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…FIPS (UBI) images (#317) Enabling a custom CA bundle on a FIPS deployment stopped the pods from starting at all. plane.s3CAInitScript ran `update-ca-certificates`, which only exists on the standard Alpine/Debian images; the FIPS images are UBI and ship `update-ca-trust`. The script is embedded in a container command that runs under `set -e`, so the missing binary exited 127 and the entrypoint was never reached -- api, external-api, worker, worker-importers and the pi-* workloads all went down. Measured on registry.access.redhat.com/ubi10/ubi-minimal: old script exit 127, new script exit 0. Detect the trust tool at runtime instead of assuming one, and use the matching anchor directory and bundle path (/etc/pki/ca-trust/source/anchors and /etc/pki/tls/certs/ca-bundle.crt on UBI, /usr/local/share/ca-certificates and /etc/ssl/certs/ca-certificates.crt on Alpine/Debian). Verified the custom CA lands in the trust store on both: 146 -> 147 certs on UBI, 119 -> 120 on Alpine, and Python's default SSL context picks it up. The correct bundle path is not knowable when the chart renders, so the SSL_* variables are now exported by the script -- an export survives the exec that follows -- overriding the Alpine/Debian defaults that plane.s3CAEnvVars still puts in the pod spec. Nothing in the script can fail the container any more. When the install does not happen -- no trust tool, a non-root container that cannot write the anchor directory, or an empty secret -- it warns and unsets those variables rather than leaving SSL_CERT_FILE pointing at a bundle the image does not have, which would break every outbound TLS call instead of just the private-CA ones. All three paths verified to leave the container running. The Node path (live, silo) already worked: it concatenates into an emptyDir from a busybox init container and sets NODE_EXTRA_CA_CERTS, with no distro-specific tooling. Unchanged. With the CA feature disabled -- the default -- the rendered output is byte-identical to 3.7.1 apart from the chart-version label and the checksum/config annotation that any version bump moves. Also document in values.yaml that these certificates are not S3-only, and that LDAP is configured separately via extraEnv against the verbatim mount at /s3-custom-ca/<key>, because OpenLDAP does not read the system store. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: WOZCODE <contact@withwoz.com>
Co-authored-by: WOZCODE <contact@withwoz.com>
…dConstraints (#325) * feat(plane-enterprise): native PodDisruptionBudgets and topologySpreadConstraints Two gaps the HA docs have been apologising for in a "Known chart gaps" table: budgets had to be applied as hand-written manifests, and AZ spreading had to be expressed as podAntiAffinity because the chart rendered no topologySpreadConstraints. Both are now values. podDisruptionBudget: enabled: true # every eligible workload with 2+ replicas maxUnavailable: 1 unhealthyPodEvictionPolicy: AlwaysAllow services: api: replicas: 3 podDisruptionBudget: { maxUnavailable: 2 } # per-workload override topologySpreadConstraints: - topologyKey: kubernetes.io/hostname whenUnsatisfiable: DoNotSchedule - topologyKey: topology.kubernetes.io/zone plane-enterprise only. plane-ce is untouched deliberately -- it ships six scalable workloads against nineteen here, and the community edition's deployments are overwhelmingly single-node, where a budget protects nothing and a spread constraint has nowhere to spread. It can follow later if there is demand. ONLY THE SCALABLE WORKLOADS GET A BUDGET, and the exclusions are enforced rather than documented: requesting one for a singleton (beatworker, pi_beat_worker, monitor, argus, the migration Jobs) or an in-chart stateful service fails the render with an explanation. With no second copy a budget cannot protect anything -- all it can do is refuse the eviction until the drain gives up or the node disappears under it, turning a routine rotation into a stuck one. An eligible workload still at replicas: 1 is skipped SILENTLY instead, so podDisruptionBudget.enabled stays a safe one-line change; the explicit request is the mistake, not the default. maxUnavailable is the documented default over minAvailable because replica counts move: at one replica maxUnavailable: 1 is a no-op, where minAvailable: 1 wedges every future drain of that node. Setting both fails the render -- the API accepts only one. THE labelSelector IS FILLED IN by plane.podSpread from the workload's own app.name label. This is the part worth reviewing: a topologySpreadConstraint whose selector matches nothing is satisfied by every possible placement, so a hand-written one fails open and silently. The values keys do not match the labels either (email_service renders email-app, pi renders pi-api), so the mapping lives in plane.pdbEligible and is consumed by both the PDB template and the validations. The migrator Jobs deliberately do NOT get spread constraints even though they share services.api / services.pi with the api and pi deployments -- a one-shot Job has nothing to spread against. 3.7.4 -> 3.8.0. The chart also gains a templates/validations.yaml, which it did not have: it renders no resource and holds the refusals. Additive: rendering with default values is byte-identical to before, verified by diffing a full render against master. Feature paths verified by asserting every rendered PDB selector matches exactly one Deployment at replicas >= 2, that no multi-replica Deployment is left uncovered, and that each refusal path refuses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(plane-enterprise): zero-safe PDB fields, required topologyKey Three CodeRabbit findings on #325, all reproduced before fixing. 1. A constraint with no topologyKey rendered `topologyKey: null` and was rejected by the API at apply time. `required` turns it into a Helm error naming the field. 2. minAvailable/maxUnavailable were resolved by truthiness, and 0 is falsey. Both values are legal and meaningful -- maxUnavailable: 0 forbids all voluntary eviction, minAvailable: 0 permits it all -- and both were silently rewritten to `maxUnavailable: 1`, which INVERTS the intent of maxUnavailable: 0. CodeRabbit caught the validation half of this; the renderer was the worse half. Both now key off hasKey, and the precedence is per LEVEL rather than per key: if the workload sets either field it decides alone, otherwise the chart-wide block does. Resolving key by key let the chart-wide maxUnavailable survive alongside a workload's minAvailable and emit both, which the API rejects. 3. values.yaml shipped `maxUnavailable: 1` as a concrete default, so a user who set only `podDisruptionBudget.minAvailable` merged into both fields and had their render refused -- the chart contradicting its own documented override. The default now lives only in the renderer's final else arm. The both-set refusals still fire, including the case truthiness could never catch: minAvailable: 0 AND maxUnavailable: 0 together. Defaults still render byte-identical to master; the feature suite (selector correctness, no uncovered multi-replica Deployment, per-service override wins) still passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(plane-enterprise): default WEBHOOK_ALLOWED_HOSTS to the license domain When env.webhook_allowed_hosts is empty, derive it from license.licenseDomain with the scheme chosen by the plane.tlsEnabled check, the same way WEB_URL is built. Document the default in values.yaml, questions.yml and the README, and bump the chart to 3.8.1. Co-Authored-By: WOZCODE <contact@withwoz.com> * release(plane-enterprise): update Plane-EE to version v3.2.3 Co-Authored-By: WOZCODE <contact@withwoz.com> * fix(plane-enterprise): add webhook allowed hosts URL example Co-authored-by: akshat5302 <55329820+akshat5302@users.noreply.github.com> --------- Co-authored-by: WOZCODE <contact@withwoz.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
… optional in-chart service (#320) * feat(plane-enterprise): add the Plane MCP server as an optional in-chart service Restores the MCP-in-enterprise work from the 2026-08-27 session (stash on release/plane-enterprise-v3.1.2): services.mcp_server values, ConfigMap/Secret, Service+Deployment, and /mcp path routing for nginx, traefik and OpenShift ingress. Disabled by default; path prefix defaults to /mcp. Co-Authored-By: WOZCODE <contact@withwoz.com> * chore(plane-enterprise): default the MCP server to v0.3.2 and hand it REDIS_URL v0.3.2 accepts REDIS_URL for its OAuth token store, so the chart now emits the same URL live uses (bundled or env.remote_redis_url) instead of parsing the URL into REDIS_HOST/PORT/PASSWORD/SSL. With external_secrets.redis the URL stays unset and the discrete REDIS_* parts come from plane.redisCredsEnv. Co-Authored-By: WOZCODE <contact@withwoz.com> * fix(plane-enterprise): give the MCP ingress route a trailing slash like the other routes Matches /api/, /silo/, /pi/ etc. on nginx, Traefik and OpenShift; the prefix is trimmed first so a value like /agent/ does not render as /agent//. Co-Authored-By: WOZCODE <contact@withwoz.com> * fix(plane-enterprise): route the MCP server's OAuth discovery paths to it MCP clients resolve the resource metadata URL the server advertises in WWW-Authenticate (/.well-known/oauth-protected-resource<prefix>/http/mcp) and the path-inserted authorization-server metadata (/.well-known/oauth-authorization-server<prefix>/http). Both sit at the host root, outside the <prefix>/ route, so they were landing on the web app. Plane serves nothing under /.well-known, so the two prefixes go to the MCP server on nginx, Traefik and OpenShift. Co-Authored-By: WOZCODE <contact@withwoz.com> * chore(plane-enterprise): address review on the MCP server service - carry the tag in services.mcp_server.image (makeplane/plane-mcp-server:v0.3.2) like iframely/postgres; drop services.mcp_server.tag from values, questions.yml and the README - give the MCP rows their own "MCP Server Deployment" README section instead of extending the Iframely table - document the /mcp and OAuth discovery routes in the custom-ingress table Co-Authored-By: WOZCODE <contact@withwoz.com> * fix(plane-enterprise): make the MCP server's Plane OAuth application mandatory CodeRabbit flagged that the README said the MCP server does not start without plane_oauth.client_id while the values table called OAuth optional. The README was right: plane-mcp-server's default http transport (the image CMD) builds its PlaneOAuthProvider unconditionally, and that provider raises on an empty client id or secret, so plane_oauth.enabled=false shipped a crash-looping pod. - drop services.mcp_server.plane_oauth.enabled from values, questions and README; client_id / client_secret are required whenever services.mcp_server.enabled is true, and the env template fails at render time with the reason (same pattern as argus) unless mcp_server_env_existingSecret supplies them - derive PLANE_OAUTH_PROVIDER_BASE_URL from the license domain and TLS setting (the server appends <path_prefix>/http itself) and move it to the ConfigMap, so an external Secret no longer has to carry a non-secret value - README: state the OAuth callback URIs in terms of services.mcp_server.path_prefix and mark /mcp in the custom-ingress table as the default prefix - Chart.yaml 3.8.0 -> 3.9.0: master took 3.8.0 with the PodDisruptionBudget PR, so this branch's version now collides and the release workflow would skip it Co-Authored-By: WOZCODE <contact@withwoz.com> * chore(plane-enterprise): bump version to 3.9.0 --------- Co-authored-by: WOZCODE <contact@withwoz.com>
…closed to anonymous pulls (#330) On 2026-09-24 MinIO closed anonymous pulls on quay.io/minio/minio and quay.io/minio/mc for every tag (the anonymous token grants no actions and manifests answer 401), two weeks after #315 moved here from the deleted Docker Hub org. Every install using the bundled MinIO fails to pull it. services.minio.image -> docker.io/pgsty/minio:RELEASE.2026-08-04T00-00-00Z services.minio.image_mc -> docker.io/pgsty/mc:RELEASE.2026-09-16T00-00-00Z Pigsty's community builds of MinIO's AGPL source: same entrypoint and root user as upstream, read data written by RELEASE.2025-09-07T16-13-09Z, amd64 + arm64, /usr/bin/mc where the bucket Job expects it. The Job's exact commands (alias set, mb, anonymous set download) verified against the pair. questions.yml and README defaults follow. plane-ce 1.8.1 -> 1.8.2, plane-enterprise 3.9.1 -> 3.9.2. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…n-chart draw.io (off by default) (#245) * feat: add DRAWIO_EMBED_URL env to plane-enterprise chart Adds the DRAWIO_EMBED_URL config var (default empty) to the plane-enterprise app-vars ConfigMap and values.yaml so operators can point the Wiki editor's diagram extension at a self-hosted drawio endpoint. Empty preserves the diagrams.net cloud default. Co-Authored-By: WOZCODE <contact@withwoz.com> * chore: document DRAWIO_EMBED_URL in questions/README and bump chart to 2.5.2 * feat(plane-enterprise): add optional in-chart draw.io, off by default Ports the commercial-deployments draw-io kustomize component (makeplane/commercial-deployments@f63056d) to the chart: - services.drawio (default enabled: false): makeplane/drawio:v2.5.0 Deployment + Service on 8080, same resources/probe as the component - /drawio subpath on Traefik (stripPrefix + trailing-slash redirect middlewares), nginx (dedicated rewrite-target + redirect Ingresses) and OpenShift (Route with rewrite-target) - DRAWIO_EMBED_URL defaults to <scheme>://<licenseDomain>/drawio when drawio is enabled and env.drawio_embed_url is empty - drawio is PDB-eligible; values, questions.yml and README documented - chart version 3.10.0 Co-Authored-By: WOZCODE <contact@withwoz.com> * refactor(plane-enterprise): gate drawio on local_setup like other bundled services Align draw.io with the postgres/redis/rabbitmq/opensearch/minio pattern: - services.drawio.enabled -> services.drawio.local_setup (default false) - env.drawio_embed_url -> env.drawio_remote_url, mirroring pgdb_remote_url / opensearch_remote_url - DRAWIO_EMBED_URL precedence follows DATABASE_URL: local_setup (in-chart /drawio URL) -> drawio_remote_url -> "" - pdb.yaml also skips a service whose local_setup is false, so no drawio PDB renders when drawio isn't deployed - questions.yml "Install Draw.io" with the remote URL shown only when local_setup is off; README updated Co-Authored-By: WOZCODE <contact@withwoz.com> * feat(plane-enterprise): default DRAWIO_EMBED_URL to the public diagrams.net cloud env.drawio_remote_url now defaults to https://embed.diagrams.net (the editor's own DEFAULT_DRAWIO_EMBED_HOST) instead of "", and the template falls back to it if the value is blanked, so DRAWIO_EMBED_URL is never empty. embed.diagrams.net also serves /js/viewer-static.min.js, so the read-only viewer keeps working with the single host. local_setup: true still points it at the in-chart /drawio. Co-Authored-By: WOZCODE <contact@withwoz.com> * revert: keep DRAWIO_EMBED_URL empty by default The frontend already falls back to the diagrams.net cloud when the value is empty, so the chart doesn't need to hard-code embed.diagrams.net. Reverts 3a3b7a5. Co-Authored-By: WOZCODE <contact@withwoz.com> * refactor(plane-enterprise): route drawio on /drawio/ like the other services Match the /silo/, /pi/ style: the drawio ingress path prefix is now /drawio/ (with trailing slash) on Traefik, nginx and OpenShift, and the bare-/drawio -> /drawio/ redirect is dropped (Traefik redirect-slash middleware and the nginx redirect Ingress). Plane only ever requests <DRAWIO_EMBED_URL>/?embed=1... and <DRAWIO_EMBED_URL>/js/..., both under /drawio/. The /drawio prefix is still stripped since the image serves at /. Co-Authored-By: WOZCODE <contact@withwoz.com> --------- Co-authored-by: WOZCODE <contact@withwoz.com>
…235) * Plane-EE: route /static to API service for Swagger UI sidecar assets Add a /static route to both the nginx Ingress and Traefik IngressRoute so DRF-Spectacular Swagger UI sidecar assets (served from /static/drf_spectacular_sidecar/) reach the API service instead of falling through to the web frontend. Bump chart version to 2.4.4. Fixes makeplane/plane#9104 Co-Authored-By: WOZCODE <contact@withwoz.com> * Plane-EE: default nginx proxy-buffer-size to 8k Ship nginx.ingress.kubernetes.io/proxy-buffer-size: 8k as a default entry in ingress.ingress_annotations so large upstream response headers (e.g. the Swagger UI / OpenAPI schema endpoints) are not truncated by the nginx ingress controller. Picked up by the existing annotations range in the Ingress template; documented in README. Co-Authored-By: WOZCODE <contact@withwoz.com> --------- Co-authored-by: WOZCODE <contact@withwoz.com>
#335) Swagger UI loads its assets from /static/drf_spectacular_sidecar/, which the plane-ce ingress sent to the web frontend, so the page stayed blank. Route /static/ to the api service on both the nginx Ingress and the Traefik IngressRoute, matching the Community proxy's Caddyfile. Also expose env.enable_drf_spectacular (default false) as ENABLE_DRF_SPECTACULAR so Swagger can be turned on without extraEnv. Bump the chart to 1.8.3. Co-authored-by: WOZCODE <contact@withwoz.com>
Update appVersion, the planeVersion default, the questions.yml default and the README to v1.4.2, and bump the chart to 1.8.4. Co-authored-by: WOZCODE <contact@withwoz.com>
…PDB validation (#338) validations.yaml read .podDisruptionBudget off every services.* value, so any scalar there (e.g. services.storage_class, as plane-cloud has) crashed rendering with "can't evaluate field podDisruptionBudget in type string". Mirrors helm-charts-private#727. Bump 3.10.1 -> 3.10.2. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: WOZCODE <contact@withwoz.com>
…her silo limits (#337) * [INFRA-1040] fix(plane-enterprise): raise MinIO idle timeout and silo limits by default MinIO resets a connection whose client stops reading for ~30-60s. Silo reads a Confluence export's entities.xml through one ranged GET and pauses it while writing each batch, so a large import failed with ECONNRESET "aborted". - services.minio.idle_timeout (default 1h) renders MINIO_IDLE_TIMEOUT on the bundled MinIO; empty keeps MinIO's default. - Silo default limits 500m/1000Mi -> 2000m/2Gi: parsing an import is CPU-bound and the throttled pauses are what outlast the idle timeout. - Chart 3.10.1 -> 3.10.2. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * [INFRA-1040] fix(plane-enterprise): make the MinIO idle timeout opt-in MINIO_IDLE_TIMEOUT is both MinIO's keep-alive idle timeout and the per-connection read/write deadline, so a 1h default lets any client hold a stalled or idle connection for an hour. Default to empty (MinIO's own 30s) and document when to raise it; silo resuming a dropped read (makeplane/plane-ee#9952) is the real fix. Addresses the CodeRabbit review on #337. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…am endpoints (#342) * fix(plane-enterprise): add traefik routes for SSE stream endpoints Route the run activity stream (/api/workspaces/*/runs/*/activities/stream/) to the API without the body-limit middleware, with priority 1000 so it wins over the /api/ route. Add an explicit route for the pi chat stream-answer endpoint, also without buffering. Chart 3.10.5 -> 3.10.6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(plane-enterprise): anchor run activity stream route regex End the PathRegexp with $ so only the stream endpoint itself skips the body-limit middleware, not paths with trailing segments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ent self-managed Garage (#331) * [INFRA-1030] fix(charts): move the bundled store to pgsty/silo RELEASE.2026-09-16 pgsty/minio is frozen: RELEASE.2026-08-04 is the last tag ever published under that name, and everything after it ships as pgsty/silo. We were three releases and six weeks behind, across a security release that lands in Plane's own path. RELEASE.2026-09-16 (SN-2026-011..014) rejects unsigned x-amz-* operation headers, verifies header-bound presigned payloads and aligns signature-age and payload-hash conditions with authenticated values. Presigned URLs are Plane's entire upload and download mechanism, so this is not a hygiene bump. The same release also removes exponential wildcard matching from policy evaluation, which the bucket's anonymous-download policy puts on the request path. image_mc was already on the 09-16 tag, so this also ends the client/server skew. Verified the rename is behaviour-neutral for how the chart uses the image: - the entrypoint still dispatches `server` (args: [server, /data] runs `silo server /data`), so the StatefulSet needs no change - /usr/bin/mc still exists in pgsty/mc, so the bucket init Job is unaffected - mb + anonymous set download + PUT/GET round trip all pass - unauthenticated GET, 404 on a missing key and the ListBucketResult body are byte-identical between the old and new image - rendered diff against master is one image: line per chart Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * [INFRA-970] docs(plane-enterprise): self-managed Garage on Kubernetes, with a MinIO-to-Garage copy Job Plane's charts keep bundling MinIO. examples/garage/ is for operators who want to run Garage instead; they own it, and Plane uses it through the existing external-storage values, so no chart change is needed. - README.md: install Garage from its own chart, bring the cluster up (layout), create Plane's key and bucket, copy existing files, switch Plane with helm upgrade, check, roll back, and operate it (redundancy, snapshots, scrub, capacity, upgrades, key rotation, region). - garage-values.yaml: values for Garage's chart: 3 nodes at replication factor 3, the multi-arch image (the chart default is amd64-only), region us-east-1, no public ingress. - copy-minio-to-garage.yaml: a standalone Secret + Job (pgsty/mc) that mirrors Plane's MinIO bucket into Garage and verifies names, sizes, object count and optionally SHA-256. It only reads MinIO, reruns copy only what is missing, and runs non-root with a read-only root fs. - plane-values.yaml: services.minio.local_setup=false, the in-cluster Garage endpoint, and env.use_storage_proxy=true so Plane's API carries file traffic and Garage stays private. plane-ce has no storage proxy; the README covers the public-endpoint + CORS path for it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
… and ESO examples (#349) * fix(plane-enterprise): external secrets fixes and corrected guidance - live-exporter now mounts app_keys_existingSecret. It runs the live image, whose env check exits without LIVE_SERVER_SECRET_KEY, and with app_keys set the live Secret no longer carries that key, so a fresh install's exporter could not start. - NOTES, README, values.yaml and the appKeysSecretRef comment described app_keys as mounted first (older groups win). Every workload mounts it last, so it wins; the duplicate warning now says what actually happens. - New NOTES warnings, no behaviour change: silo_env_existingSecret decides the API workloads' DATABASE_URL/REDIS_URL/AMQP_URL/AES_SECRET_KEY (mounted after app_env), and pgdb/rabbitmq_existingSecret only set the bundled servers' credentials while the apps still use values.yaml. - values.yaml key lists: AWS_BEARER_TOKEN_BEDROCK (ai_providers); SLACK_SIGNING_SECRET, GITHUB_WEBHOOK_SECRET, Bitbucket and HubSpot keys (silo_connectors). - examples/external-secrets: the AWS, Azure and GCP SecretStores were rejected by ESO's admission webhook (cross-namespace serviceAccountRef on a namespaced store); AWS now uses the controller's credentials, Azure/GCP a ServiceAccount in Plane's namespace. template.engine -> engineVersion (unknown field in v1). Redis parts need v3.2.0, not v3.1.0; the silo DSN templates are only for releases before v3.2.0; the rotation runbook compares a hash instead of printing a password. - external-secrets-setup.md: v1beta1 -> v1, namespace on ClusterSecretStore secret refs, pointer to the full guide. Chart 3.10.7 -> 3.10.8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(plane-enterprise): address review on external secrets PR - Azure example: annotate the ServiceAccount with azure.workload.identity/tenant-id; without a tenant ESO fails with "missing tenantID" before syncing. - silo_env warning: AES_SECRET_KEY comes from silo_env only when app_keys_existingSecret doesn't carry it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…a bare hostname (#350) The default rendered "http(s)://<licenseDomain>", but the API matches WEBHOOK_ALLOWED_HOSTS entries as bare hostnames, so the scheme kept the default from ever matching. Integrations then failed to register their webhook with "URL domain or its subdomain is not allowed" until an operator set a bare hostname by hand. Render the license domain without a scheme, which works on every app version that reads the setting, and correct the docs that suggested full URLs. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: WOZCODE <contact@withwoz.com>
…need (#328) god-mode's System checks page runs inside the api container, so the api has to know where the other containers are and which queues they read. It did not: of everything the checks look at, app-env carried only LIVE_BASE_URL. On Kubernetes the space, iframely, argus, export-worker, Plane AI, automation, agent and webhook checks therefore skipped forever -- correct behaviour, no information. Adds seven variables to app-env, none of which any request path uses. The api never calls space, argus or iframely, and never consumes those queues. The queue names are taken from the same values the consumers are handed a few files over, rather than re-defaulted here. A deployment that renames a queue would otherwise get a check watching a queue nothing uses, which reports "not deployed" for a consumer that is running -- wrong, and in the direction nobody investigates. That already bites AGENT_QUEUE_NAME. This chart defaults it to "plane.agent"; the api's own default, in run_agent_consumer.py, is "plane.agents". Passing the chart's value through settles it for the check, but the two defaults genuinely disagree and that is worth fixing separately. ARGUS_BASE_URL and IFRAMELY_URL are emitted only when their service is enabled, so a default render is unchanged for them. ARGUS_BASE_URL includes the configurable base path, because argus mounts every route under it and a URL without it 404s. Verified with helm template: all seven render with the expected values, argus and iframely are absent when disabled, and the only other change to a default render is the app-env checksum annotation -- which is the chart correctly deciding to roll the pods that consume this ConfigMap. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ivergence warning, drop dead value (#354) * [INFRA-809] feat(plane-enterprise): the two addresses #328 did not cover #328 gave the api its neighbours' addresses and the queue names they read, which covers most of what god-mode > System checks needs. Two things it does not reach, both added here on top of it: ARGUS_INTERNAL_SECRET. #328 supplies ARGUS_BASE_URL, and an address alone is not enough to read Argus's /admin-health: Argus mounts that route only when it has a secret of its own, because its router is browser-facing and otherwise unauthenticated below /healthz. Without the matching value on the api the row skips. It is also what lets Argus report its own 5xx responses back to the api, which the api already warns about. INTAKE_EMAIL_SMTP_HOST and _PORT. The traffic between the api and the email service only ever goes one way -- the service holds EMAIL_SAVE_ENDPOINT and POSTs parsed mail to the api -- so the api has never known where it lives and had nothing to probe. That left the one state an operator most needs invisible: the service speaks only SMTP, so "enabled but not working" looked exactly like "not deployed here". The check now opens a connection and reads the SMTP banner, and a server must greet with 220 before it will accept mail, so a listener that is up but wedged is caught too. Both are gated on their service being enabled, so an install without them is unchanged and the checks skip rather than fail. Templated rather than added to values.yaml: the chart already knows both names, and an address nobody types is an address nobody gets wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(plane-enterprise): warn on web_url divergence and drop dead instance_admin_email Folded in from #321, which was opened against chart 3.7.2 and had drifted into conflict against master. Both of its premises still hold on current master, so the change is re-applied rather than rebased: instance_admin_email is still read by exactly zero templates, and NOTES.txt still collects warnings the same way. The marketplace OAuth apps build their redirect URIs from license.licenseDomain, not from env.web_url. When the two disagree, the callback every integration advertises is not the host the browser is on, so the OAuth round-trip fails the redirect check -- and nothing in the logs names the cause. The install warning now says so, naming both values. instance_admin_email was never read by any template, so setting it did nothing while looking like it configured the first admin. That admin is created through the /god-mode/ setup page. Replaced with a comment rather than deleted outright, so an operator who had it set finds out why it went. Version note corrected from 3.7.3 to the version this actually ships in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d mode (#252) Adds a top-level customCA section that installs private CA certificates on connected installs, without airgapped.enabled (which also switches licensing to offline). The airgapped.s3* keys remain a fallback. Rebased onto master (chart 3.10.12) by Pratapa Lakshmi. Co-authored-by: Pratapa Lakshmi <pratapagoutham@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… requested (#360) * fix(security): encrypt the MCP server's Redis token store when TLS is requested The MCP server applies REDIS_SSL only to its startup PING on the host/port path; the token store itself connects in plaintext, sending REDIS_PASSWORD in the clear. Both charts relied on REDIS_SSL for TLS: - plane-mcp-server: services.valkey.remote_ssl - plane-enterprise: external_secrets.redis + env.redis_ssl (the documented ElastiCache setup) With TLS requested, both now give the server a password-free rediss:// REDIS_URL, which every image since v0.3.2 honours end to end; REDIS_PASSWORD fills in the password, so it needs no URL encoding and rotation keeps working. plane-enterprise composes the URL from $(REDIS_HOST):$(REDIS_PORT) so a hostKey/portKey from the Secret still applies. plane-mcp-server's default image moves v0.3.0 -> v0.3.5 (v0.3.0 has no REDIS_URL), and rendering fails if remote_ssl is set with an older semver tag instead of silently falling back to an in-memory store. Charts: plane-mcp-server 1.0.1, plane-enterprise 3.10.13. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(security): guard both MCP image paths against pre-v0.3.2 images Review follow-up on the rediss:// REDIS_URL change: - plane-mcp-server: an explicitly empty services.api.tag skipped the version check while the deployment fell back to v0.3.0. Both now share one fallback, raised to v0.3.5 to match values.yaml, so an empty tag deploys a working image and the check judges the image that actually runs. - plane-enterprise: env.redis_ssl with external_secrets.redis now fails the render when services.mcp_server.image names a semver tag below v0.3.2, which would ignore REDIS_URL and keep the plaintext token store. Images without a semver tag (stable, digests) pass through, as in the MCP chart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…'s SSE (3.11.0 / 1.0.2) (#361) * fix(plane-enterprise, plane-mcp-server): do not buffer the MCP server's SSE The `<release>-body-limit` Middleware is Traefik `buffering`, which holds the whole response until the upstream closes. The MCP server answers over SSE: <prefix>/sse stays open for the session and <prefix>/http/mcp replies as text/event-stream. Behind that middleware the SSE transport never delivers its endpoint event, and every streamed reply arrives only once complete. plane-enterprise (3.10.14): drop the middleware from the MCP route, as /pi/ and the agent-run activity stream already do, and note in the README which routes maxRequestBodyBytes does not cover. plane-mcp-server (1.0.2): every route is MCP, so drop the Middleware and the now-unused ingress.traefik.maxRequestBodyBytes (values, questions.yml, README). Traefik has no request-body cap that does not also buffer the response. nginx and OpenShift renders are unchanged: ingress-nginx defaults to proxy-buffering off, and the OpenShift Routes already carry a 300s timeout for streaming. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(plane-enterprise): version 3.11.0 instead of 3.10.14 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…drop monitor service (#359) * chore(plane-enterprise): route feature flags via api, drop monitor service - FEATURE_FLAG_SERVER_BASE_URL now points at the in-cluster api service (:8000) with FEATURE_FLAG_SERVER_PATH=/services/feature-flags/ in app-env, silo, pi-api-env and argus-env. - PAYMENT_SERVER_BASE_URL is hardcoded to https://prime.plane.so in the same ConfigMaps. - Remove the monitor StatefulSet, its ConfigMap, values, Rancher questions and README section. - Add AI_USAGE_CREDITS_PER_USD (env.pi_envs.ai_usage_credits_per_usd, default 100) to the pi ConfigMap. - Bump chart to 3.10.13. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(plane-enterprise): target Plane v4.0.0 Co-Authored-By: WOZCODE <contact@withwoz.com> * chore(plane-enterprise): default AI usage credits per USD to 10000000 Co-Authored-By: WOZCODE <contact@withwoz.com> * chore(plane-enterprise): default AI agent token budget to 10000000, restore credits per USD to 100 Co-Authored-By: WOZCODE <contact@withwoz.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: WOZCODE <contact@withwoz.com>
Bump planeVersion default, appVersion, questions.yml and README to v4.0.1. Co-authored-by: WOZCODE <contact@withwoz.com>
Bump planeVersion default, appVersion, questions.yml and README to v1.4.3. Co-authored-by: WOZCODE <contact@withwoz.com>
…LS, /dav/ route, /drawio redirect, unbuffered live SSE, search-index Job (3.11.3) (#362) * [INFRA-1261] feat(plane-enterprise): configurable email Service and TLS, /dav/ route, /drawio redirect, unbuffered live SSE Gaps found while installing the chart on EKS behind Traefik with cert-manager (makeplane/commercial-deployments#64, aws/terraform-aws-plane). Email service: - New services.email_service.service block (type, annotations, loadBalancerClass, loadBalancerSourceRanges, externalTrafficPolicy, nodePorts). Defaults render the same LoadBalancer Service as before; type ClusterIP runs the email service without a load balancer. - The keys volume mounts a Secret only when there is one: the chart's Certificate secret (smtp_domain set) or services.email_service.tls_secret_name. Otherwise it is an emptyDir and the app self-signs at startup (plane-ee apps/email/pkg/utils/certs.go, GenerateCerts from main.go), as the kustomize deployment already does. Previously the pod could not start without smtp_domain, because it waited on a Secret nothing created. - services.email_service.generateCert (default true) skips the Certificate while keeping smtp_domain for SMTP_DOMAIN / INTAKE_EMAIL_DOMAIN. - services.email_service.certIssuerRef (name, kind) points the Certificate at an existing Issuer/ClusterIssuer; the chart then stops creating <release>-cert-issuer for the email service. Ingress: - Route /dav/ (WebDAV for Office attachment editing, plane/urls.py) to the api on Traefik, nginx and OpenShift, like /api/. - With drawio local_setup, redirect exactly /drawio to /drawio/ on Traefik (redirectRegex Middleware, relative Location) and nginx (Exact-path Ingress with permanent-redirect). It used to fall through to web. - Give live's SSE responses (POST /live/api/ai/write, GET /live/export/<jobId>/sse; apps/live controllers/ai-write.controller.ts and export.controller.ts) a Traefik route without the buffering middleware, which held each stream back until it closed. Websockets stay on /live/. nginx (proxy-buffering off by default) and OpenShift need no change. PI_BASE_URL is left as is, with a comment: the api has no "off" value for it (PI_URL is "/pi" when it is empty), and the Plane AI OAuth app's redirect URI is built from it once, at first-admin creation, so blanking it would break Plane AI sign-in if pi is enabled later. Chart 3.11.3 (3.11.2 was released from master as Plane v4.0.1). * [INFRA-1261] feat(plane-enterprise): create missing OpenSearch indexes in a post-install/upgrade hook Plane never creates its search indexes, and a missing one makes workspace search return HTTP 500. `manage_search_index index create` cannot run on every upgrade: django-opensearch-dsl 0.7.0 exits 1 on the first index that already exists. New hook Job <release>-api-search-index (post-install,post-upgrade, weight 5, delete policy before-hook-creation,hook-succeeded, backoffLimit 3, activeDeadlineSeconds services.api.searchIndexJob.activeDeadlineSeconds, 3600): - api image and the migrator's env (envFrom, infra creds, extraEnv), custom CA like the api, Plane service account (SigV4 for Amazon OpenSearch). - wait_for_db, wait_for_migrations (the migrator is a plain Job, not a hook), then `manage.py shell -c`: create each registry index that does not exist, then plane_docs.sync_index("create") for docs_semantic. Connection errors and 502/503/504 are retried for ~10 minutes (bundled cluster still starting); anything else fails the Job and the release. - Rendered when services.api.searchIndexJob.enabled (missing key = enabled, explicit false honoured) and OpenSearch can be on: services.opensearch. local_setup, env.opensearch_remote_url or external_secrets. opensearch_existingSecret (the Job exits 0 if that Secret has OPENSEARCH_ENABLED != 1). Defaults (OpenSearch off) render nothing new. Mapping updates and rebuilds stay manual (README). * [INFRA-1261] chore(plane-enterprise): drop the PI_BASE_URL comment from app-env * [INFRA-1261] refactor(plane-enterprise): search-index Job uses Plane's management commands only No Python in the Job spec. The command is now plain shell: - exit 0 when OPENSEARCH_ENABLED is not 1; - wait_for_db, wait_for_migrations; - retry `manage_search_index list` (exists() on every registered index, raises on any error) up to 60 x 10s while OpenSearch comes up; - `manage_search_index index create --force --ignore-error`: skips indexes that already exist, then creates docs_semantic (sync_index("create")); - `manage_search_index list` again, failing unless every index shows [X], since --ignore-error also hides real errors. Output goes to shell variables, not files, so a read-only root filesystem is fine. * [INFRA-1261] fix(plane-enterprise): review follow-ups for the email Service and search-index Job - Document that spec.loadBalancerClass is immutable on an existing LoadBalancer Service, so changing services.email_service.service.loadBalancerClass fails `helm upgrade`; values.yaml and README describe the ClusterIP round-trip. Comment only. - Also render the search-index Job when the global extraEnv sets OPENSEARCH_ENABLED to "1"/"true" (any case) or through valueFrom, which the chart cannot read (new helper plane.extraEnvOpensearchEnabled). A false positive is harmless: the Job exits 0 unless the pod's OPENSEARCH_ENABLED is exactly "1". * refactor(plane-enterprise): rename emailNeedsChartIssuer to emailCertUsesReleaseIssuer * [INFRA-1261] refactor(plane-enterprise): rename the email issuer helper to emailCertIssuerEnabled
… god-mode; remove env.skip_env_var from values (3.11.4) (#368) * [INFRA-1332] feat(plane-enterprise): env.sync_config_from_env, and correct the skip_env_var docs (3.11.4) The chart documented env.skip_env_var: '0' as making the API read instance settings (SMTP, OAuth/OIDC/SAML/LDAP, LLM key, login toggles) from the environment. The commercial API entrypoint forces SKIP_ENV_VAR=1, so the API never sees it; only worker, beat and the consumers do, and the two disagree. Add env.sync_config_from_env (default false). It renders PLANE_SYNC_CONFIG_FROM_ENV=1, which makes the API copy every setting the environment sets into the database on start (plane-ee #10852). It is rendered only when on, so existing releases render byte-identical apart from the version label. Correct values.yaml, the README section and parameter table, the rotation runbook and the install notes. Add a notes warning when skip_env_var is not '1', and a Rancher question for the new value. * [INFRA-1332] docs(plane-enterprise): list the 73 settings sync_config_from_env applies Add a table of every instance setting the API reads from the database, by area, marking the 10 stored encrypted, with the formats that are not plain toggles (seconds, days, comma-separated, JSON). Checked against the registry in plane-ee (core + extended). Correct "seeded from the environment on first startup": the OIDC, SAML and LDAP settings never are, so on a fresh install they could only be set in god-mode. * [INFRA-1332] docs(plane-enterprise): deprecate env.skip_env_var in favour of sync_config_from_env Two switches with overlapping names left it unclear which one to use. The API never honoured skip_env_var, and with sync_config_from_env its only remaining effect is to point worker, beat and the consumers at the environment while the API reads the database. Drop it from values.yaml so sync_config_from_env is the one documented switch, and mark it deprecated in the README, parameter table, runbook and install notes, with a two-step migration (enable the sync, then remove skip_env_var). The template keeps rendering it with the same default, so every existing set of values renders byte-identical. * [INFRA-1332] docs(plane-enterprise): instance settings are managed in god-mode; remove env.skip_env_var from values Instance settings (sign-in, SSO providers, SMTP, LLM key) are managed in the god-mode admin UI by design, not through environment variables. The chart documented env.skip_env_var: '0' as making the API read them from the environment, which the commercial API never did. Drop sync_config_from_env (its app side, plane-ee #10852, was closed) and remove env.skip_env_var from values.yaml. The README, rotation runbook and install notes now say to manage and rotate these settings in god-mode. The template keeps rendering SKIP_ENV_VAR with default "1", so every existing release renders byte-identical, and the install notes warn anyone who still sets '0' to save their settings in god-mode and then remove it.
…from the README (3.11.5) (#372) - New Runner Deployment section (services.runner.*, env.runner_envs.*). - Rows for license.licenseServer, the Postgres read replica, OpenSearch/email runAsUser, live exporter pullPolicy/labels/annotations, silo aes_salt/cors, Plane AI sync/usage/OAuth settings, Bedrock, env.cors_allowed_origins, env.export_download_base_url, serviceAccount.cloudIdentity, external_secrets.storage, silo_connectors_existingSecret and extraObjects.
…itOps (3.11.6) (#369) * [INFRA-1333] feat(plane-enterprise): global.deterministicRender for GitOps (3.11.4) Every render differed, so ArgoCD showed releases as permanently out of sync: the migrator, Plane AI migrator, Argus migrator and MinIO bucket Jobs are named after the render time, and Argus stamps a `timestamp` pod annotation outside the forceRedeploy gate every other workload uses. Add global.deterministicRender (default false). When on, each of those Jobs is named after a hash of its own rendered spec. Identical values give an identical name, and any spec change (image, config checksum, chart version) gives a new Job, which Job immutability requires anyway. Argus moves onto the shared plane.podAnnotations. forceRedeploy still renames the Jobs on every render. Each Job's spec moves into a named template so its name can hash it. With the flag off, renders are byte-identical to before apart from the clock. * [INFRA-1333] fix(plane-enterprise): rerun the Argus and MinIO bucket Jobs when their own config changes (3.11.6) With global.deterministicRender on, two Jobs could keep their name when their input changed: - Argus: argus-env is not in plane.configChecksum, and the per-render timestamp is gone in this mode, so an Argus-only setting changed nothing in the Argus pod template or the migrator spec. argusPodAnnotations now adds checksum/argus-config (a hash of argus-env) in deterministic mode only. Adding argus-env to configChecksum instead would roll every workload. - MinIO bucket Job: the bucket name and credentials come from the doc-store Secret, not the Job spec. Its run suffix now hashes that Secret and the chart version, so the README's "reruns on image, config or chart version" holds. Default-mode renders are unchanged (diffed against master with the clock normalised). Version 3.11.6: master took 3.11.5 in #372.
Conflicts: Chart.yaml takes master's appVersion and moves to 3.12.0. questions.yml keeps both the consumer and mcp_server blocks. app-env.yaml keeps master's WEBHOOK_ALLOWED_HOSTS and token budget defaults plus the INTEGRATION_EVENT_* keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…consumer The template was written against an older master. It skipped the group Secrets (app_keys, silo_connectors), the runner key and plane.infraCredsEnv, so a release using externally managed Secrets would not reach this workload. It also ignored serviceAccount.name, and stamped a timestamp that defeats global.deterministicRender. Regenerate it from master's webhook-consumer, register it in plane.pdbEligible, and add its topologySpreadConstraints and podDisruptionBudget keys to values.yaml. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the
|
Stacked on #302. The consumer isn't on
masteryet, so this targets #302's branch. It brings the same fix as makeplane/helm-charts-private#762 to this chart.Reviewing this PR
The diff is large because it includes a merge of
master: #302 was 41 commits behind, and the helpers this fix relies on exist only onmaster. Review commit 22064b6, which is the actual fix. The merge commit resolves three conflicts:Chart.yaml:master'sappVersion, version 3.12.0questions.yml: keeps both the consumer andmcp_serverblocksapp-env.yaml:master'sWEBHOOK_ALLOWED_HOSTSand token budget defaults, plus theINTEGRATION_EVENT_*keysProblem
#302's template was written against an older
master, andwebhook-consumerhas since moved on. As it stands, the consumer misses:external_secrets.app_keys_existingSecretandsilo_connectors_existingSecretplane.infraCredsEnv, which delivers externally managed Postgres, RabbitMQ, Redis, OpenSearch and storage credentialsserviceAccount.nameand the service account pod labelsplane.podAnnotations, because it still stamps a timestamp, which defeatsglobal.deterministicRenderplane.rollingUpdateStrategy,plane.podSpreadand PDB eligibilityChanges
master'swebhook-consumer, so they differ only by service nameplane.pdbEligibletopologySpreadConstraintsandpodDisruptionBudgetto its valuesTesting
Rendered with
alpine/helm:3.14.0, withglobal.deterministicRenderon:app,doc-store,opensearchandsilo. This branch addsrunner,app-keysandsilo-connectors, and itsenvFrommatcheswebhook-consumerapart from each service's own vars ConfigMap.serviceAccount.name, a PDB and a spread constraint all render on the consumer.master: the only differences are the newINTEGRATION_EVENT_*keys inapp-varsandsilo-vars, thehelm.sh/chartlabel, config checksum annotations and hashed Job names.helm lintpasses.Not deployed to a cluster.
Before releasing
🤖 Generated with Claude Code