Kubit logoKubit

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
FieldRequiredPurpose
repoYesRepository resource name from spec.chart.repository.name
chartYesChart name from spec.chart.name
from_versionYesSource version or constraint
to_versionYesVersion constraint written to the resulting manifest
stepsYesOrdered transformations; use an empty list when no transform is needed
contextNoInitial 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:

ExampleMatching 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:

NameValue
packComplete mutable Pack manifest
trueBoolean true
falseBoolean false
nullNull value
context keysTemporary 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:

  1. The if condition or else branch.
  2. Item lookup with item_in_list.
  3. Assignment with set.
  4. Nested steps.
  5. 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

TagBehavior
‎!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 tagset valueif conditionitem_in_list.value
Plain YAMLYesPath onlyYes
‎!var‎YesNoYes
‎!pop‎YesNoYes
‎!pop-empty‎YesNoNo
‎!default‎YesNoNo
‎!default-var‎YesNoNo
‎!merge‎YesNoNo
‎!jinja‎YesYesNo
‎!not‎NoYesNo

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

MistakeResult or correction
Put the repository URL in repoNo match; use spec.chart.repository.name
Keep only the latest incremental migrationPacks that skipped versions have no complete path
Set spec.chart.version again in a stepRedundant; the operator applies to_version before steps
Use ‎!pop‎ for a new defaultA missing source produces nothing; use ‎!default‎ or ‎!default-var‎
Use ‎!pop-empty‎ to transfer dataTruthy values remain; use this tag only for cleanup
Use Jinja without ‎!jinja‎The expression is stored as plain text
Assume file order is execution orderTest 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:

  1. The oldest supported version and both boundaries of every from_version.
  2. A Pack before every intermediate migration and one that traverses the complete chain.
  3. Custom values, false, zero, empty values, and missing optional fields.
  4. Existing and missing list items and an invalid index.
  5. A second migration run; it must introduce no new migration-specific change.
  6. 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.

Related guides

Migrate Packs between Helm chart versions | Documentations | Kubit