State lifecycle
This page tells what happens to the state of a running system when the code of the project changes, and when a service gets a new version. State is the data that a deploy does not make again: databases, buckets, secrets, the OpenTofu state, and the records of api/ and panel/.
The rule for each type of change
| Type of change | Mechanism | What the mechanism does not do |
|---|---|---|
| An option of the operator configuration is renamed | The rename table of api/, with the log core/renames-log.json |
It changes only the stored configuration. It moves no deployed object. |
| A value of the configuration changes | The deploy writes the value into a configuration file or the environment of a unit, and the unit starts again | It does not change a value that the application copied into its own state, for example a default of a new account |
| An OpenTofu resource gets a new address | A moved block |
It does not change the object. It cannot serve an address with a key that is different for each deployment. |
| An object exists, but the state does not have it | An import block |
It does nothing for an address that the state has. |
| A resource is dropped from the configuration | The apply destroys the object. Some resources have a guard or a retire step. | It does not delete an object that is not in a state. |
| An application is disabled | The data stays. deleteData deletes it. |
Without deleteData, nothing is deleted. With it, the backup bucket stays for the grace period. |
| A deployment is destroyed | tofu destroy of the operator graph, with destroy-time steps |
It does not delete the backups at once, the domain registration, or the records of api/ and panel/. |
| A service gets a new version | systemd.stateMigrations, from the migrations.nix of the service |
It moves no object. It runs a schema command for some services only. It restores automatically for some services only. |
The schema of api/ or panel/ changes |
Alembic revisions that api/ applies, Django migrations that panel/ applies, and a version record |
No migration goes back. The dump from before the upgrade is the only way back. |
The sections below give the details and the files.
Two types of OpenTofu state
The code uses OpenTofu in two layers:
- The deployment graphs are in
core/effects/tf/. Their state is in theterraform-backendservice of the hosting provider. - The on-node configurations are generated from Nix and applied by a unit on the node, for example
core/resources/postgresql/provision.nix,core/resources/ldap/provision.nix,core/resources/garage/provision.nixandcore/components/hosting-provider/openbao/runtime/module.nix. Their state is aterraform.tfstatefile on the node.
In the two layers, the state is what connects the code to the objects. Thus a change that must move or adopt an object is a block in the OpenTofu configuration, or code written for that change.
An option is renamed
The panel and api/ store the configuration of a deployment as JSON. When an option of the operator configuration gets a new path, a stored configuration still has the old path.
core/lib/renames-for-source.nix makes a table of { from; to; } entries from the option tree: each rename that is declared with lib.mkRenamedOptionModule or lib.mkAliasOptionModule, and each rename of a key in a pinned upstream JSON Schema, which a settings-renames.json file beside the component declares. core/renames-log.json keeps the entries of earlier revisions, each with the since version that added it. api/package.nix puts the table in the package as renames.json, and its build fails for a rename that the log does not have.
core/renames-log.json has 0 entries.
Before api/ validates a stored configuration, _migrate_for_validation in api/src/fediversity_api/procedures.py moves each value from its old path to its new path (api/src/fediversity_api/migrate.py). A deployment row records the version of the log that its configuration agrees with (renames_version in api/src/fediversity_api/state.py), thus only the newer entries apply. A rename that cannot apply is reported as unmigrated, and the value stays where it is.
This mechanism changes only JSON. If the renamed option is part of the name of a deployed object, the next apply sees a different object: see the next sections.
A stored deployment also records the source of its modules. api/ stores a pin of its own revision for a deployment without a source, thus a rebuild of api/ does not change the schema of a stored deployment until its next deploy. See "Default-source baseline freeze" in api/docs/design/schema-and-sources.md.
A value changes
For most values of the operator configuration, and for the environment variables of api/ and panel/, the deploy writes the value into a configuration file or into the environment of a unit. When the file or the environment changes, the unit starts again. Thus the next deploy applies the new value.
Some applications keep a value in their own state, and that value wins over the deployed value. The code prevents this for these values:
- Pixelfed writes a setting into its
config_cachetable at the first read, and from then on the row wins.core/components/operator/pixelfed/node.nixsetsENABLE_CONFIG_CACHE = false, thus the value of the.envfile applies. - PeerTube reads
local-production.jsonin its configuration directory over the deployed configuration. Its web edit of the configuration writes that file.core/components/operator/peertube/node.nixturns off the web edit, and thepreStartofpeertube.serviceremoves the file. - The secrets of the operator are in OpenBao KV (
api/src/fediversity_api/operator_secrets.py). When the rename table renames the field of a secret, a submit copies the value from the old name to the new name. When the operator clears a secret, the next deploy deletes its KV entry with all its versions (core/effects/tf/operator-provision/kv-clear.tf).
These values apply only to new objects, or stay with the application, by design:
| Value | What keeps the old value |
|---|---|
PeerTube user.video_quota*, defaults.*, instance.default_nsfw_policy, user.default_channel_name |
PeerTube copies the value into a user, channel or video when it makes it. The new value applies to new objects only. |
PeerTube admin.email |
PeerTube makes the root account one time, with this address. The contact address of the instance follows. |
Mastodon AUTHORIZED_FETCH |
A value wins and locks the toggle of the admin. With no value, Setting.authorized_fetch in the database decides. |
Mastodon HCAPTCHA_* |
The keys follow. The captcha is on only when the keys are set and an admin turns it on. |
Authelia default_2fa_method, totp.*, regulation.ban_time |
Authelia keeps the method, the enrollment and the ban of each user. |
api/ OPENBAO_KV_MOUNT, OPENBAO_KV_DEPLOYMENTS |
The entries stay at the old path, and api/ reads the secrets as not set. A change needs a move of the entries by hand. |
api/ API_DATABASE_URL |
For an empty database, import_sqlite_state in api/src/fediversity_api/state.py copies the SQLite file in, not the data of the database before. A move between two PostgreSQL databases is by hand. |
panel/ OIDC_ISSUER |
The actors of the audit records and the UserSocialAuth rows keep the old issuer. |
The docs build generates this table and the list above from core/docs/md/technical-design/state-lifecycle.nix. The build fails when an option of a row is not in core/components/operator/operator-facing-options.csv, or when a file that a row names no longer has the code that the row describes.
A resource gets a new address
When the address of a resource changes and nothing else is done, OpenTofu destroys the object of the old address and creates an object at the new address. For an object that holds data, the new object is empty.
A moved block prevents this: it tells OpenTofu that the object of the old address is the object of the new address. The tree has these:
core/effects/tf/operator-provision/garage.tfhas the onlymovedblock in a.tffile, for aterraform_dataresource with a new name.core/lib/tf-labels.nix(movedBlocks) generates themovedblocks of the on-node configurations, from the labels<prefix>_<key>that the states of deployed nodes have, to the labels of the current code.core/resources/garage/provision.nixchanges its state file withjqbefore the apply, because amovedblock between two providers needs a provider that reads the old type.core/components/hosting-provider/incus.nixrepairs its state file in thepreStartof the unit, before themovedblocks apply.
The limits:
- A
movedblock and aremovedblock accept only static addresses. The comment beforegarage_bucket.bucketincore/effects/tf/operator-provision/garage.tfgives the result: a resource whosefor_eachkeys change with each deployment cannot get such a block. - No
.tffile in the tree has aremovedblock. - A
movedblock does not rename the object itself. A database, a bucket or a node with a new name is a new object.
An object exists, but the state does not have it
An import block adopts the object, so that the apply does not create it again:
- The on-node configurations of PostgreSQL, lldap and garage adopt an object that exists and is not in the state, for example a database that
ensureDatabasesmade. core/effects/tf/incus/operator/registrar.tfadopts a domain claim in OpenBao, a domain that is in the registrar account already, and a name server group.
notes/forward-recovery.md gives the rules for an apply that failed: there is no rollback, and the same apply runs again.
A resource is dropped
When a resource leaves the configuration, the next apply destroys its object. This is the default of OpenTofu in the two layers:
- In an on-node configuration, an object that leaves the configuration is dropped. For PostgreSQL, a grant is revoked, and a database is dropped with its data (
core/resources/postgresql/provision.nix). - In a deployment graph, the destroy-time provisioners of a resource release the objects that are not in the state, for example DNS records.
Some resources have a different rule:
| Resource | Rule | File |
|---|---|---|
| A garage bucket of a deployment | lifecycle { destroy = false }: OpenTofu only removes it from the state. terraform_data.retire_bucket then empties and deletes the bucket, or keeps a *-backups bucket for the grace period. |
core/effects/tf/operator-provision/garage.tf |
| A domain registration | prevent_destroy: a plan that destroys or replaces it is refused. A replacement is a second registration, not a rename. The registration has a state key of its own. |
core/effects/tf/openprovider/domain.nix, core/effects/tf/inwx/domain.nix, core/effects/tf/porkbun/domain.nix |
| The secrets engine mounts of OpenBao | prevent_destroy, set through resourceMeta. |
core/components/hosting-provider/openbao/runtime/module.nix, core/components/hosting-provider/openbao/database-init.nix, core/components/hosting-provider/openbao/pki-init.nix, core/components/hosting-provider/openbao/ssh-certificates.nix, core/components/hosting-provider/openbao/valkey-engine.nix |
The cert auth mount of OpenBao in the state of spire-cert-auth |
A removed block with lifecycle.destroy = false: the state forgets the mount, and openbao-spire-auth adopts it by its path. |
core/components/hosting-provider/openbao/spire-auth.nix |
An lldap user or membership in retain |
A removed block with lifecycle.destroy = false: the object leaves the state and stays in lldap. When it comes back to the configuration, its import block adopts it again. |
core/resources/ldap/provision.nix |
The Incus network ACL of an operator deployment in the module bridge_acl |
A removed block with lifecycle.destroy = false: the state forgets the ACL and Incus keeps it. The hypervisor declares one ACL for each slot of the bridge, and a deploy credential that is limited to the project of the deployments cannot delete an ACL. |
core/effects/tf/incus/operator/bridge-isolation.tf |
| The node that keeps the OpenTofu state | refuse_backend_replace: an apply that deletes or replaces it is refused with status 3 before it changes anything. A plan gets a warning. |
core/effects/tf/common/bootstrap-apply.sh |
The docs build generates this table. It fails when a .nix or .tf file below core/ has prevent_destroy = true or destroy = false and no row names the file, and when a file of a row no longer has that code.
Gaps:
- An object that is in no state has no deletion path. The on-node configuration of PostgreSQL says so for its own objects: an object that is in neither the configuration nor the state is not touched, thus a name that left the configuration before the first apply stays.
- Only the node of the state backend has a guard against a replacement. For each other node, a change that replaces the node gives a new node, and the data on the old root disk is gone unless a volume or a backup holds it.
An object outside OpenTofu is dropped
A script or a unit makes some objects, not an OpenTofu resource. Such code follows a drop only when it lists the objects that exist, compares them with the objects that the code states, and removes the others. These do:
| Object | What removes it | File |
|---|---|---|
| The Incus trust of a deploy runner | The trusted certificates and pending tokens with no deploy runner of the host | core/components/hosting-provider/incus-deploy-tokens.nix |
| A maddy account | A compare of the accounts with the configuration | core/resources/smtp/maddy-accounts.nix |
| A NetBox service user and its token | The users of the group fediversity-managed that the deploy does not state |
core/components/hosting-provider/netbox-provision-token.py |
| An OpenBao token role | A compare with the roles of the configuration | core/components/hosting-provider/openbao/runtime/tokens.nix |
| A credential file on a node | phase_prune_credentials |
core/effects/common/deploy.sh |
A KV entry below vars/secret/deployer/ |
The keys that the code does not write, with all their versions | core/components/hosting-provider/openbao/deployer-kv-prune.nix |
| A Windmill API token | The unit deletes each token that it replaces | core/components/hosting-provider/windmill/api-token.nix |
| A Windmill script | The scripts of the folder that the module does not declare, with each version | core/components/hosting-provider/windmill/script-sync.nix |
| A PeerTube plugin | The plugin rows and node_modules entries that the deploy does not state |
core/components/operator/peertube/oidc-plugin-install.nix |
| A DNS mail record | A destroy-time step for each name | core/effects/tf/dns-lane/mail-records.tf |
| A Valkey user or rule | ACL SETUSER ... reset, and the users that the configuration does not state |
core/components/shared/valkey/valkey.nix |
| A Grafana alert rule | The retired UIDs in deleteRules |
core/components/hosting-provider/telemetry.nix, core/components/hosting-provider/telemetry/retired-alert-rules.nix |
The restic repository of a backup that stops while the bucket stays, of core/components/operator/acme-backup.nix, core/components/operator/local-cache-backup.nix |
The repository is deleted when its newest object is older than the retention period | core/resources/garage/retired-repository.sh |
| The registrar auth code of a domain transfer | drop_stale_domain_transfer, at a submit whose configuration registers a different domain, or no domain |
api/src/fediversity_api/migration_kv.py |
The docs build generates this table. It fails when a file of a row no longer has the code that removes the object. The list of backups with a sweep comes from the calls of mkRetiredRepository. The build does not find a new removal that has no row.
Gaps:
- When the rename table renames the field of a secret of the operator, the KV entry of the old name stays.
- When a role leaves
core/components/operator/mastodon/role-groups.nix, the holders keep it on Mastodon (core/components/operator/mastodon/ldap-roles.rb). No path of the configuration gets to this. - The sweep of retired restic repositories serves only the backups in the table above. Other restic repositories of a backup that stops stay, for example the repository of the dump before a PostgreSQL upgrade.
An application is disabled
The option deleteData of an application is in core/components/sharedOptions.nix. Its default is false.
- When
enablebecomes false anddeleteDatais false, the data stays: the database, the buckets, the generated secrets and the cache. A newenableuses the data again. - When
enableis false anddeleteDatais true, the next deployment deletes the data.purgedApplicationsincore/setups/wiring/node-selection.nixfinds these applications.core/setups/wiring/supporting-requests.nixleaves their requests out, thus the operator graph deletes their buckets, databases and keys, and the valkey node deletes their cache.core/effects/tf/operator-provision/openbao.tfdeletes their KV directories (purged_kv_dirs, fromcore/setups/wiring/purged-kv.nix). - A
*-backupsbucket stays for the grace period, as at a destroy.
The operator documentation of the option is in the operator configuration, and its role for erasure is in Data protection.
A deployment is destroyed
A destroy of a deployment runs tofu destroy on the operator graph. It removes the nodes. core/effects/tf/operator-provision/postgresql.tf drops the databases and the login roles of the deployment on the shared PostgreSQL node: their names have the namespace of the deployment as a suffix, and this graph is the only actor that runs again when the deployment ends. terraform_data.after_instances in core/effects/tf/operator-provision/openbao.tf deletes what remains below the KV prefix of a namespaced deployment after the instances are gone. terraform_data.retire_bucket empties and deletes each bucket that is not a backup bucket.
The destroy keeps this state:
| State | Why | How long |
|---|---|---|
Each <app>-backups bucket, with the new alias retained-<YYYYMMDD>-<id prefix> |
A restore stays possible, and the name of the bucket is free for a new deployment. | The grace period: fediversity.garage.server.retainedBackups.gracePeriodDays of the garage node, default 7 days |
| The KV entries that the bucket cannot be read without, and a retention record in OpenBao | Without the restic password, the backup cannot be read. The record tells the garage node what to delete. | The same grace period. core/resources/garage/sweep-retained-buckets.sh then deletes them with the bucket. |
| The domain registration | prevent_destroy. A registration that is lost cannot be made again as before. |
Until the registration ends at the registrar |
The row of the deployment in the panel, with deleted_at |
api/ can still bill the usage (panel/src/panel/models.py). |
Until the account of the operator is removed |
The builds and the audit_event rows in the database of api/ |
The usage periods of a destroyed deployment stay available, and the audit log records each deployment_delete. |
See Data protection |
| An object of a destroy step that failed | The destroy-time provisioners run with on_failure = continue, so that a destroy of a deployment that is not complete can finish. |
Until a person removes it. The run lists the object and exits with status 4. |
The check core/checks/integrations/destroy-reports-left-behind covers the last row. The operator steps for a retained bucket are in Know what a destroy keeps. An export for a move to a different hosting provider must be complete before the destroy: no key of the operator can read a retained bucket.
A destroy of all nodes of the hosting provider also destroys the node that keeps the OpenTofu state. Thus the run first moves the state to a local file (core/checks/integrations/destroy-detaches-state).
A service gets a new version
Each service that keeps state has a systemd.stateMigrations.<name> entry, in most cases in a file migrations.nix beside the service. The module is nix/contracts/nixos/modules/system/state-migrations.nix. An entry gives the version of the service in the current code. A unit <name>-migrate-pre runs before the service, compares that version with the version in a record file on the node, and decides:
flowchart TD
start["The unit `<name>-migrate-pre` starts"] --> record{"Is there a record of the version?"}
record -- "no" --> fresh["Record the version. Run `onFreshInstall` if the entry has one and the state is new."]
record -- "yes, the same version" --> none["Do nothing"]
record -- "yes, a later version than the code" --> down{"What is the value of `onDowngrade`?"}
down -- "refuse" --> stop["Exit with status 78. The service stays stopped."]
down -- "warn or ignore" --> older["Record the older version. Run nothing."]
record -- "yes, an earlier version than the code" --> skip{"Is the step larger than `maxSkip` permits?"}
skip -- "yes" --> stop
skip -- "no" --> backup["Run `backup`, then `steps` and `onUpgrade`"]
backup --> service["The service starts"]
service --> post["The unit `<name>-migrate-post` runs `onUpgradePost` and records the version"]
The rules of the module:
- The backup.
backupruns one time for each upgrade, before each other command of the upgrade. - The downgrade.
onDowngradeisrefuseby default. Exit status 78 tells systemd not to start the unit again. Withwarnorignore, the unit records the older version and runs nothing: these are services that also read the state of a later version. - The largest step.
maxSkipis not set by default, and then each step to a later version is permitted. Withmajor, a step across more than one major release is refused. Withminor, a step across more than one minor release is also refused. - The restore. When the entry has a
restorecommand, the unit records an upgrade that is not complete. A deploy of the version from before the upgrade then runsrestore. A deploy of a third version is refused. Withoutrestore, the way back is a manual restore of the backup. - No record. When
freshInstallTestisnulland the node has no record, the unit records the version and does not migrate. Thus the first deploy of an entry onto state that exists does not make a backup.
What an entry does not do:
- It moves no object and changes no OpenTofu state.
- In most entries, it runs no schema command: the service migrates its own schema when it starts. The column "Upgrade command" of the table shows the entries that do run a command. For example,
core/components/operator/mastodon/migrations.nixruns the database migrations of Mastodon, and the post-deployment migrations only after the services are up again. - No entry has a command for one specific version: the list
stepsis empty in each entry. - An entry whose version is a release series, for example
2.5, sees no change at a patch release. Thus a patch release makes no backup.
These files below core/components and core/resources define an entry. core/setups/nixos/postgresql.nix and core/setups/nixos/mysql.nix define the entries of PostgreSQL and MariaDB.
core/components/dev/forgejo/migrations.nixcore/components/hosting-provider/audit-shipper-migrations.nixcore/components/hosting-provider/bind-migrations.nixcore/components/hosting-provider/netbox/migrations.nixcore/components/hosting-provider/openbao/migrations.nixcore/components/hosting-provider/spire/migrations.nixcore/components/hosting-provider/telemetry/migrations.nixcore/components/hosting-provider/terraform-backend/migrations.nixcore/components/hosting-provider/windmill/migrations.nixcore/components/operator/mastodon/migrations.nixcore/components/operator/peertube/migrations.nixcore/components/operator/pixelfed/migrations.nixcore/components/shared/authelia/migrations.nixcore/components/shared/lldap/migrations.nixcore/components/shared/valkey/migrations.nixcore/resources/garage/migrations.nixcore/resources/smtp/maddy-migrations.nix
The table shows the entries of the nodes of core with the default applications and the configuration of the tests. The docs build generates it from config.systemd.stateMigrations of each node of the groups hosting-provider and operator of core/testers/groups.nix. The column "Backup before the upgrade" is written by hand in core/docs/md/technical-design/state-lifecycle.nix, and the build fails when it is not in agreement with the backup script of the entry. A row is one entry, or the entries with the same name and the same values on more than one node. The version of each entry is the version of the package, or its release series (garage, for example), or its major version (PostgreSQL). An entry with a restic snapshot makes it on a node that has the offsite backup for that service. Without the offsite backup, the entry makes a local copy or a dump, or no backup.
| Entry | Nodes | Version | Largest step | Downgrade | Backup before the upgrade | Automatic restore | Upgrade command |
|---|---|---|---|---|---|---|---|
authelia |
hosting-provider/authelia, operator/authelia |
4.39.28 |
no limit | refuse |
A restic snapshot | yes | no |
bind |
hosting-provider/bind |
9.20.30 |
no limit | warn |
A restic snapshot | yes | no |
garage |
hosting-provider/garage |
2.5 |
no limit | refuse |
A local copy | no | no |
grafana |
hosting-provider/telemetry |
13.3.0 |
no limit | refuse |
A restic snapshot | yes | no |
lldap |
hosting-provider/lldap, operator/lldap |
0.6.3 |
no limit | refuse |
A restic snapshot | yes | no |
maddy |
hosting-provider/smtp, operator/smtp |
0.9.6 |
no limit | warn |
A local copy | no | no |
mastodon |
operator/mastodon |
4.7.4 |
one minor release | refuse |
A restic snapshot | yes | yes |
netbox |
hosting-provider/netbox |
4.7.3 |
one major release | refuse |
A restic snapshot | yes | no |
openbao |
hosting-provider/openbao |
2.7 |
no limit | refuse |
A local copy | no | no |
peertube |
operator/peertube |
8.3.2 |
no limit | refuse |
A restic snapshot | yes | no |
pixelfed |
operator/pixelfed |
0.12.9 |
no limit | refuse |
A restic snapshot | yes | yes |
postgresql |
hosting-provider/postgresql |
18 |
no limit | refuse |
A restic snapshot of all clusters | no | yes |
redis-<name> |
hosting-provider/valkey, operator/valkey |
9.1 |
no limit | warn |
A restic snapshot | yes | no |
spire-server |
hosting-provider/spire |
1.15 |
no limit | warn |
A local copy | no | no |
terraform-backend |
hosting-provider/terraform-backend |
0.2.4 |
no limit | refuse |
A restic snapshot | yes | no |
windmill |
hosting-provider/windmill |
1.839.0 |
no limit | refuse |
A restic snapshot | yes | no |
These nodes of that configuration have no entry: hosting-provider/otel-collector, operator/grafana.
The table does not read the overview tag, the hypervisor and the development hosts. These entries are not on a node of that configuration, thus the table does not show them:
fediversity-audit-shipperofcore/components/hosting-provider/audit-shipper-migrations.nix. It refuses a downgrade and makes a local copy of its state directory.forgejoofcore/components/dev/forgejo/migrations.nix.mysqlofcore/setups/nixos/mysql.nix, for a node that runs MariaDB.
Gaps:
- The
postgresqlentry itself does not limit the step. The guard of the PostgreSQL module, below, permits only a step of one major version.
PostgreSQL has one more rule. Each major version keeps its cluster in a directory of its own, thus a new major would start with an empty cluster. core/setups/nixos/postgresql.nix sets the major in the code. When the major goes up by one, its guard runs pg_upgrade --copy from the old cluster. For each other change of the major, it keeps PostgreSQL stopped with exit status 78. MariaDB has a rule of its own in core/setups/nixos/mysql.nix: the module runs mariadb-upgrade after each start of the server, because an upgrade is complete only after that command.
The maintainer steps for an update, and the reasons for each service, are in Update nixpkgs. Each entry has a check in core/checks/integrations/, for example garage-series and openbao-series.
The schema of the api or the panel changes
The two components keep their records in a PostgreSQL database of the hosting provider. Each applies its own migrations, and each has a systemd.stateMigrations entry that only records the version, refuses a downgrade, and makes the backup.
| Component | Entry | Version | Migrations | Definition |
|---|---|---|---|---|
api/ |
fediversity-api |
12 |
Alembic revisions in api/src/fediversity_api/state_migrations/versions |
api/nix/component/migrations.nix |
panel/ |
panel |
9 |
Django migrations in panel/src/panel/migrations |
panel/nix/component/migrations.nix |
api/applies the Alembic revisions when it opens its store (_migrateinapi/src/fediversity_api/state.py). A PostgreSQL advisory lock keeps a second process out of the same migration.api/refuses a database at a revision that its release does not know. The revisions only go forward.panel/runsmanage migratein thepreStartofpanel.service(panel/nix/configuration.nix), each time the package changes. Django does not refuse a database that later migrations changed, thus the version record is the only guard against a downgrade. The panel has no migrations that go back.- The version of each entry is the number of the last migration file. Thus the version changes only when the schema changes. For the panel, the migrations of the Django apps from nixpkgs are not in the version.
- The backup comes from
mkUpgradeBackupForincore/components/operator/app-resources.nix: on a node with the offsite backup ofcore/components/shared/postgres-backup.nix, a dump of the database before the upgrade. Without that backup, the entry makes no backup. - The largest step is not limited: the entries do not set
maxSkip. Alembic and Django apply each migration between the two versions in sequence.
The two entries are not in the table of the section before, because the nodes of api/ and panel/ are not nodes of core.
An effect fails
An effect can fail, or a person or a timeout can stop it, at any point. No effect goes back to the state from before the run. Each effect is recovered forward: run it again. For a deployment graph, notes/forward-recovery.md gives the rules and tells how to read a failure.
| Effect | When it fails or is stopped | Run again | Automatic retry or wait | Objects left behind | File |
|---|---|---|---|---|---|
A deployment graph (core/effects/tf/, core/effects/k8s/) |
The state records the steps that are done. A dead run can leave its lock. A state write that the backend rejects is kept as errored-<time>.tfstate. |
Yes: the same apply continues from the state (notes/forward-recovery.md). |
The client of the HTTP backend tries a request again 2 times, after 1 to 30 s. Before an apply, the run waits for the backend at most 1800 s. With TF_RECLAIM_OWN_LOCK=1, the run releases a lock of its own identity. |
Yes: a destroy-time step that fails gives status 4 | core/effects/tf/common/bootstrap-apply.sh, core/effects/tf/common/effect.nix |
| An Incus host of a graph | As for a deployment graph | Yes | The step waits for incus exec in the new instance 30 times, each 5 s apart, and then for its address 30 times, each 2 s apart. |
As for a deployment graph | core/effects/tf/incus/host/await-incus.sh |
| A Kubernetes rollout | A rollout that does not become ready is undone, and the apply fails | Yes | The step waits for the rollout at most 600 s. The copy of an image is tried 3 more times. | As for a deployment graph | core/effects/k8s/effect.nix |
A deploy over SSH (ssh) |
The node runs the old or the new configuration | Yes: it copies the closure and switches again | It waits for the target 30 times, each 5 s apart. When another switch holds the lock, it tries again 60 times, each 5 s apart. | No | core/effects/common/deploy.sh, core/effects/ssh/run.sh |
A deploy into an Incus instance (incus) |
The node runs the old or the new configuration. After a failed switch, /sbin/init points to the old closure again. |
Yes: it copies the closure and switches again | It waits for the target 30 times, each 5 s apart | No | core/effects/common/deploy.sh, core/effects/incus/run.sh |
An install (nixos-anywhere) |
The disks can be wiped, or hold a part of the install | Yes, but a run that installs wipes the disks again. A target that runs the expected system is skipped, and its disks stay. | None | No | core/effects/nixos-anywhere/install.sh |
An export of a migration (migration) |
The services that the run stopped start again | Yes: it makes a new snapshot | None | No | core/effects/migration/run.sh |
A restore of a migration (migration-restore) |
The restore can be partly done | It restores again. The script does not say that this is safe. | It waits for the instance to boot at most 900 s | No | core/effects/migration-restore/run.sh |
An operation of an application (app-operation) |
A deploy that restarts the node makes the operation fail | It depends on the command | None. The command stops after 300 s. | No | core/effects/app-operation/run.sh |
A retry of the objects left behind (left-behind) |
A removal that fails keeps its record, and the other removals continue | Yes: each removal has the guards of its step | None. It is itself the retry, and a person starts it from api. |
Keeps the records that it could not remove | core/effects/left-behind/retry.sh |
A sync of a zone (octodns) |
The zone can hold a part of the changes | Yes: a sync writes only the difference | None | No | core/effects/octodns/effect.nix |
A node effect that api starts (an export, a restore, an operation, a retry) |
api cancels a job that is not done after 3600 s, and the effect fails |
As for the effect | None | As for the effect | api/src/fediversity_api/node_effect.py, api/src/fediversity_api/windmill.py |
The docs build generates this table. It fails when a file of a row no longer has the code that does what the row says. The build does not find a new retry or a new effect that has no row.
Gaps:
- No effect tries a failed run again by itself. The retries in the table are inside one run.
- A restore of a migration and some operations of an application do not say that a second run is safe.
What this page does not cover
- The list of each place where state is kept is in the part
state-register. - The backups, their retention and their restore are in the hosting provider guide.
- A move of an operator between hosting providers is in the part
capability-flows.