In a Helm chart, how do `.Capabilities.APIVersions.Has` and `.Capabilities.KubeVersion` decide which apiVersion a template emits?
answer
- The object that describes the target cluster
- Discovery answers, taken before rendering
- Ask whether a group/version is served
- Compare the full version, not the minor
- Re-decided on every install and upgrade
basics
~10 sHelm reads the target cluster's discovery list and reported server version before rendering. .Capabilities.APIVersions.Has answers whether that cluster serves a given group/version, and .Capabilities.KubeVersion.Version gives its version string for a semver comparison.
solid answer
~40 s`.Capabilities` is the built-in object describing the cluster Helm is rendering for, filled from the API server's discovery response and its reported version. `.Capabilities.APIVersions.Has` takes a group/version such as `policy/v1`, or a group/version/kind such as `apps/v1/Deployment`, and returns a boolean, so a chart can emit the modern apiVersion where the cluster serves it and a fallback where it does not. `.Capabilities.KubeVersion` carries `.Version` (the full string, like `v1.31.4`), plus `.Major` and `.Minor`. Compare with `semverCompare` against `.Version`; do not compare `.Minor`, which is a string and has arrived from managed clusters with a trailing non-numeric character. The set is a snapshot taken for that render, so an API served by a CustomResourceDefinition appears exactly like a built-in one provided it is already registered, and every install or upgrade re-decides the branch.
code
yaml · 10 lines{{- if .Capabilities.APIVersions.Has "policy/v1" }}
apiVersion: policy/v1
{{- else }}
apiVersion: policy/v1beta1
{{- end }}
kind: PodDisruptionBudget
metadata:
name: {{ .Release.Name }}-gateway
spec:
maxUnavailable: 1go deeper
Know that a Helm chart can ask the cluster what it supports through .Capabilities, and that this is how one chart emits different apiVersions on different clusters without being edited.
Be able to write the branch yourself: Has with a group/version or group/version/kind, and a semverCompare against .Capabilities.KubeVersion.Version rather than any comparison on .Minor.
Explain that the values come from discovery at render time, so the same chart and values can produce different manifests after a cluster upgrade, and describe how you keep those branches exercised.
Argue about how much branching a published chart should carry at all: every capability test is a permutation someone must test, and a declared support floor is often the cheaper contract.
A chart that must run on more than one cluster eventually needs to ask a question about the cluster itself: does it serve this API, and what version is it? Helm answers both through the built-in `.Capabilities` object, which is populated before any template is executed. ### Where the data comes from When Helm is about to render for a real operation, it queries the API server: the discovery endpoint gives the list of group/versions (and their kinds) the cluster serves, and the version endpoint gives the server version. Those become `.Capabilities.APIVersions` and `.Capabilities.KubeVersion`. Two things follow immediately. First, the answers describe the *target cluster*, not the chart and not the Helm client. Second, they are a snapshot for that one render — a later upgrade of the same release queries again, so the same chart and values can render differently after the cluster is upgraded. ### `APIVersions.Has` `.Capabilities.APIVersions.Has` is a boolean test with two accepted argument forms: ```yaml {{- if .Capabilities.APIVersions.Has "policy/v1" }} # group/version {{- if .Capabilities.APIVersions.Has "apps/v1/Deployment" }} # group/version/kind ``` The group/version form asks whether the cluster serves that API at all; the group/version/kind form asks whether a specific resource exists inside it, which is the sharper test when a group gains kinds over time. Core objects live in the group-less version `v1`, so the argument there is simply `v1`. An API backed by a CustomResourceDefinition is indistinguishable from a built-in one here — if the definition is registered when the render happens, discovery advertises it and `Has` returns true. The usual shape is a two-branch header on a single manifest: ```yaml {{- if .Capabilities.APIVersions.Has "policy/v1" }} apiVersion: policy/v1 {{- else }} apiVersion: policy/v1beta1 {{- end }} kind: PodDisruptionBudget ``` Keep the branch as small as this. Charts that wrap whole files in capability tests end up with several near-duplicate copies of a manifest, only one of which is ever rendered on the clusters anyone tests. ### `KubeVersion` `.Capabilities.KubeVersion` is a small struct. `.Version` is the full reported version string, `.Major` and `.Minor` are its parts as strings. The reliable comparison is a semver range against `.Version`: ```yaml {{- if semverCompare ">=1.29.0-0" .Capabilities.KubeVersion.Version }} ``` Two details matter. The leading `v` in the version string is handled by the semver parser, so you do not need to strip it. And the `-0` on the lower bound is not decoration: real clusters, especially managed ones, report versions that carry a vendor suffix after the patch number, and a semver constraint written without a prerelease component excludes anything with one. A constraint of `>=1.29.0` can therefore reject a cluster that is plainly newer than 1.29.0. The tempting shortcut is worse: ```yaml {{- if gt .Capabilities.KubeVersion.Minor "24" }} # do not do this ``` `.Minor` is a string, so this is a lexical comparison in which "9" sorts after "24"; and when a cluster reports a minor with a trailing non-numeric character, converting it to an integer fails outright. Compare the full version. ### What `.Capabilities` is not It is not `kubeVersion` in `Chart.yaml`. That field is a gate on whether the install proceeds at all; `.Capabilities` changes what is rendered when it does. It is also not a promise of correctness: a cluster can serve an API that your manifest uses incorrectly, and `Has` will happily return true. ### Practical shape Put the decision in one named template or one variable near the top of the file rather than scattering the same test through a manifest, so there is a single place to delete when the support floor moves. And remember that the answer depends on the render reaching a cluster at all — an offline render answers these questions from a compiled-in default rather than from your cluster.
- What argument forms does `.Capabilities.APIVersions.Has` accept?Either a group/version such as `policy/v1`, or a group/version/kind such as `apps/v1/Deployment`. The kind form is the right one when you care that a specific resource exists rather than that the group is served at all. Core objects use the group-less `v1`. Both forms return a boolean that drops straight into an `if`.
- Why is `.Capabilities.KubeVersion.Minor` a poor thing to compare?It is a string, so `gt` compares it lexically and "9" sorts after "24". Managed clusters have also reported a minor carrying a trailing non-numeric character, which breaks integer conversion. Compare `.Capabilities.KubeVersion.Version` with `semverCompare` and a range whose bounds include a prerelease component, which tolerates the vendor suffixes real clusters report.
- Can the capability set change between the install and a later upgrade of the same release?Yes. Every install and upgrade re-renders the chart against a fresh discovery result, so a cluster upgraded in between can flip a branch and produce a different apiVersion for the same chart and values. That is one reason the rendered manifest is not purely a function of chart plus values.
saying these in an interview costs you the question
- Thinks `.Capabilities` describes the chart rather than the cluster
- Compares `.Capabilities.KubeVersion.Minor` as a number
- Believes `Has` reads Chart.yaml's kubeVersion field
- Assumes the capability set is fixed for the life of a release
- Expects `Has` to accept a bare API group with no version
- Writes semver bounds without a prerelease component