Migrate Packs between Helm chart versions
Pack Migration transforms existing Pack manifests when a chart changes its values, vars, or version structure, while preserving user choices. Each existing manifest does not need to be rewritten manually.
This page is the reference for creating and testing pack-migrations.yaml. For other chart-specific files and features, see Pack capabilities in Helm charts.
Define migrations with pack-migrations.yaml
pack-migrations.yaml defines Pack-manifest transformations for chart structure or version changes. Store it under this exact name in the chart root beside Chart.yaml.
Pack Operator reads migrations from the target chart. Every published version must contain the complete chain from all supported older versions to that target. A user who skips several versions must not need to install every intermediate chart separately.
migrations:
- repo: kubit-paas
chart: redis
from_version: '>=0.1.0, <0.2.0'
to_version: '~=0.2.0'
context:
metrics_default: true
steps:
- set:
values: !var pack.spec.values
values.metrics.enabled: !default-var metrics_default
- if: values.password
set:
values.auth.password: !pop values.password
- reorder:
values:
- auth
- metrics
| Field | Required | Purpose |
|---|---|---|
repo | Yes | Repository resource name from spec.chart.repository.name |
chart | Yes | Chart name from spec.chart.name |
from_version | Yes | Source version or constraint |
to_version | Yes | Version constraint written to the resulting manifest |
steps | Yes | Ordered transformations; use an empty list when no transform is needed |
context | No | Initial temporary variables for steps and Jinja expressions |
repo is the repository resource name, not its URL. repo and chart match exactly and do not accept wildcards. A Pack without spec.chart.version does not match any migration.
Select and chain versions
from_version supports semantic-version comparisons:
| Example | Matching versions |
|---|---|
'<2.0.0' | Versions below 2.0.0 |
'>=2.0.0, <2.3.0' | Versions from 2.0.0 up to, but not including, 2.3.0 |
'~=2.3.0' | The 2.3.x branch |
'<v1.19.4' | Versions below 1.19.4 with a v prefix |
Quote version expressions, especially those beginning with < or >. Comma-separated constraints must all match.
Before each migration runs, the operator sets spec.chart.version to to_version. Later migrations are then checked against the new version, so several entries can form an upgrade chain:
migrations:
- repo: kubit-paas
chart: redis
from_version: '<0.3.0'
to_version: '~=0.3.0'
steps: []
- repo: kubit-paas
chart: redis
from_version: '>=0.3.0, <0.5.0'
to_version: '~=0.5.0'
steps: []
Here, a Pack on 0.2.0 first moves to the 0.3 branch and then to 0.5. An entry's to_version must not match the same entry's from_version, or that migration may run again later.
File order is not the final execution order. The operator sorts entries by repository name, chart name, and normalized from_version. For chains such as 1.9 and 1.10, verify effective ordering with the migration engine.
Migration paths and context
Paths are dot-separated, for example pack.spec.values.service.port. Each entry has an independent context containing:
| Name | Value |
|---|---|
pack | Complete mutable Pack manifest |
true | Boolean true |
false | Boolean false |
null | Null value |
context keys | Temporary values defined by the entry |
Create aliases at the start of a migration to shorten paths:
- set:
chart: !var pack.spec.chart
values: !var pack.spec.values
chart.autoUpgrade: !default-var true
A mapping or list read through !var points to the original data, so changing values.service.port also changes pack.spec.values. A top-level temporary variable such as old_port is not written to Pack output and does not persist between entries.
Reading a missing path returns null. Writing a path creates missing intermediate mappings, but writing null is ignored.
If a mapping key contains a literal dot, escape it with \. and single-quote the complete path:
- set:
copied: !var 'values.annotations.example\.com/key'
'values.annotations.example\.com/key': enabled
A numeric path segment is not a list index. Use item_in_list to read or change list items.
Migration operations
Each steps item is one step. Operations always run in this order even when YAML keys are arranged differently:
- The
ifcondition orelsebranch. - Item lookup with
item_in_list. - Assignment with
set. - Nested
steps. - Sorting with
reorder.
Keep each step small. Combine operations in one step only when their fixed execution order is required, for example when a list item must be found before set changes it.
set tags
| Tag | Behavior |
|---|---|
!default | Writes a fixed value only when the destination key is absent |
!default-var | Writes a context-path value only when the destination is absent |
!var | Reads a context path without removing the source |
!pop | Removes the source value and moves it to the destination |
!pop-empty | Removes only falsey source values; useful for empty mappings |
!merge | Recursively merges the source mapping into the destination mapping |
!jinja | Renders a value with migration context and preserves its native type |
An untagged value overwrites its destination. To preserve user choices, read an old key with !pop and set a destination default with !default-var. Use !default for a new default that must not overwrite an existing value, including false, zero, or empty values.
!pop-empty is not a data-transfer tool. It removes only a falsey value such as an empty mapping or list, and writes false to the destination when nothing is removed. Use temporary names and clean from the deepest path toward its parent:
- set:
_: !pop values.deprecatedOption
_1: !pop-empty values.legacy.child
_2: !pop-empty values.legacy
For !merge, source and destination must be mappings. If the destination may not exist, create it first with !default.
!jinja can produce a boolean, number, list, or mapping. Use the default filter or its short form d for an optional path; direct access to an undefined variable stops migration with an error.
Conditions and nested steps
A simple condition converts a path value to boolean. !not negates the path, while !jinja supports comparisons and compound conditions:
- if: values.ingress.enabled
set:
values.ingress.className: !default nginx
- if: !not values.persistence.enabled
set:
_: !pop values.persistence
- if: !jinja '{{ values.mode | d("standard") == "legacy" }}'
steps:
- set:
values.mode: standard
else:
- set:
values.mode: !default standard
else must be beside if and contain a list of steps. When the condition is false, only else steps run; other operations in the outer step are ignored.
Change list items with item_in_list
item_in_list finds a mapping in a list by key and value, or by index. The found or created item is stored in the temporary var:
- set:
values.service.ports: !default []
- item_in_list:
list: values.service.ports
key: name
value: http
create: true
var: http_port
set:
http_port.port: !default 6379
http_port.protocol: !default TCP
Create the list first with !default []. Otherwise, a new item may be added to a temporary list that is not connected to the Pack.
key and index cannot be used together. An out-of-range index is an error. Without create: true, var is unset when no item matches; guard later steps with if. value accepts only a fixed value, !var, or !pop.
Valid tag locations
| Value or tag | set value | if condition | item_in_list.value |
|---|---|---|---|
| Plain YAML | Yes | Path only | Yes |
!var | Yes | No | Yes |
!pop | Yes | No | Yes |
!pop-empty | Yes | No | No |
!default | Yes | No | No |
!default-var | Yes | No | No |
!merge | Yes | No | No |
!jinja | Yes | Yes | No |
!not | No | Yes | No |
Sort output with reorder
reorder moves named keys to the beginning of an existing mapping in the requested order. Other keys remain after them. It changes only YAML readability, not Helm behavior.
- reorder:
pack.spec.values:
- global
- service
- ingress
pack.spec.values.service:
- enabled
- type
- ports
Common pack-migrations.yaml mistakes
| Mistake | Result or correction |
|---|---|
Put the repository URL in repo | No match; use spec.chart.repository.name |
| Keep only the latest incremental migration | Packs that skipped versions have no complete path |
Set spec.chart.version again in a step | Redundant; the operator applies to_version before steps |
Use !pop for a new default | A missing source produces nothing; use !default or !default-var |
Use !pop-empty to transfer data | Truthy values remain; use this tag only for cleanup |
Use Jinja without !jinja | The expression is stored as plain text |
| Assume file order is execution order | Test ranges and chains with the actual migration engine |
Test migrations before publishing
kubit pack migrate -f redis.pack.yaml \
--local-chartpath ./redis
The command prints the migrated manifest to stdout. Compare it with the input and confirm that only the expected version and paths changed. Output is normalized and may omit spec.managed: true; omitting spec.managed is equivalent to true.
For each migration path, test at least:
- The oldest supported version and both boundaries of every
from_version. - A Pack before every intermediate migration and one that traverses the complete chain.
- Custom values,
false, zero, empty values, and missing optional fields. - Existing and missing list items and an invalid index.
- A second migration run; it must introduce no new migration-specific change.
- Render or diff the target chart with the migrated manifest.
Also review semantic changes to values, not only the YAML text diff. Field ordering and metadata cleanup may appear even when steps do not define them directly.
After review, write the result to the file:
kubit pack migrate -f redis.pack.yaml \
--local-chartpath ./redis \
--inline
--inline changes the input file. Keep the previous version in Git and verify the file path before running it. For a target version or a Pack already in the cluster, see pack migrate command reference.
During automatic upgrades, Pack Operator first checks for a matching migration. If a valid change is produced, the Pack manifest is replaced with the result. If no migration matches or execution fails, the normal automatic-upgrade path continues; a migration error does not guarantee that the upgrade stops.