@serve.zone/appstore 33.4.0 → 33.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,7 +38,8 @@ const createLegacyVolumeRequestId = (volumeArg, indexArg, labelArg) => {
38
38
  }
39
39
  return requestId;
40
40
  };
41
- const deriveRequiredFeatureIds = (explicitRequestsArg) => {
41
+ /** The storage feature ids a config needs, in the strictly increasing order `requiresFeatures` lists them. */
42
+ const deriveRequiredFeatureIds = (explicitRequestsArg, backupExclusionsArg) => {
42
43
  const required = [];
43
44
  if (explicitRequestsArg.length > 0)
44
45
  required.push(storageFeatureIds.bindings);
@@ -48,6 +49,8 @@ const deriveRequiredFeatureIds = (explicitRequestsArg) => {
48
49
  if (explicitRequestsArg.some((requestArg) => requestArg.kind === 'objectStorage')) {
49
50
  required.push(storageFeatureIds.objectStorage);
50
51
  }
52
+ if (backupExclusionsArg.length > 0)
53
+ required.push(storageFeatureIds.objectStorageBackupExclusions);
51
54
  if (explicitRequestsArg.some((requestArg) => (requestArg.kind === 'objectStorage' && requestArg.delivery.type === 'file'))) {
52
55
  required.push(storageFeatureIds.objectStorageFile);
53
56
  }
@@ -66,8 +69,9 @@ export function normalizeAppStoreVolumes(configArg, labelArg = defaultLabel) {
66
69
  *
67
70
  * The config's storage is judged by the interfaces contract first. Legacy `volumes` become
68
71
  * filesystem requests of the reserved `legacyFilesystem` class, `platformRequirements.s3` becomes
69
- * the `legacy-s3` object-storage request, and explicit requests fail closed unless every storage
70
- * feature they need is advertised by the target runtime.
72
+ * the `legacy-s3` object-storage request, `backupExclusions` become the declared non-backup key
73
+ * prefixes by request id, and explicit requests and backup exclusions fail closed unless every
74
+ * storage feature they need is advertised by the target runtime.
71
75
  */
72
76
  export function normalizeAppStoreStorageConfig(configArg, optionsArg, labelArg = defaultLabel) {
73
77
  assertValidStorage(configArg, labelArg);
@@ -77,7 +81,8 @@ export function normalizeAppStoreStorageConfig(configArg, optionsArg, labelArg =
77
81
  const requestOrigins = {};
78
82
  for (const request of explicitRequests)
79
83
  requestOrigins[request.id] = 'explicit';
80
- const requiredFeatureIds = deriveRequiredFeatureIds(explicitRequests);
84
+ const declaredExclusions = configArg.backupExclusions ?? [];
85
+ const requiredFeatureIds = deriveRequiredFeatureIds(explicitRequests, declaredExclusions);
81
86
  const legacyVolumes = normalizeVolumes(configArg.volumes);
82
87
  if (legacyVolumes.length > 0) {
83
88
  storageClasses[legacyFilesystemClassKey] = {
@@ -135,6 +140,14 @@ export function normalizeAppStoreStorageConfig(configArg, optionsArg, labelArg =
135
140
  },
136
141
  };
137
142
  }
143
+ // The contract accepted every entry: it names an object-storage request of this normalized config once.
144
+ const backupExclusions = {};
145
+ for (const exclusion of declaredExclusions) {
146
+ backupExclusions[exclusion.storageRequest] = {
147
+ keyPrefixes: [...exclusion.keyPrefixes],
148
+ reason: exclusion.reason,
149
+ };
150
+ }
138
151
  const supportedFeatures = new Set(optionsArg.supportedFeatures);
139
152
  const unsupportedFeatures = requiredFeatureIds.filter((featureIdArg) => !supportedFeatures.has(featureIdArg));
140
153
  if (unsupportedFeatures.length > 0) {
@@ -145,7 +158,8 @@ export function normalizeAppStoreStorageConfig(configArg, optionsArg, labelArg =
145
158
  storageRequests,
146
159
  requestOrigins,
147
160
  requiredFeatureIds,
161
+ backupExclusions,
148
162
  ...(legacyCompatibility ? { legacyCompatibility } : {}),
149
163
  };
150
164
  }
151
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3RvcmFnZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzX2NsaWVudC9zdG9yYWdlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sY0FBYyxDQUFDO0FBWXhDLE1BQU0saUJBQWlCLEdBQUcsT0FBTyxDQUFDLG1CQUFtQixDQUFDLFFBQVEsQ0FBQyx5QkFBeUIsQ0FBQztBQUN6RixNQUFNLHdCQUF3QixHQUFHLGtCQUFrQixDQUFDO0FBQ3BELE1BQU0sMkJBQTJCLEdBQUcscUJBQXFCLENBQUM7QUFDMUQsTUFBTSw0QkFBNEIsR0FBRyxXQUFXLENBQUM7QUFDakQsTUFBTSx1QkFBdUIsR0FBRyx3QkFBd0IsQ0FBQztBQUN6RCxNQUFNLFlBQVksR0FBRyxpQkFBaUIsQ0FBQztBQUV2Qzs7O0dBR0c7QUFDSCxNQUFNLGtCQUFrQixHQUFHLENBQUMsU0FBZ0MsRUFBRSxRQUFnQixFQUFRLEVBQUU7SUFDdEYsTUFBTSxNQUFNLEdBQUcsT0FBTyxDQUFDLG1CQUFtQixDQUFDLFFBQVEsQ0FBQyw4QkFBOEIsQ0FBQyxTQUFTLEVBQUUsUUFBUSxDQUFDLENBQUM7SUFDeEcsSUFBSSxNQUFNLENBQUMsTUFBTSxHQUFHLENBQUM7UUFBRSxNQUFNLElBQUksS0FBSyxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztBQUM1RCxDQUFDLENBQUM7QUFFRixNQUFNLGdCQUFnQixHQUFHLENBQUMsVUFBNEMsRUFBYSxFQUFFLENBQ25GLENBQUMsVUFBVSxJQUFJLEVBQUUsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLFNBQVMsRUFBVyxFQUFFO0lBQzVDLE1BQU0sTUFBTSxHQUFHLE9BQU8sU0FBUyxLQUFLLFFBQVEsQ0FBQyxDQUFDLENBQUMsRUFBRSxTQUFTLEVBQUUsU0FBUyxFQUFFLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQztJQUNwRixPQUFPO1FBQ0wsR0FBRyxDQUFDLE1B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
165
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3RvcmFnZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzX2NsaWVudC9zdG9yYWdlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxPQUFPLE1BQU0sY0FBYyxDQUFDO0FBYXhDLE1BQU0saUJBQWlCLEdBQUcsT0FBTyxDQUFDLG1CQUFtQixDQUFDLFFBQVEsQ0FBQyx5QkFBeUIsQ0FBQztBQUN6RixNQUFNLHdCQUF3QixHQUFHLGtCQUFrQixDQUFDO0FBQ3BELE1BQU0sMkJBQTJCLEdBQUcscUJBQXFCLENBQUM7QUFDMUQsTUFBTSw0QkFBNEIsR0FBRyxXQUFXLENBQUM7QUFDakQsTUFBTSx1QkFBdUIsR0FBRyx3QkFBd0IsQ0FBQztBQUN6RCxNQUFNLFlBQVksR0FBRyxpQkFBaUIsQ0FBQztBQUV2Qzs7O0dBR0c7QUFDSCxNQUFNLGtCQUFrQixHQUFHLENBQUMsU0FBZ0MsRUFBRSxRQUFnQixFQUFRLEVBQUU7SUFDdEYsTUFBTSxNQUFNLEdBQUcsT0FBTyxDQUFDLG1CQUFtQixDQUFDLFFBQVEsQ0FBQyw4QkFBOEIsQ0FBQyxTQUFTLEVBQUUsUUFBUSxDQUFDLENBQUM7SUFDeEcsSUFBSSxNQUFNLENBQUMsTUFBTSxHQUFHLENBQUM7UUFBRSxNQUFNLElBQUksS0FBSyxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztBQUM1RCxDQUFDLENBQUM7QUFFRixNQUFNLGdCQUFnQixHQUFHLENBQUMsVUFBNEMsRUFBYSxFQUFFLENBQ25GLENBQUMsVUFBVSxJQUFJLEVBQUUsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLFNBQVMsRUFBVyxFQUFFO0lBQzVDLE1BQU0sTUFBTSxHQUFHLE9BQU8sU0FBUyxLQUFLLFFBQVEsQ0FBQyxDQUFDLENBQUMsRUFBRSxTQUFTLEVBQUUsU0FBUyxFQUFFLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQztJQUNwRixPQUFPO1FBQ0wsR0FBRyxDQUFDLE1B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
@@ -12,12 +12,23 @@ export interface IAppStoreStorageLegacyCompatibility {
12
12
  environmentOwnership: 'bindingOverridesTemplate';
13
13
  };
14
14
  }
15
+ /**
16
+ * The data a backup leaves out of one object-storage request: the key prefixes of a version's
17
+ * `backupExclusions` entry for that request and the operator-readable reason.
18
+ */
19
+ export type TAppStoreNormalizedBackupExclusion = Omit<plugins.servezoneInterfaces.appstore.IAppStoreBackupExclusion, 'storageRequest'>;
15
20
  /** The canonical storage handoff a fulfillment adapter receives for one version config. */
16
21
  export interface INormalizedAppStoreStorageConfig {
17
22
  storageClasses: Record<string, plugins.servezoneInterfaces.appstore.IAppStoreStorageClass>;
18
23
  storageRequests: plugins.servezoneInterfaces.appstore.TAppStoreStorageRequest[];
19
24
  requestOrigins: Record<string, TAppStoreStorageRequestOrigin>;
20
25
  requiredFeatureIds: plugins.servezoneInterfaces.appstore.TAppStoreStorageFeatureId[];
26
+ /**
27
+ * Declared non-backup data by object-storage request id (`legacy-s3` for
28
+ * `platformRequirements.s3`), empty when the version declares none. A backup leaves the
29
+ * objects under these key prefixes out; a restore neither brings them back nor removes them.
30
+ */
31
+ backupExclusions: Record<string, TAppStoreNormalizedBackupExclusion>;
21
32
  legacyCompatibility?: IAppStoreStorageLegacyCompatibility;
22
33
  }
23
34
  export interface IAppStoreStorageNormalizationOptions {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serve.zone/appstore",
3
- "version": "33.4.0",
3
+ "version": "33.6.0",
4
4
  "private": false,
5
5
  "description": "serve.zone App Store: the signed runtime catalog and its consumer loader.",
6
6
  "type": "module",
@@ -12,7 +12,7 @@
12
12
  "author": "Task Venture Capital GmbH",
13
13
  "license": "MIT",
14
14
  "dependencies": {
15
- "@serve.zone/interfaces": "32.53.0",
15
+ "@serve.zone/interfaces": "32.56.0",
16
16
  "modern-tar": "0.8.5"
17
17
  },
18
18
  "devDependencies": {
package/readme.md CHANGED
@@ -37,7 +37,7 @@ The catalog lists 19 apps. Every image is pinned to the digest of its multi-plat
37
37
  | App ID | Name | App Store category | Latest | Image | Port |
38
38
  | --- | --- | --- | --- | --- | --- |
39
39
  | `adminer` | Adminer | Dev Tools | `1.0.0` | `adminer@sha256:74f29c416e148b98305e84446a18db7cd2c1038264dec464359d36774ef080ce` | `8080` |
40
- | `cloudly` | Cloudly | Dev Tools | `1.28.3` | `code.foss.global/serve.zone/cloudly@sha256:ff692cf950da26d1d50e717c06eff1c58201b699e6bb94d65a541cd861abbedc` | `3000` |
40
+ | `cloudly` | Cloudly | Dev Tools | `1.28.6` | `code.foss.global/serve.zone/cloudly@sha256:0dd5399877c894b86f55d7a56df2da597bfcb7d1c8ec8adec547bfebb5d091d9` | `3000` |
41
41
  | `ghost` | Ghost | CMS | `1.0.0` | `ghost@sha256:9482099b1c8700764dc3d38c293e09c16d70f8bacc14af589c47d90b4bebd017` | `2368` |
42
42
  | `gitea` | Gitea | Dev Tools | `1.0.0` | `gitea/gitea@sha256:a9dc25679ebdd33356242d2e0667cc220043745e8339922879f187c20329a42e` | `3000` |
43
43
  | `grafana` | Grafana | Monitoring | `1.0.0` | `grafana/grafana@sha256:b28bae15e219c998fb0e0424ed724930cc61b1f61fb404d47c862f9a23f9e572` | `3000` |
@@ -68,7 +68,7 @@ Some templates ask Onebox to provision local platform services and inject connec
68
68
 
69
69
  | App ID | Requirements | Notes |
70
70
  | --- | --- | --- |
71
- | `cloudly` | MongoDB, S3 | Version `1.28.3` pins Cloudly `33.19.1` and keeps the `1.28.2` runtime contract, including the Onebox `33.8.0` minimum: its platform Corestore `32.2.4` runs NoSQLDB `10.6.1` with the D3 write-conflict fix, and on an earlier engine a node's network plan loses every commit to a `WriteConflict`, so Cloudly `33.15` must not run on it. Cloudly `33.19.1` carries `33.18.3` to `33.19.1`: a node's log report costs one append transaction that queues the batch with its payload in the new collection `cloudly_runtime_log_queue`, and a background drain writes the payloads and commits the ledger rows without holding the node's request or fencing the controller record (a full queue refuses a report with the retryable `log-intake-saturated`), object writes list nothing, workload logs are kept for at least 30 days, route verification reads the certificate each route presents on a connection of its own, and `readServiceDeploymentStatus` (interfaces `32.54.0`) reads a deployment operation without changing it. Its first start creates `cloudly_runtime_log_queue`; on NoSQLDB `10.6.1` that creation waits at most 5 s for the open transactions on the Cloudly database, and a transaction an earlier Cloudly left open by exiting before its stop completed holds the database until its session expires 30 minutes later, so after an unclean stop of the previous Cloudly start `33.19.1` no earlier than 30 minutes after that exit. It adds no data step and keeps the data version `0.30.0`, so an upgrade from `1.28.2`, `1.28.1` or `1.28.0` runs none. The `1.28.3` changelog repeats the earlier releases' upgrade path only, because a catalog changelog is bounded at 65536 characters; `1.28.2`'s carries the rest. From `1.28.1` or earlier, Cloudly `33.18.2`'s fixes apply too: Cloudly `33.18.2` is a fixes-only patch over `33.18.1`: a node's network configuration advances only once the node's application receipt names the projection it holds, so a node no longer receives a new version while it still applies the last one; upgrade every node to Pallet `36.3.0` or later (interfaces `32.50.0` or later) first, because a node that sends no receipt keeps the projection it holds and receives no successor. It adds no data step and keeps the data version `0.30.0`, so an upgrade from `1.28.1` or `1.28.0` runs none. From `1.28.0` or earlier, Cloudly `33.18.1`'s fixes apply too: Cloudly `33.18.1` is a fixes-only patch over `33.18.0`: a node's log batches append without its node fences, so they no longer miss their budget against the node's producer passes, and the deployment preflight answers within 25 s, past which it answers the retryable `DEPLOYMENT_PREFLIGHT_UNAVAILABLE`. It adds no data step and keeps the data version `0.30.0`, so an upgrade from `1.28.0` runs none. From `1.27.0` or earlier, Cloudly `33.18.0`'s changes apply too: Cloudly `33.18.0` loads the App Store catalog at runtime instead of bundling it, from the registries of its `appStoreCatalogRegistries` setting (empty means the npm registry), so a Cloudly template update needs an appstore release only; it ignores `APPSTORE_URL`, whose remote source is removed, and while no verified catalog is active it refuses App Store install details, upgrade preview and upgrade with `APPSTORE_CATALOG_UNAVAILABLE`. It adds no data step and keeps the data version `0.30.0`, so an upgrade from `1.27.0` runs none. From `1.26.0` or earlier, Cloudly `33.17.3`'s changes apply too: Cloudly `33.17.3` moves the data version from `0.28.0` to `0.30.0`: every upgrade from `1.26.0` or earlier runs the forward-only data steps `route-claim-absent-owners` to `0.29.0` (the stored nulls of absent owners are removed from every route claim) and `bounded-runtime-history` to `0.30.0` (every archived deployment record is deleted, the index `archiveState_1` is dropped and the accepted Spark observations are pruned to the rows the census reads), and every Cloudly before `33.17.3` refuses the `0.30.0` data. Runtime history is bounded from then on: a Coreflow report deletes the deployment records it no longer names instead of archiving them, `getDeployments` answers the current records whether or not it is asked to `includeArchived`, `deploymentArchiveRetentionDays` is accepted but has no effect, and the dashboard no longer offers archived deployments. A deployment preflight no longer fails `DEPLOYMENT_PREFLIGHT_UNAVAILABLE` on a route claim that stores an absent owner as `null`, and the deployment and adoption preflights log the cause of an unexpected failure. A node's requests no longer contend on the controller fence and share a 6 s budget below the cluster relay's 8 s forward deadline, a request out of budget is refused `controller-deadline`, which the node retries, and a node deletion refuses what a racing write bound to the node. Cloudly consumes the projection application receipts of Pallet `36.3.0`, so a flow a node's projection withdrew is granted again on the same leases once the node reports that it applied the withdrawal; an earlier Pallet keeps every withdrawal carried. Cloudly `33.17.2` starts a database below data version `0.25.0` that holds a runtime slot: the data step `runtime-log-attempts` read through models startup has not prepared and was refused, and now reads and writes through the driver; an install whose first start failed there stayed at data version `0.24.0` and upgrades to `1.28.3` normally. Cloudly `33.17.1` fixes the first start of `33.17.0`, which crash-looped on a database holding Pallet-era nodes: its first data step `cluster-node-hostname-intent` stated each intent through the node and cluster models, which startup has not prepared while it runs the data steps, and was refused before its first write; the step now reads and writes the stored node and cluster documents directly, like the other data steps. The migration ledger does not advance past a failed step, so an install on `1.25.0` whose first start failed stayed unchanged at data version `0.25.0` and upgrades to `1.28.3` normally. Cloudly `33.17.1` keeps the data steps of `33.17.0`, which move the data version from `0.25.0` to `0.28.0`: every upgrade from `1.24.0` or earlier, and from `1.25.0` whose first start failed, runs the forward-only data steps `cluster-node-hostname-intent` to `0.26.0` (every Pallet-era node gets the intent of the hostname it reports, and nothing is renamed), `deployment-operation-route-evidence` to `0.27.0` (a routeless deployment operation that succeeded records its empty route evidence) and `service-secret-target-predecessor` to `0.28.0` (a secret target state an earlier service deletion left behind is removed) before the `0.28.0` to `0.30.0` steps. The template stays marked breaking, migration-required, backup-before-upgrade and manual-review, because hosts read the flags from the target version alone and every upgrade from `1.26.0` or earlier runs the `0.28.0` to `0.30.0` steps, after the `0.25.0` to `0.28.0` steps from `1.24.0` or earlier; every upgrade from `1.19.0` or earlier runs the forward-only data step `runtime-log-attempts` from `0.24.0` to `0.25.0` before them (the log record of every slot's current attempt is written), and every upgrade from `1.15.0` or earlier runs `corestore-control-token-state` from `0.23.0` to `0.24.0` before that (a Corestore control token stored earlier is recorded as revision 1); every Cloudly before `33.17.3` refuses the migrated data: stop Cloudly and take a verified backup first, as rollback is full-restore-only. From `1.24.0` or earlier, Cloudly `33.17.0`'s changes apply too: Cloudly `33.17.0` lets a legacy import reserve: it takes over every route claim its legacy service holds in the import's organization, the cluster ingress keeps serving each hostname for the legacy service until the import's service declares it, and cleanup or rollback before promotion hands the claims back while the legacy service exists; `ROUTE_UNAVAILABLE` and `DEPLOYMENT_ROUTE_COLLISION` name each conflicting hostname and the services holding it. It owns the hostname a node should carry (interfaces `32.52.0`, `cloudly-worker-<name>` by default): `setClusterNodeHostname` renames a Pallet-era node and `resetClusterNodeHostname` sets the default, Spark reads the intent at `POST /spark/nodes/host`, and a Pallet service's gateway routes and inbound mail follow the node its runtime spec pins by id, so a rename moves no traffic. A routeless deployment records its empty route evidence when it succeeds; deleting a cluster's designated ingress service is refused `SERVICE_IS_CLUSTER_INGRESS`, `designateClusterIngress` recovers a designation whose bearer is no longer active, and a deleted service's secret target state goes with its row, so a service recreated under its id writes its secrets again. From `1.23.0` or earlier, Cloudly `33.16.0`'s changes apply too: Cloudly `33.16.0` captures a legacy service's runtime attestation from an administrator's host observation, without a Coreflow session: `captureLegacyServiceRuntimeAttestation` takes `hostObservation`, the legacy Swarm service's spec image and mode and, per replica, its node, Swarm task, container, image store, image id, `RepoDigests`, state and health, read at most 15 minutes before the capture; Cloudly judges it against the one root every replica runs, which it requires whole in its registry and which the legacy tag need not still name, and refuses incomplete evidence without storing anything. The registry prune keeps an attested root until a release's `cloudly-legacy-import` evidence names the attestation or the legacy service is retired. From `1.22.0` or earlier, Cloudly `33.15.3`'s changes apply too: Cloudly `33.15.3` serves a promoted route: a route's identity carried the service's runtime write fence `runtimeAuthorityFenceGeneration`, which every runtime write of the service advances, in a Pallet-born cluster the route join's own workload lease read included, so the route plan read after the cluster ingress's certificate fetch never matched the one before it and every refresh that fetched a certificate withdrew the cluster's whole route table; a route's identity now carries only the versions that change with its authority (the route intent, the runtime policy revision, and per endpoint its assignment, its node's session and its target), while a service being deleted or holding no runtime authority is still refused by the route join, so a revoked route is withdrawn as before, by name. A table withdrawn because its authority changed or its route sources could not be read names that withdrawal as the reason the cluster ingress serves none of its routes, so a failing route verification states it; that reason states facts about its own route only, because one table carries the routes of several organizations: a route whose authority changed names its changed field, every other route only that another route's authority (or the ingress designation) changed during the certificate fetch, and a source-read failure only that the route sources could not be read, its error staying in the operator log. From `1.21.0` or earlier, Cloudly `33.15.2`'s changes apply too: Cloudly `33.15.2` lets a promoted route reach CoreTraffic: a route's identity carries the route intent version `routeIntentGeneration` instead of the whole-document `routeStateGeneration`, so the status writes during the cluster ingress's certificate fetch no longer withdraw the cluster's route table, a plan change during the fetch withdraws it only when it revokes a route the table is about to serve, and the change is named by route and input. A cluster ingress flow that a compensated promotion withdrew and a later promotion wants again no longer stalls the node's network projection: Cloudly holds that flow back with its withdrawal carried and signs the rest of the node's network, while a workload or router grant wanted again while withdrawn withholds the node's projection as `withdrawn-grant-reissued` and any other contract refusal as `projection-rejected`, each naming what it holds; the cluster ingress leaves a route whose grant is held out by name, and a failed route verification states why no route is served for its hostname. A held flow is granted again only once the target or CoreTraffic runs under a new lease. From `1.20.0` or earlier, Cloudly `33.15.1`'s changes apply too: Cloudly `33.15.1` fixes the deployment preflight of an existing service Pallet runs: it read the current runtime from Coreflow alone and answered NO-GO (`CURRENT_RUNTIME_DIGEST_MISSING`, `RUNTIME_ATTESTATION_MISMATCH`) for a service whose rollout status was verified, and now reads the runs of the service's current plan, the evidence its deployment status reads, when Pallet executes that plan on every cluster it targets. It also lets a route promotion survive the status writes of its own gateway sync: the promotion fenced on `routeStateGeneration`, the service's whole-document version, which the route status its gateway sync records, Coreflow's reconciliation reports and its own verified-route status all advance, so every promotion failed `DEPLOYMENT_ROUTE_FENCE_CONFLICT` with no route evidence; it now fences on `routeIntentGeneration`, which only a write that changes the service's domains advances, so a concurrent route change, a change and back included, still fails it fenced, and a promotion interrupted under `33.15.0` is recovered as fenced (retryable), without a rollback. From `1.19.0` or earlier, Cloudly `33.15.0`'s changes apply too: Cloudly `33.15.0` keeps workload logs: a Pallet node's log batches are kept per attempt, their payloads in Cloudly's own bucket (the template's `S3_BUCKET`) under `runtime-logs/`, bounded to 8 MiB and 8192 batches per attempt and the newest 8 attempts per slot, each batch for its config's `logs.retentionMs`, and read with `getServiceRuntimeLogAttempts` and `getRuntimeAssignmentLogs` (interfaces `32.47.0`) or the service page's Logs tab. A Pallet-executed rollout that succeeded stays succeeded when a replica later fails or is replaced; a failing service runtime pass names its step, and a database error its code name and number. A launcher secret written before a service's first image plan no longer leaves the service refused `secrets-unsupported`, because every image plan is written with the service's secret deployment plan in one transaction. The designated cluster ingress runs with `SERVEZONE_CLUSTER_RELAY_ORIGIN` and `SERVEZONE_CLUSTER_NODE_ID`, which Cloudly writes into that workload's environment (a cluster without a valid relay refuses it `cluster-relay-missing`); the template sets neither. From `1.18.0` or earlier, Cloudly `33.14.1` and `33.14.2`'s changes apply too: Cloudly `33.14.2` completes a rollout Pallet executes: Cloudly reports on every rollout whose target clusters are all on Pallet, one reporter per cluster (`pallet/<clusterId>`) over the runs of exactly the current desire, so it moves to `succeeded` (or `ready-for-route`) once its runs are ready at the requested digest and fails when a run fails, and `getServiceDeploymentStatus` reads its runtime tasks from the runtime manager instead of Coreflow, so tsdeploy no longer times out on a Pallet-attached service. A launcher-secret run admitted after a Cloudly restart, reconnect or credential rotation gets its material again instead of waiting `secret-mount-uncertain` (`secret-recipient-inactive`) until its node process restarts, because a node's secret recipient is active for every session of the node process that enrolled it. A failing producer pass is logged under its producer's subject once per change of cause, with how long it ran and a value-free cause naming the failing step and check (such as `network-projection-rejected`), and its recovery once; a failed sweep the timer started is no longer dropped. From `1.17.0` or earlier, Cloudly `33.14.0`'s changes apply too: it gives a platform-owned service's published ports infrastructure-administrator authority: an administrator designates a platform organization's service (an organization id carrying `:`) with `designatePlatformOwnedService`, withdraws it with `undesignatePlatformOwnedService` and reads it with `getServicePlatformOwnership` (interfaces `32.45.0`), after which only a verified administrator reads and writes its published ports, any other organization id is refused `organization-not-platform`, and the designation, stored on the service document, is read inside the published-port request's one admitted operation; every organization service keeps the organization-owner rule. It also names and logs every assignment dispatch outcome under a closed, value-free reason, pushes a refused or unanswered push again on the next pass, and keeps a run its node refused (cause `refused`) instead of replacing it on the failure backoff until its input changes. From `1.16.0` or earlier, Cloudly `33.13.3` and `33.13.4`'s changes apply too: Cloudly `33.13.4` delivers a run's secret material through the WorkloadInit launcher again (Corestore's `MONGODB_URI` included), remembers a successful WorkloadInit approval verification per artifact, and names the check of every refused material request with a closed, value-free `reason`; since Cloudly `33.13.3` a node's session is pushed each assignment revision once instead of on every delivery pass. From `1.15.0` or earlier, Cloudly `33.11.0` to `33.13.2`'s changes apply too: they let an administrator set the Corestore control token without an exec into Cloudly's container (`setCorestoreControlToken`, interfaces `32.42.0`, with its revision and value-free receipts in the new collections `cloudly_corestore_control_token_state` and `cloudly_corestore_control_token_receipts`, and `servezone corestore set-control-token`), keep every managed VPN member across a protected-authority step that only adds, retire a node and its egress owner (`retireClusterNode`, interfaces `32.43.0`), ask a retiring node on interfaces `32.44.0` to withdraw its allocation protection so that a stopped or offline node no longer blocks allocation, and keep a stopping workload's lease in its node's projection until the node's stop receipt is accepted. From `1.14.0` or earlier, Cloudly `33.10.0`'s changes apply too: it lets an operator obtain a cluster relay's bearer without an exec into Cloudly's process: `exportClusterRelayAuthorization` (interfaces `32.41.0`) requires a freshly signed-in administrator, proves the delivered credential version against the hash its generation issued, commits an audit receipt fenced at that generation before it answers the bearer, and never logs it; the receipts live in the new collection `cloudly_cluster_relay_authorization_exports`, installed at startup and listed by `getClusterRelayAuthorizationExports`, and `servezone relay export-authorization` (`@serve.zone/cli` `33.10.0`) writes the bearer as a new `0600` file. From `1.14.0` or earlier, Cloudly `33.9.0`'s changes apply too: a node's managed VPN member is kept across its network projection changes instead of being withdrawn from the hub network on every workload change (`bindGetRuntimeManagedVpnCredentialSuccessor`, interfaces `32.39.0`), the failure cause a node reports with a failed run (interfaces `32.40.0`) is stored and shown in the runtime assignment views and the dashboard's Cause column, and `deleteServiceById` answers success once a service is marked for deletion while another deleting service drains, naming its refusals `SERVICE_LIFECYCLE_BUSY`, `SERVICE_ORGANIZATION_AUTHORITY_REQUIRED` and `SERVICE_MANAGER_STOPPING` instead of an internal error. From `1.13.0` or earlier, Cloudly `33.8.3`'s fix applies too: it lets `deleteServiceById` retire a legacy service whose Corestore resources a successor adopted: its live Corestore bindings are tombstoned with the ordinary config push once every claimed legacy resource of each binding's capability has passed to the successor's live adopting binding, while a service without such a handover is still refused `CORESTORE_CLEANUP_TARGET_AUTHORITY_REQUIRED`. A legacy service whose runtime attestation a legacy-import operation still uses (`reserving`, `reserved`, `provisioning` or `awaiting-image`) is refused `SERVICE_LEGACY_IMPORT_PENDING` (retryable) and keeps running; import and deletion are serialized on the registry repository, so an import that starts after the check refuses as `LEGACY_IMPORT_SOURCE_MISMATCH`, and a deletion that proceeds deletes the attestations no release consumed. From `1.12.0` or earlier, Cloudly `33.8.2`'s fix applies too: Cloudly names why it refused a node's report: a refused `reportRuntimeNetworkProtection`, `reportRuntimeAssignmentTerminalReceipt`, `reportRuntimeAssignmentObservation` or `reportRuntimeAssignmentLogs` is logged as a warning once per node, report and distinct reason, and the node is answered with the unchanged error text plus `{ code: 'runtime-session-report-refused', reason }` as the error's data. From `1.11.0` or earlier, Cloudly `33.8.1`'s fix applies too: deploy before any Pallet that sends `discardedCaptures` or a `not-kept` log loss reaches a fleet node, since it accepts those log batch fields of interfaces `32.38.0`, which Cloudly `33.8.0` refuses on every retry, and still keeps no workload logs. From `1.10.0` or earlier, Cloudly `33.8.0`'s changes apply too: deploy before any Pallet that captures workload output reaches a fleet node, since Cloudly `33.8.0` answers a Pallet's `reportRuntimeAssignmentLogs` (interfaces `32.37.0`) with `not-kept`, which an earlier Cloudly leaves unhandled. Cloudly `33.8.0` lets a legacy `deployOnPush` service without deployable source move to a greenfield successor that runs the exact image it ran (`captureLegacyServiceRuntimeAttestation`, a `legacyImport` reservation under the capability `deployment:import-legacy`, and `importServiceDeploymentLegacyImage` in place of a push), judges promotion against the platforms of the service's rollout target, refuses a registry repository that belongs to another service as `REGISTRY_REPOSITORY_MISMATCH`, promotes and routes a service with a Corestore `database` or `objectstorage` binding again, and reads a node whose last report is older than 180 seconds as `offline`. Its web UI moved to path URLs and a regrouped menu, so bookmarks of the old UI locations open the not-found screen; server paths such as `/status` and the OIDC callback are unchanged. From `1.9.0` or earlier, Cloudly `33.7.0`'s changes apply: Cloudly `33.7.0` lets a fresh greenfield service take over a legacy service's Corestore database and bucket with their data and retention intact: an `adoptCorestoreLegacyResources` selection entry may name `successorServiceId` (interfaces `32.34.0`, carried out by Corestore `33.2.0` or later), the successor must share the owner's organization, be an immutable-deployment service, own no legacy resource of the capability and hold no other binding of it, and each legacy resource is adopted by one service only (`legacy-resource-adopted-elsewhere`, `successor-organization-mismatch`, `successor-not-greenfield`, `successor-owns-legacy-resource`). Since Cloudly `33.6.1`, a deployment reservation whose release tag the registry already holds is refused `DEPLOYMENT_RELEASE_TAG_OCCUPIED` instead of waiting for its image forever, an administrator's manifest push to a release tag a deployment operation reserved is refused `409 release-tag-reserved`, and a binding status report reaches its cluster relay only when it changed what the relay reads. From `1.8.0` or earlier, startup runs the forward-only data step `deployment-machine-user-grants` from `0.22.0` to `0.23.0`, which keeps on every deployment machine user only the grants a capability check reads and permanently removes the rest (such as the `{ serviceId, capabilities }` grants of Cloudly 11); stop Cloudly and take a verified backup first, as the way back is the pre-upgrade backup. Cloudly `33.6.0`'s changes apply too: Cloudly stops gracefully on SIGTERM and SIGINT within 8 seconds, below the 10-second stop grace of Docker and Swarm, so a stop or restart no longer leaves a database transaction held and the next start no longer crash-loops on `EFENCE_BUSY`; a process ended by SIGKILL still holds its database session until the server's session timeout. Every new service gets its own deployment namespace, so two services of one project reserve without `DEPLOYMENT_RESOURCE_COLLISION`, while a deployed service keeps its namespace, and `getDeploymentMachineUsers` lists users configured under Cloudly 11 again. From `1.7.0` or earlier, Cloudly `33.5.0`'s changes apply: Cloudly reserves a deployment only for a deployment machine user: `reserveServiceDeployment` refuses an administrator with `DEPLOYMENT_MACHINE_IDENTITY_REQUIRED`, including a deployer that signs in with a username and password (such as tsdeploy with `TSDEPLOY_CLOUDLY_USERNAME`/`TSDEPLOY_CLOUDLY_PASSWORD`), so before upgrading create a deployment machine user per deployer (`mutateDeploymentMachineUser`, or Security → Access → Deployment machine users in the dashboard), attach its grants with `configureServiceDeploymentMachineUser` and switch the deployer to its token; an existing `api` machine user with its grants already qualifies. A machine identity or registry bearer issued by an earlier release is refused until its holder exchanges its token again with `getIdentityByToken`, and removing a token now ends the sessions opened with it. Let no deployment run during the upgrade: a run in progress keeps its pre-upgrade identity and is refused for the rest of that run (rerun it, or resume it with `deploy-retry`). From `1.3.0` through `1.6.0`, startup runs the forward-only data step `runtime-session-protocol-offer` from `0.21.0` to `0.22.0`, which states `protocol: null` on every Pallet session row recorded before Cloudly stored its node's protocol offer; stop Cloudly and take a verified backup first, as rollback is full-restore-only. From `1.6.0`, Cloudly `33.4.0`'s changes apply: Cloudly `33.4.0` lets `setServicePublishedPorts` publish a contiguous range through `hostPortEnd` (at most 1024 ports in 64 entries per protocol) and same-port outbound through `symmetric: true`, which needs public egress (`published-port-symmetric-without-egress`) and owns its inside ports (`published-port-symmetric-inside-overlap`). New handoff leases translate outbound flows only into `49152-65535`, whose ports are refused `published-port-reserved`; a node keeps its `1024-65535` lease until its router is re-created. A policy write for a service being deleted is refused `service-deleting`. A projection carrying a range or a symmetric entry is withheld as `node-contract-behind` from a Pallet node whose session states a release below interfaces `32.31.0`, or an unrecorded one, so ranges and symmetric entries take effect on a node only once it runs a Pallet release built on interfaces `32.31.0` or later. From `1.5.0`, Cloudly `33.3.0`'s changes apply: Cloudly `33.3.0` lets an administrator set a service's network egress with `setServiceNetworkEgress` (public internet, and the platform endpoints of the protected authority), and refuses a workload whose database or object-storage binding sits on a node whose Corestore endpoint its network does not select as `database-endpoint-unselected` or `objectstorage-endpoint-unselected`; a replica already running keeps running while its target reports the refusal. From `1.4.0`, Cloudly `33.2.0`'s changes apply: Cloudly takes a dcrouter admin token once through `bootstrapExternalGatewayCredential` to provision its own gateway-client credential, starts a first enrollment with the gateway settings, and stages a gateway settings change the held credential cannot carry until the next bootstrap. From `1.2.1` or earlier, startup runs the forward-only migration from `0.20.0` to `0.21.0` before that step, dropping the relay certificate and DNS collections of Cloudly's retired Cloudflare and Let's Encrypt integration and logging every relay name whose A record Cloudly had written at Cloudflare, which the operator removes at the provider. dcrouter holds every cluster relay name's A record and certificate from then on, `SERVEZONE_SSLMODE=letsencrypt` is refused, and the typed API changes as the Cloudly 33.0.0 changelog lists. Stop Cloudly and take a verified backup before upgrading; rollback is full-restore-only. Roll out Cloudly `33.19.1` before Coreflow `33.2.0`; Coreflow `33.1.0` is the minimum. From `1.3.0`, also roll out Cloudly before Coreflow and Corestore `33.0.0`, and set every node's Corestore database endpoint, because a database binding on a node without one is not granted and its service is refused `database-endpoint-missing`, and existing database bindings return no credentials until they are granted again for the endpoint. From `1.1.10` or earlier, startup first runs the `1.2.0` steps from `0.10.0` and needs completed Secrets v2 cutover retirement. Both the existing Secrets v2 keyring and operator-approved WorkloadInit authority are required encrypted inputs delivered as separate root-owned `0400` files; the public environment contains only their fixed paths. |
71
+ | `cloudly` | MongoDB, S3 | Versions `1.28.6` and `1.28.5` declare Cloudly's workload logs non-backup data: `backupExclusions` names the key prefix `runtime-logs/` of the `legacy-s3` bucket, and `requiresFeatures` names `storage.object-storage.backup-exclusions`, so a host that does not fulfil it lists them and refuses them as targets. A backup on a host that fulfils it leaves those objects out, and a restore brings back Cloudly's state, not its past workload logs. `1.28.6` is `1.28.4` (Cloudly `33.19.2`) with that declaration; `1.28.5` is `1.28.3` (Cloudly `33.19.1`) with it, so a host on `1.28.3` moves its template without moving Cloudly. `1.28.5` sorts above `1.28.4` but runs the older image: from `1.28.4`, upgrade to `1.28.6`. Version `1.28.4` pins Cloudly `33.19.2` and keeps the `1.28.3` runtime contract, including the Onebox `33.8.0` minimum: its platform Corestore `32.2.4` runs NoSQLDB `10.6.1` with the D3 write-conflict fix, and on an earlier engine a node's network plan loses every commit to a `WriteConflict`, so Cloudly `33.15` must not run on it. Cloudly `33.19.2` is a fixes-only patch over `33.19.1`: a greenfield deployment, a legacy import's successor included, proves its namespace blank on the nodes of the Pallet clusters only, so a fleet that still keeps a Coreflow-owned Swarm cluster without a relay can reserve one, while a Pallet node whose inventory cannot be read still blocks; a greenfield rollout without a Pallet runtime spec is refused `GREENFIELD_PALLET_RUNTIME_SPEC_REQUIRED` instead of falling back to Coreflow placement (state the spec with `setServiceRuntimeSpec`, then promote again); and a node's network compilation proves each workload lease and each authority or eligibility proof once per transaction. It adds no data step and keeps the data version `0.30.0`, so an upgrade from `1.28.3` runs none. The `1.28.4` changelog carries the changes of `33.19.2` and of `33.18.3` to `33.19.1` and the earlier releases' upgrade path only, because a catalog changelog is bounded at 65536 characters; `1.28.2`'s carries the rest. From `1.28.2` or earlier, Cloudly `33.19.1`'s changes apply too: Cloudly `33.19.1` carries `33.18.3` to `33.19.1`: a node's log report costs one append transaction that queues the batch with its payload in the new collection `cloudly_runtime_log_queue`, and a background drain writes the payloads and commits the ledger rows without holding the node's request or fencing the controller record (a full queue refuses a report with the retryable `log-intake-saturated`), object writes list nothing, workload logs are kept for at least 30 days, route verification reads the certificate each route presents on a connection of its own, and `readServiceDeploymentStatus` (interfaces `32.54.0`) reads a deployment operation without changing it. Its first start creates `cloudly_runtime_log_queue`; on NoSQLDB `10.6.1` that creation waits at most 5 s for the open transactions on the Cloudly database, and a transaction an earlier Cloudly left open by exiting before its stop completed holds the database until its session expires 30 minutes later, so after an unclean stop of the previous Cloudly start the upgrade no earlier than 30 minutes after that exit. It adds no data step and keeps the data version `0.30.0`, so an upgrade from `1.28.2`, `1.28.1` or `1.28.0` runs none. From `1.28.1` or earlier, Cloudly `33.18.2`'s fixes apply too: Cloudly `33.18.2` is a fixes-only patch over `33.18.1`: a node's network configuration advances only once the node's application receipt names the projection it holds, so a node no longer receives a new version while it still applies the last one; upgrade every node to Pallet `36.3.0` or later (interfaces `32.50.0` or later) first, because a node that sends no receipt keeps the projection it holds and receives no successor. It adds no data step and keeps the data version `0.30.0`, so an upgrade from `1.28.1` or `1.28.0` runs none. From `1.28.0` or earlier, Cloudly `33.18.1`'s fixes apply too: Cloudly `33.18.1` is a fixes-only patch over `33.18.0`: a node's log batches append without its node fences, so they no longer miss their budget against the node's producer passes, and the deployment preflight answers within 25 s, past which it answers the retryable `DEPLOYMENT_PREFLIGHT_UNAVAILABLE`. It adds no data step and keeps the data version `0.30.0`, so an upgrade from `1.28.0` runs none. From `1.27.0` or earlier, Cloudly `33.18.0`'s changes apply too: Cloudly `33.18.0` loads the App Store catalog at runtime instead of bundling it, from the registries of its `appStoreCatalogRegistries` setting (empty means the npm registry), so a Cloudly template update needs an appstore release only; it ignores `APPSTORE_URL`, whose remote source is removed, and while no verified catalog is active it refuses App Store install details, upgrade preview and upgrade with `APPSTORE_CATALOG_UNAVAILABLE`. It adds no data step and keeps the data version `0.30.0`, so an upgrade from `1.27.0` runs none. From `1.26.0` or earlier, Cloudly `33.17.3`'s changes apply too: Cloudly `33.17.3` moves the data version from `0.28.0` to `0.30.0`: every upgrade from `1.26.0` or earlier runs the forward-only data steps `route-claim-absent-owners` to `0.29.0` (the stored nulls of absent owners are removed from every route claim) and `bounded-runtime-history` to `0.30.0` (every archived deployment record is deleted, the index `archiveState_1` is dropped and the accepted Spark observations are pruned to the rows the census reads), and every Cloudly before `33.17.3` refuses the `0.30.0` data. Runtime history is bounded from then on: a Coreflow report deletes the deployment records it no longer names instead of archiving them, `getDeployments` answers the current records whether or not it is asked to `includeArchived`, `deploymentArchiveRetentionDays` is accepted but has no effect, and the dashboard no longer offers archived deployments. A deployment preflight no longer fails `DEPLOYMENT_PREFLIGHT_UNAVAILABLE` on a route claim that stores an absent owner as `null`, and the deployment and adoption preflights log the cause of an unexpected failure. A node's requests no longer contend on the controller fence and share a 6 s budget below the cluster relay's 8 s forward deadline, a request out of budget is refused `controller-deadline`, which the node retries, and a node deletion refuses what a racing write bound to the node. Cloudly consumes the projection application receipts of Pallet `36.3.0`, so a flow a node's projection withdrew is granted again on the same leases once the node reports that it applied the withdrawal; an earlier Pallet keeps every withdrawal carried. Cloudly `33.17.2` starts a database below data version `0.25.0` that holds a runtime slot: the data step `runtime-log-attempts` read through models startup has not prepared and was refused, and now reads and writes through the driver; an install whose first start failed there stayed at data version `0.24.0` and upgrades to `1.28.4` normally. Cloudly `33.17.1` fixes the first start of `33.17.0`, which crash-looped on a database holding Pallet-era nodes: its first data step `cluster-node-hostname-intent` stated each intent through the node and cluster models, which startup has not prepared while it runs the data steps, and was refused before its first write; the step now reads and writes the stored node and cluster documents directly, like the other data steps. The migration ledger does not advance past a failed step, so an install on `1.25.0` whose first start failed stayed unchanged at data version `0.25.0` and upgrades to `1.28.4` normally. Cloudly `33.17.1` keeps the data steps of `33.17.0`, which move the data version from `0.25.0` to `0.28.0`: every upgrade from `1.24.0` or earlier, and from `1.25.0` whose first start failed, runs the forward-only data steps `cluster-node-hostname-intent` to `0.26.0` (every Pallet-era node gets the intent of the hostname it reports, and nothing is renamed), `deployment-operation-route-evidence` to `0.27.0` (a routeless deployment operation that succeeded records its empty route evidence) and `service-secret-target-predecessor` to `0.28.0` (a secret target state an earlier service deletion left behind is removed) before the `0.28.0` to `0.30.0` steps. The template stays marked breaking, migration-required, backup-before-upgrade and manual-review, because hosts read the flags from the target version alone and every upgrade from `1.26.0` or earlier runs the `0.28.0` to `0.30.0` steps, after the `0.25.0` to `0.28.0` steps from `1.24.0` or earlier; every upgrade from `1.19.0` or earlier runs the forward-only data step `runtime-log-attempts` from `0.24.0` to `0.25.0` before them (the log record of every slot's current attempt is written), and every upgrade from `1.15.0` or earlier runs `corestore-control-token-state` from `0.23.0` to `0.24.0` before that (a Corestore control token stored earlier is recorded as revision 1); every Cloudly before `33.17.3` refuses the migrated data: stop Cloudly and take a verified backup first, as rollback is full-restore-only. From `1.24.0` or earlier, Cloudly `33.17.0`'s changes apply too: Cloudly `33.17.0` lets a legacy import reserve: it takes over every route claim its legacy service holds in the import's organization, the cluster ingress keeps serving each hostname for the legacy service until the import's service declares it, and cleanup or rollback before promotion hands the claims back while the legacy service exists; `ROUTE_UNAVAILABLE` and `DEPLOYMENT_ROUTE_COLLISION` name each conflicting hostname and the services holding it. It owns the hostname a node should carry (interfaces `32.52.0`, `cloudly-worker-<name>` by default): `setClusterNodeHostname` renames a Pallet-era node and `resetClusterNodeHostname` sets the default, Spark reads the intent at `POST /spark/nodes/host`, and a Pallet service's gateway routes and inbound mail follow the node its runtime spec pins by id, so a rename moves no traffic. A routeless deployment records its empty route evidence when it succeeds; deleting a cluster's designated ingress service is refused `SERVICE_IS_CLUSTER_INGRESS`, `designateClusterIngress` recovers a designation whose bearer is no longer active, and a deleted service's secret target state goes with its row, so a service recreated under its id writes its secrets again. From `1.23.0` or earlier, Cloudly `33.16.0`'s changes apply too: Cloudly `33.16.0` captures a legacy service's runtime attestation from an administrator's host observation, without a Coreflow session: `captureLegacyServiceRuntimeAttestation` takes `hostObservation`, the legacy Swarm service's spec image and mode and, per replica, its node, Swarm task, container, image store, image id, `RepoDigests`, state and health, read at most 15 minutes before the capture; Cloudly judges it against the one root every replica runs, which it requires whole in its registry and which the legacy tag need not still name, and refuses incomplete evidence without storing anything. The registry prune keeps an attested root until a release's `cloudly-legacy-import` evidence names the attestation or the legacy service is retired. From `1.22.0` or earlier, Cloudly `33.15.3`'s changes apply too: Cloudly `33.15.3` serves a promoted route: a route's identity carried the service's runtime write fence `runtimeAuthorityFenceGeneration`, which every runtime write of the service advances, in a Pallet-born cluster the route join's own workload lease read included, so the route plan read after the cluster ingress's certificate fetch never matched the one before it and every refresh that fetched a certificate withdrew the cluster's whole route table; a route's identity now carries only the versions that change with its authority (the route intent, the runtime policy revision, and per endpoint its assignment, its node's session and its target), while a service being deleted or holding no runtime authority is still refused by the route join, so a revoked route is withdrawn as before, by name. A table withdrawn because its authority changed or its route sources could not be read names that withdrawal as the reason the cluster ingress serves none of its routes, so a failing route verification states it; that reason states facts about its own route only, because one table carries the routes of several organizations: a route whose authority changed names its changed field, every other route only that another route's authority (or the ingress designation) changed during the certificate fetch, and a source-read failure only that the route sources could not be read, its error staying in the operator log. From `1.21.0` or earlier, Cloudly `33.15.2`'s changes apply too: Cloudly `33.15.2` lets a promoted route reach CoreTraffic: a route's identity carries the route intent version `routeIntentGeneration` instead of the whole-document `routeStateGeneration`, so the status writes during the cluster ingress's certificate fetch no longer withdraw the cluster's route table, a plan change during the fetch withdraws it only when it revokes a route the table is about to serve, and the change is named by route and input. A cluster ingress flow that a compensated promotion withdrew and a later promotion wants again no longer stalls the node's network projection: Cloudly holds that flow back with its withdrawal carried and signs the rest of the node's network, while a workload or router grant wanted again while withdrawn withholds the node's projection as `withdrawn-grant-reissued` and any other contract refusal as `projection-rejected`, each naming what it holds; the cluster ingress leaves a route whose grant is held out by name, and a failed route verification states why no route is served for its hostname. A held flow is granted again only once the target or CoreTraffic runs under a new lease. From `1.20.0` or earlier, Cloudly `33.15.1`'s changes apply too: Cloudly `33.15.1` fixes the deployment preflight of an existing service Pallet runs: it read the current runtime from Coreflow alone and answered NO-GO (`CURRENT_RUNTIME_DIGEST_MISSING`, `RUNTIME_ATTESTATION_MISMATCH`) for a service whose rollout status was verified, and now reads the runs of the service's current plan, the evidence its deployment status reads, when Pallet executes that plan on every cluster it targets. It also lets a route promotion survive the status writes of its own gateway sync: the promotion fenced on `routeStateGeneration`, the service's whole-document version, which the route status its gateway sync records, Coreflow's reconciliation reports and its own verified-route status all advance, so every promotion failed `DEPLOYMENT_ROUTE_FENCE_CONFLICT` with no route evidence; it now fences on `routeIntentGeneration`, which only a write that changes the service's domains advances, so a concurrent route change, a change and back included, still fails it fenced, and a promotion interrupted under `33.15.0` is recovered as fenced (retryable), without a rollback. From `1.19.0` or earlier, Cloudly `33.15.0`'s changes apply too: Cloudly `33.15.0` keeps workload logs: a Pallet node's log batches are kept per attempt, their payloads in Cloudly's own bucket (the template's `S3_BUCKET`) under `runtime-logs/`, bounded to 8 MiB and 8192 batches per attempt and the newest 8 attempts per slot, each batch for its config's `logs.retentionMs`, and read with `getServiceRuntimeLogAttempts` and `getRuntimeAssignmentLogs` (interfaces `32.47.0`) or the service page's Logs tab. A Pallet-executed rollout that succeeded stays succeeded when a replica later fails or is replaced; a failing service runtime pass names its step, and a database error its code name and number. A launcher secret written before a service's first image plan no longer leaves the service refused `secrets-unsupported`, because every image plan is written with the service's secret deployment plan in one transaction. The designated cluster ingress runs with `SERVEZONE_CLUSTER_RELAY_ORIGIN` and `SERVEZONE_CLUSTER_NODE_ID`, which Cloudly writes into that workload's environment (a cluster without a valid relay refuses it `cluster-relay-missing`); the template sets neither. From `1.18.0` or earlier, Cloudly `33.14.1` and `33.14.2`'s changes apply too: Cloudly `33.14.2` completes a rollout Pallet executes: Cloudly reports on every rollout whose target clusters are all on Pallet, one reporter per cluster (`pallet/<clusterId>`) over the runs of exactly the current desire, so it moves to `succeeded` (or `ready-for-route`) once its runs are ready at the requested digest and fails when a run fails, and `getServiceDeploymentStatus` reads its runtime tasks from the runtime manager instead of Coreflow, so tsdeploy no longer times out on a Pallet-attached service. A launcher-secret run admitted after a Cloudly restart, reconnect or credential rotation gets its material again instead of waiting `secret-mount-uncertain` (`secret-recipient-inactive`) until its node process restarts, because a node's secret recipient is active for every session of the node process that enrolled it. A failing producer pass is logged under its producer's subject once per change of cause, with how long it ran and a value-free cause naming the failing step and check (such as `network-projection-rejected`), and its recovery once; a failed sweep the timer started is no longer dropped. From `1.17.0` or earlier, Cloudly `33.14.0`'s changes apply too: it gives a platform-owned service's published ports infrastructure-administrator authority: an administrator designates a platform organization's service (an organization id carrying `:`) with `designatePlatformOwnedService`, withdraws it with `undesignatePlatformOwnedService` and reads it with `getServicePlatformOwnership` (interfaces `32.45.0`), after which only a verified administrator reads and writes its published ports, any other organization id is refused `organization-not-platform`, and the designation, stored on the service document, is read inside the published-port request's one admitted operation; every organization service keeps the organization-owner rule. It also names and logs every assignment dispatch outcome under a closed, value-free reason, pushes a refused or unanswered push again on the next pass, and keeps a run its node refused (cause `refused`) instead of replacing it on the failure backoff until its input changes. From `1.16.0` or earlier, Cloudly `33.13.3` and `33.13.4`'s changes apply too: Cloudly `33.13.4` delivers a run's secret material through the WorkloadInit launcher again (Corestore's `MONGODB_URI` included), remembers a successful WorkloadInit approval verification per artifact, and names the check of every refused material request with a closed, value-free `reason`; since Cloudly `33.13.3` a node's session is pushed each assignment revision once instead of on every delivery pass. From `1.15.0` or earlier, Cloudly `33.11.0` to `33.13.2`'s changes apply too: they let an administrator set the Corestore control token without an exec into Cloudly's container (`setCorestoreControlToken`, interfaces `32.42.0`, with its revision and value-free receipts in the new collections `cloudly_corestore_control_token_state` and `cloudly_corestore_control_token_receipts`, and `servezone corestore set-control-token`), keep every managed VPN member across a protected-authority step that only adds, retire a node and its egress owner (`retireClusterNode`, interfaces `32.43.0`), ask a retiring node on interfaces `32.44.0` to withdraw its allocation protection so that a stopped or offline node no longer blocks allocation, and keep a stopping workload's lease in its node's projection until the node's stop receipt is accepted. From `1.14.0` or earlier, Cloudly `33.10.0`'s changes apply too: it lets an operator obtain a cluster relay's bearer without an exec into Cloudly's process: `exportClusterRelayAuthorization` (interfaces `32.41.0`) requires a freshly signed-in administrator, proves the delivered credential version against the hash its generation issued, commits an audit receipt fenced at that generation before it answers the bearer, and never logs it; the receipts live in the new collection `cloudly_cluster_relay_authorization_exports`, installed at startup and listed by `getClusterRelayAuthorizationExports`, and `servezone relay export-authorization` (`@serve.zone/cli` `33.10.0`) writes the bearer as a new `0600` file. From `1.14.0` or earlier, Cloudly `33.9.0`'s changes apply too: a node's managed VPN member is kept across its network projection changes instead of being withdrawn from the hub network on every workload change (`bindGetRuntimeManagedVpnCredentialSuccessor`, interfaces `32.39.0`), the failure cause a node reports with a failed run (interfaces `32.40.0`) is stored and shown in the runtime assignment views and the dashboard's Cause column, and `deleteServiceById` answers success once a service is marked for deletion while another deleting service drains, naming its refusals `SERVICE_LIFECYCLE_BUSY`, `SERVICE_ORGANIZATION_AUTHORITY_REQUIRED` and `SERVICE_MANAGER_STOPPING` instead of an internal error. From `1.13.0` or earlier, Cloudly `33.8.3`'s fix applies too: it lets `deleteServiceById` retire a legacy service whose Corestore resources a successor adopted: its live Corestore bindings are tombstoned with the ordinary config push once every claimed legacy resource of each binding's capability has passed to the successor's live adopting binding, while a service without such a handover is still refused `CORESTORE_CLEANUP_TARGET_AUTHORITY_REQUIRED`. A legacy service whose runtime attestation a legacy-import operation still uses (`reserving`, `reserved`, `provisioning` or `awaiting-image`) is refused `SERVICE_LEGACY_IMPORT_PENDING` (retryable) and keeps running; import and deletion are serialized on the registry repository, so an import that starts after the check refuses as `LEGACY_IMPORT_SOURCE_MISMATCH`, and a deletion that proceeds deletes the attestations no release consumed. From `1.12.0` or earlier, Cloudly `33.8.2`'s fix applies too: Cloudly names why it refused a node's report: a refused `reportRuntimeNetworkProtection`, `reportRuntimeAssignmentTerminalReceipt`, `reportRuntimeAssignmentObservation` or `reportRuntimeAssignmentLogs` is logged as a warning once per node, report and distinct reason, and the node is answered with the unchanged error text plus `{ code: 'runtime-session-report-refused', reason }` as the error's data. From `1.11.0` or earlier, Cloudly `33.8.1`'s fix applies too: deploy before any Pallet that sends `discardedCaptures` or a `not-kept` log loss reaches a fleet node, since it accepts those log batch fields of interfaces `32.38.0`, which Cloudly `33.8.0` refuses on every retry, and still keeps no workload logs. From `1.10.0` or earlier, Cloudly `33.8.0`'s changes apply too: deploy before any Pallet that captures workload output reaches a fleet node, since Cloudly `33.8.0` answers a Pallet's `reportRuntimeAssignmentLogs` (interfaces `32.37.0`) with `not-kept`, which an earlier Cloudly leaves unhandled. Cloudly `33.8.0` lets a legacy `deployOnPush` service without deployable source move to a greenfield successor that runs the exact image it ran (`captureLegacyServiceRuntimeAttestation`, a `legacyImport` reservation under the capability `deployment:import-legacy`, and `importServiceDeploymentLegacyImage` in place of a push), judges promotion against the platforms of the service's rollout target, refuses a registry repository that belongs to another service as `REGISTRY_REPOSITORY_MISMATCH`, promotes and routes a service with a Corestore `database` or `objectstorage` binding again, and reads a node whose last report is older than 180 seconds as `offline`. Its web UI moved to path URLs and a regrouped menu, so bookmarks of the old UI locations open the not-found screen; server paths such as `/status` and the OIDC callback are unchanged. From `1.9.0` or earlier, Cloudly `33.7.0`'s changes apply: Cloudly `33.7.0` lets a fresh greenfield service take over a legacy service's Corestore database and bucket with their data and retention intact: an `adoptCorestoreLegacyResources` selection entry may name `successorServiceId` (interfaces `32.34.0`, carried out by Corestore `33.2.0` or later), the successor must share the owner's organization, be an immutable-deployment service, own no legacy resource of the capability and hold no other binding of it, and each legacy resource is adopted by one service only (`legacy-resource-adopted-elsewhere`, `successor-organization-mismatch`, `successor-not-greenfield`, `successor-owns-legacy-resource`). Since Cloudly `33.6.1`, a deployment reservation whose release tag the registry already holds is refused `DEPLOYMENT_RELEASE_TAG_OCCUPIED` instead of waiting for its image forever, an administrator's manifest push to a release tag a deployment operation reserved is refused `409 release-tag-reserved`, and a binding status report reaches its cluster relay only when it changed what the relay reads. From `1.8.0` or earlier, startup runs the forward-only data step `deployment-machine-user-grants` from `0.22.0` to `0.23.0`, which keeps on every deployment machine user only the grants a capability check reads and permanently removes the rest (such as the `{ serviceId, capabilities }` grants of Cloudly 11); stop Cloudly and take a verified backup first, as the way back is the pre-upgrade backup. Cloudly `33.6.0`'s changes apply too: Cloudly stops gracefully on SIGTERM and SIGINT within 8 seconds, below the 10-second stop grace of Docker and Swarm, so a stop or restart no longer leaves a database transaction held and the next start no longer crash-loops on `EFENCE_BUSY`; a process ended by SIGKILL still holds its database session until the server's session timeout. Every new service gets its own deployment namespace, so two services of one project reserve without `DEPLOYMENT_RESOURCE_COLLISION`, while a deployed service keeps its namespace, and `getDeploymentMachineUsers` lists users configured under Cloudly 11 again. From `1.7.0` or earlier, Cloudly `33.5.0`'s changes apply: Cloudly reserves a deployment only for a deployment machine user: `reserveServiceDeployment` refuses an administrator with `DEPLOYMENT_MACHINE_IDENTITY_REQUIRED`, including a deployer that signs in with a username and password (such as tsdeploy with `TSDEPLOY_CLOUDLY_USERNAME`/`TSDEPLOY_CLOUDLY_PASSWORD`), so before upgrading create a deployment machine user per deployer (`mutateDeploymentMachineUser`, or Security → Access → Deployment machine users in the dashboard), attach its grants with `configureServiceDeploymentMachineUser` and switch the deployer to its token; an existing `api` machine user with its grants already qualifies. A machine identity or registry bearer issued by an earlier release is refused until its holder exchanges its token again with `getIdentityByToken`, and removing a token now ends the sessions opened with it. Let no deployment run during the upgrade: a run in progress keeps its pre-upgrade identity and is refused for the rest of that run (rerun it, or resume it with `deploy-retry`). From `1.3.0` through `1.6.0`, startup runs the forward-only data step `runtime-session-protocol-offer` from `0.21.0` to `0.22.0`, which states `protocol: null` on every Pallet session row recorded before Cloudly stored its node's protocol offer; stop Cloudly and take a verified backup first, as rollback is full-restore-only. From `1.6.0`, Cloudly `33.4.0`'s changes apply: Cloudly `33.4.0` lets `setServicePublishedPorts` publish a contiguous range through `hostPortEnd` (at most 1024 ports in 64 entries per protocol) and same-port outbound through `symmetric: true`, which needs public egress (`published-port-symmetric-without-egress`) and owns its inside ports (`published-port-symmetric-inside-overlap`). New handoff leases translate outbound flows only into `49152-65535`, whose ports are refused `published-port-reserved`; a node keeps its `1024-65535` lease until its router is re-created. A policy write for a service being deleted is refused `service-deleting`. A projection carrying a range or a symmetric entry is withheld as `node-contract-behind` from a Pallet node whose session states a release below interfaces `32.31.0`, or an unrecorded one, so ranges and symmetric entries take effect on a node only once it runs a Pallet release built on interfaces `32.31.0` or later. From `1.5.0`, Cloudly `33.3.0`'s changes apply: Cloudly `33.3.0` lets an administrator set a service's network egress with `setServiceNetworkEgress` (public internet, and the platform endpoints of the protected authority), and refuses a workload whose database or object-storage binding sits on a node whose Corestore endpoint its network does not select as `database-endpoint-unselected` or `objectstorage-endpoint-unselected`; a replica already running keeps running while its target reports the refusal. From `1.4.0`, Cloudly `33.2.0`'s changes apply: Cloudly takes a dcrouter admin token once through `bootstrapExternalGatewayCredential` to provision its own gateway-client credential, starts a first enrollment with the gateway settings, and stages a gateway settings change the held credential cannot carry until the next bootstrap. From `1.2.1` or earlier, startup runs the forward-only migration from `0.20.0` to `0.21.0` before that step, dropping the relay certificate and DNS collections of Cloudly's retired Cloudflare and Let's Encrypt integration and logging every relay name whose A record Cloudly had written at Cloudflare, which the operator removes at the provider. dcrouter holds every cluster relay name's A record and certificate from then on, `SERVEZONE_SSLMODE=letsencrypt` is refused, and the typed API changes as the Cloudly 33.0.0 changelog lists. Stop Cloudly and take a verified backup before upgrading; rollback is full-restore-only. Roll out Cloudly `33.19.2` before Coreflow `33.2.0`; Coreflow `33.1.0` is the minimum. From `1.3.0`, also roll out Cloudly before Coreflow and Corestore `33.0.0`, and set every node's Corestore database endpoint, because a database binding on a node without one is not granted and its service is refused `database-endpoint-missing`, and existing database bindings return no credentials until they are granted again for the endpoint. From `1.1.10` or earlier, startup first runs the `1.2.0` steps from `0.10.0` and needs completed Secrets v2 cutover retirement. Both the existing Secrets v2 keyring and operator-approved WorkloadInit authority are required encrypted inputs delivered as separate root-owned `0400` files; the public environment contains only their fixed paths. |
72
72
  | `ghost` | MariaDB | Uses MySQL-style `database__connection__*` environment variables. |
73
73
  | `gitea` | MariaDB | Uses Gitea database environment variables with `${MARIADB_*}` placeholders. |
74
74
  | `nextcloud` | MariaDB, Valkey | Uses MySQL-compatible database variables. Valkey is declared as a platform requirement. |
@@ -254,6 +254,29 @@ declared `protection` must request at least one requirement, and
254
254
  fulfillment adapter can act on and names a request the platform storage
255
255
  contract rejects.
256
256
 
257
+ `backupExclusions` declares data an app keeps only for observability, such as
258
+ workload logs: each entry names one object-storage request (an explicit request
259
+ id, or `legacy-s3` with `platformRequirements.s3`), up to 16 key prefixes ending
260
+ in `/` and one operator-readable `reason`. A backup leaves the objects under those
261
+ prefixes out; a restore neither brings them back nor removes them. A version that
262
+ declares an exclusion must name `storage.object-storage.backup-exclusions` in
263
+ `requiresFeatures`, so a host that cannot honour it refuses the version instead
264
+ of backing up what it was told to leave out or ignoring the declaration:
265
+
266
+ ```jsonc
267
+ {
268
+ "platformRequirements": { "s3": true },
269
+ "backupExclusions": [
270
+ {
271
+ "storageRequest": "legacy-s3",
272
+ "keyPrefixes": ["runtime-logs/"],
273
+ "reason": "Workload logs; a restore brings back state, not past logs."
274
+ }
275
+ ],
276
+ "requiresFeatures": ["storage.object-storage.backup-exclusions"]
277
+ }
278
+ ```
279
+
257
280
  The full version config and every storage object are exact-key validated.
258
281
  Classes express portable policy only. Provider addresses, protocols,
259
282
  identities, host paths, drivers, and driver options are rejected instead of
@@ -275,7 +298,9 @@ const normalizedStorage = normalizeAppStoreStorageConfig(catalogVersion.config,
275
298
  ```
276
299
 
277
300
  The result contains canonical `storageClasses`, `storageRequests`,
278
- `requestOrigins`, and `requiredFeatureIds`. `requestOrigins` marks explicit,
301
+ `requestOrigins`, `requiredFeatureIds` and `backupExclusions`, the declared
302
+ non-backup key prefixes and reason by object-storage request id (`legacy-s3` for
303
+ `platformRequirements.s3`, empty when the version declares none). `requestOrigins` marks explicit,
279
304
  legacy-volume, and legacy-S3 requests so fulfillment adapters can preserve old
280
305
  delivery behavior without weakening the new contract. Legacy S3 normalization
281
306
  uses the `servezone-platform-s3` delivery profile; adapters keep
@@ -283,7 +308,8 @@ the established S3/AWS environment aliases for that profile. Those
283
308
  binding-provided values deterministically replace same-key legacy template
284
309
  environment values. Legacy inputs do not claim new feature support, while every
285
310
  explicit request fails before provisioning if a derived storage feature is
286
- unavailable.
311
+ unavailable. A declared backup exclusion fails the same way when the target does
312
+ not advertise `storage.object-storage.backup-exclusions`.
287
313
 
288
314
  Environment variable keys must start with a letter or underscore and may contain letters, numbers, and underscores. Mixed-case keys are supported because some upstream images, including Ghost and Gitea, require lowercase nested configuration segments such as `database__connection__host` or `GITEA__database__HOST`.
289
315
 
@@ -30,7 +30,7 @@ if (result.outcome === 'activated') {
30
30
  - `AppStoreCatalogLoader` loads the catalog package from the registry list `appstore.resolveAppStoreCatalogRegistries` (`@serve.zone/interfaces`) answers, npmjs by default, checks the tarball's `dist.integrity`, verifies the envelope against the consumer's trusted keys and supported feature ids, and judges its succession; `signal` aborts a load. `restoreEnvelopeText` verifies a stored last-known-good envelope again at start. `judgeImportedEnvelope` and `judgeImportedEnvelopeText` judge an operator-imported envelope the same way as a fetched one. It holds no state, runs no timer and persists nothing; every request is bounded.
31
31
  - `findAppStoreCatalogApp` and `findAppStoreCatalogVersion` look up the active catalog; withdrawn versions stay listed (`appstore.judgeAppStoreCatalogInstallTarget` refuses them as targets).
32
32
  - `projectAppStoreCatalogApp`, `projectAppStoreCatalog` and `getAppStoreCatalogListing` project the active catalog for one consumer (its supported feature ids, kind and release): every listed version with its install refusal (`versionWithdrawn`, `featuresUnsupported` or `consumerVersionTooOld`), the newest install target, and the `IAppStoreApp` / `IAppStoreAppMeta` shapes the App Store views carry, listing install targets in `versions` and every version in `versionStates`.
33
- - `normalizeAppStoreStorageConfig` and `normalizeAppStoreVolumes` produce the storage handoff of a catalog version config after the interfaces contract judged its storage.
33
+ - `normalizeAppStoreStorageConfig` and `normalizeAppStoreVolumes` produce the storage handoff of a catalog version config after the interfaces contract judged its storage; the handoff's `backupExclusions` maps each object-storage request id (`legacy-s3` for `platformRequirements.s3`) to the key prefixes a backup leaves out and why.
34
34
  - `appStoreCatalogPackageName` and `appStoreCatalogEnvelopePackagePath` name where the catalog is published.
35
35
 
36
36
  Test fixtures for consumers live in the separate entry `@serve.zone/appstore/testing`.
@@ -8,6 +8,7 @@ import type {
8
8
  type TCatalogVersionConfig = plugins.servezoneInterfaces.appstore.TAppStoreCatalogVersionConfig;
9
9
  type TStorageFeatureId = plugins.servezoneInterfaces.appstore.TAppStoreStorageFeatureId;
10
10
  type TStorageRequest = plugins.servezoneInterfaces.appstore.TAppStoreStorageRequest;
11
+ type TBackupExclusion = plugins.servezoneInterfaces.appstore.IAppStoreBackupExclusion;
11
12
  type TVolume = plugins.servezoneInterfaces.appstore.IAppStoreVolume;
12
13
 
13
14
  const storageFeatureIds = plugins.servezoneInterfaces.appstore.appStoreStorageFeatureIds;
@@ -55,7 +56,11 @@ const createLegacyVolumeRequestId = (volumeArg: TVolume, indexArg: number, label
55
56
  return requestId;
56
57
  };
57
58
 
58
- const deriveRequiredFeatureIds = (explicitRequestsArg: TStorageRequest[]): TStorageFeatureId[] => {
59
+ /** The storage feature ids a config needs, in the strictly increasing order `requiresFeatures` lists them. */
60
+ const deriveRequiredFeatureIds = (
61
+ explicitRequestsArg: TStorageRequest[],
62
+ backupExclusionsArg: readonly TBackupExclusion[],
63
+ ): TStorageFeatureId[] => {
59
64
  const required: TStorageFeatureId[] = [];
60
65
  if (explicitRequestsArg.length > 0) required.push(storageFeatureIds.bindings);
61
66
  if (explicitRequestsArg.some((requestArg) => requestArg.kind === 'filesystem')) {
@@ -64,6 +69,7 @@ const deriveRequiredFeatureIds = (explicitRequestsArg: TStorageRequest[]): TStor
64
69
  if (explicitRequestsArg.some((requestArg) => requestArg.kind === 'objectStorage')) {
65
70
  required.push(storageFeatureIds.objectStorage);
66
71
  }
72
+ if (backupExclusionsArg.length > 0) required.push(storageFeatureIds.objectStorageBackupExclusions);
67
73
  if (explicitRequestsArg.some((requestArg) => (
68
74
  requestArg.kind === 'objectStorage' && requestArg.delivery.type === 'file'
69
75
  ))) {
@@ -89,8 +95,9 @@ export function normalizeAppStoreVolumes(
89
95
  *
90
96
  * The config's storage is judged by the interfaces contract first. Legacy `volumes` become
91
97
  * filesystem requests of the reserved `legacyFilesystem` class, `platformRequirements.s3` becomes
92
- * the `legacy-s3` object-storage request, and explicit requests fail closed unless every storage
93
- * feature they need is advertised by the target runtime.
98
+ * the `legacy-s3` object-storage request, `backupExclusions` become the declared non-backup key
99
+ * prefixes by request id, and explicit requests and backup exclusions fail closed unless every
100
+ * storage feature they need is advertised by the target runtime.
94
101
  */
95
102
  export function normalizeAppStoreStorageConfig(
96
103
  configArg: TCatalogVersionConfig,
@@ -104,7 +111,8 @@ export function normalizeAppStoreStorageConfig(
104
111
  const storageRequests: TStorageRequest[] = [...explicitRequests];
105
112
  const requestOrigins: Record<string, TAppStoreStorageRequestOrigin> = {};
106
113
  for (const request of explicitRequests) requestOrigins[request.id] = 'explicit';
107
- const requiredFeatureIds = deriveRequiredFeatureIds(explicitRequests);
114
+ const declaredExclusions = configArg.backupExclusions ?? [];
115
+ const requiredFeatureIds = deriveRequiredFeatureIds(explicitRequests, declaredExclusions);
108
116
 
109
117
  const legacyVolumes = normalizeVolumes(configArg.volumes);
110
118
  if (legacyVolumes.length > 0) {
@@ -165,6 +173,15 @@ export function normalizeAppStoreStorageConfig(
165
173
  };
166
174
  }
167
175
 
176
+ // The contract accepted every entry: it names an object-storage request of this normalized config once.
177
+ const backupExclusions: INormalizedAppStoreStorageConfig['backupExclusions'] = {};
178
+ for (const exclusion of declaredExclusions) {
179
+ backupExclusions[exclusion.storageRequest] = {
180
+ keyPrefixes: [...exclusion.keyPrefixes],
181
+ reason: exclusion.reason,
182
+ };
183
+ }
184
+
168
185
  const supportedFeatures = new Set(optionsArg.supportedFeatures);
169
186
  const unsupportedFeatures = requiredFeatureIds.filter((featureIdArg) => !supportedFeatures.has(featureIdArg));
170
187
  if (unsupportedFeatures.length > 0) {
@@ -176,6 +193,7 @@ export function normalizeAppStoreStorageConfig(
176
193
  storageRequests,
177
194
  requestOrigins,
178
195
  requiredFeatureIds,
196
+ backupExclusions,
179
197
  ...(legacyCompatibility ? { legacyCompatibility } : {}),
180
198
  };
181
199
  }
@@ -21,12 +21,27 @@ export interface IAppStoreStorageLegacyCompatibility {
21
21
  };
22
22
  }
23
23
 
24
+ /**
25
+ * The data a backup leaves out of one object-storage request: the key prefixes of a version's
26
+ * `backupExclusions` entry for that request and the operator-readable reason.
27
+ */
28
+ export type TAppStoreNormalizedBackupExclusion = Omit<
29
+ plugins.servezoneInterfaces.appstore.IAppStoreBackupExclusion,
30
+ 'storageRequest'
31
+ >;
32
+
24
33
  /** The canonical storage handoff a fulfillment adapter receives for one version config. */
25
34
  export interface INormalizedAppStoreStorageConfig {
26
35
  storageClasses: Record<string, plugins.servezoneInterfaces.appstore.IAppStoreStorageClass>;
27
36
  storageRequests: plugins.servezoneInterfaces.appstore.TAppStoreStorageRequest[];
28
37
  requestOrigins: Record<string, TAppStoreStorageRequestOrigin>;
29
38
  requiredFeatureIds: plugins.servezoneInterfaces.appstore.TAppStoreStorageFeatureId[];
39
+ /**
40
+ * Declared non-backup data by object-storage request id (`legacy-s3` for
41
+ * `platformRequirements.s3`), empty when the version declares none. A backup leaves the
42
+ * objects under these key prefixes out; a restore neither brings them back nor removes them.
43
+ */
44
+ backupExclusions: Record<string, TAppStoreNormalizedBackupExclusion>;
30
45
  legacyCompatibility?: IAppStoreStorageLegacyCompatibility;
31
46
  }
32
47