004 — Maps over lists as the default collection shape
- Status: Accepted
- Date: 2025-04-25
Context
Many Kubernetes resource specs are lists where each item carries a name: containers, volumes, volumeMounts, ports, env, imagePullSecrets, Ingress hosts, Service ports. Helm chart values can mirror that list shape directly, or transpose it into a map keyed by the natural-key field. The choice shapes everything users do downstream:
- Overlay merging. Helm merges values from
-foverlays. Maps merge per key; lists are replaced wholesale. Lists therefore force the overlay to redeclare every entry. - Duplicate prevention. YAML maps reject duplicate keys at parse time; lists let you have two
containers[].name: fooand the kubelet rejects the pod at runtime. - Targeted disabling / overriding. A map gives you
entry.<name>.enabled: falsefor free; with a list you have to filter it during template render. - Diff stability. A reordered list looks like a meaningful diff to Argo CD; a reordered map round-trips identically because we sort it.
The 2.0.0 refactor pushed as many list-shaped values as possible into maps. Where the map key would be an optional Kubernetes field (e.g. ingress.tls[].secretName), the native list shape was retained or restored to avoid forcing a required value.
Decision
Default to maps. A values.yaml collection becomes a map keyed by its natural unique field whenever such a field exists. The rendered YAML uses the map key as the resource field’s name (volume name, port name, host name, container name, secret name, etc.) — users do not repeat the name inside the value.
A collection stays as a list only when one of these is true:
- No natural key. Examples:
tolerations(matched by combinations of fields, not name),rbac.rules(a tuple ofapiGroups + resources + verbs), Gateway APIrules(an ordered list of match-and-forward records),ingress.hosts.<host>.paths(path order matters in some controllers). - Order is part of the contract. Examples: pod
initContainersin tasks-modejobGroups(we sort by name to make order deterministic, but users still see a list-like ordering by alphabetised key); HTTP route precedence (handled with an explicitpriorityfield — see ADR 014). - Duplicates are legitimate. Example:
dataFromin External Secrets, where the sameremoteKeycan appear with different conversion strategies. - A list of plain strings reads better than a one-key-per-string map. Examples:
imagePullSecrets,envSecrets,envConfigMaps,priorityClassNamelists,hostAliases.hostnames. The chart wraps each entry into the matching Kubernetes object shape at render time — for example, animagePullSecretsentry becomes an object with anamefield underspec.imagePullSecrets[], and anenvSecretsentry becomes asecretRefobject underenvFrom[].
When a map is used, the chart always iterates with keys ... | sortAlpha so output order is deterministic regardless of values.yaml author’s keystroke order. Special cases (port name ordering, init container ordering) live in ADR 014.
Consequences
What this enables:
- Overlay values can target one entry without rewriting the rest:
# base.yaml deployments: api: service: ports: http: port: 80 targetPort: 8080 metrics: port: 9090 targetPort: 9090# prod-overlay.yaml — bumps the metrics port only deployments: api: service: ports: metrics: port: 9091 - Duplicate volume names, port names and the like are caught by the YAML parser before the chart even runs.
- Per-entry disabling reads naturally:
configMaps.app-config.enabled: false. - Helper code is consistent: every map-iterating loop reuses the same
keys ... | sortAlphapattern.
What it costs:
- Migrating a list to a map is a breaking change to the
values.yamlAPI. The 2.0.0 release line absorbs this cost; future migrations will need ADRs of their own. - Map keys collide with reserved identifier rules: a port name must be ≤15 chars (RFC 6335), a host must be a valid DNS name.
values.schema.jsonenforces these at chart-install time. See ADR 013. - Some users prefer the list shape because it visually resembles raw Kubernetes manifests. We accept the small re-learning cost in exchange for the merge / dedup / dedupe properties above.
Migration philosophy (2.0.0)
The pre-2.0.0 chart had a mix of list and map shapes for similar concepts. The 2.0.0 line landed three rounds of consolidation:
monitoringproviders became a defaults-driven matrix (incidental side-effect: trimmed lists in scrape config).- The
integrationsnamespace was introduced (incidental side-effect: ecosystem knobs that used to be top-level lists became maps underintegrations.*). - The remaining lists were explicitly collapsed into maps where a natural key existed:
volumes,volumeMounts,service.ports,ports,ingress.hosts,httpRoutes,referenceGrants,configMaps,sidecars. Lists kept:imagePullSecrets,envSecrets,envConfigMaps(now lists of plain strings),ingress.tls(native k8s list —secretNameis optional in k8s so using it as a map key would make it falsely mandatory), Gateway APIrules,rbac.rules,tolerations,hostAliases, ESOdataFrom.
Alternatives considered
- Keep everything as a list (mirror Kubernetes API verbatim). Rejected: gives up overlay-merge wins, duplicate prevention, and per-entry disabling.
- Keep everything as a map (no lists at all). Rejected: the four exceptions above are real — a deduplicated map representation of
tolerationsor Gateway APIruleseither misrepresents the upstream type or invents synthetic keys. - Custom DSL on top of values. Rejected: more to learn; less inspectable.