@serve.zone/interfaces 32.69.0 → 32.71.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.
- package/changelog.md +12 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/backup.d.ts +67 -1
- package/dist_ts/data/backup.js +16 -2
- package/dist_ts/data/backuprestorerefusal.d.ts +85 -0
- package/dist_ts/data/backuprestorerefusal.js +228 -0
- package/dist_ts/data/index.d.ts +1 -0
- package/dist_ts/data/index.js +2 -1
- package/dist_ts/requests/backup.d.ts +8 -0
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/backup.ts +69 -1
- package/ts/data/backuprestorerefusal.ts +324 -0
- package/ts/data/index.ts +1 -0
- package/ts/requests/backup.ts +8 -0
package/changelog.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026-11-05 - 32.71.0
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- All-or-nothing backup restore refusals (AGL-1501, Linus 2026-11-05 ruling): `data.backupRestoreRefusalNames` freezes the three reasons an in-place restore is refused before it writes anything: `restore-snapshot-unavailable` (`missing`, a non-empty list of `data.IBackupRestoreMissingSnapshot`: `snapshotId`, `capability` (`data.TBackupResourceType`) and an optional `resourceName`), `restore-preflight-unsupported` (the node's Corestore offers no restore check) and `restore-relay-contract-behind` (`relayInterfacesVersion`, a release version or `null`, below `minimumInterfacesVersion`). `data.TBackupRestoreRefusal` is discriminated by `name`; `data.validateBackupRestoreRefusal`, `data.isBackupRestoreRefusal` and `data.readBackupRestoreRefusal` judge untrusted `errorData` against each exact schema, and `data.buildBackupRestoreSnapshotUnavailableRefusal`, `data.buildBackupRestorePreflightUnsupportedRefusal` and `data.buildBackupRestoreRelayContractBehindRefusal` build them; bounds are `data.backupRestoreRefusalContract`. A restore event (`data.IBackupRestoreEvent`) adds the status `refused`, meaning nothing was written, with its `refusal`; `data.validateBackupRestoreEvent` judges only `status` and `refusal`, requiring `refusal` exactly when the status is `refused`. Additive: every existing restore event stays valid, including the unjudged `resourceTypes` and null `errorText` Cloudly already stores. Spec: `test.backuprestorerefusal`.
|
|
8
|
+
|
|
9
|
+
## 2026-11-05 - 32.70.0
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
- Partial restore of an incomplete backup (Linus 2026-11-05 ruling): a `failed` backup record can name its failure's kind, `data.IBackupRecord.failure` (`data.TBackupFailure`): `incomplete` with `uncaptured`, each resource its service declares that the backup did not capture (`data.TBackupUncapturedResource`: a volume by mount path, always with reason `volume-capture-unavailable`, or a binding by id, always with reason `binding-not-snapshotted`; `data.TBackupUncapturedReason` names either), or `archive-unreadable`. `requests.backup.IReq_Any_Cloudly_RestoreServiceBackup` takes `confirmPartialRestore`, which an administrator sets to restore what an `incomplete` backup captured: `resourceTypes` is required and names only captured types, nothing in `uncaptured` is restored, so the service then holds data from two points in time, and the backup stays `failed`; it is refused for any other backup. `data.readPartialRestoreResourceTypes` answers the types such a restore may name: those the backup holds a snapshot of, except `volume` (a `volume` snapshot captures no service volume) and the type of any uncaptured resource; none for any other backup. A restore event (`data.IBackupRestoreEvent`) states the `resourceTypes` it was limited to and `partial` for such a restore. Additive: no validator reads these shapes. Spec: `test.backuppartialrestore`.
|
|
14
|
+
|
|
3
15
|
## 2026-11-05 - 32.69.0
|
|
4
16
|
|
|
5
17
|
### Features
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@serve.zone/interfaces',
|
|
6
|
-
version: '32.
|
|
6
|
+
version: '32.71.0',
|
|
7
7
|
description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
|
|
8
8
|
};
|
|
9
9
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSx3QkFBd0I7SUFDOUIsT0FBTyxFQUFFLFNBQVM7SUFDbEIsV0FBVyxFQUFFLHVGQUF1RjtDQUNyRyxDQUFBIn0=
|
package/dist_ts/data/backup.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TBackupRestoreRefusal } from './backuprestorerefusal.js';
|
|
1
2
|
import type { ICorestoreDatabaseAllocationReference } from './corestore.js';
|
|
2
3
|
export type TBackupStatus = 'pending' | 'running' | 'replicating' | 'replicated' | 'ready' | 'failed' | 'restoring' | 'restored';
|
|
3
4
|
export type TBackupTrigger = 'manual' | 'scheduled';
|
|
@@ -48,10 +49,62 @@ export interface IBackupReplicationResult {
|
|
|
48
49
|
totalSize: number;
|
|
49
50
|
completedAt: number;
|
|
50
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* One resource a service declares that a backup of it did not capture, with why; each kind of
|
|
54
|
+
* resource has its own reason.
|
|
55
|
+
*
|
|
56
|
+
* `volume-capture-unavailable`: a service volume is a node-local storage claim on its Pallet node,
|
|
57
|
+
* which no relay can capture yet; a `volume` snapshot is a Corestore Docker volume and captures no
|
|
58
|
+
* service volume. `binding-not-snapshotted`: the backup holds no snapshot of the Corestore resource
|
|
59
|
+
* the binding names.
|
|
60
|
+
*/
|
|
61
|
+
export type TBackupUncapturedResource = {
|
|
62
|
+
type: 'volume';
|
|
63
|
+
mountPath: string;
|
|
64
|
+
reason: 'volume-capture-unavailable';
|
|
65
|
+
} | {
|
|
66
|
+
type: 'database' | 'objectstorage';
|
|
67
|
+
bindingId: string;
|
|
68
|
+
reason: 'binding-not-snapshotted';
|
|
69
|
+
};
|
|
70
|
+
/** Why a resource a service declares is not in one of its backups (`TBackupUncapturedResource`). */
|
|
71
|
+
export type TBackupUncapturedReason = TBackupUncapturedResource['reason'];
|
|
72
|
+
/**
|
|
73
|
+
* The named kind of a `failed` backup's failure; a failure without one is named only in `errorText`.
|
|
74
|
+
*
|
|
75
|
+
* `incomplete`: the backup did not capture every resource its service declares, and `uncaptured`
|
|
76
|
+
* names each one it missed. What it did capture is restored only by a partial restore an
|
|
77
|
+
* administrator confirms (`IReq_Any_Cloudly_RestoreServiceBackup` `confirmPartialRestore`), which
|
|
78
|
+
* restores none of `uncaptured`, and the backup stays `failed`.
|
|
79
|
+
* `archive-unreadable`: its stored archive is missing or damaged, so it restores nothing.
|
|
80
|
+
*/
|
|
81
|
+
export type TBackupFailure = {
|
|
82
|
+
kind: 'incomplete';
|
|
83
|
+
uncaptured: TBackupUncapturedResource[];
|
|
84
|
+
} | {
|
|
85
|
+
kind: 'archive-unreadable';
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* One restore of a backup, appended to its `restoreHistory`; `validateBackupRestoreEvent` judges its
|
|
89
|
+
* `status` and `refusal`.
|
|
90
|
+
*
|
|
91
|
+
* `refused`: the restore was refused before it wrote anything (`refusal` says why), so the service
|
|
92
|
+
* keeps the data it had. `failed`: it failed after it began writing.
|
|
93
|
+
*/
|
|
51
94
|
export interface IBackupRestoreEvent {
|
|
95
|
+
/** When the restore ended, or when it was refused. */
|
|
52
96
|
restoredAt: number;
|
|
53
|
-
status: 'restored' | 'failed';
|
|
97
|
+
status: 'restored' | 'failed' | 'refused';
|
|
54
98
|
errorText?: string;
|
|
99
|
+
/** The resource types the restore was limited to; absent when it restored every snapshot. */
|
|
100
|
+
resourceTypes?: TBackupResourceType[];
|
|
101
|
+
/**
|
|
102
|
+
* A confirmed partial restore of an `incomplete` backup: it restored only `resourceTypes`, and
|
|
103
|
+
* none of the backup's `failure.uncaptured`.
|
|
104
|
+
*/
|
|
105
|
+
partial?: true;
|
|
106
|
+
/** Why a `refused` restore wrote nothing; present exactly when `status` is `refused`. */
|
|
107
|
+
refusal?: TBackupRestoreRefusal;
|
|
55
108
|
}
|
|
56
109
|
/**
|
|
57
110
|
* Durable retention hold owned by one isolated restore. An entry is active
|
|
@@ -88,6 +141,8 @@ export interface IBackupRecord {
|
|
|
88
141
|
completedAt?: number;
|
|
89
142
|
requestedBy?: string;
|
|
90
143
|
errorText?: string;
|
|
144
|
+
/** The named kind of a `failed` backup's failure (`TBackupFailure`). */
|
|
145
|
+
failure?: TBackupFailure;
|
|
91
146
|
restoreHistory?: IBackupRestoreEvent[];
|
|
92
147
|
/**
|
|
93
148
|
* Authoritative isolated-restore holds. Cloudly atomically appends the
|
|
@@ -97,6 +152,17 @@ export interface IBackupRecord {
|
|
|
97
152
|
restoreHolds?: IBackupRestoreHold[];
|
|
98
153
|
tags?: Record<string, string>;
|
|
99
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* The resource types a partial restore of a backup may name (`confirmPartialRestore`).
|
|
157
|
+
*
|
|
158
|
+
* For a `failed` backup whose failure is `incomplete`: every type it holds a snapshot of, except
|
|
159
|
+
* `volume` (a `volume` snapshot is a Corestore Docker volume and captures no service volume) and the
|
|
160
|
+
* type of any resource it did not capture, so a partial restore never restores a snapshot standing
|
|
161
|
+
* for a resource the backup missed. For every other backup: none. Database before object storage.
|
|
162
|
+
*/
|
|
163
|
+
export declare const readPartialRestoreResourceTypes: (backupArg: Pick<IBackupRecord, 'status' | 'failure'> & {
|
|
164
|
+
snapshots: ReadonlyArray<Pick<TBackupSnapshot, 'type'>>;
|
|
165
|
+
}) => Array<Exclude<TBackupResourceType, 'volume'>>;
|
|
100
166
|
/** Retention policy understood by containerarchive prune. */
|
|
101
167
|
export interface IArchiveRetentionPolicy {
|
|
102
168
|
keepLast?: number;
|
package/dist_ts/data/backup.js
CHANGED
|
@@ -1,2 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The resource types a partial restore of a backup may name (`confirmPartialRestore`).
|
|
3
|
+
*
|
|
4
|
+
* For a `failed` backup whose failure is `incomplete`: every type it holds a snapshot of, except
|
|
5
|
+
* `volume` (a `volume` snapshot is a Corestore Docker volume and captures no service volume) and the
|
|
6
|
+
* type of any resource it did not capture, so a partial restore never restores a snapshot standing
|
|
7
|
+
* for a resource the backup missed. For every other backup: none. Database before object storage.
|
|
8
|
+
*/
|
|
9
|
+
export const readPartialRestoreResourceTypes = (backupArg) => {
|
|
10
|
+
const failure = backupArg.failure;
|
|
11
|
+
if (backupArg.status !== 'failed' || failure?.kind !== 'incomplete')
|
|
12
|
+
return [];
|
|
13
|
+
const uncapturedTypes = new Set(failure.uncaptured.map((resourceArg) => resourceArg.type));
|
|
14
|
+
return ['database', 'objectstorage'].filter((typeArg) => (!uncapturedTypes.has(typeArg) && backupArg.snapshots.some((snapshotArg) => snapshotArg.type === typeArg)));
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYmFja3VwLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvZGF0YS9iYWNrdXAudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBK0tBOzs7Ozs7O0dBT0c7QUFDSCxNQUFNLENBQUMsTUFBTSwrQkFBK0IsR0FBRyxDQUM3QyxTQUFrSCxFQUNuRSxFQUFFO0lBQ2pELE1BQU0sT0FBTyxHQUFHLFNBQVMsQ0FBQyxPQUFPLENBQUM7SUFDbEMsSUFBSSxTQUFTLENBQUMsTUFBTSxLQUFLLFFBQVEsSUFBSSxPQUFPLEVBQUUsSUFBSSxLQUFLLFlBQVk7UUFBRSxPQUFPLEVBQUUsQ0FBQztJQUMvRSxNQUFNLGVBQWUsR0FBRyxJQUFJLEdBQUcsQ0FBc0IsT0FBTyxDQUFDLFVBQVUsQ0FBQyxHQUFHLENBQUMsQ0FBQyxXQUFXLEVBQUUsRUFBRSxDQUFDLFdBQVcsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO0lBQ2hILE9BQVEsQ0FBQyxVQUFVLEVBQUUsZUFBZSxDQUFXLENBQUMsTUFBTSxDQUFDLENBQUMsT0FBTyxFQUFFLEVBQUUsQ0FBQyxDQUNsRSxDQUFDLGVBQWUsQ0FBQyxHQUFHLENBQUMsT0FBTyxDQUFDLElBQUksU0FBUyxDQUFDLFNBQVMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxXQUFXLEVBQUUsRUFBRSxDQUFDLFdBQVcsQ0FBQyxJQUFJLEtBQUssT0FBTyxDQUFDLENBQ3pHLENBQUMsQ0FBQztBQUNMLENBQUMsQ0FBQyJ9
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { TBackupResourceType } from './backup.js';
|
|
2
|
+
/**
|
|
3
|
+
* Why an in-place restore of a backup (`IReq_Any_Cloudly_RestoreServiceBackup`) was refused before
|
|
4
|
+
* it wrote anything. A restore is all-or-nothing: Corestore checks that it can read every snapshot
|
|
5
|
+
* the restore names before it replaces any data, and Cloudly dispatches the restore only to a relay
|
|
6
|
+
* whose contract carries that check. A refusal therefore always means nothing was written; the
|
|
7
|
+
* service keeps the data it had.
|
|
8
|
+
*
|
|
9
|
+
* - `restore-snapshot-unavailable`: the node's Corestore cannot read one or more snapshots the
|
|
10
|
+
* restore names; `missing` names each of them.
|
|
11
|
+
* - `restore-preflight-unsupported`: the node's Corestore does not offer the restore check (a
|
|
12
|
+
* Corestore built before it answers the check route with HTTP 404), so it cannot prove the
|
|
13
|
+
* restore would be whole and nothing is restored.
|
|
14
|
+
* - `restore-relay-contract-behind`: the cluster relay speaks an interfaces release below the one
|
|
15
|
+
* that carries the restore check, so Cloudly does not dispatch the restore; the relay states
|
|
16
|
+
* `relayInterfacesVersion` (`null` when it stated none) and the restore needs
|
|
17
|
+
* `minimumInterfacesVersion`.
|
|
18
|
+
*
|
|
19
|
+
* It travels as `TypedResponseError.errorData` of a refused restore and is recorded as the
|
|
20
|
+
* `refusal` of the backup's `refused` restore event (`IBackupRestoreEvent`). Each refusal is
|
|
21
|
+
* exactly its schema: `readBackupRestoreRefusal` refuses any other member, so a producer never adds
|
|
22
|
+
* one without gating it on the reader's interfaces version.
|
|
23
|
+
*/
|
|
24
|
+
export declare const backupRestoreRefusalNames: readonly ["restore-snapshot-unavailable", "restore-preflight-unsupported", "restore-relay-contract-behind"];
|
|
25
|
+
export type TBackupRestoreRefusalName = typeof backupRestoreRefusalNames[number];
|
|
26
|
+
export declare const backupRestoreRefusalContract: Readonly<{
|
|
27
|
+
/** At most this many snapshots are named in one `restore-snapshot-unavailable`. */
|
|
28
|
+
readonly maximumMissingSnapshots: 256;
|
|
29
|
+
/** A snapshot id or resource name is printable ASCII without spaces, at most this long. */
|
|
30
|
+
readonly maximumNameLength: 255;
|
|
31
|
+
}>;
|
|
32
|
+
/** One snapshot a restore names that the node's Corestore cannot read. */
|
|
33
|
+
export interface IBackupRestoreMissingSnapshot {
|
|
34
|
+
snapshotId: string;
|
|
35
|
+
/** The snapshot's resource type (`TBackupSnapshot['type']`). */
|
|
36
|
+
capability: TBackupResourceType;
|
|
37
|
+
/** The Corestore resource of a database or object-storage snapshot, when it is known. */
|
|
38
|
+
resourceName?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface IBackupRestoreSnapshotUnavailableRefusal {
|
|
41
|
+
name: 'restore-snapshot-unavailable';
|
|
42
|
+
/** Non-empty; each snapshot once. */
|
|
43
|
+
missing: IBackupRestoreMissingSnapshot[];
|
|
44
|
+
}
|
|
45
|
+
export interface IBackupRestorePreflightUnsupportedRefusal {
|
|
46
|
+
name: 'restore-preflight-unsupported';
|
|
47
|
+
}
|
|
48
|
+
export interface IBackupRestoreRelayContractBehindRefusal {
|
|
49
|
+
name: 'restore-relay-contract-behind';
|
|
50
|
+
/** The interfaces release the relay speaks, or `null` when it stated none. */
|
|
51
|
+
relayInterfacesVersion: string | null;
|
|
52
|
+
/** The lowest interfaces release a relay must speak to carry a restore. */
|
|
53
|
+
minimumInterfacesVersion: string;
|
|
54
|
+
}
|
|
55
|
+
/** A refused restore, discriminated by `name` (`backupRestoreRefusalNames`). */
|
|
56
|
+
export type TBackupRestoreRefusal = IBackupRestoreSnapshotUnavailableRefusal | IBackupRestorePreflightUnsupportedRefusal | IBackupRestoreRelayContractBehindRefusal;
|
|
57
|
+
/** Validates one restore refusal against its exact schema and names every reason it is refused. */
|
|
58
|
+
export declare const validateBackupRestoreRefusal: (valueArg: unknown, pathArg?: string) => string[];
|
|
59
|
+
/** Whether a value is exactly one restore refusal. */
|
|
60
|
+
export declare const isBackupRestoreRefusal: (valueArg: unknown) => valueArg is TBackupRestoreRefusal;
|
|
61
|
+
/**
|
|
62
|
+
* The restore refusal inside an error payload or a stored restore event, as an inert frozen copy,
|
|
63
|
+
* or `null` when the value is not exactly one. It reads own data properties only, so a refusal
|
|
64
|
+
* read here carries nothing that runs.
|
|
65
|
+
*/
|
|
66
|
+
export declare const readBackupRestoreRefusal: (valueArg: unknown) => TBackupRestoreRefusal | null;
|
|
67
|
+
/** `restore-snapshot-unavailable` naming each snapshot the node's Corestore cannot read. */
|
|
68
|
+
export declare const buildBackupRestoreSnapshotUnavailableRefusal: (missingArg: IBackupRestoreMissingSnapshot[]) => IBackupRestoreSnapshotUnavailableRefusal;
|
|
69
|
+
/** `restore-preflight-unsupported`: the node's Corestore offers no restore check. */
|
|
70
|
+
export declare const buildBackupRestorePreflightUnsupportedRefusal: () => IBackupRestorePreflightUnsupportedRefusal;
|
|
71
|
+
/**
|
|
72
|
+
* `restore-relay-contract-behind`: the relay speaks an interfaces release below the minimum.
|
|
73
|
+
*
|
|
74
|
+
* Throws when a stated version is not a release version or is not below the minimum, so a caller
|
|
75
|
+
* passes `null` for a relay whose stated version it cannot read.
|
|
76
|
+
*/
|
|
77
|
+
export declare const buildBackupRestoreRelayContractBehindRefusal: (relayInterfacesVersionArg: string | null, minimumInterfacesVersionArg: string) => IBackupRestoreRelayContractBehindRefusal;
|
|
78
|
+
/**
|
|
79
|
+
* Validates what this contract adds to a restore event of a backup's `restoreHistory` and names
|
|
80
|
+
* every reason it is refused: `status` is `restored`, `failed` or `refused`, and `refusal` is
|
|
81
|
+
* present exactly when `status` is `refused`, as one restore refusal. Every other member is left as
|
|
82
|
+
* it was written (records Cloudly already stores keep any `restoredAt`, `errorText`, `partial` and
|
|
83
|
+
* `resourceTypes` they hold), so every stored event stays valid.
|
|
84
|
+
*/
|
|
85
|
+
export declare const validateBackupRestoreEvent: (valueArg: unknown, pathArg?: string) => string[];
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { hasExactKeys, readDataElements, readDataProperties, readExactDataRecord, } from '../private/dataproperties.js';
|
|
2
|
+
import { compareReleaseVersions, parseReleaseVersion } from './releaseversion.js';
|
|
3
|
+
/**
|
|
4
|
+
* Why an in-place restore of a backup (`IReq_Any_Cloudly_RestoreServiceBackup`) was refused before
|
|
5
|
+
* it wrote anything. A restore is all-or-nothing: Corestore checks that it can read every snapshot
|
|
6
|
+
* the restore names before it replaces any data, and Cloudly dispatches the restore only to a relay
|
|
7
|
+
* whose contract carries that check. A refusal therefore always means nothing was written; the
|
|
8
|
+
* service keeps the data it had.
|
|
9
|
+
*
|
|
10
|
+
* - `restore-snapshot-unavailable`: the node's Corestore cannot read one or more snapshots the
|
|
11
|
+
* restore names; `missing` names each of them.
|
|
12
|
+
* - `restore-preflight-unsupported`: the node's Corestore does not offer the restore check (a
|
|
13
|
+
* Corestore built before it answers the check route with HTTP 404), so it cannot prove the
|
|
14
|
+
* restore would be whole and nothing is restored.
|
|
15
|
+
* - `restore-relay-contract-behind`: the cluster relay speaks an interfaces release below the one
|
|
16
|
+
* that carries the restore check, so Cloudly does not dispatch the restore; the relay states
|
|
17
|
+
* `relayInterfacesVersion` (`null` when it stated none) and the restore needs
|
|
18
|
+
* `minimumInterfacesVersion`.
|
|
19
|
+
*
|
|
20
|
+
* It travels as `TypedResponseError.errorData` of a refused restore and is recorded as the
|
|
21
|
+
* `refusal` of the backup's `refused` restore event (`IBackupRestoreEvent`). Each refusal is
|
|
22
|
+
* exactly its schema: `readBackupRestoreRefusal` refuses any other member, so a producer never adds
|
|
23
|
+
* one without gating it on the reader's interfaces version.
|
|
24
|
+
*/
|
|
25
|
+
export const backupRestoreRefusalNames = Object.freeze([
|
|
26
|
+
'restore-snapshot-unavailable',
|
|
27
|
+
'restore-preflight-unsupported',
|
|
28
|
+
'restore-relay-contract-behind',
|
|
29
|
+
]);
|
|
30
|
+
export const backupRestoreRefusalContract = Object.freeze({
|
|
31
|
+
/** At most this many snapshots are named in one `restore-snapshot-unavailable`. */
|
|
32
|
+
maximumMissingSnapshots: 256,
|
|
33
|
+
/** A snapshot id or resource name is printable ASCII without spaces, at most this long. */
|
|
34
|
+
maximumNameLength: 255,
|
|
35
|
+
});
|
|
36
|
+
/** Every resource type, keyed so a new `TBackupResourceType` member fails to compile here. */
|
|
37
|
+
const backupResourceTypeKeys = Object.freeze({
|
|
38
|
+
volume: true,
|
|
39
|
+
database: true,
|
|
40
|
+
objectstorage: true,
|
|
41
|
+
});
|
|
42
|
+
const isBackupResourceType = (valueArg) => (typeof valueArg === 'string' && Object.hasOwn(backupResourceTypeKeys, valueArg));
|
|
43
|
+
const printableNamePattern = /^[\x21-\x7e]+$/;
|
|
44
|
+
const isRestoreName = (valueArg) => (typeof valueArg === 'string'
|
|
45
|
+
&& valueArg.length <= backupRestoreRefusalContract.maximumNameLength
|
|
46
|
+
&& printableNamePattern.test(valueArg));
|
|
47
|
+
const nameRule = 'must be printable ASCII without spaces, at most '
|
|
48
|
+
+ `${backupRestoreRefusalContract.maximumNameLength} characters`;
|
|
49
|
+
const isRefusalName = (valueArg) => (typeof valueArg === 'string'
|
|
50
|
+
&& backupRestoreRefusalNames.includes(valueArg));
|
|
51
|
+
const validateMissingSnapshot = (valueArg, pathArg) => {
|
|
52
|
+
const snapshot = readDataProperties(valueArg);
|
|
53
|
+
if (snapshot === null || !hasExactKeys(snapshot, ['snapshotId', 'capability'], ['resourceName'])) {
|
|
54
|
+
return [`${pathArg} must use its exact schema`];
|
|
55
|
+
}
|
|
56
|
+
const errors = [];
|
|
57
|
+
if (!isRestoreName(snapshot.snapshotId)) {
|
|
58
|
+
errors.push(`${pathArg}.snapshotId ${nameRule}`);
|
|
59
|
+
}
|
|
60
|
+
if (!isBackupResourceType(snapshot.capability)) {
|
|
61
|
+
errors.push(`${pathArg}.capability must be volume, database or objectstorage`);
|
|
62
|
+
}
|
|
63
|
+
if (Object.hasOwn(snapshot, 'resourceName') && !isRestoreName(snapshot.resourceName)) {
|
|
64
|
+
errors.push(`${pathArg}.resourceName ${nameRule}`);
|
|
65
|
+
}
|
|
66
|
+
return errors;
|
|
67
|
+
};
|
|
68
|
+
const validateMissingSnapshots = (valueArg, pathArg) => {
|
|
69
|
+
const elements = readDataElements(valueArg, backupRestoreRefusalContract.maximumMissingSnapshots);
|
|
70
|
+
if (elements === null) {
|
|
71
|
+
return [`${pathArg} must be a list of at most `
|
|
72
|
+
+ `${backupRestoreRefusalContract.maximumMissingSnapshots} snapshots`];
|
|
73
|
+
}
|
|
74
|
+
if (elements.length === 0) {
|
|
75
|
+
return [`${pathArg} must name at least one snapshot`];
|
|
76
|
+
}
|
|
77
|
+
const errors = elements.flatMap((elementArg, indexArg) => (validateMissingSnapshot(elementArg, `${pathArg}[${indexArg}]`)));
|
|
78
|
+
if (errors.length > 0) {
|
|
79
|
+
return errors;
|
|
80
|
+
}
|
|
81
|
+
const snapshotIds = elements.map((elementArg) => elementArg.snapshotId);
|
|
82
|
+
if (new Set(snapshotIds).size !== snapshotIds.length) {
|
|
83
|
+
errors.push(`${pathArg} must name each snapshot once`);
|
|
84
|
+
}
|
|
85
|
+
return errors;
|
|
86
|
+
};
|
|
87
|
+
const validateRelayContractBehind = (refusalArg, pathArg) => {
|
|
88
|
+
const relay = refusalArg.relayInterfacesVersion === null
|
|
89
|
+
? null
|
|
90
|
+
: parseReleaseVersion(refusalArg.relayInterfacesVersion);
|
|
91
|
+
const minimum = parseReleaseVersion(refusalArg.minimumInterfacesVersion);
|
|
92
|
+
const errors = [];
|
|
93
|
+
if (refusalArg.relayInterfacesVersion !== null && relay === null) {
|
|
94
|
+
errors.push(`${pathArg} relayInterfacesVersion must be a release version or null`);
|
|
95
|
+
}
|
|
96
|
+
if (minimum === null) {
|
|
97
|
+
errors.push(`${pathArg} minimumInterfacesVersion must be a release version`);
|
|
98
|
+
}
|
|
99
|
+
if (relay !== null && minimum !== null && compareReleaseVersions(relay, minimum) >= 0) {
|
|
100
|
+
errors.push(`${pathArg} relayInterfacesVersion must be below minimumInterfacesVersion`);
|
|
101
|
+
}
|
|
102
|
+
return errors;
|
|
103
|
+
};
|
|
104
|
+
/** The exact keys of each refusal. */
|
|
105
|
+
const refusalKeys = Object.freeze({
|
|
106
|
+
'restore-snapshot-unavailable': Object.freeze(['name', 'missing']),
|
|
107
|
+
'restore-preflight-unsupported': Object.freeze(['name']),
|
|
108
|
+
'restore-relay-contract-behind': Object.freeze([
|
|
109
|
+
'name', 'relayInterfacesVersion', 'minimumInterfacesVersion',
|
|
110
|
+
]),
|
|
111
|
+
});
|
|
112
|
+
/** Validates one restore refusal against its exact schema and names every reason it is refused. */
|
|
113
|
+
export const validateBackupRestoreRefusal = (valueArg, pathArg = 'backup restore refusal') => {
|
|
114
|
+
const properties = readDataProperties(valueArg);
|
|
115
|
+
if (properties === null || !isRefusalName(properties.name)) {
|
|
116
|
+
return [`${pathArg} name must be one of ${backupRestoreRefusalNames.join(', ')}`];
|
|
117
|
+
}
|
|
118
|
+
const refusal = readExactDataRecord(valueArg, refusalKeys[properties.name]);
|
|
119
|
+
if (refusal === null) {
|
|
120
|
+
return [`${pathArg} must use the exact schema of ${properties.name}`];
|
|
121
|
+
}
|
|
122
|
+
switch (properties.name) {
|
|
123
|
+
case 'restore-snapshot-unavailable':
|
|
124
|
+
return validateMissingSnapshots(refusal.missing, `${pathArg} missing`);
|
|
125
|
+
case 'restore-preflight-unsupported':
|
|
126
|
+
return [];
|
|
127
|
+
case 'restore-relay-contract-behind':
|
|
128
|
+
return validateRelayContractBehind(refusal, pathArg);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
/** Whether a value is exactly one restore refusal. */
|
|
132
|
+
export const isBackupRestoreRefusal = (valueArg) => (validateBackupRestoreRefusal(valueArg).length === 0);
|
|
133
|
+
const freezeMissingSnapshot = (snapshotArg) => Object.freeze({
|
|
134
|
+
snapshotId: snapshotArg.snapshotId,
|
|
135
|
+
capability: snapshotArg.capability,
|
|
136
|
+
...(snapshotArg.resourceName === undefined ? {} : { resourceName: snapshotArg.resourceName }),
|
|
137
|
+
});
|
|
138
|
+
const copySnapshotUnavailableRefusal = (refusalArg) => {
|
|
139
|
+
const missing = refusalArg.missing.map(freezeMissingSnapshot);
|
|
140
|
+
Object.freeze(missing);
|
|
141
|
+
return Object.freeze({ name: refusalArg.name, missing });
|
|
142
|
+
};
|
|
143
|
+
const copyPreflightUnsupportedRefusal = (refusalArg) => Object.freeze({ name: refusalArg.name });
|
|
144
|
+
const copyRelayContractBehindRefusal = (refusalArg) => Object.freeze({
|
|
145
|
+
name: refusalArg.name,
|
|
146
|
+
relayInterfacesVersion: refusalArg.relayInterfacesVersion,
|
|
147
|
+
minimumInterfacesVersion: refusalArg.minimumInterfacesVersion,
|
|
148
|
+
});
|
|
149
|
+
/** An inert frozen copy of a refusal that passed `validateBackupRestoreRefusal`. */
|
|
150
|
+
const copyRefusal = (refusalArg) => {
|
|
151
|
+
switch (refusalArg.name) {
|
|
152
|
+
case 'restore-snapshot-unavailable':
|
|
153
|
+
return copySnapshotUnavailableRefusal(refusalArg);
|
|
154
|
+
case 'restore-preflight-unsupported':
|
|
155
|
+
return copyPreflightUnsupportedRefusal(refusalArg);
|
|
156
|
+
case 'restore-relay-contract-behind':
|
|
157
|
+
return copyRelayContractBehindRefusal(refusalArg);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* The restore refusal inside an error payload or a stored restore event, as an inert frozen copy,
|
|
162
|
+
* or `null` when the value is not exactly one. It reads own data properties only, so a refusal
|
|
163
|
+
* read here carries nothing that runs.
|
|
164
|
+
*/
|
|
165
|
+
export const readBackupRestoreRefusal = (valueArg) => (
|
|
166
|
+
// A value that passed holds own data properties of exactly the refusal's schema at every level,
|
|
167
|
+
// so the copy reads the same data the validation judged.
|
|
168
|
+
isBackupRestoreRefusal(valueArg) ? copyRefusal(valueArg) : null);
|
|
169
|
+
/** The refusal a builder composed, unchanged, or an error naming every reason it is refused. */
|
|
170
|
+
const requireRefusal = (refusalArg) => {
|
|
171
|
+
const errors = validateBackupRestoreRefusal(refusalArg);
|
|
172
|
+
if (errors.length > 0) {
|
|
173
|
+
throw new Error(errors.join('; '));
|
|
174
|
+
}
|
|
175
|
+
return refusalArg;
|
|
176
|
+
};
|
|
177
|
+
/** `restore-snapshot-unavailable` naming each snapshot the node's Corestore cannot read. */
|
|
178
|
+
export const buildBackupRestoreSnapshotUnavailableRefusal = (missingArg) => copySnapshotUnavailableRefusal(requireRefusal({
|
|
179
|
+
name: 'restore-snapshot-unavailable',
|
|
180
|
+
missing: missingArg.map((snapshotArg) => ({ ...snapshotArg })),
|
|
181
|
+
}));
|
|
182
|
+
/** `restore-preflight-unsupported`: the node's Corestore offers no restore check. */
|
|
183
|
+
export const buildBackupRestorePreflightUnsupportedRefusal = () => (copyPreflightUnsupportedRefusal(requireRefusal({ name: 'restore-preflight-unsupported' })));
|
|
184
|
+
/**
|
|
185
|
+
* `restore-relay-contract-behind`: the relay speaks an interfaces release below the minimum.
|
|
186
|
+
*
|
|
187
|
+
* Throws when a stated version is not a release version or is not below the minimum, so a caller
|
|
188
|
+
* passes `null` for a relay whose stated version it cannot read.
|
|
189
|
+
*/
|
|
190
|
+
export const buildBackupRestoreRelayContractBehindRefusal = (relayInterfacesVersionArg, minimumInterfacesVersionArg) => copyRelayContractBehindRefusal(requireRefusal({
|
|
191
|
+
name: 'restore-relay-contract-behind',
|
|
192
|
+
relayInterfacesVersion: relayInterfacesVersionArg,
|
|
193
|
+
minimumInterfacesVersion: minimumInterfacesVersionArg,
|
|
194
|
+
}));
|
|
195
|
+
const restoreEventStatuses = Object.freeze([
|
|
196
|
+
'restored', 'failed', 'refused',
|
|
197
|
+
]);
|
|
198
|
+
/**
|
|
199
|
+
* Validates what this contract adds to a restore event of a backup's `restoreHistory` and names
|
|
200
|
+
* every reason it is refused: `status` is `restored`, `failed` or `refused`, and `refusal` is
|
|
201
|
+
* present exactly when `status` is `refused`, as one restore refusal. Every other member is left as
|
|
202
|
+
* it was written (records Cloudly already stores keep any `restoredAt`, `errorText`, `partial` and
|
|
203
|
+
* `resourceTypes` they hold), so every stored event stays valid.
|
|
204
|
+
*/
|
|
205
|
+
export const validateBackupRestoreEvent = (valueArg, pathArg = 'backup restore event') => {
|
|
206
|
+
const event = readDataProperties(valueArg);
|
|
207
|
+
if (event === null) {
|
|
208
|
+
return [`${pathArg} must be a plain record of data properties`];
|
|
209
|
+
}
|
|
210
|
+
const errors = [];
|
|
211
|
+
if (!restoreEventStatuses.includes(event.status)) {
|
|
212
|
+
errors.push(`${pathArg} status must be one of ${restoreEventStatuses.join(', ')}`);
|
|
213
|
+
}
|
|
214
|
+
const hasRefusal = Object.hasOwn(event, 'refusal');
|
|
215
|
+
if (event.status === 'refused') {
|
|
216
|
+
if (!hasRefusal) {
|
|
217
|
+
errors.push(`${pathArg} refusal is required when status is refused`);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
errors.push(...validateBackupRestoreRefusal(event.refusal, `${pathArg} refusal`));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else if (hasRefusal) {
|
|
224
|
+
errors.push(`${pathArg} refusal is allowed only when status is refused`);
|
|
225
|
+
}
|
|
226
|
+
return errors;
|
|
227
|
+
};
|
|
228
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYmFja3VwcmVzdG9yZXJlZnVzYWwuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL2JhY2t1cHJlc3RvcmVyZWZ1c2FsLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBLE9BQU8sRUFDTCxZQUFZLEVBQUUsZ0JBQWdCLEVBQUUsa0JBQWtCLEVBQUUsbUJBQW1CLEdBQ3hFLE1BQU0sOEJBQThCLENBQUM7QUFDdEMsT0FBTyxFQUFFLHNCQUFzQixFQUFFLG1CQUFtQixFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFFbEY7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQXFCRztBQUNILE1BQU0sQ0FBQyxNQUFNLHlCQUF5QixHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDckQsOEJBQThCO0lBQzlCLCtCQUErQjtJQUMvQiwrQkFBK0I7Q0FDdkIsQ0FBQyxDQUFDO0FBSVosTUFBTSxDQUFDLE1BQU0sNEJBQTRCLEdBQUcsTUFBTSxDQUFDLE1BQU0sQ0FBQztJQUN4RCxtRkFBbUY7SUFDbkYsdUJBQXVCLEVBQUUsR0FBRztJQUM1QiwyRkFBMkY7SUFDM0YsaUJBQWlCLEVBQUUsR0FBRztDQUNkLENBQUMsQ0FBQztBQW1DWiw4RkFBOEY7QUFDOUYsTUFBTSxzQkFBc0IsR0FBZ0QsTUFBTSxDQUFDLE1BQU0sQ0FBQztJQUN4RixNQUFNLEVBQUUsSUFBSTtJQUNaLFFBQVEsRUFBRSxJQUFJO0lBQ2QsYUFBYSxFQUFFLElBQUk7Q0FDcEIsQ0FBQyxDQUFDO0FBRUgsTUFBTSxvQkFBb0IsR0FBRyxDQUFDLFFBQWlCLEVBQW1DLEVBQUUsQ0FBQyxDQUNuRixPQUFPLFFBQVEsS0FBSyxRQUFRLElBQUksTUFBTSxDQUFDLE1BQU0sQ0FBQyxzQkFBc0IsRUFBRSxRQUFRLENBQUMsQ0FDaEYsQ0FBQztBQUVGLE1BQU0sb0JBQW9CLEdBQUcsZ0JBQWdCLENBQUM7QUFFOUMsTUFBTSxhQUFhLEdBQUcsQ0FBQyxRQUFpQixFQUFzQixFQUFFLENBQUMs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
|
package/dist_ts/data/index.d.ts
CHANGED
|
@@ -75,6 +75,7 @@ export * from './secret.js';
|
|
|
75
75
|
export * from './baremetal.js';
|
|
76
76
|
export * from './baseos.js';
|
|
77
77
|
export * from './backup.js';
|
|
78
|
+
export * from './backuprestorerefusal.js';
|
|
78
79
|
export * from './clusternode.js';
|
|
79
80
|
export * from './clusternodehostname.js';
|
|
80
81
|
export * from './settings.js';
|
package/dist_ts/data/index.js
CHANGED
|
@@ -70,6 +70,7 @@ export * from './secret.js';
|
|
|
70
70
|
export * from './baremetal.js';
|
|
71
71
|
export * from './baseos.js';
|
|
72
72
|
export * from './backup.js';
|
|
73
|
+
export * from './backuprestorerefusal.js';
|
|
73
74
|
export * from './clusternode.js';
|
|
74
75
|
export * from './clusternodehostname.js';
|
|
75
76
|
export * from './settings.js';
|
|
@@ -91,4 +92,4 @@ export * from './user.js';
|
|
|
91
92
|
export * from './version.js';
|
|
92
93
|
export * from './webpush.js';
|
|
93
94
|
export * from './workloadreplicaidentity.js';
|
|
94
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
95
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLGlCQUFpQixDQUFDO0FBQ2hDLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyxnQ0FBZ0MsQ0FBQztBQUMvQyxjQUFjLDZCQUE2QixDQUFDO0FBQzVDLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLDhCQUE4QixDQUFDO0FBQzdDLGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYyxlQUFlLENBQUM7QUFDOUIsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLGlCQUFpQixDQUFDO0FBQ2hDLGNBQWMseUJBQXlCLENBQUM7QUFDeEMsY0FBYyw2QkFBNkIsQ0FBQztBQUM1QyxjQUFjLDRCQUE0QixDQUFDO0FBQzNDLGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYywwQkFBMEIsQ0FBQztBQUN6QyxjQUFjLGdDQUFnQyxDQUFDO0FBQy9DLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyxVQUFVLENBQUM7QUFDekIsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxZQUFZLENBQUM7QUFDM0IsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLG1CQUFtQixDQUFDO0FBQ2xDLGNBQWMsY0FBYyxDQUFDO0FBQzdCLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyxZQUFZLENBQUM7QUFDM0IsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLHNCQUFzQixDQUFDO0FBQ3JDLGNBQWMsNkJBQTZCLENBQUM7QUFDNUMsY0FBYyxXQUFXLENBQUM7QUFDMUIsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMseUJBQXlCLENBQUM7QUFDeEMsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLHlCQUF5QixDQUFDO0FBQ3hDLGNBQWMseUJBQXlCLENBQUM7QUFDeEMsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLHdCQUF3QixDQUFDO0FBQ3ZDLGNBQWMsc0JBQXNCLENBQUM7QUFDckMsY0FBYyw4QkFBOEIsQ0FBQztBQUM3QyxjQUFjLG1CQUFtQixDQUFDO0FBQ2xDLGNBQWMsa0JBQWtCLENBQUM7QUFDakMsY0FBYyx3QkFBd0IsQ0FBQztBQUl2QyxPQUFPLEVBQUUsMEJBQTBCLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUc5RCxPQUFPLEVBQUUscUNBQXFDLEVBQUUsTUFBTSw2QkFBNkIsQ0FBQztBQUVwRixPQUFPLEVBQUUsMENBQTBDLEVBQUUsMENBQTBDLEVBQUUsTUFBTSxrQ0FBa0MsQ0FBQztBQUMxSSxjQUFjLHdCQUF3QixDQUFDO0FBQ3ZDLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLGVBQWUsQ0FBQztBQUM5QixjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMsd0JBQXdCLENBQUM7QUFDdkMsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLDRCQUE0QixDQUFDO0FBQzNDLGNBQWMsNkJBQTZCLENBQUM7QUFDNUMsY0FBYyxvQkFBb0IsQ0FBQztBQUNuQyxjQUFjLDBCQUEwQixDQUFDO0FBQ3pDLGNBQWMsK0JBQStCLENBQUM7QUFDOUMsY0FBYyxnQ0FBZ0MsQ0FBQztBQUMvQyxjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMseUJBQXlCLENBQUM7QUFDeEMsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLDZCQUE2QixDQUFDO0FBQzVDLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYywyQkFBMkIsQ0FBQztBQUMxQyxjQUFjLGtCQUFrQixDQUFDO0FBQ2pDLGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYyxlQUFlLENBQUM7QUFDOUIsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLHNCQUFzQixDQUFDO0FBQ3JDLGNBQWMsaUJBQWlCLENBQUM7QUFDaEMsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLGNBQWMsQ0FBQztBQUM3QixjQUFjLHdCQUF3QixDQUFDO0FBQ3ZDLGNBQWMsK0JBQStCLENBQUM7QUFDOUMsY0FBYyxtQkFBbUIsQ0FBQztBQUNsQyxjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYyxrQ0FBa0MsQ0FBQztBQUNqRCxjQUFjLGFBQWEsQ0FBQztBQUM1QixjQUFjLG9CQUFvQixDQUFDO0FBQ25DLGNBQWMsY0FBYyxDQUFDO0FBQzdCLGNBQWMsV0FBVyxDQUFDO0FBQzFCLGNBQWMsY0FBYyxDQUFDO0FBQzdCLGNBQWMsY0FBYyxDQUFDO0FBQzdCLGNBQWMsOEJBQThCLENBQUMifQ==
|
|
@@ -45,6 +45,14 @@ export interface IReq_Any_Cloudly_RestoreServiceBackup extends plugins.typedrequ
|
|
|
45
45
|
backupId: string;
|
|
46
46
|
clear?: boolean;
|
|
47
47
|
resourceTypes?: Array<TBackupSnapshot['type']>;
|
|
48
|
+
/**
|
|
49
|
+
* Must be `true` to restore a `failed` backup whose failure is `incomplete`, and is refused for
|
|
50
|
+
* any other backup. Such a restore restores what the backup captured: `resourceTypes` is
|
|
51
|
+
* required and names only types it captured, none of the resources it did not capture
|
|
52
|
+
* (`failure.uncaptured`) is restored, so the service then holds data from two points in time,
|
|
53
|
+
* and the backup stays `failed`.
|
|
54
|
+
*/
|
|
55
|
+
confirmPartialRestore?: boolean;
|
|
48
56
|
};
|
|
49
57
|
response: {
|
|
50
58
|
backup: IBackupRecord;
|
package/package.json
CHANGED
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/data/backup.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TBackupRestoreRefusal } from './backuprestorerefusal.js';
|
|
1
2
|
import type { ICorestoreDatabaseAllocationReference } from './corestore.js';
|
|
2
3
|
|
|
3
4
|
export type TBackupStatus =
|
|
@@ -72,10 +73,56 @@ export interface IBackupReplicationResult {
|
|
|
72
73
|
completedAt: number;
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
/**
|
|
77
|
+
* One resource a service declares that a backup of it did not capture, with why; each kind of
|
|
78
|
+
* resource has its own reason.
|
|
79
|
+
*
|
|
80
|
+
* `volume-capture-unavailable`: a service volume is a node-local storage claim on its Pallet node,
|
|
81
|
+
* which no relay can capture yet; a `volume` snapshot is a Corestore Docker volume and captures no
|
|
82
|
+
* service volume. `binding-not-snapshotted`: the backup holds no snapshot of the Corestore resource
|
|
83
|
+
* the binding names.
|
|
84
|
+
*/
|
|
85
|
+
export type TBackupUncapturedResource =
|
|
86
|
+
| { type: 'volume'; mountPath: string; reason: 'volume-capture-unavailable' }
|
|
87
|
+
| { type: 'database' | 'objectstorage'; bindingId: string; reason: 'binding-not-snapshotted' };
|
|
88
|
+
|
|
89
|
+
/** Why a resource a service declares is not in one of its backups (`TBackupUncapturedResource`). */
|
|
90
|
+
export type TBackupUncapturedReason = TBackupUncapturedResource['reason'];
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The named kind of a `failed` backup's failure; a failure without one is named only in `errorText`.
|
|
94
|
+
*
|
|
95
|
+
* `incomplete`: the backup did not capture every resource its service declares, and `uncaptured`
|
|
96
|
+
* names each one it missed. What it did capture is restored only by a partial restore an
|
|
97
|
+
* administrator confirms (`IReq_Any_Cloudly_RestoreServiceBackup` `confirmPartialRestore`), which
|
|
98
|
+
* restores none of `uncaptured`, and the backup stays `failed`.
|
|
99
|
+
* `archive-unreadable`: its stored archive is missing or damaged, so it restores nothing.
|
|
100
|
+
*/
|
|
101
|
+
export type TBackupFailure =
|
|
102
|
+
| { kind: 'incomplete'; uncaptured: TBackupUncapturedResource[] }
|
|
103
|
+
| { kind: 'archive-unreadable' };
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* One restore of a backup, appended to its `restoreHistory`; `validateBackupRestoreEvent` judges its
|
|
107
|
+
* `status` and `refusal`.
|
|
108
|
+
*
|
|
109
|
+
* `refused`: the restore was refused before it wrote anything (`refusal` says why), so the service
|
|
110
|
+
* keeps the data it had. `failed`: it failed after it began writing.
|
|
111
|
+
*/
|
|
75
112
|
export interface IBackupRestoreEvent {
|
|
113
|
+
/** When the restore ended, or when it was refused. */
|
|
76
114
|
restoredAt: number;
|
|
77
|
-
status: 'restored' | 'failed';
|
|
115
|
+
status: 'restored' | 'failed' | 'refused';
|
|
78
116
|
errorText?: string;
|
|
117
|
+
/** The resource types the restore was limited to; absent when it restored every snapshot. */
|
|
118
|
+
resourceTypes?: TBackupResourceType[];
|
|
119
|
+
/**
|
|
120
|
+
* A confirmed partial restore of an `incomplete` backup: it restored only `resourceTypes`, and
|
|
121
|
+
* none of the backup's `failure.uncaptured`.
|
|
122
|
+
*/
|
|
123
|
+
partial?: true;
|
|
124
|
+
/** Why a `refused` restore wrote nothing; present exactly when `status` is `refused`. */
|
|
125
|
+
refusal?: TBackupRestoreRefusal;
|
|
79
126
|
}
|
|
80
127
|
|
|
81
128
|
/**
|
|
@@ -114,6 +161,8 @@ export interface IBackupRecord {
|
|
|
114
161
|
completedAt?: number;
|
|
115
162
|
requestedBy?: string;
|
|
116
163
|
errorText?: string;
|
|
164
|
+
/** The named kind of a `failed` backup's failure (`TBackupFailure`). */
|
|
165
|
+
failure?: TBackupFailure;
|
|
117
166
|
restoreHistory?: IBackupRestoreEvent[];
|
|
118
167
|
/**
|
|
119
168
|
* Authoritative isolated-restore holds. Cloudly atomically appends the
|
|
@@ -124,6 +173,25 @@ export interface IBackupRecord {
|
|
|
124
173
|
tags?: Record<string, string>;
|
|
125
174
|
}
|
|
126
175
|
|
|
176
|
+
/**
|
|
177
|
+
* The resource types a partial restore of a backup may name (`confirmPartialRestore`).
|
|
178
|
+
*
|
|
179
|
+
* For a `failed` backup whose failure is `incomplete`: every type it holds a snapshot of, except
|
|
180
|
+
* `volume` (a `volume` snapshot is a Corestore Docker volume and captures no service volume) and the
|
|
181
|
+
* type of any resource it did not capture, so a partial restore never restores a snapshot standing
|
|
182
|
+
* for a resource the backup missed. For every other backup: none. Database before object storage.
|
|
183
|
+
*/
|
|
184
|
+
export const readPartialRestoreResourceTypes = (
|
|
185
|
+
backupArg: Pick<IBackupRecord, 'status' | 'failure'> & { snapshots: ReadonlyArray<Pick<TBackupSnapshot, 'type'>> },
|
|
186
|
+
): Array<Exclude<TBackupResourceType, 'volume'>> => {
|
|
187
|
+
const failure = backupArg.failure;
|
|
188
|
+
if (backupArg.status !== 'failed' || failure?.kind !== 'incomplete') return [];
|
|
189
|
+
const uncapturedTypes = new Set<TBackupResourceType>(failure.uncaptured.map((resourceArg) => resourceArg.type));
|
|
190
|
+
return (['database', 'objectstorage'] as const).filter((typeArg) => (
|
|
191
|
+
!uncapturedTypes.has(typeArg) && backupArg.snapshots.some((snapshotArg) => snapshotArg.type === typeArg)
|
|
192
|
+
));
|
|
193
|
+
};
|
|
194
|
+
|
|
127
195
|
/** Retention policy understood by containerarchive prune. */
|
|
128
196
|
export interface IArchiveRetentionPolicy {
|
|
129
197
|
keepLast?: number;
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import type { IBackupRestoreEvent, TBackupResourceType } from './backup.js';
|
|
2
|
+
import {
|
|
3
|
+
hasExactKeys, readDataElements, readDataProperties, readExactDataRecord,
|
|
4
|
+
} from '../private/dataproperties.js';
|
|
5
|
+
import { compareReleaseVersions, parseReleaseVersion } from './releaseversion.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Why an in-place restore of a backup (`IReq_Any_Cloudly_RestoreServiceBackup`) was refused before
|
|
9
|
+
* it wrote anything. A restore is all-or-nothing: Corestore checks that it can read every snapshot
|
|
10
|
+
* the restore names before it replaces any data, and Cloudly dispatches the restore only to a relay
|
|
11
|
+
* whose contract carries that check. A refusal therefore always means nothing was written; the
|
|
12
|
+
* service keeps the data it had.
|
|
13
|
+
*
|
|
14
|
+
* - `restore-snapshot-unavailable`: the node's Corestore cannot read one or more snapshots the
|
|
15
|
+
* restore names; `missing` names each of them.
|
|
16
|
+
* - `restore-preflight-unsupported`: the node's Corestore does not offer the restore check (a
|
|
17
|
+
* Corestore built before it answers the check route with HTTP 404), so it cannot prove the
|
|
18
|
+
* restore would be whole and nothing is restored.
|
|
19
|
+
* - `restore-relay-contract-behind`: the cluster relay speaks an interfaces release below the one
|
|
20
|
+
* that carries the restore check, so Cloudly does not dispatch the restore; the relay states
|
|
21
|
+
* `relayInterfacesVersion` (`null` when it stated none) and the restore needs
|
|
22
|
+
* `minimumInterfacesVersion`.
|
|
23
|
+
*
|
|
24
|
+
* It travels as `TypedResponseError.errorData` of a refused restore and is recorded as the
|
|
25
|
+
* `refusal` of the backup's `refused` restore event (`IBackupRestoreEvent`). Each refusal is
|
|
26
|
+
* exactly its schema: `readBackupRestoreRefusal` refuses any other member, so a producer never adds
|
|
27
|
+
* one without gating it on the reader's interfaces version.
|
|
28
|
+
*/
|
|
29
|
+
export const backupRestoreRefusalNames = Object.freeze([
|
|
30
|
+
'restore-snapshot-unavailable',
|
|
31
|
+
'restore-preflight-unsupported',
|
|
32
|
+
'restore-relay-contract-behind',
|
|
33
|
+
] as const);
|
|
34
|
+
|
|
35
|
+
export type TBackupRestoreRefusalName = typeof backupRestoreRefusalNames[number];
|
|
36
|
+
|
|
37
|
+
export const backupRestoreRefusalContract = Object.freeze({
|
|
38
|
+
/** At most this many snapshots are named in one `restore-snapshot-unavailable`. */
|
|
39
|
+
maximumMissingSnapshots: 256,
|
|
40
|
+
/** A snapshot id or resource name is printable ASCII without spaces, at most this long. */
|
|
41
|
+
maximumNameLength: 255,
|
|
42
|
+
} as const);
|
|
43
|
+
|
|
44
|
+
/** One snapshot a restore names that the node's Corestore cannot read. */
|
|
45
|
+
export interface IBackupRestoreMissingSnapshot {
|
|
46
|
+
snapshotId: string;
|
|
47
|
+
/** The snapshot's resource type (`TBackupSnapshot['type']`). */
|
|
48
|
+
capability: TBackupResourceType;
|
|
49
|
+
/** The Corestore resource of a database or object-storage snapshot, when it is known. */
|
|
50
|
+
resourceName?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface IBackupRestoreSnapshotUnavailableRefusal {
|
|
54
|
+
name: 'restore-snapshot-unavailable';
|
|
55
|
+
/** Non-empty; each snapshot once. */
|
|
56
|
+
missing: IBackupRestoreMissingSnapshot[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface IBackupRestorePreflightUnsupportedRefusal {
|
|
60
|
+
name: 'restore-preflight-unsupported';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface IBackupRestoreRelayContractBehindRefusal {
|
|
64
|
+
name: 'restore-relay-contract-behind';
|
|
65
|
+
/** The interfaces release the relay speaks, or `null` when it stated none. */
|
|
66
|
+
relayInterfacesVersion: string | null;
|
|
67
|
+
/** The lowest interfaces release a relay must speak to carry a restore. */
|
|
68
|
+
minimumInterfacesVersion: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A refused restore, discriminated by `name` (`backupRestoreRefusalNames`). */
|
|
72
|
+
export type TBackupRestoreRefusal =
|
|
73
|
+
| IBackupRestoreSnapshotUnavailableRefusal
|
|
74
|
+
| IBackupRestorePreflightUnsupportedRefusal
|
|
75
|
+
| IBackupRestoreRelayContractBehindRefusal;
|
|
76
|
+
|
|
77
|
+
/** Every resource type, keyed so a new `TBackupResourceType` member fails to compile here. */
|
|
78
|
+
const backupResourceTypeKeys: Readonly<Record<TBackupResourceType, true>> = Object.freeze({
|
|
79
|
+
volume: true,
|
|
80
|
+
database: true,
|
|
81
|
+
objectstorage: true,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const isBackupResourceType = (valueArg: unknown): valueArg is TBackupResourceType => (
|
|
85
|
+
typeof valueArg === 'string' && Object.hasOwn(backupResourceTypeKeys, valueArg)
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const printableNamePattern = /^[\x21-\x7e]+$/;
|
|
89
|
+
|
|
90
|
+
const isRestoreName = (valueArg: unknown): valueArg is string => (
|
|
91
|
+
typeof valueArg === 'string'
|
|
92
|
+
&& valueArg.length <= backupRestoreRefusalContract.maximumNameLength
|
|
93
|
+
&& printableNamePattern.test(valueArg)
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const nameRule = 'must be printable ASCII without spaces, at most '
|
|
97
|
+
+ `${backupRestoreRefusalContract.maximumNameLength} characters`;
|
|
98
|
+
|
|
99
|
+
const isRefusalName = (valueArg: unknown): valueArg is TBackupRestoreRefusalName => (
|
|
100
|
+
typeof valueArg === 'string'
|
|
101
|
+
&& (backupRestoreRefusalNames as readonly string[]).includes(valueArg)
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const validateMissingSnapshot = (valueArg: unknown, pathArg: string): string[] => {
|
|
105
|
+
const snapshot = readDataProperties(valueArg);
|
|
106
|
+
if (snapshot === null || !hasExactKeys(snapshot, ['snapshotId', 'capability'], ['resourceName'])) {
|
|
107
|
+
return [`${pathArg} must use its exact schema`];
|
|
108
|
+
}
|
|
109
|
+
const errors: string[] = [];
|
|
110
|
+
if (!isRestoreName(snapshot.snapshotId)) {
|
|
111
|
+
errors.push(`${pathArg}.snapshotId ${nameRule}`);
|
|
112
|
+
}
|
|
113
|
+
if (!isBackupResourceType(snapshot.capability)) {
|
|
114
|
+
errors.push(`${pathArg}.capability must be volume, database or objectstorage`);
|
|
115
|
+
}
|
|
116
|
+
if (Object.hasOwn(snapshot, 'resourceName') && !isRestoreName(snapshot.resourceName)) {
|
|
117
|
+
errors.push(`${pathArg}.resourceName ${nameRule}`);
|
|
118
|
+
}
|
|
119
|
+
return errors;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const validateMissingSnapshots = (valueArg: unknown, pathArg: string): string[] => {
|
|
123
|
+
const elements = readDataElements(valueArg, backupRestoreRefusalContract.maximumMissingSnapshots);
|
|
124
|
+
if (elements === null) {
|
|
125
|
+
return [`${pathArg} must be a list of at most `
|
|
126
|
+
+ `${backupRestoreRefusalContract.maximumMissingSnapshots} snapshots`];
|
|
127
|
+
}
|
|
128
|
+
if (elements.length === 0) {
|
|
129
|
+
return [`${pathArg} must name at least one snapshot`];
|
|
130
|
+
}
|
|
131
|
+
const errors = elements.flatMap((elementArg, indexArg) => (
|
|
132
|
+
validateMissingSnapshot(elementArg, `${pathArg}[${indexArg}]`)
|
|
133
|
+
));
|
|
134
|
+
if (errors.length > 0) {
|
|
135
|
+
return errors;
|
|
136
|
+
}
|
|
137
|
+
const snapshotIds = elements.map((elementArg) => (elementArg as IBackupRestoreMissingSnapshot).snapshotId);
|
|
138
|
+
if (new Set(snapshotIds).size !== snapshotIds.length) {
|
|
139
|
+
errors.push(`${pathArg} must name each snapshot once`);
|
|
140
|
+
}
|
|
141
|
+
return errors;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const validateRelayContractBehind = (
|
|
145
|
+
refusalArg: Record<string, unknown>,
|
|
146
|
+
pathArg: string,
|
|
147
|
+
): string[] => {
|
|
148
|
+
const relay = refusalArg.relayInterfacesVersion === null
|
|
149
|
+
? null
|
|
150
|
+
: parseReleaseVersion(refusalArg.relayInterfacesVersion);
|
|
151
|
+
const minimum = parseReleaseVersion(refusalArg.minimumInterfacesVersion);
|
|
152
|
+
const errors: string[] = [];
|
|
153
|
+
if (refusalArg.relayInterfacesVersion !== null && relay === null) {
|
|
154
|
+
errors.push(`${pathArg} relayInterfacesVersion must be a release version or null`);
|
|
155
|
+
}
|
|
156
|
+
if (minimum === null) {
|
|
157
|
+
errors.push(`${pathArg} minimumInterfacesVersion must be a release version`);
|
|
158
|
+
}
|
|
159
|
+
if (relay !== null && minimum !== null && compareReleaseVersions(relay, minimum) >= 0) {
|
|
160
|
+
errors.push(`${pathArg} relayInterfacesVersion must be below minimumInterfacesVersion`);
|
|
161
|
+
}
|
|
162
|
+
return errors;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/** The exact keys of each refusal. */
|
|
166
|
+
const refusalKeys: Readonly<Record<TBackupRestoreRefusalName, readonly string[]>> = Object.freeze({
|
|
167
|
+
'restore-snapshot-unavailable': Object.freeze(['name', 'missing']),
|
|
168
|
+
'restore-preflight-unsupported': Object.freeze(['name']),
|
|
169
|
+
'restore-relay-contract-behind': Object.freeze([
|
|
170
|
+
'name', 'relayInterfacesVersion', 'minimumInterfacesVersion',
|
|
171
|
+
]),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
/** Validates one restore refusal against its exact schema and names every reason it is refused. */
|
|
175
|
+
export const validateBackupRestoreRefusal = (
|
|
176
|
+
valueArg: unknown,
|
|
177
|
+
pathArg = 'backup restore refusal',
|
|
178
|
+
): string[] => {
|
|
179
|
+
const properties = readDataProperties(valueArg);
|
|
180
|
+
if (properties === null || !isRefusalName(properties.name)) {
|
|
181
|
+
return [`${pathArg} name must be one of ${backupRestoreRefusalNames.join(', ')}`];
|
|
182
|
+
}
|
|
183
|
+
const refusal = readExactDataRecord(valueArg, refusalKeys[properties.name]);
|
|
184
|
+
if (refusal === null) {
|
|
185
|
+
return [`${pathArg} must use the exact schema of ${properties.name}`];
|
|
186
|
+
}
|
|
187
|
+
switch (properties.name) {
|
|
188
|
+
case 'restore-snapshot-unavailable':
|
|
189
|
+
return validateMissingSnapshots(refusal.missing, `${pathArg} missing`);
|
|
190
|
+
case 'restore-preflight-unsupported':
|
|
191
|
+
return [];
|
|
192
|
+
case 'restore-relay-contract-behind':
|
|
193
|
+
return validateRelayContractBehind(refusal, pathArg);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/** Whether a value is exactly one restore refusal. */
|
|
198
|
+
export const isBackupRestoreRefusal = (valueArg: unknown): valueArg is TBackupRestoreRefusal => (
|
|
199
|
+
validateBackupRestoreRefusal(valueArg).length === 0
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const freezeMissingSnapshot = (
|
|
203
|
+
snapshotArg: IBackupRestoreMissingSnapshot,
|
|
204
|
+
): IBackupRestoreMissingSnapshot => Object.freeze({
|
|
205
|
+
snapshotId: snapshotArg.snapshotId,
|
|
206
|
+
capability: snapshotArg.capability,
|
|
207
|
+
...(snapshotArg.resourceName === undefined ? {} : { resourceName: snapshotArg.resourceName }),
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const copySnapshotUnavailableRefusal = (
|
|
211
|
+
refusalArg: IBackupRestoreSnapshotUnavailableRefusal,
|
|
212
|
+
): IBackupRestoreSnapshotUnavailableRefusal => {
|
|
213
|
+
const missing = refusalArg.missing.map(freezeMissingSnapshot);
|
|
214
|
+
Object.freeze(missing);
|
|
215
|
+
return Object.freeze({ name: refusalArg.name, missing });
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const copyPreflightUnsupportedRefusal = (
|
|
219
|
+
refusalArg: IBackupRestorePreflightUnsupportedRefusal,
|
|
220
|
+
): IBackupRestorePreflightUnsupportedRefusal => Object.freeze({ name: refusalArg.name });
|
|
221
|
+
|
|
222
|
+
const copyRelayContractBehindRefusal = (
|
|
223
|
+
refusalArg: IBackupRestoreRelayContractBehindRefusal,
|
|
224
|
+
): IBackupRestoreRelayContractBehindRefusal => Object.freeze({
|
|
225
|
+
name: refusalArg.name,
|
|
226
|
+
relayInterfacesVersion: refusalArg.relayInterfacesVersion,
|
|
227
|
+
minimumInterfacesVersion: refusalArg.minimumInterfacesVersion,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
/** An inert frozen copy of a refusal that passed `validateBackupRestoreRefusal`. */
|
|
231
|
+
const copyRefusal = (refusalArg: TBackupRestoreRefusal): TBackupRestoreRefusal => {
|
|
232
|
+
switch (refusalArg.name) {
|
|
233
|
+
case 'restore-snapshot-unavailable':
|
|
234
|
+
return copySnapshotUnavailableRefusal(refusalArg);
|
|
235
|
+
case 'restore-preflight-unsupported':
|
|
236
|
+
return copyPreflightUnsupportedRefusal(refusalArg);
|
|
237
|
+
case 'restore-relay-contract-behind':
|
|
238
|
+
return copyRelayContractBehindRefusal(refusalArg);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The restore refusal inside an error payload or a stored restore event, as an inert frozen copy,
|
|
244
|
+
* or `null` when the value is not exactly one. It reads own data properties only, so a refusal
|
|
245
|
+
* read here carries nothing that runs.
|
|
246
|
+
*/
|
|
247
|
+
export const readBackupRestoreRefusal = (valueArg: unknown): TBackupRestoreRefusal | null => (
|
|
248
|
+
// A value that passed holds own data properties of exactly the refusal's schema at every level,
|
|
249
|
+
// so the copy reads the same data the validation judged.
|
|
250
|
+
isBackupRestoreRefusal(valueArg) ? copyRefusal(valueArg) : null
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
/** The refusal a builder composed, unchanged, or an error naming every reason it is refused. */
|
|
254
|
+
const requireRefusal = <T extends TBackupRestoreRefusal>(refusalArg: T): T => {
|
|
255
|
+
const errors = validateBackupRestoreRefusal(refusalArg);
|
|
256
|
+
if (errors.length > 0) {
|
|
257
|
+
throw new Error(errors.join('; '));
|
|
258
|
+
}
|
|
259
|
+
return refusalArg;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
/** `restore-snapshot-unavailable` naming each snapshot the node's Corestore cannot read. */
|
|
263
|
+
export const buildBackupRestoreSnapshotUnavailableRefusal = (
|
|
264
|
+
missingArg: IBackupRestoreMissingSnapshot[],
|
|
265
|
+
): IBackupRestoreSnapshotUnavailableRefusal => copySnapshotUnavailableRefusal(requireRefusal({
|
|
266
|
+
name: 'restore-snapshot-unavailable',
|
|
267
|
+
missing: missingArg.map((snapshotArg) => ({ ...snapshotArg })),
|
|
268
|
+
}));
|
|
269
|
+
|
|
270
|
+
/** `restore-preflight-unsupported`: the node's Corestore offers no restore check. */
|
|
271
|
+
export const buildBackupRestorePreflightUnsupportedRefusal = (): IBackupRestorePreflightUnsupportedRefusal => (
|
|
272
|
+
copyPreflightUnsupportedRefusal(requireRefusal({ name: 'restore-preflight-unsupported' }))
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* `restore-relay-contract-behind`: the relay speaks an interfaces release below the minimum.
|
|
277
|
+
*
|
|
278
|
+
* Throws when a stated version is not a release version or is not below the minimum, so a caller
|
|
279
|
+
* passes `null` for a relay whose stated version it cannot read.
|
|
280
|
+
*/
|
|
281
|
+
export const buildBackupRestoreRelayContractBehindRefusal = (
|
|
282
|
+
relayInterfacesVersionArg: string | null,
|
|
283
|
+
minimumInterfacesVersionArg: string,
|
|
284
|
+
): IBackupRestoreRelayContractBehindRefusal => copyRelayContractBehindRefusal(requireRefusal({
|
|
285
|
+
name: 'restore-relay-contract-behind',
|
|
286
|
+
relayInterfacesVersion: relayInterfacesVersionArg,
|
|
287
|
+
minimumInterfacesVersion: minimumInterfacesVersionArg,
|
|
288
|
+
}));
|
|
289
|
+
|
|
290
|
+
const restoreEventStatuses: readonly IBackupRestoreEvent['status'][] = Object.freeze([
|
|
291
|
+
'restored', 'failed', 'refused',
|
|
292
|
+
]);
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Validates what this contract adds to a restore event of a backup's `restoreHistory` and names
|
|
296
|
+
* every reason it is refused: `status` is `restored`, `failed` or `refused`, and `refusal` is
|
|
297
|
+
* present exactly when `status` is `refused`, as one restore refusal. Every other member is left as
|
|
298
|
+
* it was written (records Cloudly already stores keep any `restoredAt`, `errorText`, `partial` and
|
|
299
|
+
* `resourceTypes` they hold), so every stored event stays valid.
|
|
300
|
+
*/
|
|
301
|
+
export const validateBackupRestoreEvent = (
|
|
302
|
+
valueArg: unknown,
|
|
303
|
+
pathArg = 'backup restore event',
|
|
304
|
+
): string[] => {
|
|
305
|
+
const event = readDataProperties(valueArg);
|
|
306
|
+
if (event === null) {
|
|
307
|
+
return [`${pathArg} must be a plain record of data properties`];
|
|
308
|
+
}
|
|
309
|
+
const errors: string[] = [];
|
|
310
|
+
if (!(restoreEventStatuses as readonly unknown[]).includes(event.status)) {
|
|
311
|
+
errors.push(`${pathArg} status must be one of ${restoreEventStatuses.join(', ')}`);
|
|
312
|
+
}
|
|
313
|
+
const hasRefusal = Object.hasOwn(event, 'refusal');
|
|
314
|
+
if (event.status === 'refused') {
|
|
315
|
+
if (!hasRefusal) {
|
|
316
|
+
errors.push(`${pathArg} refusal is required when status is refused`);
|
|
317
|
+
} else {
|
|
318
|
+
errors.push(...validateBackupRestoreRefusal(event.refusal, `${pathArg} refusal`));
|
|
319
|
+
}
|
|
320
|
+
} else if (hasRefusal) {
|
|
321
|
+
errors.push(`${pathArg} refusal is allowed only when status is refused`);
|
|
322
|
+
}
|
|
323
|
+
return errors;
|
|
324
|
+
};
|
package/ts/data/index.ts
CHANGED
|
@@ -76,6 +76,7 @@ export * from './secret.js';
|
|
|
76
76
|
export * from './baremetal.js';
|
|
77
77
|
export * from './baseos.js';
|
|
78
78
|
export * from './backup.js';
|
|
79
|
+
export * from './backuprestorerefusal.js';
|
|
79
80
|
export * from './clusternode.js';
|
|
80
81
|
export * from './clusternodehostname.js';
|
|
81
82
|
export * from './settings.js';
|
package/ts/requests/backup.ts
CHANGED
|
@@ -81,6 +81,14 @@ extends plugins.typedrequestInterfaces.implementsTR<
|
|
|
81
81
|
backupId: string;
|
|
82
82
|
clear?: boolean;
|
|
83
83
|
resourceTypes?: Array<TBackupSnapshot['type']>;
|
|
84
|
+
/**
|
|
85
|
+
* Must be `true` to restore a `failed` backup whose failure is `incomplete`, and is refused for
|
|
86
|
+
* any other backup. Such a restore restores what the backup captured: `resourceTypes` is
|
|
87
|
+
* required and names only types it captured, none of the resources it did not capture
|
|
88
|
+
* (`failure.uncaptured`) is restored, so the service then holds data from two points in time,
|
|
89
|
+
* and the backup stays `failed`.
|
|
90
|
+
*/
|
|
91
|
+
confirmPartialRestore?: boolean;
|
|
84
92
|
};
|
|
85
93
|
response: {
|
|
86
94
|
backup: IBackupRecord;
|