From acff677300b54692d5a5a7c41a0785f7cb00d17e Mon Sep 17 00:00:00 2001 From: Fabien Papet Date: Thu, 12 Mar 2026 22:49:44 +0100 Subject: [PATCH 1/2] feat: add generic volume and volumeMount support (closes #2) Add `volumes` and `volumeMounts` values to inject arbitrary Kubernetes volumes (Secrets, ConfigMaps, PVCs, etc.) into all workloads: deployment, workers, crons, and jobs. - charts/frankenphp/values.yaml: add volumes/volumeMounts with commented Symfony decrypt key example - templates/deployment.yaml: render extra volumeMounts and volumes - templates/worker.yaml: render extra volumeMounts and volumes alongside existing php.ini mount - templates/jobs.yaml: same as worker - templates/crons.yaml: add volumeMounts and volumes support - charts/frankenphp/values.schema.json: add schema for volumes/volumeMounts - README.md: document Volume Management with step-by-step instructions - examples/11-secrets-volumes.yaml: Symfony decrypt key example Secrets must be created externally before chart installation; the chart does not manage secret creation to keep sensitive data out of Helm history. --- README.md | 38 ++++++++++++++++ charts/frankenphp/templates/crons.yaml | 10 +++++ charts/frankenphp/templates/deployment.yaml | 6 +++ charts/frankenphp/templates/jobs.yaml | 14 +++++- charts/frankenphp/templates/worker.yaml | 14 +++++- charts/frankenphp/values.schema.json | 14 ++++++ charts/frankenphp/values.yaml | 17 +++++++ examples/11-secrets-volumes.yaml | 50 +++++++++++++++++++++ 8 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 examples/11-secrets-volumes.yaml diff --git a/README.md b/README.md index cdd6f97..ca71f32 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ This repository contains a Helm chart to deploy docker images built with [Franke - **Jobs/Hooks**: Run migrations or one-off tasks using Helm hooks. - **Monitoring**: Integrated `PodMonitor` for Prometheus (requires Prometheus Operator). - **Scheduling**: Support for `nodeSelector`, `affinity`, and `tolerations` for all pods. +- **Volume Management**: Mount any Kubernetes volume (Secrets, ConfigMaps, PVCs) into all pods. ## Examples @@ -31,6 +32,7 @@ Some configuration examples are available in the [examples/](examples/) director - [Jobs](examples/07-jobs.yaml) - [High Availability](examples/08-high-availability.yaml) - [Production Setup](examples/02-production.yaml) +- [Secrets & Volume Mounts](examples/11-secrets-volumes.yaml) ## Installation @@ -53,6 +55,42 @@ helm install my-app frankenphp/frankenphp -f values.yaml ## Development +### Volume Management + +The chart supports mounting arbitrary volumes (Kubernetes Secrets, ConfigMaps, PersistentVolumeClaims, etc.) into **all pods** (deployment, workers, crons, jobs) via `volumes` and `volumeMounts`. + +> [!IMPORTANT] +> Kubernetes Secrets must be created **before** installing or upgrading the chart. The chart does not create secrets for you — this keeps sensitive data out of Helm values and release history. + +**Example: Symfony decrypt key** + +1. Create the Kubernetes Secret: + +```bash +kubectl create secret generic symfony-secrets \ + --from-file=prod.decrypt.private.php=config/secrets/prod/prod.decrypt.private.php +``` + +2. Reference it in your `values.yaml`: + +```yaml +volumes: + - name: symfony-decrypt-key + secret: + secretName: symfony-secrets + items: + - key: prod.decrypt.private.php + path: prod.decrypt.private.php + +volumeMounts: + - name: symfony-decrypt-key + mountPath: /app/config/secrets/prod/prod.decrypt.private.php + subPath: prod.decrypt.private.php + readOnly: true +``` + +See [examples/11-secrets-volumes.yaml](examples/11-secrets-volumes.yaml) for a complete example. + ### Run Unit Tests & Validation This chart uses the `helm-unittest` plugin and `kubeconform`. diff --git a/charts/frankenphp/templates/crons.yaml b/charts/frankenphp/templates/crons.yaml index 16ea156..bdc56b2 100644 --- a/charts/frankenphp/templates/crons.yaml +++ b/charts/frankenphp/templates/crons.yaml @@ -37,6 +37,12 @@ spec: env: {{- toYaml . | nindent 16 }} {{- end }} + {{- if or $.Values.volumeMounts }} + volumeMounts: + {{- with $.Values.volumeMounts }} + {{- toYaml . | nindent 16 }} + {{- end }} + {{- end }} {{- with $.Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 12 }} @@ -49,4 +55,8 @@ spec: tolerations: {{- toYaml . | nindent 12 }} {{- end }} + {{- with $.Values.volumes }} + volumes: + {{- toYaml . | nindent 12 }} + {{- end }} {{- end }} diff --git a/charts/frankenphp/templates/deployment.yaml b/charts/frankenphp/templates/deployment.yaml index 6a39f8e..4dc6ad2 100644 --- a/charts/frankenphp/templates/deployment.yaml +++ b/charts/frankenphp/templates/deployment.yaml @@ -58,6 +58,9 @@ spec: mountPath: /usr/local/etc/php/conf.d/99-custom.ini subPath: php.ini {{- end }} + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} @@ -84,4 +87,7 @@ spec: configMap: name: {{ .Release.Name }}-php-ini {{- end }} + {{- with .Values.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} {{- end }} diff --git a/charts/frankenphp/templates/jobs.yaml b/charts/frankenphp/templates/jobs.yaml index cae0dda..352aa4a 100644 --- a/charts/frankenphp/templates/jobs.yaml +++ b/charts/frankenphp/templates/jobs.yaml @@ -43,11 +43,16 @@ spec: {{- end }} resources: {{- toYaml ($job.resources | default $context.Values.resources) | nindent 12 }} - {{- if $context.Values.php.ini }} + {{- if or $context.Values.php.ini $context.Values.volumeMounts }} volumeMounts: + {{- if $context.Values.php.ini }} - name: php-ini mountPath: /usr/local/etc/php/conf.d/99-custom.ini subPath: php.ini + {{- end }} + {{- with $context.Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} {{- end }} {{- with $context.Values.nodeSelector }} nodeSelector: @@ -61,10 +66,15 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} - {{- if $context.Values.php.ini }} + {{- if or $context.Values.php.ini $context.Values.volumes }} volumes: + {{- if $context.Values.php.ini }} - name: php-ini configMap: name: {{ $context.Release.Name }}-php-ini + {{- end }} + {{- with $context.Values.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} {{- end }} {{- end }} diff --git a/charts/frankenphp/templates/worker.yaml b/charts/frankenphp/templates/worker.yaml index 1335f47..f7f9c04 100644 --- a/charts/frankenphp/templates/worker.yaml +++ b/charts/frankenphp/templates/worker.yaml @@ -45,11 +45,16 @@ spec: - {{ .command | quote }} resources: {{- toYaml $.Values.resources | nindent 12 }} - {{- if $.Values.php.ini }} + {{- if or $.Values.php.ini $.Values.volumeMounts }} volumeMounts: + {{- if $.Values.php.ini }} - name: php-ini mountPath: /usr/local/etc/php/conf.d/99-custom.ini subPath: php.ini + {{- end }} + {{- with $.Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} {{- end }} {{- with $.Values.nodeSelector }} nodeSelector: @@ -63,10 +68,15 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} - {{- if $.Values.php.ini }} + {{- if or $.Values.php.ini $.Values.volumes }} volumes: + {{- if $.Values.php.ini }} - name: php-ini configMap: name: {{ $.Release.Name }}-php-ini + {{- end }} + {{- with $.Values.volumes }} + {{- toYaml . | nindent 8 }} + {{- end }} {{- end }} {{- end }} diff --git a/charts/frankenphp/values.schema.json b/charts/frankenphp/values.schema.json index 8fb65c8..e596ba2 100644 --- a/charts/frankenphp/values.schema.json +++ b/charts/frankenphp/values.schema.json @@ -338,6 +338,20 @@ "items": { "type": "object" } + }, + "volumes": { + "type": "array", + "description": "Extra volumes to add to all pods", + "items": { + "type": "object" + } + }, + "volumeMounts": { + "type": "array", + "description": "Extra volume mounts to add to all containers", + "items": { + "type": "object" + } } } } \ No newline at end of file diff --git a/charts/frankenphp/values.yaml b/charts/frankenphp/values.yaml index 62ab8f9..32bfd6e 100644 --- a/charts/frankenphp/values.yaml +++ b/charts/frankenphp/values.yaml @@ -56,6 +56,23 @@ nodeSelector: {} tolerations: [] +# Extra volumes to mount into all pods (deployment, workers, crons, jobs). +# Use this to inject secrets or configmaps as files into your containers. +volumes: [] +# - name: symfony-decrypt-key +# secret: +# secretName: my-symfony-secrets +# items: +# - key: decrypt +# path: prod.decrypt.private.php + +# Extra volume mounts to add to all containers. +volumeMounts: [] +# - name: symfony-decrypt-key +# mountPath: /app/config/secrets/prod/prod.decrypt.private.php +# subPath: prod.decrypt.private.php +# readOnly: true + env: - name: "SERVER_NAME" value: ":80" diff --git a/examples/11-secrets-volumes.yaml b/examples/11-secrets-volumes.yaml new file mode 100644 index 0000000..8e45ecf --- /dev/null +++ b/examples/11-secrets-volumes.yaml @@ -0,0 +1,50 @@ +# Secrets & Volume Mounts Example +# +# This example demonstrates how to inject a Kubernetes Secret as a file +# into your container — a common pattern for Symfony's decrypt private key +# (config/secrets/prod/prod.decrypt.private.php). +# +# Step 1: Create the Kubernetes Secret BEFORE installing/upgrading the chart. +# +# kubectl create secret generic symfony-secrets \ +# --from-file=prod.decrypt.private.php=config/secrets/prod/prod.decrypt.private.php +# +# Or from a literal value (base64-encoded at rest by Kubernetes): +# +# kubectl create secret generic symfony-secrets \ +# --from-literal=prod.decrypt.private.php="$(cat config/secrets/prod/prod.decrypt.private.php)" +# +# Step 2: Reference the secret in your values file (this file). + +image: + repository: my-registry/my-symfony-app + tag: "1.0.0" + +deployment: + replicas: 2 + +env: + - name: SERVER_NAME + value: ":80" + - name: APP_ENV + value: prod + - name: APP_SECRET + valueFrom: + secretKeyRef: + name: symfony-app-secrets + key: app-secret + +# Mount the Symfony decrypt key from a pre-existing Kubernetes Secret. +volumes: + - name: symfony-decrypt-key + secret: + secretName: symfony-secrets + items: + - key: prod.decrypt.private.php + path: prod.decrypt.private.php + +volumeMounts: + - name: symfony-decrypt-key + mountPath: /app/config/secrets/prod/prod.decrypt.private.php + subPath: prod.decrypt.private.php + readOnly: true From 6bcdb8447fda6f5433fc07021d6a1f16933c345f Mon Sep 17 00:00:00 2001 From: Fabien Papet Date: Thu, 12 Mar 2026 23:06:28 +0100 Subject: [PATCH 2/2] test: add unit tests for volume and volumeMount management - tests/volumes_test.yaml: 9 tests covering deployment, worker, cron, job - extra volumes/volumeMounts are rendered in all workload templates - extra mounts are appended after php-ini when both are set - volumes section is omitted when nothing is configured - Fix deployment.yaml: make volumeMounts and volumes sections conditional to avoid empty keys in the rendered manifest --- README.md | 36 ---- charts/frankenphp/templates/deployment.yaml | 4 + charts/frankenphp/tests/volumes_test.yaml | 226 ++++++++++++++++++++ 3 files changed, 230 insertions(+), 36 deletions(-) create mode 100644 charts/frankenphp/tests/volumes_test.yaml diff --git a/README.md b/README.md index ca71f32..d22afdc 100644 --- a/README.md +++ b/README.md @@ -55,42 +55,6 @@ helm install my-app frankenphp/frankenphp -f values.yaml ## Development -### Volume Management - -The chart supports mounting arbitrary volumes (Kubernetes Secrets, ConfigMaps, PersistentVolumeClaims, etc.) into **all pods** (deployment, workers, crons, jobs) via `volumes` and `volumeMounts`. - -> [!IMPORTANT] -> Kubernetes Secrets must be created **before** installing or upgrading the chart. The chart does not create secrets for you — this keeps sensitive data out of Helm values and release history. - -**Example: Symfony decrypt key** - -1. Create the Kubernetes Secret: - -```bash -kubectl create secret generic symfony-secrets \ - --from-file=prod.decrypt.private.php=config/secrets/prod/prod.decrypt.private.php -``` - -2. Reference it in your `values.yaml`: - -```yaml -volumes: - - name: symfony-decrypt-key - secret: - secretName: symfony-secrets - items: - - key: prod.decrypt.private.php - path: prod.decrypt.private.php - -volumeMounts: - - name: symfony-decrypt-key - mountPath: /app/config/secrets/prod/prod.decrypt.private.php - subPath: prod.decrypt.private.php - readOnly: true -``` - -See [examples/11-secrets-volumes.yaml](examples/11-secrets-volumes.yaml) for a complete example. - ### Run Unit Tests & Validation This chart uses the `helm-unittest` plugin and `kubeconform`. diff --git a/charts/frankenphp/templates/deployment.yaml b/charts/frankenphp/templates/deployment.yaml index 4dc6ad2..d86e9df 100644 --- a/charts/frankenphp/templates/deployment.yaml +++ b/charts/frankenphp/templates/deployment.yaml @@ -48,6 +48,7 @@ spec: env: {{- toYaml . | nindent 12 }} {{- end }} + {{- if or .Values.caddyfile .Values.php.ini .Values.volumeMounts }} volumeMounts: {{- if .Values.caddyfile }} - name: caddyfile @@ -61,6 +62,7 @@ spec: {{- with .Values.volumeMounts }} {{- toYaml . | nindent 12 }} {{- end }} + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} @@ -73,6 +75,7 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} + {{- if or .Values.caddyfile .Values.php.ini .Values.volumes }} volumes: {{- if .Values.caddyfile }} - name: caddyfile # To access this volume, this name must be used inside volumeMounts of the container @@ -90,4 +93,5 @@ spec: {{- with .Values.volumes }} {{- toYaml . | nindent 8 }} {{- end }} + {{- end }} {{- end }} diff --git a/charts/frankenphp/tests/volumes_test.yaml b/charts/frankenphp/tests/volumes_test.yaml new file mode 100644 index 0000000..0f16a73 --- /dev/null +++ b/charts/frankenphp/tests/volumes_test.yaml @@ -0,0 +1,226 @@ +suite: Volume Management +templates: + - deployment.yaml + - worker.yaml + - crons.yaml + - jobs.yaml + +tests: + # --- Deployment --- + + - it: should mount extra volume in Deployment + template: deployment.yaml + set: + volumes: + - name: symfony-decrypt-key + secret: + secretName: symfony-secrets + volumeMounts: + - name: symfony-decrypt-key + mountPath: /app/config/secrets/prod/prod.decrypt.private.php + subPath: prod.decrypt.private.php + readOnly: true + asserts: + - equal: + path: spec.template.spec.volumes[0].name + value: symfony-decrypt-key + - equal: + path: spec.template.spec.volumes[0].secret.secretName + value: symfony-secrets + - equal: + path: spec.template.spec.containers[0].volumeMounts[0].name + value: symfony-decrypt-key + - equal: + path: spec.template.spec.containers[0].volumeMounts[0].mountPath + value: /app/config/secrets/prod/prod.decrypt.private.php + - equal: + path: spec.template.spec.containers[0].volumeMounts[0].readOnly + value: true + + - it: should not add volumes section when volumes is empty in Deployment + template: deployment.yaml + asserts: + - notExists: + path: spec.template.spec.volumes + + - it: should append extra volumeMount after php-ini in Deployment + template: deployment.yaml + set: + php: + ini: "memory_limit = 512M" + volumes: + - name: my-secret + secret: + secretName: my-secret + volumeMounts: + - name: my-secret + mountPath: /app/secret + asserts: + - equal: + path: spec.template.spec.containers[0].volumeMounts[0].name + value: php-ini + - equal: + path: spec.template.spec.containers[0].volumeMounts[1].name + value: my-secret + - equal: + path: spec.template.spec.volumes[0].name + value: php-ini + - equal: + path: spec.template.spec.volumes[1].name + value: my-secret + + # --- Worker --- + + - it: should mount extra volume in Worker + template: worker.yaml + set: + consumers: + - name: queue + command: "php bin/console messenger:consume" + volumes: + - name: symfony-decrypt-key + secret: + secretName: symfony-secrets + volumeMounts: + - name: symfony-decrypt-key + mountPath: /app/config/secrets/prod/prod.decrypt.private.php + subPath: prod.decrypt.private.php + readOnly: true + asserts: + - equal: + path: spec.template.spec.volumes[0].name + value: symfony-decrypt-key + - equal: + path: spec.template.spec.containers[0].volumeMounts[0].name + value: symfony-decrypt-key + - equal: + path: spec.template.spec.containers[0].volumeMounts[0].mountPath + value: /app/config/secrets/prod/prod.decrypt.private.php + + - it: should append extra volumeMount after php-ini in Worker + template: worker.yaml + set: + consumers: + - name: queue + command: "php bin/console messenger:consume" + php: + ini: "memory_limit = 512M" + volumes: + - name: my-secret + secret: + secretName: my-secret + volumeMounts: + - name: my-secret + mountPath: /app/secret + asserts: + - equal: + path: spec.template.spec.containers[0].volumeMounts[0].name + value: php-ini + - equal: + path: spec.template.spec.containers[0].volumeMounts[1].name + value: my-secret + - equal: + path: spec.template.spec.volumes[0].name + value: php-ini + - equal: + path: spec.template.spec.volumes[1].name + value: my-secret + + # --- CronJob --- + + - it: should mount extra volume in CronJob + template: crons.yaml + set: + crons: + - name: my-cron + command: "php bin/console app:run" + schedule: "*/5 * * * *" + volumes: + - name: symfony-decrypt-key + secret: + secretName: symfony-secrets + volumeMounts: + - name: symfony-decrypt-key + mountPath: /app/config/secrets/prod/prod.decrypt.private.php + subPath: prod.decrypt.private.php + readOnly: true + asserts: + - equal: + path: spec.jobTemplate.spec.template.spec.volumes[0].name + value: symfony-decrypt-key + - equal: + path: spec.jobTemplate.spec.template.spec.containers[0].volumeMounts[0].name + value: symfony-decrypt-key + - equal: + path: spec.jobTemplate.spec.template.spec.containers[0].volumeMounts[0].mountPath + value: /app/config/secrets/prod/prod.decrypt.private.php + + - it: should not add volumes in CronJob when volumes is empty + template: crons.yaml + set: + crons: + - name: my-cron + command: "php bin/console app:run" + schedule: "*/5 * * * *" + asserts: + - notExists: + path: spec.jobTemplate.spec.template.spec.volumes + - notExists: + path: spec.jobTemplate.spec.template.spec.containers[0].volumeMounts + + # --- Job --- + + - it: should mount extra volume in Job + template: jobs.yaml + set: + jobs: + - name: migrate + command: "php bin/console doctrine:migrations:migrate" + volumes: + - name: symfony-decrypt-key + secret: + secretName: symfony-secrets + volumeMounts: + - name: symfony-decrypt-key + mountPath: /app/config/secrets/prod/prod.decrypt.private.php + subPath: prod.decrypt.private.php + readOnly: true + asserts: + - equal: + path: spec.template.spec.volumes[0].name + value: symfony-decrypt-key + - equal: + path: spec.template.spec.containers[0].volumeMounts[0].name + value: symfony-decrypt-key + - equal: + path: spec.template.spec.containers[0].volumeMounts[0].mountPath + value: /app/config/secrets/prod/prod.decrypt.private.php + + - it: should append extra volumeMount after php-ini in Job + template: jobs.yaml + set: + jobs: + - name: migrate + command: "php bin/console doctrine:migrations:migrate" + php: + ini: "memory_limit = 512M" + volumes: + - name: my-secret + secret: + secretName: my-secret + volumeMounts: + - name: my-secret + mountPath: /app/secret + asserts: + - equal: + path: spec.template.spec.containers[0].volumeMounts[0].name + value: php-ini + - equal: + path: spec.template.spec.containers[0].volumeMounts[1].name + value: my-secret + - equal: + path: spec.template.spec.volumes[0].name + value: php-ini + - equal: + path: spec.template.spec.volumes[1].name + value: my-secret