diff --git a/kustomize/README.md b/kustomize/README.md index 4f90e4a..01a6d04 100644 --- a/kustomize/README.md +++ b/kustomize/README.md @@ -160,7 +160,7 @@ Additional features that can be enabled: ### Optional but Recommended -- **Ingress Controller** – Traefik is the recommended default. Install it yourself (e.g. below or via Helm). Optional overlay components `ingress-nginx` and `aws-load-balancer-controller` only patch the plane Ingress with controller-specific annotations. See [AWS_load_balancer_setup.md](AWS_load_balancer_setup.md) for the ALB controller. +- **Ingress Controller** – Traefik is the recommended default. Install it yourself (e.g. below or via Helm). Optional overlay components `ingress-nginx` and `aws-load-balancer-controller` only patch the plane Ingress with controller-specific annotations. See [AWS_load_balancer_setup.md](AWS_load_balancer_setup.md) for the ALB controller. On OpenShift use `ingress-openshift` instead, which swaps the Ingress for native Routes — see [Running on OpenShift](#running-on-openshift). ```bash # Add Traefik Helm repo and install Traefik ingress controller helm repo add traefik https://traefik.github.io/charts @@ -620,6 +620,44 @@ components: No extra configuration is required. The component patches all base Deployments (api, web, space, admin, live, worker, etc.) with the same non-root security context. Ensure your container images support running as the configured user (e.g. UID 1000); Plane Commercial images are built to run as non-root. +## Running on OpenShift + +OpenShift is the inverse case: it refuses to let you pick the UID at all. The default `restricted-v2` SCC ignores the image's `USER`, assigns an arbitrary UID from the namespace's range, and places the process in group 0. It also validates the pod's own request with `MustRunAsRange`, so a manifest asking for a *specific* `runAsUser`/`runAsGroup`/`fsGroup` outside that range is **rejected at admission** — meaning `nonroot-security-context`, which pins 1000, stops every pod from scheduling. + +**Start from [`overlays/openshift/`](overlays/openshift/README.md)**, which wires this up and ships its own `vars.yaml.example` / `secrets-vars.yaml.example`: + +```bash +cd overlays/openshift +cp vars.yaml.example vars.yaml +cp secrets-vars.yaml.example secrets-vars.yaml +# edit both, then: +oc new-project plane-openshift +oc kustomize . | oc apply -f - +``` + +Copying `default` and toggling components does not work — it pins `runAsUser: 1000` and ships the in-cluster datastores, and OpenShift rejects both. The overlay differs from `default` in exactly three ways: + +```yaml +# overlays/openshift/kustomization.yaml +components: + - ../../components/openshift-security-context # NOT nonroot-security-context + - ../../components/ingress-openshift # NOT ingress-nginx / ingress-traefik + # postgres / redis / rabbitmq / minio / opensearch omitted — external only +``` + +| Component | What it does | +| --------- | ------------ | +| `openshift-security-context` | Same hardening as `nonroot-security-context` — `runAsNonRoot`, `capabilities.drop: [ALL]`, `seccompProfile: RuntimeDefault` — but names no UID, so the SCC assigns one. | +| `ingress-openshift` | Removes the base nginx `Ingress` and adds one `route.openshift.io/v1` Route per path, each with `haproxy.router.openshift.io/timeout: 300s`. Carries its own `APP_DOMAIN` replacement, so no extra overlay wiring is needed. | + +Three things to know: + +- **Image requirement.** The images must grant group 0 write access to the paths they write at runtime. Older images crash under an arbitrary UID — nginx exits with `mkdir() "/var/cache/nginx/client_temp" failed (13: Permission denied)` and celery beat cannot create its schedule DB. +- **Remove the bundled datastores.** `postgres`, `redis`, `rabbitmq`, `minio` and `opensearch` are third-party images with baked-in UID and data-directory ownership. They cannot run under an arbitrary UID, and (as with `nonroot-security-context`) the security-context component does not patch them — they are listed after it in the overlay. Drop those components, point the overlay at managed services, or grant their ServiceAccounts a relaxed SCC. +- **Upgrading an existing deployment is safe.** Moving a running install from the pinned-uid-1000 posture to this one needs no data migration: kubelet re-applies `fsGroup` to PVC contents on mount, so data written by the old deployment stays readable and writable by the new UID. + +The alternative to `ingress-openshift` is to keep the base `Ingress` and set `INGRESS_CLASS: "openshift-default"` in `vars.yaml`, letting OpenShift's ingress-to-route controller do the conversion. That works, but the controller only converts an `Ingress` whose class maps to `openshift.io/ingress-to-route` (`nginx` is ignored), and whether the per-route HAProxy annotations survive the conversion varies by OCP version. Note that **Routes have no request-body size limit** — Traefik's `plane-body-limit` middleware has no equivalent, so enforce upload limits in the application or at a WAF/CDN. + ## Configuration ### Configuration System diff --git a/kustomize/components/ingress-openshift/kustomization.yaml b/kustomize/components/ingress-openshift/kustomization.yaml new file mode 100644 index 0000000..9b1b9ee --- /dev/null +++ b/kustomize/components/ingress-openshift/kustomization.yaml @@ -0,0 +1,64 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component + +# OpenShift ingress: replaces the base nginx Ingress with one +# route.openshift.io/v1 Route per path, served by the cluster's HAProxy router. +# +# Pair with the `openshift-security-context` component; do NOT also enable +# `ingress-nginx`, `ingress-traefik` or `aws-load-balancer-controller`. +# +# WHY EXPLICIT ROUTES +# +# OpenShift's ingress-to-route controller can convert a plain Ingress into Routes, +# and that path also works: skip this component, keep the base Ingress, and set +# INGRESS_CLASS to "openshift-default" in vars.yaml (the controller only picks up +# an Ingress whose class maps to openshift.io/ingress-to-route -- "nginx" is +# ignored). But Plane needs a per-route HAProxy timeout, and whether annotations +# survive that conversion varies by OCP version. Declaring the Routes removes the +# guesswork. +# +# WHAT IS LOST vs THE TRAEFIK PATH +# +# - No body-size limit. Traefik's `plane-body-limit` Middleware +# (buffering.maxRequestBodyBytes) has no Route equivalent -- HAProxy cannot cap +# request bodies per route. Enforce upload limits in the app or at a WAF/CDN. +# - No path rewriting. A path needing a prefix strip (e.g. the draw-io component's +# stripPrefix) requires `haproxy.router.openshift.io/rewrite-target` on that +# Route instead, and Routes cannot express Traefik's redirectRegex at all. +# - Path-based Routes require edge or reencrypt termination; passthrough cannot +# do path routing. +# +# TLS: these Routes edge-terminate on the Ingress Operator's wildcard certificate. +# To serve your own cert, add `spec.tls.externalCertificate.name: ` +# (OpenShift 4.16+, and the router needs a RoleBinding to read that Secret). + +resources: + - routes.yaml + +# The base nginx Ingress is inert on OpenShift (no controller serves class +# "nginx", and ingress-to-route ignores it). Remove it so the Routes are the only +# ingress surface and there is nothing stale to mislead an operator. +patches: + - target: + kind: Ingress + name: plane-ingress + patch: | + $patch: delete + apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: plane-ingress + +# The overlay's own replacement targets `kind: Ingress` for the host, which no +# longer exists once the patch above runs. Carry the equivalent here so an overlay +# needs no extra wiring beyond enabling this component. +replacements: + - source: + kind: ConfigMap + name: overlay-vars + fieldPath: data.APP_DOMAIN + targets: + - select: + kind: Route + fieldPaths: + - spec.host diff --git a/kustomize/components/ingress-openshift/routes.yaml b/kustomize/components/ingress-openshift/routes.yaml new file mode 100644 index 0000000..381f1c1 --- /dev/null +++ b/kustomize/components/ingress-openshift/routes.yaml @@ -0,0 +1,164 @@ +# OpenShift Routes — one per path, mirroring base/ingress.yaml. +# +# `host` is the placeholder that the overlay's DOMAIN_NAME/APP_DOMAIN replacement +# rewrites (see kustomization.yaml in this directory, which carries the matching +# replacement so the overlay needs no extra wiring). +# +# Ordering is irrelevant: HAProxy matches the most specific path prefix, so the +# "/" catch-all cannot shadow the others the way it would in Traefik. +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: plane-route-spaces + namespace: plane + annotations: + # The router default is 30s, which severs /live/'s collaborative-editing + # WebSockets and /pi/'s streaming responses. Applied uniformly for simplicity. + haproxy.router.openshift.io/timeout: 300s +spec: + host: plane.example.com + path: /spaces/ + to: { kind: Service, name: plane-space, weight: 100 } + port: { targetPort: 3000 } + tls: { termination: edge, insecureEdgeTerminationPolicy: Redirect } +--- +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: plane-route-god-mode + namespace: plane + annotations: + haproxy.router.openshift.io/timeout: 300s +spec: + host: plane.example.com + path: /god-mode/ + to: { kind: Service, name: plane-admin, weight: 100 } + port: { targetPort: 3000 } + tls: { termination: edge, insecureEdgeTerminationPolicy: Redirect } +--- +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: plane-route-api + namespace: plane + annotations: + haproxy.router.openshift.io/timeout: 300s +spec: + host: plane.example.com + path: /api/ + to: { kind: Service, name: plane-api, weight: 100 } + port: { targetPort: 8000 } + tls: { termination: edge, insecureEdgeTerminationPolicy: Redirect } +--- +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: plane-route-auth + namespace: plane + annotations: + haproxy.router.openshift.io/timeout: 300s +spec: + host: plane.example.com + path: /auth/ + to: { kind: Service, name: plane-api, weight: 100 } + port: { targetPort: 8000 } + tls: { termination: edge, insecureEdgeTerminationPolicy: Redirect } +--- +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: plane-route-graphql + namespace: plane + annotations: + haproxy.router.openshift.io/timeout: 300s +spec: + host: plane.example.com + path: /graphql/ + to: { kind: Service, name: plane-api, weight: 100 } + port: { targetPort: 8000 } + tls: { termination: edge, insecureEdgeTerminationPolicy: Redirect } +--- +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: plane-route-marketplace + namespace: plane + annotations: + haproxy.router.openshift.io/timeout: 300s +spec: + host: plane.example.com + path: /marketplace/ + to: { kind: Service, name: plane-api, weight: 100 } + port: { targetPort: 8000 } + tls: { termination: edge, insecureEdgeTerminationPolicy: Redirect } +--- +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: plane-route-dav + namespace: plane + annotations: + haproxy.router.openshift.io/timeout: 300s +spec: + host: plane.example.com + path: /dav/ + to: { kind: Service, name: plane-api, weight: 100 } + port: { targetPort: 8000 } + tls: { termination: edge, insecureEdgeTerminationPolicy: Redirect } +--- +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: plane-route-live + namespace: plane + annotations: + haproxy.router.openshift.io/timeout: 300s +spec: + host: plane.example.com + path: /live/ + to: { kind: Service, name: plane-live, weight: 100 } + port: { targetPort: 3000 } + tls: { termination: edge, insecureEdgeTerminationPolicy: Redirect } +--- +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: plane-route-silo + namespace: plane + annotations: + haproxy.router.openshift.io/timeout: 300s +spec: + host: plane.example.com + path: /silo/ + to: { kind: Service, name: plane-silo, weight: 100 } + port: { targetPort: 3000 } + tls: { termination: edge, insecureEdgeTerminationPolicy: Redirect } +--- +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: plane-route-pi + namespace: plane + annotations: + haproxy.router.openshift.io/timeout: 300s +spec: + host: plane.example.com + path: /pi/ + to: { kind: Service, name: plane-pi-api, weight: 100 } + port: { targetPort: 8000 } + tls: { termination: edge, insecureEdgeTerminationPolicy: Redirect } +--- +# Catch-all LAST, purely for readability. +apiVersion: route.openshift.io/v1 +kind: Route +metadata: + name: plane-route-web + namespace: plane + annotations: + haproxy.router.openshift.io/timeout: 300s +spec: + host: plane.example.com + path: / + to: { kind: Service, name: plane-web, weight: 100 } + port: { targetPort: 3000 } + tls: { termination: edge, insecureEdgeTerminationPolicy: Redirect } diff --git a/kustomize/components/openshift-security-context/kustomization.yaml b/kustomize/components/openshift-security-context/kustomization.yaml new file mode 100644 index 0000000..b04dd06 --- /dev/null +++ b/kustomize/components/openshift-security-context/kustomization.yaml @@ -0,0 +1,46 @@ +apiVersion: kustomize.config.k8s.io/v1alpha1 +kind: Component + +# OpenShift "restricted-v2" SCC security context. +# +# Use this INSTEAD OF `nonroot-security-context` on OpenShift. The two differ in +# exactly one respect: this one does not name a UID. +# +# OpenShift ignores the image's USER, assigns an arbitrary UID from the +# namespace's range, and places the process in group 0. It also validates the +# pod's own request with MustRunAsRange, so a manifest asking for a specific +# runAsUser / runAsGroup / fsGroup outside that range is REJECTED at admission -- +# `nonroot-security-context` pins 1000 and therefore prevents every pod from +# scheduling. Everything else (runAsNonRoot, dropped capabilities, seccomp) is +# kept; only the IDs are left for the platform to assign. +# +# REQUIRES images that grant group 0 write access to the paths they write at +# runtime (plane-ee #9018). Older images crash under an arbitrary UID -- nginx +# exits with `mkdir() "/var/cache/nginx/client_temp" failed (13: Permission +# denied)` and celery beat cannot create its schedule DB. +# +# NOT for the bundled datastores. postgres / redis / rabbitmq / minio / +# opensearch are third-party images with baked-in UID and data-directory +# ownership; they cannot run under an arbitrary UID. On OpenShift use managed +# services and leave those components out of the overlay, or grant their +# ServiceAccounts a relaxed SCC. + +patches: + - path: patches/pod-security-context.json + target: + kind: Deployment + - path: patches/container-security-context.json + target: + kind: Deployment + - path: patches/pod-security-context.json + target: + kind: StatefulSet + - path: patches/container-security-context.json + target: + kind: StatefulSet + - path: patches/pod-security-context.json + target: + kind: Job + - path: patches/container-security-context.json + target: + kind: Job diff --git a/kustomize/components/openshift-security-context/patches/container-security-context.json b/kustomize/components/openshift-security-context/patches/container-security-context.json new file mode 100644 index 0000000..24daa08 --- /dev/null +++ b/kustomize/components/openshift-security-context/patches/container-security-context.json @@ -0,0 +1,16 @@ +[ + { + "op": "add", + "path": "/spec/template/spec/containers/0/securityContext", + "value": { + "allowPrivilegeEscalation": false, + "capabilities": { + "drop": ["ALL"] + }, + "runAsNonRoot": true, + "seccompProfile": { + "type": "RuntimeDefault" + } + } + } +] diff --git a/kustomize/components/openshift-security-context/patches/pod-security-context.json b/kustomize/components/openshift-security-context/patches/pod-security-context.json new file mode 100644 index 0000000..96d5a17 --- /dev/null +++ b/kustomize/components/openshift-security-context/patches/pod-security-context.json @@ -0,0 +1,12 @@ +[ + { + "op": "add", + "path": "/spec/template/spec/securityContext", + "value": { + "runAsNonRoot": true, + "seccompProfile": { + "type": "RuntimeDefault" + } + } + } +] diff --git a/kustomize/overlays/openshift/README.md b/kustomize/overlays/openshift/README.md new file mode 100644 index 0000000..55e697a --- /dev/null +++ b/kustomize/overlays/openshift/README.md @@ -0,0 +1,121 @@ +# overlay: openshift + +Deploys Plane to the `plane-openshift` namespace under OpenShift's default +**`restricted-v2`** SCC, with **native Routes** instead of an Ingress and **no +bundled datastores**. + +Start from this overlay for any OpenShift cluster. Copying `default` and toggling +components does not work: `default` pins `runAsUser: 1000` and ships the +in-cluster datastores, and both are rejected on OpenShift. + +--- + +## What OpenShift changes + +`restricted-v2` ignores the image's `USER`, assigns an **arbitrary UID** from the +namespace's range, and places the process in **group 0**. It also validates the +pod's own request with `MustRunAsRange`, so a manifest asking for a *specific* +`runAsUser`/`runAsGroup`/`fsGroup` outside that range is **rejected at +admission** — nothing schedules, and the error comes from admission rather than +the workload. + +Three consequences, all handled by this overlay: + +| | `default` overlay | this overlay | +|---|---|---| +| Hardening | `nonroot-security-context` (pins uid 1000) | `openshift-security-context` (no UID named) | +| Ingress | base nginx `Ingress` | `ingress-openshift` → one Route per path | +| Datastores | postgres/redis/rabbitmq/minio/opensearch in-cluster | **external only** | + +--- + +## Prerequisites + +- **Images that support an arbitrary UID.** They must grant group 0 write access + to the paths they write at runtime. Older images crash — nginx exits with + `mkdir() "/var/cache/nginx/client_temp" failed (13: Permission denied)` and + celery beat cannot create its schedule DB. +- **External PostgreSQL, Redis, RabbitMQ, S3 and (optionally) OpenSearch.** The + bundled ones are third-party images with baked-in UID and data-directory + ownership; they cannot run under an arbitrary UID. +- A **storage class** for the monitor PVC — check `oc get storageclass` and set + `MONITOR_STORAGE_CLASS` accordingly (`gp3-csi` on ROSA, `thin-csi` on vSphere, + `ocs-storagecluster-ceph-rbd` on ODF). + +--- + +## Quick start + +```bash +# 1. Copy and fill in the config files +cp vars.yaml.example vars.yaml +cp secrets-vars.yaml.example secrets-vars.yaml + +# 2. Edit vars.yaml — set APP_DOMAIN, WEB_URL, CORS_ALLOWED_ORIGINS, +# MONITOR_STORAGE_CLASS +# 3. Edit secrets-vars.yaml — point every connection string at your EXTERNAL +# services, then set SECRET_KEY, AES_SECRET_KEY, LIVE_SERVER_SECRET_KEY +# (generate with: openssl rand -hex 32) + +# 4. Create the project +oc new-project plane-openshift + +# 5. Validate before applying — this is where an SCC rejection would surface +oc kustomize . | oc apply --dry-run=server -f - + +# 6. Apply +oc kustomize . | oc apply -f - +``` + +Confirm the SCC that was actually applied, and the UID it handed out: + +```bash +oc get pods -o custom-columns=\ +NAME:.metadata.name,SCC:.metadata.annotations.openshift\\.io/scc,UID:.spec.securityContext.runAsUser +``` + +--- + +## Enabled components + +| Component | Status | Notes | +|-----------|--------|-------| +| `openshift-security-context` | **enabled** | Hardening without a pinned UID. Never enable `nonroot-security-context` here. | +| `ingress-openshift` | **enabled** | Removes the base Ingress, adds 11 Routes with `haproxy.router.openshift.io/timeout: 300s`. | +| `email-service` | enabled | Transactional email | +| `static-db-url` | enabled | Injects `DATABASE_URL` into **both** `plane-app-secrets` and `plane-silo-secrets`. Silo gets it from nowhere else. | +| `s3-static-credentials` | enabled | Injects the AWS keys. Drop it only if the ServiceAccount assumes a cloud role — and then omit the key fields entirely rather than blanking them, since an empty `AWS_ACCESS_KEY_ID` shadows the role. | +| `runner` | enabled | Node automation runner | +| `external-api` | enabled | Cloud-entrypoint API workload | +| `worker-importers` | enabled | `celery.importer` queue | +| `webhook-consumer` | enabled | RabbitMQ consumer for webhook delivery | +| `agent-consumer` | enabled | RabbitMQ consumer for agent events | +| `custom-ca` | disabled | Uncomment to mount a private CA | +| `opensearch-external-auth` | disabled | Uncomment for OpenSearch basic auth | +| `otel-observability` | disabled | See `overlays/phoenix` for a worked example | +| `postgres` / `redis` / `rabbitmq` / `minio` / `opensearch` | **omitted** | Cannot run under an arbitrary UID — use external services | +| `ingress-nginx` / `ingress-traefik` / `aws-load-balancer-controller` | **omitted** | Would conflict with the Routes | + +--- + +## Known gaps + +- **No request-body limit.** OpenShift Routes cannot cap request bodies the way + Traefik's `plane-body-limit` middleware does, so `FILE_SIZE_LIMIT` in + `secrets-vars.yaml` is the only upload guard. +- **No path rewriting.** A path needing a prefix strip (e.g. the `draw-io` + component) requires `haproxy.router.openshift.io/rewrite-target` on that Route. + `draw-io` is therefore not enabled here. +- **TLS** is the Ingress Operator's wildcard certificate, via `termination: edge`. + To serve your own, add `spec.tls.externalCertificate.name` to the Routes + (OpenShift 4.16+, and the router needs a RoleBinding to read that Secret). + +--- + +## Upgrading an existing install onto this overlay + +Moving from the pinned-uid-1000 posture needs **no data migration**. kubelet +re-applies `fsGroup` to PVC contents on mount, so data written by the old +deployment stays readable and writable by the newly assigned UID. This was +verified end to end against the monitor PVC, which is the only stateful +first-party workload. diff --git a/kustomize/overlays/openshift/kustomization.yaml b/kustomize/overlays/openshift/kustomization.yaml new file mode 100644 index 0000000..9c6842f --- /dev/null +++ b/kustomize/overlays/openshift/kustomization.yaml @@ -0,0 +1,486 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: plane-openshift + +resources: + - ../../base + - vars.yaml + - secrets-vars.yaml + +# ============================================================ +# COMPONENTS +# ============================================================ +# Enable/disable optional capabilities by commenting in/out entries. +components: + # Email service (transactional email) + - ../../components/email-service + + # Credential injection from secrets-vars.yaml. These are about STATIC + # credentials vs a cloud workload identity -- NOT about in-cluster vs external + # datastores -- so they are needed here too: + # static-db-url -> DATABASE_URL into plane-app-secrets AND + # plane-silo-secrets (silo gets it from nowhere else) + # s3-static-credentials -> AWS keys into plane-doc-store-secrets + # Drop s3-static-credentials only if the ServiceAccount assumes a cloud role + # instead, and then leave the AWS key fields out of secrets-vars.yaml entirely + # rather than setting them empty -- an empty AWS_ACCESS_KEY_ID shadows the role. + - ../../components/static-db-url + - ../../components/s3-static-credentials + + # Ingress: OpenShift Routes. Replaces the base nginx Ingress, which no + # controller on OpenShift serves. Do NOT also enable ingress-nginx, + # ingress-traefik or aws-load-balancer-controller. + - ../../components/ingress-openshift + + # Hardening: the OpenShift variant. Same controls as nonroot-security-context + # but names no UID, because restricted-v2 assigns one from the namespace's + # range and REJECTS any pod that asks for a specific runAsUser/fsGroup. + - ../../components/openshift-security-context + + # Node runner service (automation script execution) + - ../../components/runner + + # External API (cloud-entrypoint API workload) + - ../../components/external-api + + # Worker importers (celery.importer queue) + - ../../components/worker-importers + + # Webhook consumer (RabbitMQ consumer for webhook delivery) + - ../../components/webhook-consumer + + # Agent consumer (RabbitMQ consumer for agent events) + - ../../components/agent-consumer + + # Optional: external OpenSearch with username/password auth + # - ../../components/opensearch-external-auth + + # Optional: mount a custom CA certificate into all containers + # Paste your CA cert (PEM) into ../../components/custom-ca/customCA.crt, then uncomment: + # - ../../components/custom-ca + + # Optional: OpenTelemetry env vars for the backend-api. Enable AFTER custom-ca, + # add the OTEL_* vars to vars.yaml and the OTEL replacements to this file + # (see overlays/phoenix for a worked example). + # - ../../components/otel-observability + + # ------------------------------------------------------------------ + # NO BUNDLED DATASTORES. + # + # postgres / redis / rabbitmq / minio / opensearch are third-party images with + # baked-in UID and data-directory ownership. They cannot run under the + # arbitrary UID restricted-v2 assigns, so this overlay expects EXTERNAL + # services and the corresponding hosts/credentials in vars.yaml + + # secrets-vars.yaml. + # + # If you must run them in-cluster, grant their ServiceAccounts a relaxed SCC + # (`oc adm policy add-scc-to-user anyuid -z plane-srv-account`), re-add the + # components below, and restore the storage-class patches that this overlay + # drops along with them. + # + # - ../../components/postgres + # - ../../components/redis + # - ../../components/rabbitmq + # - ../../components/minio + # - ../../components/opensearch + # ------------------------------------------------------------------ + +# ============================================================ +# APPLICATION VERSION +# ============================================================ +# Pin ALL images to the same release tag. Update this for upgrades. +# Image tags for all artifacts.plane.so/makeplane/*-commercial workloads are +# driven from APP_VERSION in vars.yaml at build time (see the APP_VERSION +# replacement in the replacements: block below). The `newTag` values here +# exist ONLY so the `images:` transformer tags bare image references injected +# by patches (e.g. custom-ca's init container) — they are ALWAYS overridden +# by the replacement. To bump versions, edit APP_VERSION in vars.yaml. +images: + - name: artifacts.plane.so/makeplane/backend-commercial + newTag: app-version-driven + - name: artifacts.plane.so/makeplane/web-commercial + newTag: app-version-driven + - name: artifacts.plane.so/makeplane/space-commercial + newTag: app-version-driven + - name: artifacts.plane.so/makeplane/admin-commercial + newTag: app-version-driven + - name: artifacts.plane.so/makeplane/live-commercial + newTag: app-version-driven + - name: artifacts.plane.so/makeplane/monitor-commercial + newTag: app-version-driven + - name: artifacts.plane.so/makeplane/silo-commercial + newTag: app-version-driven + - name: artifacts.plane.so/makeplane/email-commercial + newTag: app-version-driven + - name: artifacts.plane.so/makeplane/node-runner-commercial + newTag: app-version-driven + # Independent versioning — not driven by APP_VERSION: + - name: artifacts.plane.so/makeplane/iframely + newTag: v1.2.0 + +# ============================================================ +# REPLACEMENTS +# ============================================================ +replacements: + # ====== FROM vars.yaml (ConfigMap) ====== + + - source: + kind: ConfigMap + name: overlay-vars + fieldPath: data.APP_VERSION + targets: + - select: + kind: ConfigMap + name: plane-monitor-vars + fieldPaths: + - data.APP_VERSION + - select: + kind: ConfigMap + name: plane-app-vars + fieldPaths: + - data.APP_VERSION + + # Drive every workload's image tag from APP_VERSION (single source of truth). + # plane-iframely-wl has independent versioning — rejected. + # In-cluster infra StatefulSets (postgres/redis/rabbitmq/minio/opensearch) and + # the minio bucket-create Job use unrelated images — only the matching + # workloads (plane-monitor StatefulSet, plane-migrator Job) are explicitly + # selected so infra is never retagged. + # If custom-ca is enabled in this overlay, add an initContainers.*.image target + # block (mirroring the phoenix overlay) to tag the CA-cert init containers. + - source: + kind: ConfigMap + name: overlay-vars + fieldPath: data.APP_VERSION + targets: + - select: + kind: Deployment + reject: + - name: plane-iframely-wl + - name: plane-draw-io-wl # pinned makeplane/drawio tag — not APP_VERSION-driven + fieldPaths: + - spec.template.spec.containers.*.image + options: + delimiter: ":" + index: 1 + - select: + kind: StatefulSet + name: plane-monitor + fieldPaths: + - spec.template.spec.containers.*.image + options: + delimiter: ":" + index: 1 + - select: + kind: Job + name: plane-migrator + fieldPaths: + - spec.template.spec.containers.*.image + options: + delimiter: ":" + index: 1 + + - source: + kind: ConfigMap + name: overlay-vars + fieldPath: data.APP_DOMAIN + targets: + - select: + kind: ConfigMap + name: plane-app-vars + fieldPaths: + - data.APP_DOMAIN + - select: + kind: ConfigMap + name: plane-monitor-vars + fieldPaths: + - data.APP_DOMAIN + + # NOTE: the `default` overlay has two replacements here that target + # kind: Ingress (spec.rules.0.host from APP_DOMAIN, spec.ingressClassName from + # INGRESS_CLASS). Both are omitted: the ingress-openshift component deletes the + # base Ingress, and it carries its own APP_DOMAIN -> Route spec.host + # replacement, so nothing here needs to know about ingress. + + - source: + kind: ConfigMap + name: overlay-vars + fieldPath: data.WEB_URL + targets: + - select: + kind: ConfigMap + name: plane-app-vars + fieldPaths: + - data.WEB_URL + - data.APP_BASE_URL + - select: + kind: ConfigMap + name: plane-silo-vars + fieldPaths: + - data.APP_BASE_URL + - data.SILO_API_BASE_URL + + - source: + kind: ConfigMap + name: overlay-vars + fieldPath: data.CORS_ALLOWED_ORIGINS + targets: + - select: + kind: ConfigMap + name: plane-app-vars + fieldPaths: + - data.CORS_ALLOWED_ORIGINS + + - source: + kind: ConfigMap + name: overlay-vars + fieldPath: data.ALLOW_ALL_ATTACHMENT_TYPES + targets: + - select: + kind: ConfigMap + name: plane-app-vars + fieldPaths: + - data.ALLOW_ALL_ATTACHMENT_TYPES + + - source: + kind: ConfigMap + name: overlay-vars + fieldPath: data.IS_AIRGAPPED + targets: + - select: + kind: ConfigMap + name: plane-monitor-vars + fieldPaths: + - data.IS_AIRGAPPED + - select: + kind: ConfigMap + name: plane-app-vars + fieldPaths: + - data.IS_AIRGAPPED + + - source: + kind: ConfigMap + name: overlay-vars + fieldPath: data.MONITOR_STORAGE_CLASS + targets: + - select: + kind: StatefulSet + name: plane-monitor + fieldPaths: + - spec.volumeClaimTemplates.0.spec.storageClassName + + - source: + kind: ConfigMap + name: overlay-vars + fieldPath: data.PRIME_HOST + targets: + - select: + kind: ConfigMap + name: plane-app-vars + fieldPaths: + - data.PRIME_HOST + - select: + kind: ConfigMap + name: plane-monitor-vars + fieldPaths: + - data.PRIME_HOST + + - source: + kind: ConfigMap + name: overlay-vars + fieldPath: data.OPENSEARCH_INDEX_PREFIX + targets: + - select: + kind: Secret + name: plane-opensearch-secrets + fieldPaths: + - stringData.OPENSEARCH_INDEX_PREFIX + + # ====== FROM secrets-vars.yaml (Secret) ====== + # DATABASE_URL — overlay-level replacement overrides the postgres component's app-db-url patch, + # which otherwise wins because component patches run before component replacements. + - source: + kind: Secret + name: overlay-secret-vars + fieldPath: stringData.DATABASE_URL + targets: + - select: + kind: Secret + name: plane-app-secrets + fieldPaths: + - stringData.DATABASE_URL + + # AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY are injected by the s3-static-credentials component + + - source: + kind: Secret + name: overlay-secret-vars + fieldPath: stringData.REDIS_URL + targets: + - select: + kind: Secret + name: plane-app-secrets + fieldPaths: + - stringData.REDIS_URL + - select: + kind: Secret + name: plane-live-secrets + fieldPaths: + - stringData.REDIS_URL + - select: + kind: Secret + name: plane-silo-secrets + fieldPaths: + - stringData.REDIS_URL + + - source: + kind: Secret + name: overlay-secret-vars + fieldPath: stringData.AMQP_URL + targets: + - select: + kind: Secret + name: plane-app-secrets + fieldPaths: + - stringData.AMQP_URL + - select: + kind: Secret + name: plane-silo-secrets + fieldPaths: + - stringData.AMQP_URL + - select: + kind: Secret + name: plane-live-secrets + fieldPaths: + - stringData.AMQP_URL + + - source: + kind: Secret + name: overlay-secret-vars + fieldPath: stringData.SECRET_KEY + targets: + - select: + kind: Secret + name: plane-app-secrets + fieldPaths: + - stringData.SECRET_KEY + + - source: + kind: Secret + name: overlay-secret-vars + fieldPath: stringData.AES_SECRET_KEY + targets: + - select: + kind: Secret + name: plane-app-secrets + fieldPaths: + - stringData.AES_SECRET_KEY + + # LIVE_SERVER_SECRET_KEY must match across app and live for HMAC signatures to validate. + # (Add a plane-pi-api-secrets target if the pi-service component is enabled in this overlay.) + - source: + kind: Secret + name: overlay-secret-vars + fieldPath: stringData.LIVE_SERVER_SECRET_KEY + targets: + - select: + kind: Secret + name: plane-app-secrets + fieldPaths: + - stringData.LIVE_SERVER_SECRET_KEY + - select: + kind: Secret + name: plane-live-secrets + fieldPaths: + - stringData.LIVE_SERVER_SECRET_KEY + + - source: + kind: Secret + name: overlay-secret-vars + fieldPath: stringData.OPENSEARCH_ENABLED + targets: + - select: + kind: Secret + name: plane-opensearch-secrets + fieldPaths: + - stringData.OPENSEARCH_ENABLED + + - source: + kind: Secret + name: overlay-secret-vars + fieldPath: stringData.OPENSEARCH_URL + targets: + - select: + kind: Secret + name: plane-opensearch-secrets + fieldPaths: + - stringData.OPENSEARCH_URL + + - source: + kind: Secret + name: overlay-secret-vars + fieldPath: stringData.AWS_S3_ENDPOINT_URL + targets: + - select: + kind: Secret + name: plane-doc-store-secrets + fieldPaths: + - stringData.AWS_S3_ENDPOINT_URL + + - source: + kind: Secret + name: overlay-secret-vars + fieldPath: stringData.AWS_S3_BUCKET_NAME + targets: + - select: + kind: Secret + name: plane-doc-store-secrets + fieldPaths: + - stringData.AWS_S3_BUCKET_NAME + + - source: + kind: Secret + name: overlay-secret-vars + fieldPath: stringData.AWS_REGION + targets: + - select: + kind: Secret + name: plane-doc-store-secrets + fieldPaths: + - stringData.AWS_REGION + + - source: + kind: Secret + name: overlay-secret-vars + fieldPath: stringData.USE_MINIO + targets: + - select: + kind: Secret + name: plane-doc-store-secrets + fieldPaths: + - stringData.USE_MINIO + + - source: + kind: Secret + name: overlay-secret-vars + fieldPath: stringData.FILE_SIZE_LIMIT + targets: + - select: + kind: Secret + name: plane-doc-store-secrets + fieldPaths: + - stringData.FILE_SIZE_LIMIT + + # SMTP_DOMAIN — replaces the hardcoded value in plane-email-vars configmap + # and the dnsNames[0] in the Certificate resource (when email-tls-cert component is enabled) + - source: + kind: ConfigMap + name: overlay-vars + fieldPath: data.SMTP_DOMAIN + targets: + - select: + kind: ConfigMap + name: plane-email-vars + fieldPaths: + - data.SMTP_DOMAIN diff --git a/kustomize/overlays/openshift/secrets-vars.yaml.example b/kustomize/overlays/openshift/secrets-vars.yaml.example new file mode 100644 index 0000000..d84218b --- /dev/null +++ b/kustomize/overlays/openshift/secrets-vars.yaml.example @@ -0,0 +1,65 @@ +# Example secrets configuration file for the OpenShift overlay. +# +# Copy this file to secrets-vars.yaml and update with your actual values: +# cp secrets-vars.yaml.example secrets-vars.yaml +# +# WARNING: secrets-vars.yaml contains sensitive data and should NOT be committed to git! +# It is already included in .gitignore. +# +# Unlike the `default` overlay, this one runs NO in-cluster datastores. postgres, +# redis, rabbitmq, minio and opensearch are third-party images with baked-in UID +# and data-directory ownership, and cannot run under the arbitrary UID that +# OpenShift's restricted-v2 SCC assigns. Every connection string below therefore +# points at an EXTERNAL service you provide. + +apiVersion: v1 +kind: Secret +metadata: + name: overlay-secret-vars + annotations: + config.kubernetes.io/local-config: "true" +type: Opaque +stringData: + # ── Database ────────────────────────────────────────────────────────────────── + # External PostgreSQL (RDS, Cloud SQL, Crunchy/CloudNativePG on-cluster, ...). + DATABASE_URL: "postgresql://:@:5432/plane" + + # ── Message broker ──────────────────────────────────────────────────────────── + # External RabbitMQ (Amazon MQ, CloudAMQP, the RabbitMQ Cluster Operator, ...). + AMQP_URL: "amqp://:@:5672/" + + # ── Cache ───────────────────────────────────────────────────────────────────── + # External Redis/Valkey (ElastiCache, Memorystore, ...). + REDIS_URL: "redis://:6379/0" + + # ── Object storage ──────────────────────────────────────────────────────────── + # External S3-compatible storage. USE_MINIO is 0 because no MinIO is deployed. + # On AWS prefer a workload identity (IRSA / Pod Identity) over static keys and + # leave the two key fields out entirely rather than setting them empty — + # an empty AWS_ACCESS_KEY_ID shadows the assumed role. + AWS_ACCESS_KEY_ID: "" + AWS_SECRET_ACCESS_KEY: "" + AWS_S3_ENDPOINT_URL: "https://s3..amazonaws.com" + AWS_S3_BUCKET_NAME: "plane-bucket" + AWS_REGION: "" + USE_MINIO: "0" + # Maximum file upload size in bytes (5242880 = 5MB). + # NOTE: OpenShift Routes cannot cap request bodies the way Traefik's buffering + # middleware does, so this application-side limit is the only upload guard. + FILE_SIZE_LIMIT: "5242880" + + # ── OpenSearch ──────────────────────────────────────────────────────────────── + # External OpenSearch (Amazon OpenSearch Service, or the OpenSearch operator). + # Set OPENSEARCH_ENABLED to "0" to run without search. + OPENSEARCH_ENABLED: "1" + OPENSEARCH_URL: "https://:9200" + # Only needed when the opensearch-external-auth component is enabled: + # OPENSEARCH_USERNAME: "plane" + # OPENSEARCH_PASSWORD: "your-opensearch-password" + + # ── Application secrets ─────────────────────────────────────────────────────── + # Generate new random values before deploying: + # openssl rand -hex 32 + SECRET_KEY: "GENERATE-A-RANDOM-64-HEX-STRING" + AES_SECRET_KEY: "GENERATE-A-RANDOM-32-CHARACTER-STRING" + LIVE_SERVER_SECRET_KEY: "GENERATE-A-RANDOM-64-HEX-STRING" diff --git a/kustomize/overlays/openshift/vars.yaml.example b/kustomize/overlays/openshift/vars.yaml.example new file mode 100644 index 0000000..285f6c4 --- /dev/null +++ b/kustomize/overlays/openshift/vars.yaml.example @@ -0,0 +1,45 @@ +# This ConfigMap holds all the variables for this overlay. +# It is NOT deployed to the cluster — it is only used as a source for replacements. +apiVersion: v1 +kind: ConfigMap +metadata: + name: overlay-vars + annotations: + config.kubernetes.io/local-config: "true" +data: + # Application version used across all images and config + APP_VERSION: "latest" + + # draw.io image tag (third-party, independently versioned) — used by the draw-io component + DRAWIO_VERSION: "v2.5.0" + + # Domain configuration — UPDATE THESE before applying + APP_DOMAIN: "kustomize.plane.town" + + # Web URL (with protocol) + WEB_URL: "https://kustomize.plane.town" + + # CORS allowed origins (comma-separated) + CORS_ALLOWED_ORIGINS: "http://kustomize.plane.town,https://kustomize.plane.town" + + # Allow all attachment types (leave empty to use default restrictions, set to "1" to allow all) + ALLOW_ALL_ATTACHMENT_TYPES: "1" + + # NO INGRESS_CLASS HERE. + # The ingress-openshift component removes the base Ingress and creates Routes + # instead, which carry no class. APP_DOMAIN above is what lands on each Route's + # spec.host, via the replacement the component ships. + + # Other configuration flags + IS_AIRGAPPED: "0" + + # Storage class for the monitor StatefulSet PVC. "gp2" is an EKS name — on + # OpenShift use what the cluster actually offers (`oc get storageclass`): + # gp3-csi on ROSA, thin-csi on vSphere, ocs-storagecluster-ceph-rbd on ODF. + MONITOR_STORAGE_CLASS: "gp3-csi" + + # OpenSearch configuration + OPENSEARCH_INDEX_PREFIX: "plane" + PRIME_HOST: "https://prime.plane.so" + # SMTP domain for the inbound email service TLS certificate + SMTP_DOMAIN: "smtp.gmail.com" \ No newline at end of file