Versioned migrations see history edit this page

Talks about: , , and

Some changes only need to happen once, when you cross a release boundary — a one-time data backfill on the way to 2.0, a schema conversion between 1.x and 2.x. Versioned migrations run a ladder of actions exactly when the deployed version steps over the boundary, and never again.

Versioning is off until you set spec.version.

Declaring the version

The controller needs to know what version is currently being deployed. There are three ways to declare it; pick by where the version lives.

SourceThe version lives…Best for
version.valueon the StageSetenvironment-pinned versions, quick starts
version.fromObjectinside the manifestsany source, including JaaS — the recommended default
version.fromArtifacta file in the artifactGit/OCI/Bucket sources that can ship a VERSION file

Whichever you choose, the resolved value is trimmed and parsed as semver (a leading v is accepted). A missing stage/object/file, an empty value, or an unparseable one fails terminally with the InvalidVersion reason (see its runbook) — a half-versioned system is worse than an unversioned one.

Inline — version.value

The StageSet author pins the version directly. Use this when the version is a property of the environment rather than of the content, or to get started quickly:

spec:
  version:
    value: "2.1.0"      # bump this when you cut a release

The trade-off: the version is declared here, not carried by the content, so you bump it by editing the StageSet.

From a rendered object — version.fromObject

The recommended way to let the version travel with the content. Kubernetes has a standard place for an application’s version: the app.kubernetes.io/version label. Well-formed manifests set it, so the version is already inside the manifests — fromObject reads it back. This works for every source kind, including a single-document renderer like JaaS that has no room for a separate file.

spec:
  version:
    fromObject:
      stage: app            # which stage's rendered manifests carry it
      kind: Deployment      # the object to read
      name: web
      # fieldPath omitted → reads metadata.labels['app.kubernetes.io/version']
  stages:
    - name: app
      sourceRef:
        name: my-app

The controller builds the app stage’s manifests (the same render it applies), finds the Deployment/web object, and reads its app.kubernetes.io/version label. Because the label is part of the manifests, the version changes in lockstep with the content — no second file to keep in sync.

Reading a different field. Set fieldPath to a kubectl-style JSONPath that resolves to the bare version string. (It must be the version only; a JSONPath can’t split an image: web:2.1.0 value, so prefer the label.) apiVersion is optional and narrows the match when a Kind+Name pair would be ambiguous:

spec:
  version:
    fromObject:
      stage: app
      apiVersion: v1
      kind: ConfigMap
      name: app-meta
      fieldPath: "{.data.version}"   # must resolve to a bare semver, e.g. 2.1.0

This is the path the Jsonnet-to-rollout tutorial uses: the snippet renders the version into the manifest’s version label, and the StageSet reads it straight back.

From a file in the artifact — version.fromArtifact

The version travels with the content as a dedicated file containing a single semver. This fits Git/OCI/Bucket sources, where you can ship an extra file beside the manifests. (It does not fit JaaS rendered output, which is a single rendered.json; use fromObject there.)

Who writes it, and where: the artifact’s producer. For a Git source, commit a VERSION file in the repo; for an OCI/Bucket artifact, include it in the pushed tree. The file lives at path inside the named stage’s artifact, relative to the artifact root:

# VERSION — committed alongside the manifests it versions
2.1.0
spec:
  version:
    fromArtifact:
      stage: app          # which stage's artifact carries the file
      path: VERSION       # the file's path inside that artifact (cleaned; no leading ./)
  stages:
    - name: app
      sourceRef:
        kind: GitRepository
        name: my-app

The controller fetches the app stage’s artifact and reads the file at path.

Declaring migrations

Each migration names the boundary it crosses (to, optionally from), the stage it anchors before, and the actions to run:

spec:
  version:
    fromArtifact:
      stage: app
      path: VERSION
  migrations:
    - name: backfill-ledger-2-0
      from: "1.*"               # optional: only when coming from a 1.x
      to:   "2.0.0"             # the boundary this migration crosses
      stage: app               # runs before this stage's pre-actions
      actions:
        - name: backfill
          job:
            sourceRef:
              name: ledger-backfill-job
  stages:
    - name: app
      sourceRef:
        name: my-app

When the deployed version crosses from a 1.x into 2.0.0, the backfill job runs once, anchored before the app stage. The controller tracks progress so a retry doesn’t re-run a completed migration:

Migrations emit MigrationStarted / MigrationCompleted events (and MigrationFailed on error). A downgrade that would skip a required migration is refused with the DowngradeRequiresMigration reason — see its runbook.