@homeflare/alchemy 0.37.1 → 0.37.3
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/dist/proxmox/backup-job-config.d.ts +95 -0
- package/dist/proxmox/backup-job-config.d.ts.map +1 -0
- package/dist/proxmox/backup-job-distilled.d.ts +46 -0
- package/dist/proxmox/backup-job-distilled.d.ts.map +1 -0
- package/dist/proxmox/backup-job.d.ts +9 -26
- package/dist/proxmox/backup-job.d.ts.map +1 -1
- package/dist/proxmox/ceph-fs-distilled.d.ts +17 -10
- package/dist/proxmox/ceph-fs-distilled.d.ts.map +1 -1
- package/dist/proxmox/ceph-fs-wire.d.ts +5 -22
- package/dist/proxmox/ceph-fs-wire.d.ts.map +1 -1
- package/dist/proxmox/ceph-fs.d.ts +3 -3
- package/dist/proxmox/distilled-pbs.d.ts +12 -0
- package/dist/proxmox/distilled-pbs.d.ts.map +1 -0
- package/dist/proxmox/fake-pbs-jobs.d.ts +23 -0
- package/dist/proxmox/fake-pbs-jobs.d.ts.map +1 -0
- package/dist/proxmox/fake-pbs-notify.d.ts.map +1 -1
- package/dist/proxmox/fake-pve.d.ts +1 -1
- package/dist/proxmox/fake-pve.d.ts.map +1 -1
- package/dist/proxmox/group-wire.d.ts +2 -2
- package/dist/proxmox/group-wire.d.ts.map +1 -1
- package/dist/proxmox/index.js +1382 -506
- package/dist/proxmox/index.js.map +51 -27
- package/dist/proxmox/live-core-fixtures.d.ts +40 -0
- package/dist/proxmox/live-core-fixtures.d.ts.map +1 -0
- package/dist/proxmox/metric-server-config.d.ts +49 -0
- package/dist/proxmox/metric-server-config.d.ts.map +1 -0
- package/dist/proxmox/metric-server-distilled.d.ts +43 -0
- package/dist/proxmox/metric-server-distilled.d.ts.map +1 -0
- package/dist/proxmox/metric-server.d.ts +2 -10
- package/dist/proxmox/metric-server.d.ts.map +1 -1
- package/dist/proxmox/node-network-wire.d.ts +1 -1
- package/dist/proxmox/node-network-wire.d.ts.map +1 -1
- package/dist/proxmox/notification-matcher-distilled.d.ts +9 -0
- package/dist/proxmox/notification-matcher-distilled.d.ts.map +1 -0
- package/dist/proxmox/notification-matcher-lifecycle.d.ts +27 -0
- package/dist/proxmox/notification-matcher-lifecycle.d.ts.map +1 -0
- package/dist/proxmox/notification-matcher.d.ts +5 -5
- package/dist/proxmox/notification-matcher.d.ts.map +1 -1
- package/dist/proxmox/notification-sdk-input.d.ts +22 -0
- package/dist/proxmox/notification-sdk-input.d.ts.map +1 -0
- package/dist/proxmox/notification-target-distilled.d.ts +12 -0
- package/dist/proxmox/notification-target-distilled.d.ts.map +1 -0
- package/dist/proxmox/notification-target-form.d.ts +2 -0
- package/dist/proxmox/notification-target-form.d.ts.map +1 -1
- package/dist/proxmox/notification-target-lifecycle.d.ts +29 -0
- package/dist/proxmox/notification-target-lifecycle.d.ts.map +1 -0
- package/dist/proxmox/notification-target-spec.d.ts +5 -0
- package/dist/proxmox/notification-target-spec.d.ts.map +1 -0
- package/dist/proxmox/notification-target.d.ts +4 -5
- package/dist/proxmox/notification-target.d.ts.map +1 -1
- package/dist/proxmox/pbs-datastore-endpoint.d.ts +1 -1
- package/dist/proxmox/pbs-datastore-form.d.ts +18 -42
- package/dist/proxmox/pbs-datastore-form.d.ts.map +1 -1
- package/dist/proxmox/pbs-datastore-guard.d.ts +7 -4
- package/dist/proxmox/pbs-datastore-guard.d.ts.map +1 -1
- package/dist/proxmox/pbs-datastore-lifecycle.d.ts +56 -0
- package/dist/proxmox/pbs-datastore-lifecycle.d.ts.map +1 -0
- package/dist/proxmox/pbs-datastore-wire.d.ts +21 -0
- package/dist/proxmox/pbs-datastore-wire.d.ts.map +1 -0
- package/dist/proxmox/pbs-datastore.d.ts +5 -6
- package/dist/proxmox/pbs-datastore.d.ts.map +1 -1
- package/dist/proxmox/pbs-notification-input.d.ts +18 -0
- package/dist/proxmox/pbs-notification-input.d.ts.map +1 -0
- package/dist/proxmox/pbs-notification-matcher-distilled.d.ts +11 -0
- package/dist/proxmox/pbs-notification-matcher-distilled.d.ts.map +1 -0
- package/dist/proxmox/pbs-notification-matcher.d.ts +4 -3
- package/dist/proxmox/pbs-notification-matcher.d.ts.map +1 -1
- package/dist/proxmox/pbs-notification-target-distilled.d.ts +20 -0
- package/dist/proxmox/pbs-notification-target-distilled.d.ts.map +1 -0
- package/dist/proxmox/pbs-notification-target-lifecycle.d.ts +4 -4
- package/dist/proxmox/pbs-notification-target-lifecycle.d.ts.map +1 -1
- package/dist/proxmox/pbs-notification-target.d.ts +1 -1
- package/dist/proxmox/pbs-prune-job-lifecycle.d.ts +22 -0
- package/dist/proxmox/pbs-prune-job-lifecycle.d.ts.map +1 -0
- package/dist/proxmox/pbs-prune-job-spec.d.ts +5 -0
- package/dist/proxmox/pbs-prune-job-spec.d.ts.map +1 -0
- package/dist/proxmox/pbs-prune-job-wire.d.ts +11 -0
- package/dist/proxmox/pbs-prune-job-wire.d.ts.map +1 -0
- package/dist/proxmox/pbs-prune-job.d.ts +9 -8
- package/dist/proxmox/pbs-prune-job.d.ts.map +1 -1
- package/dist/proxmox/pbs-sync-job-attributes.d.ts +34 -0
- package/dist/proxmox/pbs-sync-job-attributes.d.ts.map +1 -0
- package/dist/proxmox/pbs-sync-job-form.d.ts +21 -22
- package/dist/proxmox/pbs-sync-job-form.d.ts.map +1 -1
- package/dist/proxmox/pbs-sync-job-lifecycle.d.ts +22 -0
- package/dist/proxmox/pbs-sync-job-lifecycle.d.ts.map +1 -0
- package/dist/proxmox/pbs-sync-job-wire.d.ts +11 -0
- package/dist/proxmox/pbs-sync-job-wire.d.ts.map +1 -0
- package/dist/proxmox/pbs-sync-job.d.ts +9 -39
- package/dist/proxmox/pbs-sync-job.d.ts.map +1 -1
- package/dist/proxmox/pbs-verify-job-lifecycle.d.ts +22 -0
- package/dist/proxmox/pbs-verify-job-lifecycle.d.ts.map +1 -0
- package/dist/proxmox/pbs-verify-job-spec.d.ts +16 -0
- package/dist/proxmox/pbs-verify-job-spec.d.ts.map +1 -0
- package/dist/proxmox/pbs-verify-job-wire.d.ts +11 -0
- package/dist/proxmox/pbs-verify-job-wire.d.ts.map +1 -0
- package/dist/proxmox/pbs-verify-job.d.ts +12 -12
- package/dist/proxmox/pbs-verify-job.d.ts.map +1 -1
- package/dist/proxmox/pool.d.ts +10 -1
- package/dist/proxmox/pool.d.ts.map +1 -1
- package/dist/proxmox/storage-wire.d.ts +2 -2
- package/dist/proxmox/storage-wire.d.ts.map +1 -1
- package/dist/proxmox/user-wire.d.ts +2 -2
- package/dist/proxmox/user-wire.d.ts.map +1 -1
- package/dist/proxmox/zfs-pool-wire.d.ts +1 -1
- package/dist/proxmox/zfs-pool-wire.d.ts.map +1 -1
- package/docs/changelog/README.md +22 -0
- package/docs/changelog/archive-01.md +176 -0
- package/docs/changelog/archive-02.md +180 -0
- package/docs/changelog/archive-03.md +179 -0
- package/docs/changelog/archive-04.md +178 -0
- package/docs/changelog/archive-05.md +177 -0
- package/docs/changelog/archive-06.md +180 -0
- package/docs/changelog/archive-07.md +180 -0
- package/docs/changelog/archive-08.md +178 -0
- package/docs/changelog/archive-09.md +181 -0
- package/docs/changelog/archive-10.md +182 -0
- package/docs/changelog/archive-11.md +182 -0
- package/docs/changelog/archive-12.md +183 -0
- package/docs/changelog/archive-13.md +180 -0
- package/docs/changelog/archive-14.md +178 -0
- package/docs/changelog/archive-15.md +181 -0
- package/docs/changelog/archive-16.md +182 -0
- package/docs/changelog/archive-17.md +183 -0
- package/docs/changelog/archive-18.md +55 -0
- package/docs/pbs-distilled-jobs.md +31 -0
- package/docs/pbs-notifications.md +8 -2
- package/docs/proxmox-notification-sdk.md +28 -0
- package/docs/proxmox-sdk-migration.md +60 -0
- package/docs/proxmox.md +9 -3
- package/docs/upstream-conformance-families.md +6 -2
- package/package.json +3 -3
- package/src/proxmox/adopt-noop.test.ts +4 -5
- package/src/proxmox/backup-job-config.ts +95 -0
- package/src/proxmox/backup-job-distilled.ts +86 -0
- package/src/proxmox/backup-job.ts +78 -116
- package/src/proxmox/ceph-fs-delete.test.ts +5 -8
- package/src/proxmox/ceph-fs-destroy-sdk.test.ts +98 -0
- package/src/proxmox/ceph-fs-distilled.ts +41 -13
- package/src/proxmox/ceph-fs-wire.ts +5 -93
- package/src/proxmox/ceph-fs.ts +7 -7
- package/src/proxmox/constraints.test.ts +9 -8
- package/src/proxmox/distilled-pbs.test.ts +87 -0
- package/src/proxmox/distilled-pbs.ts +50 -0
- package/src/proxmox/fake-pbs-jobs.ts +99 -0
- package/src/proxmox/fake-pbs-notify.ts +4 -1
- package/src/proxmox/fake-pve.ts +3 -2
- package/src/proxmox/live-core-fixtures.ts +126 -0
- package/src/proxmox/live-core-lifecycle.test.ts +115 -0
- package/src/proxmox/live-core-wire.test.ts +190 -0
- package/src/proxmox/metric-server-config.ts +87 -0
- package/src/proxmox/metric-server-distilled.ts +81 -0
- package/src/proxmox/metric-server.ts +77 -88
- package/src/proxmox/notification-distilled.test.ts +162 -0
- package/src/proxmox/notification-matcher-distilled.ts +60 -0
- package/src/proxmox/notification-matcher-lifecycle.ts +71 -0
- package/src/proxmox/notification-matcher.ts +9 -26
- package/src/proxmox/notification-replacement.test.ts +190 -0
- package/src/proxmox/notification-sdk-input.ts +39 -0
- package/src/proxmox/notification-target-distilled.ts +160 -0
- package/src/proxmox/notification-target-form.ts +2 -0
- package/src/proxmox/notification-target-lifecycle.ts +75 -0
- package/src/proxmox/notification-target-spec.ts +68 -0
- package/src/proxmox/notification-target.ts +6 -68
- package/src/proxmox/pbs-datastore-endpoint.ts +1 -1
- package/src/proxmox/pbs-datastore-form.ts +18 -49
- package/src/proxmox/pbs-datastore-guard.ts +7 -4
- package/src/proxmox/pbs-datastore-lifecycle.ts +114 -0
- package/src/proxmox/pbs-datastore-wire.ts +94 -0
- package/src/proxmox/pbs-datastore.ts +7 -133
- package/src/proxmox/pbs-jobs-delete.test.ts +40 -0
- package/src/proxmox/pbs-jobs-sdk.test.ts +215 -0
- package/src/proxmox/pbs-notification-errors.test.ts +93 -0
- package/src/proxmox/pbs-notification-input.ts +25 -0
- package/src/proxmox/pbs-notification-mail.test.ts +64 -0
- package/src/proxmox/pbs-notification-matcher-distilled.ts +65 -0
- package/src/proxmox/pbs-notification-matcher.ts +70 -17
- package/src/proxmox/pbs-notification-target-distilled.ts +124 -0
- package/src/proxmox/pbs-notification-target-lifecycle.ts +26 -17
- package/src/proxmox/pbs-notification-target.ts +1 -1
- package/src/proxmox/pbs-notifications-stack.test.ts +25 -0
- package/src/proxmox/pbs-prune-job-lifecycle.ts +52 -0
- package/src/proxmox/pbs-prune-job-spec.ts +53 -0
- package/src/proxmox/pbs-prune-job-wire.ts +68 -0
- package/src/proxmox/pbs-prune-job.ts +10 -58
- package/src/proxmox/pbs-sync-job-attributes.ts +33 -0
- package/src/proxmox/pbs-sync-job-form.ts +7 -6
- package/src/proxmox/pbs-sync-job-lifecycle.ts +46 -0
- package/src/proxmox/pbs-sync-job-wire.ts +71 -0
- package/src/proxmox/pbs-sync-job.ts +10 -41
- package/src/proxmox/pbs-verify-job-lifecycle.ts +52 -0
- package/src/proxmox/pbs-verify-job-spec.ts +76 -0
- package/src/proxmox/pbs-verify-job-wire.ts +58 -0
- package/src/proxmox/pbs-verify-job.ts +13 -85
- package/src/proxmox/pool.ts +92 -10
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/** PBS 4.2.6-1 values and vendor form guards; SDK transport is in pbs-verify-job-wire.ts. */
|
|
2
|
+
import type { PbsVerifyJobAttributes, PbsVerifyJobProps } from './pbs-verify-job.ts';
|
|
3
|
+
import type { PveSpec } from './resource-spec.ts';
|
|
4
|
+
import { rechecks, shape } from './pbs-verify-job-form.ts';
|
|
5
|
+
import { bool, int, text } from './values.ts';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ⚠️ EVERY FIELD IS READ THROUGH `values.ts` THOUGH PBS RETURNS REAL JSON TYPES. Unlike PVE's
|
|
9
|
+
* SectionConfig round-trip, `GET /config/verify/{id}` serialises a Rust struct, so
|
|
10
|
+
* `ignore-verified` arrives as a JSON boolean and `max-depth` as a JSON number. `bool` and `int`
|
|
11
|
+
* accept both spellings, and using them costs nothing while covering the version that does not.
|
|
12
|
+
* ⚠️ `digest` IS NOT AN ATTRIBUTE AND CANNOT BECOME ONE BY ACCIDENT. PBS puts it on the rpcenv, and
|
|
13
|
+
* the JSON formatter adds rpcenv attributes as SIBLINGS of `data` — `{"data":{…},"digest":"…"}` —
|
|
14
|
+
* so both the old client and the SDK expose only the resource data. Attributes deliberately
|
|
15
|
+
* exclude it: the digest covers verification.cfg as a FILE, so keeping it would churn state
|
|
16
|
+
* whenever an unrelated verify job was edited.
|
|
17
|
+
*/
|
|
18
|
+
export const spec: PveSpec<PbsVerifyJobProps, PbsVerifyJobAttributes> = {
|
|
19
|
+
attributes: (live, props) => {
|
|
20
|
+
const ignoreVerified = bool(live['ignore-verified'], true);
|
|
21
|
+
const outdatedAfter = int(live['outdated-after'], -1);
|
|
22
|
+
return {
|
|
23
|
+
comment: text(live['comment'], ''),
|
|
24
|
+
id: props.id,
|
|
25
|
+
'ignore-verified': ignoreVerified,
|
|
26
|
+
'max-depth': int(live['max-depth'], -1),
|
|
27
|
+
ns: text(live['ns'], ''),
|
|
28
|
+
'outdated-after': outdatedAfter,
|
|
29
|
+
rechecks: rechecks(ignoreVerified, outdatedAfter),
|
|
30
|
+
schedule: text(live['schedule'], ''),
|
|
31
|
+
store: text(live['store'], props.store),
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
/**
|
|
35
|
+
* 🔴 THIS WAS `config/verification` AND PBS ANSWERS 404 FOR IT. MEASURED on the live 4.2 server:
|
|
36
|
+
* `GET /api2/json/config/verification` -> 404 "Path not found"; `GET /api2/json/config/verify`
|
|
37
|
+
* -> 200 with both jobs. The struct is `VerificationJobConfig` and the CLI subcommand is
|
|
38
|
+
* `verify-job`, so the long spelling reads right and is simply not the route.
|
|
39
|
+
* ⛔ THE OLD FAILURE MODE: `read` folded every failure into `undefined`, so a 404 from a
|
|
40
|
+
* WRONG PATH was indistinguishable from an object that was not
|
|
41
|
+
* there. The first plan against the live estate said `create` for two verification jobs that
|
|
42
|
+
* have existed for weeks — and a create would then have POSTed to a 404 as well, so it fails
|
|
43
|
+
* loudly rather than duplicating anything. Nothing caught it earlier because no verification
|
|
44
|
+
* job had ever been declared; registering the provider exercises no path at all.
|
|
45
|
+
*/
|
|
46
|
+
collection: () => 'config/verify',
|
|
47
|
+
/** ⛔ `id` IS SENT. PBS refuses a duplicate id, and that refusal is this family's safety net. */
|
|
48
|
+
createForm: (props) => ({ ...shape(props), id: props.id }),
|
|
49
|
+
/**
|
|
50
|
+
* ⚠️ AN UNDECLARED FIELD IS NEITHER SENT NOR COMPARED — the `Proxmox.BackupJob` trade, for the
|
|
51
|
+
* same reason and with one extra: this form carries NO `delete` list, so there is nothing it
|
|
52
|
+
* could clear even if it wanted to. See the ⛔ on `withClears` in pbs-verify-job-form.ts.
|
|
53
|
+
* ⚠️ `store` IS COMPARED AND IS MUTABLE, which is unusual and worth reading twice. PBS's update
|
|
54
|
+
* handler assigns it and re-checks privileges on both the old and the new path, so changing
|
|
55
|
+
* `store` MOVES the job — and the datastore it left is then verified by nothing. That is a
|
|
56
|
+
* one-word edit with the same effect as deleting the job, and it plans as a quiet `update`.
|
|
57
|
+
* ⚠️ `rechecks` IS ABSENT HERE ON PURPOSE. It is a rendering of the two fields on the lines
|
|
58
|
+
* above; comparing it too would report the same drift twice.
|
|
59
|
+
*/
|
|
60
|
+
/** The vendor rules these forms are checked against at plan time — resource-spec.ts. */
|
|
61
|
+
endpoint: { create: 'pbs:POST /config/verify', update: 'pbs:PUT /config/verify/{id}' },
|
|
62
|
+
matches: (attributes, props) =>
|
|
63
|
+
attributes.store === props.store &&
|
|
64
|
+
// ⚠️ `null` IS COMPARED AGAINST `''`, NOT SKIPPED. A parked job is a declaration like any
|
|
65
|
+
// other, so declaring `null` against a job that HAS a schedule must plan an update — the
|
|
66
|
+
// form then omits `schedule`, which is a set-only form, so see the ⛔ on clearing below.
|
|
67
|
+
attributes.schedule === (props.schedule ?? '') &&
|
|
68
|
+
attributes['ignore-verified'] === (props['ignore-verified'] !== false) &&
|
|
69
|
+
(props['outdated-after'] === undefined ||
|
|
70
|
+
attributes['outdated-after'] === props['outdated-after']) &&
|
|
71
|
+
(props['max-depth'] === undefined || attributes['max-depth'] === props['max-depth']) &&
|
|
72
|
+
(props.ns === undefined || attributes.ns === props.ns) &&
|
|
73
|
+
(props.comment === undefined || attributes.comment === props.comment),
|
|
74
|
+
path: (props) => `config/verify/${props.id}`,
|
|
75
|
+
updateForm: shape,
|
|
76
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** PBS 4.2.6-1 operations through @distilled.cloud/proxmox-backup. */
|
|
2
|
+
import * as config from '@distilled.cloud/proxmox-backup/config';
|
|
3
|
+
import * as Effect from 'effect/Effect';
|
|
4
|
+
import { runPbs } from './distilled-pbs.ts';
|
|
5
|
+
import type { PbsVerifyJobProps } from './pbs-verify-job.ts';
|
|
6
|
+
import { shape } from './pbs-verify-job-form.ts';
|
|
7
|
+
import { spec } from './pbs-verify-job-spec.ts';
|
|
8
|
+
|
|
9
|
+
/** SDK member names differ from the vendor form names; omission stays unmanaged. */
|
|
10
|
+
const members = (form: Record<string, string>) => ({
|
|
11
|
+
...(form['ignore-verified'] === undefined ? {} : { ignore_verified: form['ignore-verified'] }),
|
|
12
|
+
...(form['store'] === undefined ? {} : { store: form['store'] }),
|
|
13
|
+
...(form['schedule'] === undefined ? {} : { schedule: form['schedule'] }),
|
|
14
|
+
...(form['comment'] === undefined ? {} : { comment: form['comment'] }),
|
|
15
|
+
...(form['max-depth'] === undefined ? {} : { max_depth: form['max-depth'] }),
|
|
16
|
+
...(form['ns'] === undefined ? {} : { ns: form['ns'] }),
|
|
17
|
+
...(form['outdated-after'] === undefined ? {} : { outdated_after: form['outdated-after'] }),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const createRequest = (props: PbsVerifyJobProps): config.VerifyConfigRequest => ({
|
|
21
|
+
...members({ ...shape(props), id: props.id }),
|
|
22
|
+
id: props.id,
|
|
23
|
+
store: props.store,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const updateRequest = (props: PbsVerifyJobProps): config.PutConfigVerifyRequest => ({
|
|
27
|
+
...members(shape(props)),
|
|
28
|
+
id: props.id,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/** Preserve the state attribute shape while the SDK decodes its typed field names. */
|
|
32
|
+
const wire = (row: config.GetConfigVerifyResponse): Record<string, unknown> => ({
|
|
33
|
+
...row,
|
|
34
|
+
'ignore-verified': row.ignore_verified,
|
|
35
|
+
'max-depth': row.max_depth,
|
|
36
|
+
'outdated-after': row.outdated_after,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/** ⛔ Only the SDK VerifyJobNotFound is absence; permission, transport and malformed replies propagate. */
|
|
40
|
+
export const readOne = (props: PbsVerifyJobProps) =>
|
|
41
|
+
runPbs(props.target, 'read', config.getConfigVerify({ id: props.id })).pipe(
|
|
42
|
+
Effect.map((row) => spec.attributes(wire(row), props)),
|
|
43
|
+
Effect.catchTag('VerifyJobNotFound', () => Effect.succeed(undefined)),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
export const createOne = (props: PbsVerifyJobProps) =>
|
|
47
|
+
runPbs(props.target, 'provision', config.verifyConfig(createRequest(props)));
|
|
48
|
+
|
|
49
|
+
export const updateOne = (props: PbsVerifyJobProps) =>
|
|
50
|
+
runPbs(props.target, 'provision', config.putConfigVerify(updateRequest(props)));
|
|
51
|
+
|
|
52
|
+
/** Deleting the schedule never deletes backups; already missing is a successful deletion. */
|
|
53
|
+
export const deleteOne = (props: PbsVerifyJobProps) =>
|
|
54
|
+
Effect.gen(function* () {
|
|
55
|
+
// ⛔ Missing-item tags are measured for GET only. Do not guess the DELETE error shape.
|
|
56
|
+
if ((yield* readOne(props)) === undefined) return;
|
|
57
|
+
return yield* runPbs(props.target, 'provision', config.deleteConfigVerify({ id: props.id }));
|
|
58
|
+
});
|
|
@@ -9,22 +9,22 @@
|
|
|
9
9
|
* green `Proxmox.BackupJob` makes it worse rather than better, because a vzdump job that says
|
|
10
10
|
* "OK" every night is exactly the evidence people stop at.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* ⚠️ HISTORICAL BOOTSTRAP BLOCKER, 2026-09-13: TWO THINGS OUTSIDE THIS FILE WERE MISSING
|
|
13
|
+
* before PBS could authenticate. Both are now resolved by the PBS lane and SDK:
|
|
14
14
|
* 1. `credentials.ts` has no PBS mint. MEASURED 2026-09-13 from this Mac: the agent's
|
|
15
15
|
* approle is refused `sys/mounts`, `kv/infra/proxmox` and `proxmox-pbs/creds/read` alike —
|
|
16
16
|
* 403 permission denied on all three — so there is no mount to name and none is invented.
|
|
17
17
|
* 2. `client.ts`'s `authorization()` builds a PVE header, and PBS will not accept it. See the
|
|
18
18
|
* ⛔ on `PbsTarget`.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* Before both landed, every call 401'd, `pveOperations.read` folded that into "absent", and the plan
|
|
20
|
+
* reported `create` for a job that was plainly there. The deploy then FAILED at the POST rather
|
|
21
21
|
* than clobbering anything — an unauthenticated POST 401s, and an authenticated one is refused
|
|
22
22
|
* because PBS will not take a duplicate section id — so the cost is a LYING PLAN and a broken
|
|
23
23
|
* run, not a lost job. That is still reason enough not to wire it into a stack yet: a plan
|
|
24
24
|
* nobody can trust is its own outage.
|
|
25
25
|
*
|
|
26
|
-
* ⚠️ THE SHAPES
|
|
27
|
-
* taken from `pbs-api-types/src/jobs.rs`, `src/api2/config/verify.rs`, `src/server/verify_job.rs`
|
|
26
|
+
* ⚠️ THE ORIGINAL SHAPES WERE READ FROM PUBLISHED SOURCE, NOT MEASURED ON THIS ESTATE. Everything is
|
|
27
|
+
* originally taken from `pbs-api-types/src/jobs.rs`, `src/api2/config/verify.rs`, `src/server/verify_job.rs`
|
|
28
28
|
* and `src/backup/verify.rs` at HEAD of the Proxmox git mirrors on 2026-09-13. What WAS measured
|
|
29
29
|
* here: `pbs.example.com:8007` and `pbs.mgmt.example.com:8007` both answer HTTP 401 under
|
|
30
30
|
* STRICT TLS (curl `ssl_verify_result=0`), so no `-k` is needed and `client.ts`'s plain `fetch`
|
|
@@ -34,10 +34,9 @@
|
|
|
34
34
|
import { Resource } from 'alchemy';
|
|
35
35
|
import * as Provider from 'alchemy/Provider';
|
|
36
36
|
import * as Effect from 'effect/Effect';
|
|
37
|
+
import { handlers } from './pbs-verify-job-lifecycle.ts';
|
|
37
38
|
import type { PbsTarget } from './credentials.ts';
|
|
38
|
-
import {
|
|
39
|
-
import { type PveRequirements, pveHandlers } from './resource.ts';
|
|
40
|
-
import { bool, int, text } from './values.ts';
|
|
39
|
+
import type { PveRequirements } from './resource-spec.ts';
|
|
41
40
|
|
|
42
41
|
export interface PbsVerifyJobProps {
|
|
43
42
|
/** ⛔ A PBS host, never a PVE cluster. See `PbsTarget`. */
|
|
@@ -141,78 +140,7 @@ export interface PbsVerifyJob extends Resource<
|
|
|
141
140
|
export const PbsVerifyJob = Resource<PbsVerifyJob>('Pbs.VerifyJob');
|
|
142
141
|
|
|
143
142
|
/**
|
|
144
|
-
*
|
|
145
|
-
* SectionConfig round-trip, `GET /config/verify/{id}` serialises a Rust struct, so
|
|
146
|
-
* `ignore-verified` arrives as a JSON boolean and `max-depth` as a JSON number. `bool` and `int`
|
|
147
|
-
* accept both spellings, and using them costs nothing while covering the version that does not.
|
|
148
|
-
* ⚠️ `digest` IS NOT AN ATTRIBUTE AND CANNOT BECOME ONE BY ACCIDENT. PBS puts it on the rpcenv, and
|
|
149
|
-
* the JSON formatter adds rpcenv attributes as SIBLINGS of `data` — `{"data":{…},"digest":"…"}` —
|
|
150
|
-
* so `client.ts`, which returns `body.data`, never sees it. That is lucky rather than designed:
|
|
151
|
-
* the digest covers verification.cfg as a FILE, so keeping it would churn this resource's state
|
|
152
|
-
* whenever an unrelated verify job was edited.
|
|
153
|
-
*/
|
|
154
|
-
const handlers = pveHandlers<PbsVerifyJobProps, PbsVerifyJobAttributes>({
|
|
155
|
-
attributes: (live, props) => {
|
|
156
|
-
const ignoreVerified = bool(live['ignore-verified'], true);
|
|
157
|
-
const outdatedAfter = int(live['outdated-after'], -1);
|
|
158
|
-
return {
|
|
159
|
-
comment: text(live['comment'], ''),
|
|
160
|
-
id: props.id,
|
|
161
|
-
'ignore-verified': ignoreVerified,
|
|
162
|
-
'max-depth': int(live['max-depth'], -1),
|
|
163
|
-
ns: text(live['ns'], ''),
|
|
164
|
-
'outdated-after': outdatedAfter,
|
|
165
|
-
rechecks: rechecks(ignoreVerified, outdatedAfter),
|
|
166
|
-
schedule: text(live['schedule'], ''),
|
|
167
|
-
store: text(live['store'], props.store),
|
|
168
|
-
};
|
|
169
|
-
},
|
|
170
|
-
/**
|
|
171
|
-
* 🔴 THIS WAS `config/verification` AND PBS ANSWERS 404 FOR IT. MEASURED on the live 4.2 server:
|
|
172
|
-
* `GET /api2/json/config/verification` -> 404 "Path not found"; `GET /api2/json/config/verify`
|
|
173
|
-
* -> 200 with both jobs. The struct is `VerificationJobConfig` and the CLI subcommand is
|
|
174
|
-
* `verify-job`, so the long spelling reads right and is simply not the route.
|
|
175
|
-
* ⛔ THE FAILURE MODE IS THE ONE THIS PACKAGE KEEPS FINDING: `read` folds every failure into
|
|
176
|
-
* `undefined`, so a 404 from a WRONG PATH is indistinguishable from an object that is not
|
|
177
|
-
* there. The first plan against the live estate said `create` for two verification jobs that
|
|
178
|
-
* have existed for weeks — and a create would then have POSTed to a 404 as well, so it fails
|
|
179
|
-
* loudly rather than duplicating anything. Nothing caught it earlier because no verification
|
|
180
|
-
* job had ever been declared; registering the provider exercises no path at all.
|
|
181
|
-
*/
|
|
182
|
-
collection: () => 'config/verify',
|
|
183
|
-
/** ⛔ `id` IS SENT. PBS refuses a duplicate id, and that refusal is this family's safety net. */
|
|
184
|
-
createForm: (props) => ({ ...shape(props), id: props.id }),
|
|
185
|
-
/**
|
|
186
|
-
* ⚠️ AN UNDECLARED FIELD IS NEITHER SENT NOR COMPARED — the `Proxmox.BackupJob` trade, for the
|
|
187
|
-
* same reason and with one extra: this form carries NO `delete` list, so there is nothing it
|
|
188
|
-
* could clear even if it wanted to. See the ⛔ on `withClears` in pbs-verify-job-form.ts.
|
|
189
|
-
* ⚠️ `store` IS COMPARED AND IS MUTABLE, which is unusual and worth reading twice. PBS's update
|
|
190
|
-
* handler assigns it and re-checks privileges on both the old and the new path, so changing
|
|
191
|
-
* `store` MOVES the job — and the datastore it left is then verified by nothing. That is a
|
|
192
|
-
* one-word edit with the same effect as deleting the job, and it plans as a quiet `update`.
|
|
193
|
-
* ⚠️ `rechecks` IS ABSENT HERE ON PURPOSE. It is a rendering of the two fields on the lines
|
|
194
|
-
* above; comparing it too would report the same drift twice.
|
|
195
|
-
*/
|
|
196
|
-
/** The vendor rules these forms are checked against at plan time — resource-spec.ts. */
|
|
197
|
-
endpoint: { create: 'pbs:POST /config/verify', update: 'pbs:PUT /config/verify/{id}' },
|
|
198
|
-
matches: (attributes, props) =>
|
|
199
|
-
attributes.store === props.store &&
|
|
200
|
-
// ⚠️ `null` IS COMPARED AGAINST `''`, NOT SKIPPED. A parked job is a declaration like any
|
|
201
|
-
// other, so declaring `null` against a job that HAS a schedule must plan an update — the
|
|
202
|
-
// form then omits `schedule`, which is a set-only form, so see the ⛔ on clearing below.
|
|
203
|
-
attributes.schedule === (props.schedule ?? '') &&
|
|
204
|
-
attributes['ignore-verified'] === (props['ignore-verified'] !== false) &&
|
|
205
|
-
(props['outdated-after'] === undefined ||
|
|
206
|
-
attributes['outdated-after'] === props['outdated-after']) &&
|
|
207
|
-
(props['max-depth'] === undefined || attributes['max-depth'] === props['max-depth']) &&
|
|
208
|
-
(props.ns === undefined || attributes.ns === props.ns) &&
|
|
209
|
-
(props.comment === undefined || attributes.comment === props.comment),
|
|
210
|
-
path: (props) => `config/verify/${props.id}`,
|
|
211
|
-
updateForm: shape,
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* ⛔ `list` IS EMPTY, INHERITED FROM `pveHandlers`, AND THE REASON IS SHARPER HERE THAN ELSEWHERE.
|
|
143
|
+
* ⛔ `list` STAYS EXPLICITLY EMPTY, AND THE REASON IS SHARPER HERE THAN ELSEWHERE.
|
|
216
144
|
* `GET /config/verify` returns every verification job the caller can see, including the
|
|
217
145
|
* ones the PBS installer and a human made. Adopting those would put Alchemy one dropped line
|
|
218
146
|
* away from DELETING a datastore's only verification schedule — and deleting it destroys nothing
|
|
@@ -220,10 +148,10 @@ const handlers = pveHandlers<PbsVerifyJobProps, PbsVerifyJobAttributes>({
|
|
|
220
148
|
* to say when the rot started. Adoption stays explicit: declare the existing id.
|
|
221
149
|
*
|
|
222
150
|
* ⚠️ NO `digest` IS SENT ON UPDATE OR DELETE, SO THIS IS LAST-WRITER-WINS. PBS accepts an optional
|
|
223
|
-
* `digest` on both and would refuse a write made against a stale read.
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* rather than refused.
|
|
151
|
+
* `digest` on both and would refuse a write made against a stale read. This migration preserves
|
|
152
|
+
* the existing forms, which omit it; the SDK runner reuses leased credentials but does not add
|
|
153
|
+
* an optimistic-lock check. A concurrent edit from the PBS UI may therefore be overwritten
|
|
154
|
+
* rather than refused. PBS offers that stronger guarantee as a separate provider change.
|
|
227
155
|
*/
|
|
228
156
|
export const PbsVerifyJobProvider = () =>
|
|
229
157
|
Provider.effect(PbsVerifyJob, Effect.succeed(PbsVerifyJob.Provider.of(handlers)));
|
package/src/proxmox/pool.ts
CHANGED
|
@@ -16,14 +16,17 @@
|
|
|
16
16
|
* reconcile needs, so widening stays a deliberate act rather than a reaction to a 403.
|
|
17
17
|
*/
|
|
18
18
|
import { Resource } from 'alchemy';
|
|
19
|
+
import { isResolved } from 'alchemy/Diff';
|
|
19
20
|
import * as Provider from 'alchemy/Provider';
|
|
21
|
+
import * as pools from '@distilled.cloud/proxmox/pools';
|
|
22
|
+
import { ProxmoxParseError } from '@distilled.cloud/proxmox/Errors';
|
|
23
|
+
import * as Schema from 'effect/Schema';
|
|
20
24
|
import * as Effect from 'effect/Effect';
|
|
21
|
-
import type {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
} from './
|
|
26
|
-
import { type PveRequirements, type WithTarget, pveHandlers } from './resource.ts';
|
|
25
|
+
import type { PoolsPoolidPutParams, PoolsPostParams } from './generated/pve.ts';
|
|
26
|
+
import type { PveRequirements, PveSpec, WithTarget } from './resource-spec.ts';
|
|
27
|
+
import { runPve } from './distilled-pve.ts';
|
|
28
|
+
import { specGuards } from './resource-guard.ts';
|
|
29
|
+
import { formToSend } from './update-guard.ts';
|
|
27
30
|
|
|
28
31
|
export interface PoolProps extends WithTarget {
|
|
29
32
|
/** PVE's primary key for a pool. Changing it is a replace, not an update. */
|
|
@@ -60,9 +63,9 @@ export const poolUpdateForm = (props: PoolProps): Pick<PoolsPoolidPutParams, 'co
|
|
|
60
63
|
comment: props.comment ?? '',
|
|
61
64
|
});
|
|
62
65
|
|
|
63
|
-
const
|
|
66
|
+
const spec = {
|
|
64
67
|
attributes: (live, props) => {
|
|
65
|
-
const row = live
|
|
68
|
+
const row = live;
|
|
66
69
|
return {
|
|
67
70
|
comment: typeof row.comment === 'string' ? row.comment : '',
|
|
68
71
|
members: Array.isArray(row.members) ? row.members.length : 0,
|
|
@@ -76,7 +79,86 @@ const handlers = pveHandlers<PoolProps, PoolAttributes>({
|
|
|
76
79
|
matches: (attributes, props) => attributes.comment === (props.comment ?? ''),
|
|
77
80
|
path: (props) => `pools/${props.poolid}`,
|
|
78
81
|
updateForm: poolUpdateForm,
|
|
79
|
-
}
|
|
82
|
+
} satisfies PveSpec<PoolProps, PoolAttributes>;
|
|
83
|
+
|
|
84
|
+
const { guardCreate, guardUpdate } = specGuards(spec);
|
|
85
|
+
|
|
86
|
+
/** Pool absence is PVE's typed 500, not a catch-all; pve-manager 9.2.11. */
|
|
87
|
+
export const readPool = (props: PoolProps) =>
|
|
88
|
+
runPve(props.target, 'read', false, pools.getPool({ poolid: props.poolid })).pipe(
|
|
89
|
+
Effect.flatMap((live) =>
|
|
90
|
+
Schema.decodeUnknownEffect(Schema.toType(pools.GetPoolResponse))(live).pipe(
|
|
91
|
+
Effect.mapError(
|
|
92
|
+
() =>
|
|
93
|
+
new ProxmoxParseError({
|
|
94
|
+
body: undefined,
|
|
95
|
+
cause: 'Pool response does not match its vendor schema',
|
|
96
|
+
}),
|
|
97
|
+
),
|
|
98
|
+
Effect.as(live),
|
|
99
|
+
),
|
|
100
|
+
),
|
|
101
|
+
Effect.map((live) => spec.attributes({ ...live }, props)),
|
|
102
|
+
Effect.catchTag('PoolNotFound', () => Effect.succeed(undefined)),
|
|
103
|
+
Effect.catchTag('NotFound', () => Effect.succeed(undefined)),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
export const deletePool = (props: PoolProps) =>
|
|
107
|
+
runPve(props.target, 'provision', true, pools.deletePool({ poolid: props.poolid })).pipe(
|
|
108
|
+
Effect.catchTag('PoolNotFound', () => Effect.void),
|
|
109
|
+
Effect.catchTag('NotFound', () => Effect.void),
|
|
110
|
+
);
|
|
80
111
|
|
|
81
112
|
export const ProxmoxPoolProvider = () =>
|
|
82
|
-
Provider.effect(
|
|
113
|
+
Provider.effect(
|
|
114
|
+
ProxmoxPool,
|
|
115
|
+
Effect.succeed(
|
|
116
|
+
ProxmoxPool.Provider.of({
|
|
117
|
+
/** Pools are adopted explicitly; an index must not silently claim every existing pool. */
|
|
118
|
+
list: () => Effect.succeed([]),
|
|
119
|
+
read: ({ olds }) => readPool(olds),
|
|
120
|
+
diff: Effect.fn(function* ({ news, output }) {
|
|
121
|
+
if (!isResolved(news)) return undefined;
|
|
122
|
+
yield* guardCreate(news, output === undefined);
|
|
123
|
+
yield* guardUpdate(news);
|
|
124
|
+
if (output === undefined) return undefined;
|
|
125
|
+
const live = yield* readPool(news);
|
|
126
|
+
if (live === undefined) {
|
|
127
|
+
yield* guardCreate(news, true);
|
|
128
|
+
return { action: 'update' } as const;
|
|
129
|
+
}
|
|
130
|
+
return { action: spec.matches(live, news) ? 'noop' : 'update' } as const;
|
|
131
|
+
}),
|
|
132
|
+
reconcile: Effect.fn(function* ({ news }) {
|
|
133
|
+
const live = yield* readPool(news);
|
|
134
|
+
yield* guardCreate(news, live === undefined);
|
|
135
|
+
yield* guardUpdate(news);
|
|
136
|
+
if (live === undefined) {
|
|
137
|
+
yield* runPve(news.target, 'provision', true, pools.createPool(poolCreateForm(news)));
|
|
138
|
+
} else {
|
|
139
|
+
const form = formToSend(spec.matches, live, news, poolUpdateForm(news));
|
|
140
|
+
if (form !== undefined)
|
|
141
|
+
yield* runPve(
|
|
142
|
+
news.target,
|
|
143
|
+
'provision',
|
|
144
|
+
true,
|
|
145
|
+
pools.putPool({
|
|
146
|
+
...form,
|
|
147
|
+
poolid: news.poolid,
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
const after = yield* readPool(news);
|
|
152
|
+
if (after === undefined)
|
|
153
|
+
return yield* Effect.fail(
|
|
154
|
+
new Error(
|
|
155
|
+
`pools/${news.poolid}: write returned success but the pool is still absent`,
|
|
156
|
+
),
|
|
157
|
+
);
|
|
158
|
+
return after;
|
|
159
|
+
}),
|
|
160
|
+
/** A nonempty-pool refusal still propagates; there is no force-delete fallback. */
|
|
161
|
+
delete: ({ olds }) => deletePool(olds),
|
|
162
|
+
}),
|
|
163
|
+
),
|
|
164
|
+
);
|