Skip to content

Add volumes and volumeMounts values to the Helm chart for file-based credentials - #62

Merged
ThoSap merged 6 commits into
mainfrom
add-helm-volumes-values
Sep 4, 2026
Merged

Add volumes and volumeMounts values to the Helm chart for file-based credentials#62
ThoSap merged 6 commits into
mainfrom
add-helm-volumes-values

Conversation

@ThoSap

@ThoSap ThoSap commented Sep 4, 2026

Copy link
Copy Markdown
Member

Follow-up to #60.

adminSecretFileRef needs the credentials file inside the operator Pod, but the Helm chart exposed no way to mount one.
A user of the published chart had to patch the Deployment after helm install.

The chart now exposes app.volumes and app.volumeMounts.
Both take raw Kubernetes syntax, so any volume source works, a Secrets Store CSI volume included.
That last case is the one #60 was added for.

Changes

  • operator/src/main/kubernetes/kubernetes.yml: baseline volumes: [~], plus a named
    container with volumeMounts: [~].
  • operator/src/main/helm/values.yaml (new): [] defaults for app.volumes,
    app.volumeMounts and app.imagePullSecrets.
  • operator/src/main/resources/application.yml: two new quarkus.helm.values entries,
    three values-schema entries, and a simpler image-pull-secrets expression.
  • operator/src/test/java/it/aboutbits/postgresql/helm/HelmTest.java: extended assertions,
    plus a new test that renders the chart with a real volume.
  • docs/cluster-connection.md: a Secret example and a CSI example.
  • .gitignore: ignore the .kube/ folder that the fabric8 Kubernetes client writes
    into the module directory when the tests run.

Why the [~] placeholders

quarkus-helm can only replace a value node that already exists. quarkus.helm.values.<x>.paths uses YamlPath, which never creates a key.
An empty list [] in kubernetes.yml does not survive either, because the fabric8 model marks PodSpec.volumes, PodSpec.imagePullSecrets and Container.volumeMounts with @JsonInclude(NON_EMPTY).
A list with one null element does survive.
operator/src/main/helm/values.yaml then replaces the resulting - {} default with a real empty list.

Upstream request: quarkiverse/quarkus-helm#453

Alternatives considered

The Debezium operator solves the same problem for imagePullSecrets in debezium/debezium-operator#219.
Two of its three tricks are the same as the ones here:
a src/main/helm/values.yaml for the [] default, and the description in values-schema.properties.
So those two choices are established practice, not invention.

The difference is how the missing key gets created. Debezium anchors a quarkus.helm.expressions entry on the existing serviceAccountName scalar and appends a whole multi-line YAML block to it:

quarkus.helm.expressions.0.path=(kind == Deployment).spec.template.spec.serviceAccountName
quarkus.helm.expressions.0.expression=debezium-operator\n      {{- with .Values.app.imagePullSecrets }}\n      imagePullSecrets:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}

We evaluated that and did not take it, for four reasons:

  • It hardcodes the value of the anchor field. The literal debezium-operator is the service account name.
    Rename the service account and the rendered template silently loses it.
  • It hardcodes the indentation inside the string, six spaces in that example.
  • It couples imagePullSecrets to an unrelated field.
    A reader of application.yml cannot tell why serviceAccountName carries a pull-secret block.
  • quarkus-helm reinserts the line breaks of an adapted expression with System.lineSeparator(), in applyKnownPatterns, so the generated template depends on the build platform.

volumeMounts would make all four worse, because that field sits inside a container list entry.
It would need a second anchor on another container field, and it would hardcode that field's value as well.
The baseline kubernetes.yml keeps each field at its own path, so the expression stays a one-liner and the indentation lives in nindent.

The other routes we tried

  • value-as-list: [] or value: "[]" on the value entry.
    This would give the [] default with no baseline file at all.
    It cannot work. ValueReferenceConfig.valueAsList() is an Optional<List<String>>, and an empty list resolves to Optional.empty(), so toValue falls through.
    A string "[]" reaches values.yaml as a quoted string and types the schema as string.
  • A real generated volume, through quarkus.kubernetes.secret-volumes.<n>.secret-name
    with optional=true plus quarkus.kubernetes.mounts.<n>.path, or empty-dir-volumes.

    This does create the nodes. It also ships a concrete dummy volume in the default values.yaml and mounts it at runtime, so every user who does not need a volume still gets one.
  • quarkus.helm.add-if-statement.
    It wraps a whole resource in a condition. It cannot add a field.
  • A hand-written src/main/helm/templates/deployment.yaml.
    quarkus-helm keeps only the {{- define }} blocks of a user template and prepends them.
    It does not replace or merge the generated Deployment, so the file adds nothing. Verified by build.
  • An in-repo Quarkus extension that produces an AdditionalHelmTemplateBuildItem with ReplacedResource("Deployment", ...).
    Possible since quarkus-helm 1.4.1.
    It would mean hand-maintaining the entire Deployment template, and quarkus.helm.values.<x>.paths do not apply to a replaced resource.
    Rejected as far more work than the placeholder.
  • Upgrading and waiting.
    Checked whether a newer version removes the need: it does not.
    The relevant code is unchanged in quarkus-helm 1.4.1, 1.4.2 and 1.4.3, and in Quarkus kubernetes 3.36 through 3.39.2.
    Upstream PRs #508 and #512 only act on build items that a Quarkus extension deployment module produces, so no quarkus.helm.* property reaches them.

Behaviour change for chart users

app.imagePullSecrets moves onto the same pattern.
Its default changes from - {} to [], and the {{- if eq ... "- {}" }} guard is gone. helm template now renders imagePullSecrets: [] instead of imagePullSecrets: null.
Both are valid, and anyone who already sets the value is unaffected.
The old - {} default was a trap: a user who copied it and appended a secret name shipped an invalid entry.

Known trade-off

The generated chart README shows a blank Description for the three list values.
The descriptions live in values.schema.json instead.
Cause: a key supplied through src/main/helm/values.yaml overwrites the ConfigReference that carries the description,
and quarkus-helm merges the generated one with putIfAbsent.
An upstream fix is planned.
Four other rows in that table (app.replicas, app.imagePullPolicy and the two app.resources.* values) are already blank for an unrelated reason.

Three ways to get the descriptions back were weighed:

  • Hand-write the chart README
    with quarkus.helm.create-readme-file: false and a HelmTest loop that asserts every app.* key appears in it.
    Rejected for now, because it moves 44 generated rows into a file that has to be maintained by hand.
  • Keep the [{}] defaults and the "- {}" guard expression.
    Rejected, because the - {} default is a trap for anyone who copies it out of helm show values.
  • Patch the README after the build, in Gradle.
    Rejected, because quarkus.helm.create-tar-file: true has already packed the .tgz that the release workflow uploads, so the task would have to repack it.

The upstream fix is the chosen route, because it removes the cause instead of the symptom.


Note: HelmTest.helmInstall_createsDeployment fails on a machine with Helm 4 installed, with a CRD field-manager conflict. This PR does not cause it, and it reproduces on a clean main.
The test-mode operator applies the CRDs with the fabric8-kubernetes-client field manager, then Helm 4 applies them again with server-side apply.
CI is unaffected, because the ubuntu-24.04 runner still ships Helm 3.

…sed credentials

Follow-up to #60. The chart now exposes `app.volumes` and `app.volumeMounts`, so a
user of the published chart can mount a credentials file for `adminSecretFileRef`
through `values.yaml`. `app.imagePullSecrets` moves onto the same pattern and loses
its `- {}` default.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ThoSap ThoSap self-assigned this Sep 4, 2026
@ThoSap ThoSap added documentation Improvements or additions to documentation enhancement New feature or request labels Sep 4, 2026
@ThoSap
ThoSap requested a review from stplasim September 4, 2026 12:30

@stplasim stplasim left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well done! Just two little things. One is just a doc thing.

P.s. sorry for the bad formatting, I reviewed this on mobile

private static final String ENV_VAR_KUBECONFIG = "KUBECONFIG";

/// Must match `quarkus.kubernetes.name`.
private static final String CONTAINER_NAME = "postgresql-operator";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This is the third place we have the container name, after kubernetes.yml and quarkus.kubernetes.name. The constructor already injects quarkus.helm.name, so could this take quarkus.kubernetes.name the same way? Then a rename fails on the actual problem (Dekorate adding a second container) rather than on getName().

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No you are right, I'm going to inject quarkus.kubernetes.name and use that whenever possible.

secretProviderClass: db-credentials
volumeMounts:
- name: db-credentials
mountPath: /mnt/secrets

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it is just me but both new sections stop at the mount, no?
But the point of the PR is making adminSecretFileRef usable from the chart. Could you end each example with the matching path?

adminSecretFileRef:
      path: /mnt/secrets/db-credentials.json

With the CSI one especially it's not obvious that the filename comes from objectAlias. And a line saying the Secrets Store CSI driver has to be installed first would save someone a confused pod.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for carefully reading this 🙏🏼

I reworked the whole file, as since PR #60 it was really confusing and duplicated some stuff from the #### Mount the credentials file section partly again in ### Examples.

I moved these sections now under ## Examples and cleaned it up a bit.

@ThoSap
ThoSap merged commit 3b1d1f6 into main Sep 4, 2026
4 checks passed
@ThoSap
ThoSap deleted the add-helm-volumes-values branch September 4, 2026 23:06
@ThoSap

ThoSap commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants