Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions charts/plane-ce/questions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -633,3 +633,72 @@ questions:
type: string
default: ""
group: "External Secrets"
- variable: external_secrets.live_env_existingSecret
label: "Live Envs Secrets File Name"
type: string
default: ""
group: "External Secrets"

# --- Shared key groups --------------------------------------------------------
- variable: external_secrets.app_keys_existingSecret
label: "Signing Keys Secret"
description: "SECRET_KEY and LIVE_SERVER_SECRET_KEY. SECRET_KEY derives the key that encrypts the instance-configuration rows — SMTP password, OAuth client secrets — so changing it makes them undecryptable, silently. Keep it per environment and never rotate it."
type: string
default: ""
group: "External Secrets"
- variable: external_secrets.ssl_token_existingSecret
label: "DNS-01 Token Secret"
description: "cert-manager DNS provider API token, under the key api-token. Usually one token covers a whole wildcard domain, so this is safe to share."
type: string
default: ""
group: "External Secrets"
- variable: external_secrets.storage.secretName
label: "Object Storage Credential Secret"
description: "Leave empty on a cloud and annotate the ServiceAccount instead — a pod identity beats a stored access key. Ignored while the bundled MinIO is deployed."
type: string
default: ""
group: "External Secrets"
- variable: external_secrets.storage.accessKeyIdKey
label: "Storage Access Key ID Key"
type: string
default: ""
group: "External Secrets"
show_if: "external_secrets.storage.secretName!="
- variable: external_secrets.storage.secretAccessKeyKey
label: "Storage Secret Access Key Key"
type: string
default: ""
group: "External Secrets"
show_if: "external_secrets.storage.secretName!="
- variable: dockerRegistry.existingSecret
label: "Existing Image Pull Secret"
description: "Use a pull secret that already exists in the namespace instead of building one from the login below."
type: string
default: ""
group: "Docker Registry"

# --- Cloud identity -----------------------------------------------------------
- variable: serviceAccount.create
label: "Create the ServiceAccount"
description: "Turn off to run as a ServiceAccount managed outside this chart."
type: boolean
default: true
group: "Service Account"
- variable: serviceAccount.name
label: "ServiceAccount Name"
description: "Defaults to <release>-srv-account."
type: string
default: ""
group: "Service Account"
- variable: serviceAccount.cloudIdentity
label: "ServiceAccount Has a Cloud Identity"
description: "Advisory. Turns on the install notes that catch a static credential shadowing the identity."
type: boolean
default: false
group: "Service Account"
- variable: env.requireExplicitSecrets
label: "Refuse to Render Default Signing Keys"
description: "Fails the render instead of falling back to this chart's PUBLIC example values for SECRET_KEY and LIVE_SERVER_SECRET_KEY."
type: boolean
default: false
group: "Service Account"
129 changes: 129 additions & 0 deletions charts/plane-ce/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,132 @@ Caller must nindent to the correct depth.
{{- end -}}
{{- end -}}
{{- end -}}

{{/*
================================================================================
ServiceAccount and cloud identity
================================================================================
*/}}

{{/*
Name of the ServiceAccount every workload runs as. Defaults to the release-scoped
account the chart creates; override to run as one you manage yourself (created by
Terraform or Crossplane, already bound to a cloud IAM role, or the target of an EKS Pod
Identity association).
*/}}
{{- define "plane.serviceAccountName" -}}
{{- .Values.serviceAccount.name | default (printf "%s-srv-account" .Release.Name) -}}
{{- end -}}

{{/*
Returns "true" when the chart should render the ServiceAccount itself. Skipped when it
is managed outside the chart and only referenced here.
*/}}
{{- define "plane.createServiceAccount" -}}
{{- if .Values.serviceAccount.create -}}
true
{{- end -}}
{{- end -}}

{{/*
Pod-template labels some workload-identity implementations require — Azure Workload
Identity needs azure.workload.identity/use on the pod. Indentation is baked in for the
pod-template label position, so call it bare.
*/}}
{{- define "plane.serviceAccountPodLabels" -}}
{{- with .Values.serviceAccount.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end -}}

{{/*
================================================================================
Externalized secrets
================================================================================
*/}}

{{/*
Resolve a secret value with an optional insecure fallback.

Returns .value when set; otherwise fails the render when env.requireExplicitSecrets is
true, and falls back when it is false (the historical behaviour, kept so existing
installs keep working). The fallbacks this chart ships are PUBLIC CONSTANTS — any real
install must supply its own.

Call with: (dict "context" $ "name" "env.secret_key" "value" .Values.env.secret_key "fallback" "...")
*/}}
{{- define "plane.secretValue" -}}
{{- if .value -}}
{{- .value -}}
{{- else if .context.Values.env.requireExplicitSecrets -}}
{{- required (printf "%s has no value. Set it in values.yaml, supply it through external_secrets.app_keys_existingSecret, or set env.requireExplicitSecrets=false to fall back to the chart's insecure default." .name) nil -}}
{{- else -}}
{{- .fallback -}}
{{- end -}}
{{- end -}}

{{/*
envFrom entry for the Secret carrying the shared signing keys (SECRET_KEY,
LIVE_SERVER_SECRET_KEY). Renders nothing unless
external_secrets.app_keys_existingSecret is set.

These keys are duplicated across the app and live Secrets and must agree for the two to
talk to each other, so pointing both at one Secret makes that agreement structural.
While it is set the chart emits neither key itself.

SECRET_KEY additionally derives the key that encrypts the instance-configuration rows,
so it must never be rotated on a running instance.

Indentation is baked in for the container envFrom position, so call it bare.
*/}}
{{- define "plane.appKeysSecretRef" -}}
{{- with .Values.external_secrets.app_keys_existingSecret }}
- secretRef:
name: {{ . }}
optional: false
{{- end }}
{{- end -}}

{{/*
Returns "true" when object-storage credentials come from an externally managed Secret.
Never true while the bundled MinIO is deployed, which supplies its own.
*/}}
{{- define "plane.externalStorage" -}}
{{- if and .Values.external_secrets.storage.secretName (not .Values.minio.local_setup) -}}
true
{{- end -}}
{{- end -}}

{{/*
Emit one env entry sourced from a key inside an externally managed Secret.
Emits a leading newline so call sites can use a left-trim marker without swallowing the
separator from the previous entry.
*/}}
{{- define "plane.secretKeyEnv" }}
- name: {{ .name }}
valueFrom:
secretKeyRef:
name: {{ .secret }}
key: {{ .key }}
{{- end -}}

{{/*
Object-storage credentials as explicit env entries, which win over envFrom.

On a cloud, prefer a pod identity and leave this unset: with env.aws_access_key empty
the chart omits those variables entirely, which is what lets the SDK credential chain
reach the pod's role. Use this for an S3-compatible backend with no workload identity.

Caller must indent to the correct depth (env list items).
*/}}
{{- define "plane.storageCredsEnv" -}}
{{- $st := .Values.external_secrets.storage -}}
{{- if include "plane.externalStorage" . }}
{{- with $st.accessKeyIdKey }}
{{- include "plane.secretKeyEnv" (dict "name" "AWS_ACCESS_KEY_ID" "secret" $st.secretName "key" .) }}
{{- end }}
{{- with $st.secretAccessKeyKey }}
{{- include "plane.secretKeyEnv" (dict "name" "AWS_SECRET_ACCESS_KEY" "secret" $st.secretName "key" .) }}
{{- end }}
{{- end }}
{{- end -}}
7 changes: 5 additions & 2 deletions charts/plane-ce/templates/certs/cert-issuers.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{{- $issuerTokenSecret := .Values.external_secrets.ssl_token_existingSecret | default (printf "%s-issuer-api-token-secret" .Release.Name) }}
{{- if and .Values.ingress.enabled .Values.ssl.createIssuer (empty .Values.ssl.tls_secret_name) }}
{{- if empty .Values.external_secrets.ssl_token_existingSecret }}

apiVersion: v1
kind: Secret
Expand All @@ -11,6 +13,7 @@ type: Opaque
stringData:
api-token: {{ .Values.ssl.token | default "default-api-token" | quote }}

{{- end }}
---
apiVersion: cert-manager.io/v1
kind: Issuer
Expand All @@ -30,14 +33,14 @@ spec:
- dns01:
cloudflare:
apiTokenSecretRef:
name: {{ .Release.Name }}-issuer-api-token-secret
name: {{ $issuerTokenSecret }}
key: api-token
{{- end }}
{{- if eq .Values.ssl.issuer "digitalocean" }}
- dns01:
digitalocean:
tokenSecretRef:
name: {{ .Release.Name }}-issuer-api-token-secret
name: {{ $issuerTokenSecret }}
key: api-token
{{- end }}
{{- if eq .Values.ssl.issuer "http" }}
Expand Down
6 changes: 4 additions & 2 deletions charts/plane-ce/templates/config-secrets/app-env.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ metadata:
labels:
{{- include "plane.commonLabels" $ | nindent 4 }}
stringData:
SECRET_KEY: {{ .Values.env.secret_key | default "60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5" | quote }}
LIVE_SERVER_SECRET_KEY: {{ .Values.env.live_server_secret_key | default "htbqvBJAgpm9bzvf3r4urJer0ENReatceh" | quote }}
{{- if empty .Values.external_secrets.app_keys_existingSecret }}
SECRET_KEY: {{ include "plane.secretValue" (dict "context" $ "name" "env.secret_key" "value" .Values.env.secret_key "fallback" "60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5") | quote }}
LIVE_SERVER_SECRET_KEY: {{ include "plane.secretValue" (dict "context" $ "name" "env.live_server_secret_key" "value" .Values.env.live_server_secret_key "fallback" "htbqvBJAgpm9bzvf3r4urJer0ENReatceh") | quote }}
{{- end }}

{{- if .Values.redis.local_setup }}
REDIS_URL: "redis://{{ .Release.Name }}-redis.{{ .Release.Namespace }}.svc.{{ .Values.env.default_cluster_domain | default "cluster.local" }}:6379/"
Expand Down
21 changes: 17 additions & 4 deletions charts/plane-ce/templates/config-secrets/doc-store.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,23 @@ stringData:
AWS_S3_ENDPOINT_URL: http://{{ .Release.Name }}-minio:9000
{{- else }}
USE_MINIO: "0"
AWS_ACCESS_KEY_ID: {{ .Values.env.aws_access_key | default "" | quote }}
AWS_SECRET_ACCESS_KEY: {{ .Values.env.aws_secret_access_key | default "" | quote }}
AWS_S3_ENDPOINT_URL: {{ .Values.env.aws_s3_endpoint_url | quote }}
AWS_REGION: {{ .Values.env.aws_region | default "" | quote }}
{{/* Rendered only when supplied. Leaving these unset is the preferred configuration on
a cloud: the SDK then walks its default credential chain and picks up the pod's
identity. An empty AWS_ACCESS_KEY_ID would be found first and used, failing every
request; and an empty AWS_REGION is *present*, so a code-side default never
applies and a signed request would go out with no region. */}}
{{- with .Values.env.aws_access_key }}
AWS_ACCESS_KEY_ID: {{ . | quote }}
{{- end }}
{{- with .Values.env.aws_secret_access_key }}
AWS_SECRET_ACCESS_KEY: {{ . | quote }}
{{- end }}
{{- with .Values.env.aws_s3_endpoint_url }}
AWS_S3_ENDPOINT_URL: {{ . | quote }}
{{- end }}
{{- with .Values.env.aws_region }}
AWS_REGION: {{ . | quote }}
{{- end }}
{{- end }}
---
{{- end }}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{{- if empty .Values.dockerRegistry.existingSecret }}
{{- if .Values.dockerRegistry.enabled }}

apiVersion: v1
Expand All @@ -11,4 +12,5 @@ data:
.dockerconfigjson: {{ include "imagePullSecret" .}}
type: kubernetes.io/dockerconfigjson

{{- end }}
{{- end }}
{{- end }}
4 changes: 3 additions & 1 deletion charts/plane-ce/templates/config-secrets/live-env.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ metadata:
labels:
{{- include "plane.commonLabels" $ | nindent 4 }}
stringData:
LIVE_SERVER_SECRET_KEY: {{ .Values.env.live_server_secret_key | default "htbqvBJAgpm9bzvf3r4urJer0ENReatceh" | quote }}
{{- if empty .Values.external_secrets.app_keys_existingSecret }}
LIVE_SERVER_SECRET_KEY: {{ include "plane.secretValue" (dict "context" $ "name" "env.live_server_secret_key" "value" .Values.env.live_server_secret_key "fallback" "htbqvBJAgpm9bzvf3r4urJer0ENReatceh") | quote }}
{{- end }}
{{- if .Values.redis.local_setup }}
REDIS_URL: "redis://{{ .Release.Name }}-redis.{{ .Release.Namespace }}.svc.cluster.local:6379/"
{{- else }}
Expand Down
15 changes: 13 additions & 2 deletions charts/plane-ce/templates/service-account.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
{{- if include "plane.createServiceAccount" . }}
apiVersion: v1
automountServiceAccountToken: true
kind: ServiceAccount
metadata:
namespace: {{ .Release.Namespace }}
name: {{ .Release.Name }}-srv-account
name: {{ include "plane.serviceAccountName" . }}
labels:
{{- include "plane.commonLabels" $ | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
{{/* Cloud workload-identity bindings go here — eks.amazonaws.com/role-arn (IRSA),
iam.gke.io/gcp-service-account, azure.workload.identity/client-id. */}}
annotations: {{ toYaml . | nindent 4 }}
{{- end }}
{{- if .Values.dockerRegistry.enabled }}
imagePullSecrets:
{{- if .Values.dockerRegistry.existingSecret }}
- name: {{ .Values.dockerRegistry.existingSecret }}
{{- else }}
- name: {{ .Release.Name }}-docker-registry-credentials
{{- end}}
{{- end }}
{{- end }}
{{- end }}
5 changes: 3 additions & 2 deletions charts/plane-ce/templates/workloads/admin.deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ spec:
labels:
app.name: {{ .Release.Namespace }}-{{ .Release.Name }}-admin
{{- include "plane.commonLabels" $ | nindent 8 }}
{{- include "plane.serviceAccountPodLabels" . }}
annotations:
timestamp: {{ now | quote }}
spec:
Expand All @@ -55,6 +56,6 @@ spec:
memory: {{ .Values.admin.memoryLimit | default "1000Mi" | quote }}
cpu: {{ .Values.admin.cpuLimit | default "500m" | quote}}
{{- include "plane.podScheduling" .Values.admin }}
serviceAccount: {{ .Release.Name }}-srv-account
serviceAccountName: {{ .Release.Name }}-srv-account
serviceAccount: {{ include "plane.serviceAccountName" . }}
serviceAccountName: {{ include "plane.serviceAccountName" . }}
---
15 changes: 13 additions & 2 deletions charts/plane-ce/templates/workloads/api.deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ spec:
labels:
app.name: {{ .Release.Namespace }}-{{ .Release.Name }}-api
{{- include "plane.commonLabels" $ | nindent 8 }}
{{- include "plane.serviceAccountPodLabels" . }}
annotations:
timestamp: {{ now | quote }}
spec:
Expand Down Expand Up @@ -66,6 +67,16 @@ spec:
- secretRef:
name: {{ if not (empty .Values.external_secrets.doc_store_existingSecret) }}{{ .Values.external_secrets.doc_store_existingSecret }}{{ else }}{{ .Release.Name }}-doc-store-secrets{{ end }}
optional: false
{{- /* LAST in envFrom, deliberately. envFrom is later-source-wins, so this has to
follow app_env/live_env: those may carry their own SECRET_KEY, and this hook
exists precisely to make the app and live copies agree. First would let a
stale key in an operator-supplied Secret win on one workload and not the
other, which breaks live-server auth with no error. */}}
{{- include "plane.appKeysSecretRef" . }}
{{- with (include "plane.storageCredsEnv" .) }}
env:
{{ trim . | indent 10 }}
{{- end }}
readinessProbe:
failureThreshold: 30
httpGet:
Expand All @@ -77,6 +88,6 @@ spec:
timeoutSeconds: 1

{{- include "plane.podScheduling" .Values.api }}
serviceAccount: {{ .Release.Name }}-srv-account
serviceAccountName: {{ .Release.Name }}-srv-account
serviceAccount: {{ include "plane.serviceAccountName" . }}
serviceAccountName: {{ include "plane.serviceAccountName" . }}
---
Loading