You've successfully subscribed to Nuvotex Blog
Great! Next, complete checkout for full access to Nuvotex Blog
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.
Success! Your billing info is updated.
Billing info update failed.

Helm's Hidden Quirks: Merging boolean values

Helm is a powerful tool for manageing Kubernetes applications. Despite its ubiquity in the kubernetes-world, there are some nuances to consider when working with boolean values—especially when merging complex objects.

Felix Zimmermann

The easy case: Overwrite boolean values

A common use case in Helm charts is defining multiple components and allowing users to enable or disable them based on their needs. For example, many charts provide an Ingress resource by default but allow users to disable it. This might look like the following:

[...]
application:
  ingress:
    enabled: false
[...]

As expected, the enabled: true value from the application's chart can be overridden with enabled: false by the user. This works seamlessly and is straightforward to understand.

The complex case: Merging

Now, let’s imagine a more complex scenario. Suppose you’re deploying an application consisting of multiple smaller components, each with its own configuration. Many of these components share common settings, such as Ingress configurations. To avoid redundancy, you might define a default configuration and merge it with component-specific overrides. Here’s an example of what such a values file might look like:

default:
  ingresses:
    default-ingress:
      enabled: true
      service: default-service
      port: 80
  
config:
  component1:
  component2:
    ingresses:
      additional-ingress:
        enabled: true
        service: additional-service
        port: 123
  component3:
    ingresses:
      default-ingress:
        enabled: false

In this setup, the default section contains the shared part of the configuration, while the config section defines overrides for specific components. To use this, you could loop over the components, merge the component-specific config with the default, and create the resources. For merging dictionaries, there is an dedicated merge function, which could be used like this:

{{ $merged := merge $.Values.default.ingresses (default dict ($deployment).ingresses) }}

The resulting YAML output for each component would look like this:

component1:
  default-ingress:
  enabled: true
  port: 80
  service: default-service
---
component2:
  additional-ingress:
    enabled: true
    port: 123
    service: additional-service
  default-ingress:
    enabled: true
    port: 80
    service: default-service
---
component3:
  additional-ingress:
    enabled: true
    port: 123
    service: additional-service
  default-ingress:
    enabled: true
    port: 80
    service: default-service

At first glance, Components 1 and 2 look correct. However, Component 3 has two issues:

  1. It includes an extra Ingress (additional-ingress) that shouldn’t be there.
  2. The default-ingress is enabled, even though it was explicitly set to false in the configuration.

The Problem: merge

The root of the issue lies in how Helm's merge function is defined. merge does not overwrite boolean fields that are set to True. Because this behavior is not included in the method documentation, it can be quite surprising.

To address this issue, you need to use mergeOverwrite. This method does overwrite True values, which is what we need here. Additionally, the precedence changes: while merge gives precedence from left to right, mergeOverwrite gives precedence from right to left.

The second thing to keep in mind: The first argument for merge and mergeOverwrite is used as the return variable. Therefore, in a loop, you need to ensure that the values stored in the first argument are not passed to the following loop iteration. This is what causes the two ingresses in the third component shown in the example.

Merging, but this time correctly

To implement the merge correctly, there are two options.

  1. Copy the first argument:
{{ $merged := mergeOverwrite (deepCopy $.Values.default.ingresses) (default dict ($deployment).ingresses) }}
  1. pass an empty dictionary as first argument:
{{ $merged := mergeOverwrite (dict) ($.Values.default.ingresses) (default dict ($deployment).ingresses) }}

In both cases, the result is what we want to achieve. Component 1 has only the default ingress defined, Component 2 an additional ingress and Component 3 disables the default - without adding an custom one:

name: component1
  default-ingress:
    enabled: true
    port: 80
    service: default-service
---
name: component2
  additional-ingress:
    enabled: true
    port: 123
    service: additional-service
  default-ingress:
    enabled: true
    port: 80
    service: default-service
---
name: component3
  default-ingress:
    enabled: false
    port: 80
    service: default-service
KubernetesIaCLinuxCloudContainer