Reusable helper templates for Kubernetes manifests
This section explains the terminology we use to for data structures and how the template code deals with type errors. Feel free to
skip directly to the helper reference
and come back later as needed.
This library contains helpers that simplify common tasks in Helm templating.
While all the code operates on YAML data, it focus on Helm Objects (most frequently Values) - which are YAML trees of key/value mappings represented in Go -and thus Helm- as string-keyed maps. In other words: map[string]interface{}
.
To disambiguate these and other entities with similar but not-quite-exactly-equal semantics across the different environments related to Helm, we'll strive for consistency in the words used throughout code and documentation:
helm-template-utils | Helm | Sprig | Go text/template | YAML | JSON |
---|---|---|---|---|---|
scalar | scalar | scalar | scalar | Scalar | Scalar |
map | map | dict | struct, map | Mapping | Object |
(map) member | k/v pair | k/v pair | field, element | Mapping node | Member |
(k/v pair) key | key | key | name, key | Key | Name |
list | list, slice | list | slice | Block Sequence | Array |
(list) item | item | item | element | Block Sequence node | Element |
value | value | value | value | Value | Value |
-
With list we refer to Helm lists (which are referred to also as slices in Helm's documentation)
-
With map we refer to Helm maps
-
With key we refer to the key in a Helm key/value pair
-
With value we refer to the universal idea of a value, but often to the value in a Helm key/value pair
The takeaway:
"not all valid YAML is valid Helm Values, and not all valid Helm Values are valid Kubernetes manifest data"
Examples:
---
# ✅ YAML: parses this as a single scalar value (string)
# ❌ Helm: the root node has to be a map
# ❓ K8S: can't be a whole manifest, but it can appear as data
"hello from root node"
---
# ❌ YAML: if the root node is a scalar, that's all the data the document will have
# ❌ Helm: can't parse invalid YAML
# ❌ K8S: same as Helm
"hello from root"
"two root nodes? no way!"
---
# ✅ YAML: the root node can be a Scalar, Sequence or Mapping
# ❌ Helm: the root node has to be an explicit map
# ❓ K8S: can't be a whole manifest, but it can appear as data
- 1
2
3
---
# ✅ YAML ✅ Helm ✅ Kubernetes
# Scalar value in Helm (keyed, this is a Go map)
# { key: "configurationKey1", value: true }
configurationKey1: true
---
# ✅ YAML ✅ Helm ✅ Kubernetes
# Nested map, (top-level map with the value being a map itself)
# { key: "aMap", value: { "subKey1": "a string", "subKey2": 42 } }
aMap:
subKey1: "a string"
subKey2: 42
---
# ✅ YAML ✅ Helm ❌ Kubernetes
# Mixed list, (top-level map with the value being a list with one map entry and one plain string)
# { key: "aList", value: [ { "subKey1": "a string" }, "other string" ] }
aList:
- subKey1: "a string"
- "other string"
We see that some values that are valid YAML won’t work as Helm values. Also, some values that are valid in YAML and Helm may still be considered invalid or unsupported by this library when generating content for Kubernetes manifests.
To avoid surprises keep in mind that the code currently filters out invalid Kubernetes data without warnings or errors. For example, if you process the aList
list with toEnv
, the plain string "other string"
will be silently ignored, because it lacks a key and cannot be mapped to a valid EnvVar
.
In future expansions, optional levels of strictness might be added that would have some helpers work more as lightweight static checkers, to help with debugging or enforcing guarantees on the input. In the current state the code is intended to "just work" with common real-world input, and behaves more like a filter than a checker, skipping over data that it can't validate.
Normalizes strings into SCREAMING_SNAKE_CASE (useful for EnvVar
name formatting)
{{ include "util.SSCase" string }}
- Converts all non-alphanumeric characters to underscores (
_
) - Deduplicates and trims leading and trailing underscores
- Filters the result through
snakecase
anduppercase
Maps values to Kubernetes EnvVar
fields
# Process target values, no prefix is added to variable names
{{ include "util.toEnv" targetKey }}
# Process target values and add a custom prefix to variable names
{{ include "util.toEnv" (list targetKey "custom prefix") }}
# Process target values and use target key as prefix
{{ include "util.toEnv" (list targetParentKey "targetSubkey") }}
Target values | Outcome |
---|---|
scalar | renders one EnvVar item |
valueFrom |
checked via util.leafKind and rendered as one EnvVar item |
map | renders multiple EnvVar items |
list | renders multiple EnvVar items |
improper valueFrom maps |
ignored |
list items other than scalar or valueFrom |
ignored |
map members other than scalar or valueFrom |
ignored |
undefined, null, invalid | ignored |
- A second
string
argument becomes a prefix for the generated names - If the prefix exists as a member key in the target map, that member becomes the target for processing. This is a shorthand notation to avoid passing the same key twice when wanting to use it as the prefix.
- Uses
util.SSCase
for name and prefix formatting
Input values:
database:
host: localhost
port: 5432
Include:
env: {{ include "util.toEnv" (list .Values.database "DB config") }}
Rendered output:
- name: DB_CONFIG_HOST
value: "localhost"
- name: DB_CONFIG_PORT
value: "5432"
More examples can be found in the test chart
in this repo.
- configurable strictness (extra
leafKind
typechecks, warn or error instead of discard) - regex filters for keys or values
This is used as a toEnv
helper that checks the kind of its context and returns it as string for leaf values (scalars and valueFrom
), or null
otherwise
# use as a predicate
{{ if include "util.leafKind" target }} ...
# use as a predicate and save the result in a single pass
{{ if $kind := include "util.leafKind" target }} ...
- A scalar (
bool
,int
,int64
,float64
orstring
) will render its kind - A well-formed
valueFrom
will render the string"valueFrom"
"Well-formed" means for now a trivial check that we have a map with a single valueFrom
member, no comprehensive guarantees are issued.
- optional extra checks on
valueFrom
A simple predicate testing for maps with a single member
{{ if include "util.isKeyValue" target }} ...
- Renders
"true"
if the input is a map with exactly one key, otherwise it renders nothing
The templates and assertions in the test chart
should cover every possible case, but if you spot omissions be welcome to open an issue about it.
Run the test template with:
make test
To run the helm-unittest templates, install that plugin and run:
make unittest