1. Introduction

Helm is a Kubernetes package manager that deals with charts as its units of deployment. Like any other package manager, Helm resolves dependencies between its packages, i.e., charts. Further, it supports manually listing dependencies in certain conditions. However, it doesn’t provide a feature that shows a dependency tree for a given installed chart, i.e., release.

In this tutorial, we explore Helm dependency enumeration and resolution. First, we briefly refresh our knowledge about dependencies. After that, we see a native way to list dependencies with Helm. Next, we use shell scripting to leverage this native method for an installed chart also called a release. Finally, we demonstrate a more complex script that can extract charts from a release and list dependencies recursively.

We tested the code in this tutorial on Debian 12 (Bookworm) with GNU Bash 5.2.15 and Helm 3.14. Unless otherwise specified, it should work in most POSIX-compliant environments.

2. Chart Dependencies

Even very basic Kubernetes deployments have many moving parts.

For instance, an empty Kubernetes cluster still runs multiple pods with different functions:

$ kubectl get all --namespace=kube-system
NAME                               READY   STATUS    RESTARTS        AGE
pod/coredns-5dd0666b68-lp7b5       1/1     Running   1               9d
pod/etcd-xost                      1/1     Running   1               9d
pod/kube-apiserver-xost            1/1     Running   1               9d
pod/kube-controller-manager-xost   1/1     Running   1               9d
pod/kube-proxy-tldfh               1/1     Running   1               9d
pod/kube-scheduler-xost            1/1     Running   1               9d
pod/storage-provisioner            1/1     Running   1 (6d22h ago)   9d

NAME               TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                  AGE
service/kube-dns   ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP,9153/TCP   9d

NAME                        DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
daemonset.apps/kube-proxy   1         1         1       1            1           kubernetes.io/os=linux   9d

NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/coredns   1/1     1            1           9d

NAME                                 DESIRED   CURRENT   READY   AGE
replicaset.apps/coredns-5dd0666b68   1         1         1       9d

Further, Helm chart deployments usually result in releases with many pods and resources:

$ helm install gabe565/nightscout --generate-name --create-namespace --namespace nightscout
[...]
$ kubectl get all --namespace nightscout
NAME                                        READY   STATUS    RESTARTS   AGE
pod/nightscout-1709766626-f7d430108-tzp8j   1/1     Running   0          49s
pod/nightscout-1709766626-mongodb-0         1/1     Running   0          49s

NAME                                    TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)     AGE
service/nightscout-1709766626           ClusterIP   10.105.224.56   <none>        1337/TCP    49s
service/nightscout-1709766626-mongodb   ClusterIP   10.101.52.236   <none>        27017/TCP   49s

NAME                                    READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nightscout-1709766626   1/1     1            1           49s

NAME                                              DESIRED   CURRENT   READY   AGE
replicaset.apps/nightscout-1709766626-f7d430108   1         1         1       49s

NAME                                             READY   AGE
statefulset.apps/nightscout-1709766626-mongodb   1/1     49s

Because of these natural object trees, Helm officially supports dependencies with a formal syntax. For instance, a dependency chart can be a direct part of the dependent chart metadata via a name, version, and URL.

Notably, the exact syntax has changed through Helm versions:

  • Helm 2 uses a separate requirements.yaml file
  • Helm 3 embeds information in the Chart.yaml file

Still, dependency resolution works the same for both.

3. List Chart Dependencies

To begin with, let’s understand how to use the Helm dependency subcommand.

Unlike other package managers, Helm doesn’t provide dependency trees for installed or online packages.

Instead, we first have to download a chart and unpack it:

$ helm pull gabe565/ascii-movie --untar

In fact, pull supports –version selection similar to other Helm subcommands:

$ helm pull gabe565/ascii-movie --version 0.0.3 --untar

After [pull]ing the chart and unpacking it, we enter the resulting directory and list the files within.

$ cd ascii-movie
$ ls
Chart.lock  charts  Chart.yaml  README.md  templates  values.yaml

Since we can see the Chart.yaml file along with the charts subdirectory, we can run the dependency subcommand of Helm:

$ helm dependency list
NAME    VERSION REPOSITORY                              STATUS
common  1.5.1   https://bjw-s.github.io/helm-charts     unpacked

Evidently, the ascii-movie chart has one dependency named common from the given REPOSITORY.

Let’s turn this process into a single command and generalize it:

$ helm pull <CHART> --untar && helm dependency list ./<CHART_NAME> && rm -rf <CHART_NAME>

Here, the syntax of CHART can be any of the options supported by the install subcommand. On the other hand, CHART_NAME is the name of the chart and usually the resulting directory and file names.

4. Installed Chart Dependencies

Although it’s not an official feature, we can come up with a way to get the dependencies of an installed chart via its release.

First, we get information about the release we want:

$ helm list nightscout-1709766626
NAME                   NAMESPACE   REVISION  UPDATED                                  STATUS    CHART              APP VERSION
nightscout-1709766626  nightscout  1         2024-03-08 03:08:24.619666646 -0500 EST  deployed  nightscout-0.10.0  15.0.2

In this case, the CHART that generated the release is nightscout-0.10.0.

So, *let’s find the chart information by [list]ing and [–filter]ing the releases from –all-namespaces*:

$ helm list --filter nightscout --all-namespaces
NAME                    CHART VERSION   APP VERSION     DESCRIPTION
gabe565/nightscout      0.10.0          15.0.2          Web-based CGM (Continuous Glucose Monitor) to a...

Notably, there can be complications during this step:

  • the chart might be from the Artifact Hub
  • multiple charts can have the same name in different repositories
  • the repository might not be available

In these cases, we might have to switch to hub searching or perform some research on the Web. Ironically, we can use the dependencies of a chart to identify it among others.

Here, the chart is gabe565/nightscout.

At this point, we can fill in the values:

  • CHART = gabe565/nightscout or oci://ghcr.io/gabe565/charts/nightscout
  • CHART_NAME = nightscout

Now, we use the approach from earlier by replacing the relevant fields:

$ helm pull gabe565/nightscout --untar && helm dependency list ./nightscout && rm -rf nightscout
NAME    VERSION REPOSITORY                              STATUS
common  1.5.1   https://bjw-s.github.io/helm-charts     unpacked
mongodb 14.5.1  https://charts.bitnami.com/bitnami      unpacked

Further, we can use an OCI or regular URL if the repository isn’t configured locally:

$ helm pull oci://ghcr.io/gabe565/charts/nightscout --untar && helm dependency list ./nightscout && rm -rf nightscout

Critically, our approach only lists first-level dependencies. In other words, we don’t see the dependencies of each dependency, i.e., the dependency tree.

5. Automate Installed Chart Dependency Listing

If we set several preconditions, we can automate dependency listing for an installed release:

  • chart repository must be configured locally or the chart should be a part of the Artifact Hub
  • chart identifier contains dash-separated name and version
  • chart dependencies are non-circular

Thus, the input parameters should only include the potentially partial RELEASE_FILTER that indicates the release we want to process.

5.1. Initialization

First, we start with the usual shebang and parameter assignment:

#!/usr/bin/env bash

release_filter=$1
multilevel=$2

# global array with chart identifiers, i.e., chart_name-chart_version-repo_url strings
charts=()

In addition, we define a global charts array that holds that charts queue.

5.2. Chart Dependency Processor

After the initialization, we create the chart_deps() function, which processes the first element of the global queue and removes it:

function chart_deps() {
  [ ${#charts[@]} -eq 0 ] && return

  local chart_identifier="${charts[0]}"
  local chart_name chart_version repo_url
  local status
  local deps

  # get chart name and version
  IFS=- read chart_name chart_version repo_url <<< "$chart_identifier"
  
  printf 'Processing %s from %s ...\n' $chart_name $repo_url

  # pull chart via URL
  if [[ $repo_url =~ 'oci://' ]]; then
    helm pull $repo_url --version $chart_version --untar >/dev/null 2>&1
  else
    helm pull $chart_name --repo $repo_url --version $chart_version --untar >/dev/null 2>&1
  fi
  
  if [ $? -ne 0 ]; then
    printf 'Unable to pull %s from %s.\n\n' $chart_name $repo_url
    return
  fi

  deps="$(helm dependency list ./$chart_name)"

  printf '%s deps:\n%s\n\n' "$chart_name-$chart_version" "$deps"

  if [ -n "$deps" ] && [ -n "$multilevel" ]; then
    charts+=($(echo "$deps" | awk 'NR > 1 { print $1"-"$2"-"$3; }'))
  fi

  rm -rf $chart_name
  rm -rf $chart_name-$chart_version.tgz
}

The processing comes down to parsing the input, pulling the relevant chart, and showing dependency subcommand output for it.

Further, we add any new dependencies to the end of the queue if the $multilevel argument has a value.

5.3. Main

Before we get into dependencies, we get the release filter argument and use it to extract the release chart:

# count releases from all namespaces that match the filter
releases="$(helm list --filter $release_filter --all-namespaces --no-headers
  | wc --lines)"
# exit if more than one release matches
if [ $releases -ne 1 ]; then
  >&2 printf 'No or multiple release matches: --filter %s.' $release_filter
  exit 1
fi

# get partial chart identifier from the YAML information about the single matching release
chart_identifier="$(helm list --filter $release_filter --all-namespaces --output=yaml
  | awk '/chart:/ { print $2; }')"

# extract chart name and version
IFS=- read chart_name chart_version <<< "$chart_identifier"

# get chart repository URL from repo or hub
chart_path="$(helm search repo $chart_name --version $chart_version --output yaml
  | grep 'version: '$chart_version -B 1
  | awk '/name:/ { print $2; }')"
if [ -n "$chart_path" ]; then
  IFS=/ read repo_name chart_name <<< "$chart_path"
  repo_url="$(helm repo list | awk '/'$repo_name'/ { print $2; }')"
else
  repo_url="$(helm search hub $chart_name --output yaml
    | grep 'version: '$chart_version -B 2
    | awk 'NR == 1 && /url:/ { print $2; }')"
fi
if [ -z "$repo_url" ]; then
  >&2 printf 'Repository not found for release chart %s version %s.' $chart_name $chart_version
  exit 1
fi

# first chart identifier comes
chart_identifier=$chart_identifier-$repo_url
charts+=("$chart_identifier")

Once we have the chart, we search for it in the hub and any configured repositories. Once found, we place the resulting information as the first queue element.

Finally, we begin processing the queue, removing elements as we go:

while [ ${#charts[@]} -ne 0 ]; do
  chart_deps 
  charts=("${charts[@]:1}")
done

Although not perfect, this method should list all dependencies of the provided release filter. In addition, it can try to list dependencies of dependencies.

If required, the method can be adjusted to work for any chart, not only release charts.

6. Summary

In this article, we talked about dependency checks via Helm.

In conclusion, although no native feature exists, we can use the Helm functions and shell scripting to implement ways to list the dependencies of an installed chart.