Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions bin/validate-manifest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,23 @@ const CONTRACTS = {
// server drops an `accepts` naming it with a warning; here it is an error, so
// a manifest saying something meaningless fails CI rather than deploying.
'container-logs@1': { block: null, blockRequired: false, appProvided: true, impliedByBlock: false, acceptable: false },
// NOT a brokered contract, and the only entry here that isn't: Hola's CONTRACTS
// registry gains no entry for restore@1 and the server never brokers it between
// two parties. It is a participation MARKER (Hola spec 007, restore-on-install)
// meaning "this app has considered being restored", and it lives in this table
// only because `accepts` is where an app declares it.
//
// `blockRequired` is false for the same reason backup@1's is, and it is the
// whole point: `restore@1` with no `restore` block is the positive claim "a
// plain file copy back is all I need" — true for every SQLite and flat-file app
// — which must stay distinguishable from an app nobody considered. Acceptance
// is declared, never derived from the block (ADR 0004 §2).
//
// `appProvided: false` because nothing provides restore@1 — not the platform,
// not an app. A manifest naming it in `provides` is an error, which is right,
// though the shared message says "provided by the Hola platform itself" and is
// imprecise for this one ref.
'restore@1': { block: 'restore', blockRequired: false, appProvided: false, impliedByBlock: false },
};

/** Manifest fields that take a bare string or an array of them. */
Expand Down
41 changes: 37 additions & 4 deletions schemas/manifest.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,37 @@
}
]
},
"restore": {
"type": "array",
"description": "Per-backup-participation restore declarations (Hola spec 007: restore-on-install). Each entry names the BACKUP participation id it restores (\"default\" for the legacy singular `backup` block above, which every app in this catalog currently uses), the data-root-relative paths to discard after the restored files land and before any container starts, and an optional hook -- reusing the exact `#/$defs/backupHook` shape, run after discards against the started-and-healthy service. An app that declares `accepts: [\"restore@1\"]` with NO entry here is restored by plain file copy (nothing discarded, no hook) -- meaningfully distinct from an app that doesn't declare `restore@1` at all, which isn't offered as restorable. `restore@1` here is a participation marker this feature's app declares, not a brokered capability contract.",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["id"],
"properties": {
"id": {
"type": "string",
"minLength": 1,
"description": "The backup participation id this restores (\"default\" for the legacy singular `backup` block)."
},
"discard": {
"type": "array",
"minItems": 1,
"items": { "type": "string", "minLength": 1 },
"description": "Data-root-relative paths removed after the restored files land, before any container starts. A file-level tar of a LIVE Postgres data dir is only crash-consistent (a smear across the capture window); discarding it and loading the pg_dump the restore hook applies is what makes the restored data correct rather than merely present."
},
"hook": {
"$ref": "#/$defs/backupHook",
"description": "Run after discards, against the started-and-healthy service (Hola waits on the service's OWN declared healthcheck, not a bespoke poll)."
},
"requiresEnv": {
"type": "boolean",
"description": "true turns a restore candidate's missing captured configuration from a warning into a REFUSAL -- for an app that cannot sensibly restore without it. Default false."
}
}
}
},
"push": {
"type": "array",
"minItems": 1,
Expand Down Expand Up @@ -513,21 +544,23 @@
"$defs": {
"contractRef": {
"type": "string",
"description": "A capability contract reference, `<id>@<version>`. CLOSED SET: a contract is a promise about *server* behavior, so matching two strings does nothing unless Hola implements the middle. Mirrors CONTRACTS in try-hola/hola packages/shared/src/contracts.ts -- adding a contract there is what makes it declarable here. Enumerated rather than left open because the server drops an unrecognized ref with a warning (ADR 0003 forward-compat), which would make a typo'd `backups@1` a silent no-op instead of a CI failure.",
"description": "A value an app may declare in a contract list. Most are capability contract refs (`<id>@<version>`) the server BROKERS -- a promise about *server* behavior, so matching two strings does nothing unless Hola implements the middle; those mirror CONTRACTS in try-hola/hola packages/shared/src/contracts.ts, and adding a contract there is what makes one declarable here. `restore@1` is the one exception: a participation MARKER for Hola spec 007 (restore-on-install), NOT a brokered contract -- CONTRACTS gains no entry for it and the server never brokers it between two parties; it only says \"this app has considered being restored\" (see the top-level `restore` block). Enumerated rather than left open because the server drops an unrecognized ref with a warning (ADR 0003 forward-compat), which would make a typo'd `backups@1` a silent no-op instead of a CI failure.",
"enum": [
"auth@1",
"backup@1",
"container-logs@1",
"push@1"
"push@1",
"restore@1"
]
},
"acceptableContractRef": {
"type": "string",
"description": "The subset of #/$defs/contractRef an app can OPT IN to, i.e. those whose participation is `declared`. An `implicit` contract (container-logs@1) has no acceptor side: every install is a subject by virtue of running, and the server drops an `accepts` naming one with a warning, so declaring it here is a manifest error rather than a silent no-op.",
"description": "The subset of #/$defs/contractRef an app can OPT IN to, i.e. those whose participation is `declared`, plus the `restore@1` participation marker. An `implicit` contract (container-logs@1) has no acceptor side: every install is a subject by virtue of running, and the server drops an `accepts` naming one with a warning, so declaring it here is a manifest error rather than a silent no-op.",
"enum": [
"auth@1",
"backup@1",
"push@1"
"push@1",
"restore@1"
]
},
"appProvidedContractRef": {
Expand Down
12 changes: 11 additions & 1 deletion src/guacamole/src/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,20 @@
"command": ["sh", "-c", "rm -f /backups/guacamole_db.sql"]
}
},
"restore": [
{
"id": "default",
"discard": ["postgres"],
"hook": {
"service": "postgres",
"command": ["sh", "-c", "psql -v ON_ERROR_STOP=1 -U guacamole_user -d guacamole_db -f /backups/guacamole_db.sql"]
}
}
],
"upgrade": {
"breaking": true,
"preUpgradeBackup": "required",
"upgradeNotesUrl": "https://github.com/pgautoupgrade/docker-pgautoupgrade"
},
"accepts": ["backup@1"]
"accepts": ["backup@1", "restore@1"]
}
18 changes: 16 additions & 2 deletions src/immich/src/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"upgradeNotesUrl": "https://immich.app/blog/v3-migration",
"preUpgradeBackup": "recommended"
},
"accepts": ["backup@1"],
"accepts": ["backup@1", "restore@1"],
"backup": {
"preHook": {
"service": "immich-postgres",
Expand All @@ -75,5 +75,19 @@
"rm -f /backups/immich.sql"
]
}
}
},
"restore": [
{
"id": "default",
"discard": ["postgres"],
"hook": {
"service": "immich-postgres",
"command": [
"sh",
"-c",
"psql -v ON_ERROR_STOP=1 --dbname=immich --username=immich -f /backups/immich.sql"
]
}
}
]
}
16 changes: 15 additions & 1 deletion src/mealie/src/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,24 @@
]
}
},
"restore": [
{
"id": "default",
"discard": ["postgres"],
"hook": {
"service": "mealie-postgres",
"command": [
"sh",
"-c",
"psql -v ON_ERROR_STOP=1 -U mealie -d mealie -f /backups/mealie.sql"
]
}
}
],
"upgrade": {
"breaking": true,
"preUpgradeBackup": "required",
"upgradeNotesUrl": "https://github.com/pgautoupgrade/docker-pgautoupgrade"
},
"accepts": ["backup@1"]
"accepts": ["backup@1", "restore@1"]
}
16 changes: 15 additions & 1 deletion src/paperless-ngx/src/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,24 @@
]
}
},
"restore": [
{
"id": "default",
"discard": ["pgdata"],
"hook": {
"service": "db",
"command": [
"sh",
"-c",
"psql -v ON_ERROR_STOP=1 -U paperless -d paperless -f /backups/paperless.sql"
]
}
}
],
"upgrade": {
"breaking": true,
"preUpgradeBackup": "required",
"upgradeNotesUrl": "https://github.com/pgautoupgrade/docker-pgautoupgrade"
},
"accepts": ["backup@1"]
"accepts": ["backup@1", "restore@1"]
}
33 changes: 32 additions & 1 deletion src/postiz/src/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,37 @@
"upgradeNotesUrl": "https://github.com/pgautoupgrade/docker-pgautoupgrade"
},
"accepts": [
"backup@1"
"backup@1",
"restore@1"
],
"restore": [
{
"id": "app-db",
"discard": [
"postgres"
],
"hook": {
"service": "postiz-postgres",
"command": [
"sh",
"-c",
"psql -v ON_ERROR_STOP=1 -U postiz-user -d postiz-db-local -f /backups/postiz.sql"
]
}
},
{
"id": "temporal-db",
"discard": [
"temporal-postgres"
],
"hook": {
"service": "temporal-postgres",
"command": [
"sh",
"-c",
"psql -v ON_ERROR_STOP=1 -U temporal -d postgres -f /backups/temporal.sql"
]
}
}
]
}
Loading