Skip to main content

Versioning and updates

How Gentian OS and the apps on it move from one version to the next. The platform knows which release it runs, every app says which platform it needs, the cluster administrator decides how each app moves, a move is one guarded operation that can go back, and one tenant's move never touches another tenant.

1. Three versions​

An installed app carries three versions, and each answers one question.

VersionNamesAnswers
the upstream versionthe application itself: Nextcloud 33, Odoo 18whether data written by one build can be read by another
the profile buildthe bundle: its digest, and spec.version, bumped on every change to the bundlewhat a tenant runs, what the store and every listing show
the platform version linethe OS: 0.5, 0.6, …whether a build can run on this cluster

Compatibility with the OS is a property of the profile build, because the profile is what the operator reads. The upstream version has nothing to say about it.

The operator does not know the upstream version: it sits inside the chart. A restore therefore judges the data by the chart version the bundle's manifest records for each app against the chart version installed (buildRule, internal/controller/tenantrestore_plan.go). The same version is restored; a newer one installed is restored too, because an app upgrades older data when it starts; an older one, or a version that cannot be compared, is refused for that app unless the restore sets spec.skipVersionCheck (operations.md §9.4). An import is a restore into a new tenant and is judged the same way.

1.1 Numbering​

How the platform's chart and a profile's spec.version are numbered between releases is in deployment.md §5.5.

2. The platform knows its version; a build declares its floor​

The Cluster claim's status (status.platform) carries the platform version line and the build the operator runs, written by the operator from its own binary, and the newest release the platform's feed publishes (GENTIAN_RELEASE_FEED_URL). The director reads it from there and answers it on the cluster's settings, the administration console shows it, and every check below compares against it. The version line is derived from the chart's version (internal/platformversion, generated by scripts/gen/gen-platform-version.py), so the two cannot disagree.

A profile declares the minimum platform it needs:

spec:
requires:
platform: ">=0.5"

The director refuses an install whose floor the cluster does not meet and lists the entry with the reason ("needs platform 0.5"). A profile without the field is read as >=0.0.

No profile declares an upper bound. An author cannot know what a later platform drops, so the platform carries that half of the contract: a field or behavior is read for one more version line after its replacement ships, and the release notes of each line list what the next one removes.

CI holds both sides to it. The apps repository validates every bundle against the CRD schema of each version line its floor admits (scripts/validate-platform-schema.py there). This repository fetches the released and the development catalog and runs every bundle through the operator's own bundle check (internal/profilebundle, TestThePublishedCataloguesConform), so a platform change that breaks a published profile fails the platform's pull request, not a tenant's install.

3. One build object per build​

The ComponentProfile is one object per name, the newest build any tenant installed, because the app's identity (its group, its routes, its authorization objects) is spelled from that name. Beside it the cluster holds one ProfileBuild per build any tenant installed, named by the profile and a prefix of its digest: the bundle at that digest, verified by the operator, its profile's spec written on its status for a Composition to read. A tenant's Component pins a build; one pinned to a build that is not the profile's current one is rendered from its ProfileBuild. Two tenants on two builds of one app share nothing but their companions, which are one object per name like the profile; a build whose companions differ from the current build's is held (CompanionMismatch) until the tenant moves.

A build is its digest: the object's profile and digest cannot change. It stays on the cluster while any Component pins it. The director and the installer write it beside the profile into the catalog directory of the deployments repository (builds/<profile>-<12 hex of the digest>.yaml), and a build the cluster holds is installed again without fetching it, which is what gives rollback (§5).

4. Who moves an app​

The cluster administrator sets a mode per app, and may override it per tenant. A tenant administrator cannot move an app unless its mode says so.

ModeWho moves the appTo what
latest (the default)the platformthe newest build on the app's catalog that the platform's version line admits, inside the maintenance window
manualthe cluster administratora build they name, for the whole cluster or for one tenant
tenantthe tenant administratora build the catalog offers

The cluster's policy is on the Cluster claim (spec.catalogue.updates): mode for every app, apps[] for the apps whose mode differs. The per-tenant override is Tenant.spec.apps[].updateMode, written through the director under can_configure, so by the cluster administrator alone (PUT /v1/clusters/{c}/tenants/{t}/apps/{p}/update-mode, kubectl gentian apps update-mode). The answer for one app is the tenant's override, else the cluster's answer for the app, else a customization hold, else the cluster's mode. An add-on has no mode of its own: it follows its base and moves with it.

An app with a Customization record at L3 or above that carries upgradeRisk is in manual mode for the tenants the record applies to, unless an override names another mode. The ladder exists to say what the next upstream release breaks; latest does not walk past it.

4.1 The maintenance window and the staged rollout​

latest moves apps inside a window set on the Cluster claim (spec.catalogue.updates.window: a daily start and end in a timezone; absent, any time). The mover runs inside the director every five minutes and reads the update report (§4.2); nothing is stored between runs, so what it acts on is what a person reads. Within the window it moves one tenant first, waits for that move to verify (§5), then the rest in batches of five per run. A move that fails or is rolled back holds every other tenant's move to that build: the report says so, and a person decides. The commits name policy:latest as the mover.

4.2 The update report​

There is no floor in tenant mode: a tenant may stay where it is. Instead the director computes, for every installed app, whether a newer build the platform admits exists on its catalog, and what keeps it where it is: its mode, a Customization with upgradeRisk, a move that was rolled back. The report reads three things together and stores nothing: what git says each tenant has installed and at which build, what the cluster runs and how the last move went (the operator's rollouts), and what the catalog each install came from offers now. A pinned add-on is listed under its app, at its own build. The director serves the report for the cluster (GET /v1/clusters/{c}/updates, can_audit) and per tenant (GET /v1/tenants/{t}/updates); the administration console shows it under the cluster's apps with one action, "move to the newest build", for all listed or for a selection (POST /v1/clusters/{c}/actions/move-outdated); kubectl gentian apps outdated prints the same report.

The OS appears in the same place: the operator reads the release feed and writes the newest release, with the address of its notes, on the cluster's status beside the version it runs (§2).

5. A move is one guarded operation​

Every move, whatever its mode and whoever asked, is the one director operation POST /v1/tenants/{t}/apps/{p} with a digest. The director writes the pin, with movedAt and movedBy (a person's id, or policy:latest), and the operator carries the move out (internal/controller/component_move.go):

  1. Refuse when the cluster does not meet the build's floor, or the bundle does not hash to the digest.
  2. Export the app's data to the platform's own storage, as a manual export of that app; the instance keeps running the build it ran until the export is ready, and an export that fails ends the move with nothing changed.
  3. Materialize the new build beside the old one and repoint the Component.
  4. Verify: wait for the release, run the profile's spec.backup.restore.verify command in the app's pod. A build that does not come up within fifteen minutes, or does not verify, is rolled back: the instance is rendered from the build it ran, which the cluster still holds as a ProfileBuild, and the reason is recorded.
  5. Record who or what moved it. The Component's status.rollout says the build the instance runs, the target, the phase and why; the pin says what was asked.

rollbackLimit stays in the rendered release's values (internal/apprender/values_release.go), for the chart's own history; the move's rollback replaces it as the mechanism and does not uninstall a release at revision 1.

A move that was rolled back, or did not start because its export failed, stays where it ended until it is asked for again. Asking again is installing the pinned build once more, or "move to the newest build" (kubectl gentian apps move-outdated), which takes in every such move: the director writes a fresh movedAt and movedBy on the entry, with the pin unchanged, and the operator starts the move from step 2. Who may ask is who may move the app (§4). The platform's latest mover never asks again by itself, so a build that keeps failing is not tried over and over; the report lists it as rolled back.