skip to content

In a Helm chart, how do `.Capabilities.APIVersions.Has` and `.Capabilities.KubeVersion` decide which apiVersion a template emits?

level: middleimportance: must knowfreq 62%

answer

  1. The object that describes the target cluster
  2. Discovery answers, taken before rendering
  3. Ask whether a group/version is served
  4. Compare the full version, not the minor
  5. Re-decided on every install and upgrade

basics

~10 s

Helm 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
yaml
{{- if .Capabilities.APIVersions.Has "policy/v1" }}
apiVersion: policy/v1
{{- else }}
apiVersion: policy/v1beta1
{{- end }}
kind: PodDisruptionBudget
metadata:
  name: {{ .Release.Name }}-gateway
spec:
  maxUnavailable: 1

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context