@homeflare/alchemy 0.22.0 → 0.23.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/dist/proxmox/api-token-form.d.ts +12 -1
- package/dist/proxmox/api-token-form.d.ts.map +1 -1
- package/dist/proxmox/api-token.d.ts +8 -3
- package/dist/proxmox/api-token.d.ts.map +1 -1
- package/dist/proxmox/generated/pve/cluster-sdn-fabrics.d.ts +13 -13
- package/dist/proxmox/generated/pve/cluster-sdn-fabrics.d.ts.map +1 -1
- package/dist/proxmox/index.d.ts +9 -2
- package/dist/proxmox/index.d.ts.map +1 -1
- package/dist/proxmox/index.js +17 -9
- package/dist/proxmox/index.js.map +8 -7
- package/dist/proxmox/zfs-pool-spec.d.ts +27 -0
- package/dist/proxmox/zfs-pool-spec.d.ts.map +1 -0
- package/dist/proxmox/zfs-pool-write.d.ts +48 -7
- package/dist/proxmox/zfs-pool-write.d.ts.map +1 -1
- package/dist/proxmox/zfs-pool.d.ts +42 -20
- package/dist/proxmox/zfs-pool.d.ts.map +1 -1
- package/docs/adopted-deploys.md +14 -0
- package/package.json +1 -1
- package/src/proxmox/api-token-adopt.test.ts +138 -0
- package/src/proxmox/api-token-form.ts +19 -2
- package/src/proxmox/api-token.ts +8 -3
- package/src/proxmox/generated/pve/cluster-sdn-fabrics.ts +23 -13
- package/src/proxmox/index.test.ts +5 -2
- package/src/proxmox/index.ts +9 -2
- package/src/proxmox/zfs-pool-adopt.test.ts +141 -0
- package/src/proxmox/zfs-pool-spec.ts +76 -0
- package/src/proxmox/zfs-pool-write.ts +63 -9
- package/src/proxmox/zfs-pool.ts +45 -95
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `PveSpec` for `Proxmox.ZfsPool`: where a pool lives, what its GET reports, and the vendor
|
|
3
|
+
* endpoint its create form is checked against.
|
|
4
|
+
*
|
|
5
|
+
* ★ SPLIT OUT OF zfs-pool.ts TO KEEP THAT FILE UNDER THE 250-LINE CAP, and the seam is the same
|
|
6
|
+
* one metric-server-form.ts and zfs-pool-write.ts already cut on: zfs-pool.ts answers "what is
|
|
7
|
+
* a zpool and when has it changed", this file answers "what PVE's own GET/{node}/disks/zfs
|
|
8
|
+
* collection and item say, read into that shape". `leafDevices` moved with `spec` because it
|
|
9
|
+
* exists for exactly one field of `spec.attributes` and nothing else in this package calls it.
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ THE `import type` BACK TO zfs-pool.ts IS A CYCLE ON PAPER ONLY — type-only, erased before
|
|
12
|
+
* anything runs, so the resource's public shape stays in the file that declares the resource.
|
|
13
|
+
* Same reasoning as zfs-pool-write.ts.
|
|
14
|
+
*/
|
|
15
|
+
import type { PveSpec } from './resource.ts';
|
|
16
|
+
import type { ZfsPoolAttributes, ZfsPoolProps } from './zfs-pool.ts';
|
|
17
|
+
/**
|
|
18
|
+
* The device paths at the bottom of PVE's vdev tree, in the order ZFS reports them.
|
|
19
|
+
*
|
|
20
|
+
* ⚠️ "NO CHILDREN" IS THE LEAF TEST, AND IT IS NOT A GUESS — MEASURED in the node's `preparetree`,
|
|
21
|
+
* which sets `leaf` to 0 when there are children and 1 otherwise. Reading the flag would work
|
|
22
|
+
* equally well; recursing on `children` needs no second field to be present and is what makes
|
|
23
|
+
* nested sections (mirrors inside a raid10, `spares`, `cache`) flatten correctly.
|
|
24
|
+
*/
|
|
25
|
+
export declare const leafDevices: (children: unknown) => string[];
|
|
26
|
+
export declare const spec: PveSpec<ZfsPoolProps, ZfsPoolAttributes>;
|
|
27
|
+
//# sourceMappingURL=zfs-pool-spec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zfs-pool-spec.d.ts","sourceRoot":"","sources":["../../src/proxmox/zfs-pool-spec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAG7C,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAErE;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,aAAc,OAAO,KAAG,MAAM,EAO9C,CAAC;AAET,eAAO,MAAM,IAAI,EAAE,OAAO,CAAC,YAAY,EAAE,iBAAiB,CAuCzD,CAAC"}
|
|
@@ -35,15 +35,44 @@ import type { ZfsPoolAttributes, ZfsPoolProps } from './zfs-pool.ts';
|
|
|
35
35
|
* declaration asked for, and this is the one call that writes to physical disks.
|
|
36
36
|
*/
|
|
37
37
|
/**
|
|
38
|
-
* The vendor rules
|
|
38
|
+
* The vendor rules a FULL declaration's create form is checked against at plan time.
|
|
39
39
|
*
|
|
40
40
|
* ⛔ HERE RATHER THAN INLINE IN zfs-pool.ts BECAUSE THAT FILE IS ALREADY OVER THE 250-LINE CAP,
|
|
41
41
|
* and a `pve:POST …` literal has to live in a non-test source file for the generator to table
|
|
42
|
-
* it at all.
|
|
42
|
+
* it at all. zfs-pool-spec.ts imports it on the line it already imports `createForm` from.
|
|
43
43
|
* ⚠️ NO UPDATE KEY: PVE registers no PUT under `/nodes/{node}/disks/zfs`, which is exactly why
|
|
44
44
|
* the spec declares no `updateForm` and a changed prop is a REPLACE.
|
|
45
|
+
* ⚠️ `spec.endpoint` DOES NOT RETURN THIS CONSTANT DIRECTLY — see the ★ on `zfsPoolEndpoint`
|
|
46
|
+
* below, and the ⛔ on `devices`/`raidlevel` in zfs-pool.ts (decision 9, 2026-09-23).
|
|
45
47
|
*/
|
|
46
48
|
export declare const ZFS_POOL_ENDPOINT: EndpointPair;
|
|
49
|
+
/**
|
|
50
|
+
* ★ `PveSpec['endpoint']` AS A FUNCTION OF PROPS, THE WAY notification-target.ts AND
|
|
51
|
+
* ceph-daemon.ts ALREADY DO IT FOR A DIFFERENT REASON (resource-spec.ts's ⚠️ on that field).
|
|
52
|
+
* Here the reason is decision 9's adopt-only mode: `guardCreate` runs on the FIRST plan of
|
|
53
|
+
* ANY declaration (resource.ts `ops.diff`, `output === undefined` branch) and checks the
|
|
54
|
+
* create form's REQUIRED fields whenever `endpoint.create` resolves to a key — and PVE marks
|
|
55
|
+
* both `devices` and `raidlevel` required (measured, codegen/constraints's `pve-nodes-disks`
|
|
56
|
+
* row). An adopt-only declaration deliberately omits both, so returning the real key there
|
|
57
|
+
* would refuse a MATCHING adoption at plan time, before `read` ever ran — exactly the gap
|
|
58
|
+
* `guardCreate`/`guardUpdate` being exported (resource.ts's ★) says this family must not fall
|
|
59
|
+
* through. Returning `{}` (no `create` key) makes `guardCreate` a no-op (`constraint-guard.ts`:
|
|
60
|
+
* `create === undefined ? Effect.void : …`) for an adopt-only declaration, while a FULL one
|
|
61
|
+
* (`devices` and `raidlevel` both present) is checked exactly as before.
|
|
62
|
+
* ⚠️ THE RUNTIME BACKSTOP IS `createPool` BELOW, NOT THIS FUNCTION. Skipping the plan-time guard
|
|
63
|
+
* for an adopt-only declaration does not skip the check that the pool is actually there —
|
|
64
|
+
* `createPool` refuses before any POST when the read comes back absent and either field is
|
|
65
|
+
* undefined, which is what a genuinely absent adopt-only declaration hits at apply time.
|
|
66
|
+
*/
|
|
67
|
+
export declare const zfsPoolEndpoint: (props: ZfsPoolProps) => EndpointPair;
|
|
68
|
+
/**
|
|
69
|
+
* ⚠️ EVERY KEY IS OMITTED WHEN ABSENT, `devices` AND `raidlevel` INCLUDED — not just the
|
|
70
|
+
* optional ones. An adopt-only declaration (both undefined, decision 9) must produce a form
|
|
71
|
+
* `createPool`'s own guard can inspect for their absence, and one this function itself does
|
|
72
|
+
* not throw building: `props.devices.join(',')` on an undefined `devices` is exactly the
|
|
73
|
+
* `TypeError` the plan-time guard hit before this change, because `guardCreate` evaluates
|
|
74
|
+
* `spec.createForm(props)` eagerly (resource-guard.ts) — before it even looks at `presence`.
|
|
75
|
+
*/
|
|
47
76
|
export declare const createForm: (props: ZfsPoolProps) => Record<string, string>;
|
|
48
77
|
/**
|
|
49
78
|
* Create the pool if it is absent, and otherwise leave it completely alone.
|
|
@@ -69,14 +98,26 @@ export declare const createForm: (props: ZfsPoolProps) => Record<string, string>
|
|
|
69
98
|
* ⚠️ SHORT POLLS WITH A HARD CAP, NOT ONE LONG WAIT. `read` cannot tell "not yet" from "forbidden"
|
|
70
99
|
* — both are `undefined` — so the loop gives up with the UPID rather than retrying forever, and
|
|
71
100
|
* the UPID is the only thing that leads to the worker's real error.
|
|
101
|
+
*
|
|
102
|
+
* ⛔ THE ADOPT-ONLY GUARD, RIGHT AFTER THE READ AND BEFORE ANYTHING ELSE. Decision 9 (2026-09-23):
|
|
103
|
+
* omitting `devices` and `raidlevel` declares a pool this resource may only ADOPT, for a pool
|
|
104
|
+
* PVE's own schema cannot fully describe — n1's `speed` is a stripe, and `raidlevel`'s enum has
|
|
105
|
+
* no stripe (zfs-pool.ts). `zfsPoolEndpoint` above already keeps the vendor constraint guard
|
|
106
|
+
* from refusing such a declaration at PLAN time; this is the matching refusal at APPLY time, for
|
|
107
|
+
* the one case that guard cannot see — the pool genuinely is not there. MEASURED without it: a
|
|
108
|
+
* tolerant `createForm` sends `POST … {name: "rpool"}` with no `devices`/`raidlevel`, which PVE's
|
|
109
|
+
* own schema would 400 on eventually, but only after the request left this process; refusing
|
|
110
|
+
* here is the same `Effect.die` idiom as every other refusal in this package, and it costs
|
|
111
|
+
* nothing this pool did not already lack.
|
|
72
112
|
*/
|
|
73
113
|
export declare const createPool: <E, R>(props: ZfsPoolProps, collection: string, path: string, read: (props: ZfsPoolProps) => Effect.Effect<ZfsPoolAttributes | undefined, E, R>) => Effect.Effect<ZfsPoolAttributes, E | Error, R | import("effect/unstable/http/HttpClient").HttpClient>;
|
|
74
114
|
/**
|
|
75
|
-
* ⛔ `DELETE /nodes/{node}/disks/zfs/{name}` RUNS `zpool destroy` AND THE DATASETS GO WITH IT.
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* `defaultRemovalPolicy: 'retain'`
|
|
79
|
-
*
|
|
115
|
+
* ⛔ `DELETE /nodes/{node}/disks/zfs/{name}` RUNS `zpool destroy` AND THE DATASETS GO WITH IT. Per
|
|
116
|
+
* S14 (alchemy-provider-standard), it is implemented rather than stubbed — a `delete` that
|
|
117
|
+
* silently does nothing lies to whoever reads the plan — and what keeps an ordinary removed
|
|
118
|
+
* declaration from reaching it is the resource's `defaultRemovalPolicy: 'retain'` (zfs-pool.ts):
|
|
119
|
+
* an orphaned pool is forgotten, not destroyed, unless a caller opts in with
|
|
120
|
+
* `.pipe(RemovalPolicy.destroy())`. See the corrected header in zfs-pool.ts.
|
|
80
121
|
*
|
|
81
122
|
* ⚠️ `cleanup-config` IS NOT SENT. It would additionally remove the PVE storage entry pointing
|
|
82
123
|
* at this pool, which is a different object with its own resource (`Proxmox.Storage`); one
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zfs-pool-write.d.ts","sourceRoot":"","sources":["../../src/proxmox/zfs-pool-write.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAErE;;;;;;;;;;;;;GAaG;AACH
|
|
1
|
+
{"version":3,"file":"zfs-pool-write.d.ts","sourceRoot":"","sources":["../../src/proxmox/zfs-pool-write.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAErE;;;;;;;;;;;;;GAaG;AACH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB,EAAE,YAA6D,CAAC;AAE9F;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,eAAe,UAAW,YAAY,KAAG,YACiC,CAAC;AAExF;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,UAAW,YAAY,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAOpE,CAAC;AAKH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,EAAE,CAAC,SACtB,YAAY,cACP,MAAM,QACZ,MAAM,QACN,CAAC,KAAK,EAAE,YAAY,KAAK,MAAM,CAAC,MAAM,CAAC,iBAAiB,GAAG,SAAS,EAAE,CAAC,EAAE,CAAC,CAAC,0GA8C/E,CAAC;AAEL;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,WAAW,SAAU,YAAY,QAAQ,MAAM,wFACb,CAAC"}
|
|
@@ -6,20 +6,22 @@
|
|
|
6
6
|
* moment the worker runs. Every other decision in this file follows from that one sentence, and
|
|
7
7
|
* each of them is written out below rather than left to be inferred.
|
|
8
8
|
*
|
|
9
|
-
* ⛔
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* `
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
9
|
+
* ⛔ ★ CORRECTED 2026-09-23 — THIS PARAGRAPH PREVIOUSLY SAID "DESTROY IS REFUSED" AND "`delete`
|
|
10
|
+
* MAKES NO API CALL". THAT WAS FALSE: MEASURED against this file's own `handlers.delete` below,
|
|
11
|
+
* which calls `destroyPool`, which sends `DELETE /nodes/{node}/disks/zfs/{name}` — the node's
|
|
12
|
+
* own source forks `zfsremove`, which runs `zpool destroy <name>` unconditionally, and with
|
|
13
|
+
* `cleanup-disks` then calls `wipe_blockdev` on every member (neither flag is offered as a prop,
|
|
14
|
+
* because there is no call here for them to reach). `delete` is FULLY IMPLEMENTED, per S14
|
|
15
|
+
* (alchemy-provider-standard): a `delete` that silently does nothing lies to whoever reads the
|
|
16
|
+
* plan. What actually keeps an ordinary removed declaration from reaching it is
|
|
17
|
+
* `defaultRemovalPolicy: 'retain'` below — Alchemy SKIPS `provider.delete` entirely for an
|
|
18
|
+
* orphaned or destroyed resource under `retain`, so the state row drops and the zpool and its
|
|
19
|
+
* data are left exactly where they are. The asymmetry is still the whole argument: an ORPHANED
|
|
20
|
+
* pool is recoverable (declare it again, or destroy it by hand once a human has looked at which
|
|
21
|
+
* disks are in it), a DESTROYED pool is not — which is why `retain` is the default rather than a
|
|
22
|
+
* documented caution, and why `delete` runs only for a caller who opts in by name with
|
|
23
|
+
* `.pipe(RemovalPolicy.destroy())`. See the ★ on `defaultRemovalPolicy` in resource-spec.ts, and
|
|
24
|
+
* zfs-pool-adopt.test.ts's mutation row, which pins exactly one DELETE reaching the fake cluster.
|
|
23
25
|
*
|
|
24
26
|
* ⛔ THERE IS NO PUT ON THIS FAMILY AT ALL. MEASURED from the cluster's own published schema
|
|
25
27
|
* (`/usr/share/pve-docs/api-viewer/apidoc.js`, read on node-b 2026-09-13): `/nodes/{node}/disks/zfs`
|
|
@@ -47,7 +49,8 @@
|
|
|
47
49
|
* ⚠️ TWO HANDLERS ARE OVERRIDDEN AND THE OTHER THREE COME FROM THE FACTORY — see the ★ above
|
|
48
50
|
* `handlers`. `delete` is the ⛔ above. `reconcile` is because the create is a FORKED WORKER, so
|
|
49
51
|
* the factory's immediate read-back would report a successful create as a failure; the measured
|
|
50
|
-
* detail
|
|
52
|
+
* detail, and the adopt-only refusal decision 9 adds, are both in `createPool` in
|
|
53
|
+
* zfs-pool-write.ts.
|
|
51
54
|
*
|
|
52
55
|
* ⚠️ PRIVILEGES, FROM THE SCHEMA. Read and diff need `Sys.Audit` on `/` — both GETs check it.
|
|
53
56
|
* Reconcile needs `Sys.Modify` on `/` for the POST. `Datastore.Allocate` on `/storage` is NOT
|
|
@@ -56,10 +59,19 @@
|
|
|
56
59
|
*/
|
|
57
60
|
import { Resource } from 'alchemy';
|
|
58
61
|
import * as Provider from 'alchemy/Provider';
|
|
62
|
+
import type { NodesNodeDisksZfsPostParams } from './generated/pve.ts';
|
|
59
63
|
import { type PveRequirements, type WithTarget } from './resource.ts';
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
/**
|
|
65
|
+
* PVE's layouts, generated (2026-09-23) from `NodesNodeDisksZfsPostParams['raidlevel']`
|
|
66
|
+
* (`generated/pve.ts`, manifest `pve-apidoc` pve-manager 9.2.11 sha256 9def8f13…) rather than
|
|
67
|
+
* hand-typed, per decision 9. ⚠️ Each has a minimum disk count PVE enforces, and `raid10` needs an
|
|
68
|
+
* even one. ⛔ THERE IS NO `stripe` IN THIS ENUM. n1's `speed` pool is one (decision 14 accepts it
|
|
69
|
+
* as-is): `ZfsPoolProps` below is why a stripe pool is declarable at all — as an ADOPT-ONLY
|
|
70
|
+
* declaration with no `raidlevel`, never as a full one.
|
|
71
|
+
*/
|
|
72
|
+
export type ZfsRaidLevel = NonNullable<NodesNodeDisksZfsPostParams['raidlevel']>;
|
|
73
|
+
/** Generated the same way, from `NodesNodeDisksZfsPostParams['compression']`. */
|
|
74
|
+
export type ZfsCompression = NonNullable<NodesNodeDisksZfsPostParams['compression']>;
|
|
63
75
|
/**
|
|
64
76
|
* ⛔ THERE IS NO `add_storage`, `cleanup-config` OR `cleanup-disks` PROP, AND THAT IS NOT AN
|
|
65
77
|
* OVERSIGHT. The last two belong to the DELETE this resource refuses to make. `add_storage=1`
|
|
@@ -91,9 +103,19 @@ export interface ZfsPoolProps extends WithTarget {
|
|
|
91
103
|
* `csv` sorts, and sorting this list silently re-pairs the mirrors into a different pool.
|
|
92
104
|
* ⚠️ PVE's only guard is `assert_disk_unused`, i.e. its own `disk_is_used` over the same `used`
|
|
93
105
|
* column the Disks view shows. A blank disk it considers free is wiped with no confirmation.
|
|
106
|
+
*
|
|
107
|
+
* ⛔ OPTIONAL SINCE DECISION 9 (2026-09-23), AND OMITTING IT MEANS SOMETHING SPECIFIC: leaving
|
|
108
|
+
* both this and `raidlevel` undeclared declares an ADOPT-ONLY pool — one this resource may
|
|
109
|
+
* read and report but never build. That is the only shape available for a pool PVE's own POST
|
|
110
|
+
* schema cannot fully describe, such as n1's `speed`, whose layout is a stripe and whose
|
|
111
|
+
* `raidlevel` enum (above) has no stripe to declare. `createForm` (zfs-pool-write.ts) omits
|
|
112
|
+
* both keys when they are absent, and `createPool` refuses before any POST if the pool it
|
|
113
|
+
* would need to build is not already there — an adopt-only declaration never creates. Declare
|
|
114
|
+
* both fields together for a pool this resource should build from scratch; declaring only one
|
|
115
|
+
* of the two is refused the same way, since PVE's POST requires both.
|
|
94
116
|
*/
|
|
95
|
-
devices
|
|
96
|
-
raidlevel
|
|
117
|
+
devices?: readonly string[];
|
|
118
|
+
raidlevel?: ZfsRaidLevel;
|
|
97
119
|
/**
|
|
98
120
|
* Sector size exponent, 9–16, PVE default 12.
|
|
99
121
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zfs-pool.d.ts","sourceRoot":"","sources":["../../src/proxmox/zfs-pool.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"zfs-pool.d.ts","sourceRoot":"","sources":["../../src/proxmox/zfs-pool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0DG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAE7C,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAA8B,MAAM,eAAe,CAAC;AAIlG;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG,WAAW,CAAC,2BAA2B,CAAC,WAAW,CAAC,CAAC,CAAC;AAEjF,iFAAiF;AACjF,MAAM,MAAM,cAAc,GAAG,WAAW,CAAC,2BAA2B,CAAC,aAAa,CAAC,CAAC,CAAC;AAErF;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAa,SAAQ,UAAU;IAC9C;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,+FAA+F;IAC/F,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,SAAS,CAAC,EAAE,YAAY,CAAC;IACzB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,WAAW,CAAC,EAAE,cAAc,CAAC;IAC7B;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,mFAAmF;IACnF,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAe,SAAQ,QAAQ,CAC9C,iBAAiB,EACjB,YAAY,EACZ,iBAAiB,EACjB,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,mGAAmG;AACnG,eAAO,MAAM,cAAc,iDAEzB,CAAC;AAqBH,eAAO,MAAM,sBAAsB,oIACoD,CAAC"}
|
package/docs/adopted-deploys.md
CHANGED
|
@@ -57,6 +57,8 @@ API token on the cluster, just as it does for `alchemy plan`.
|
|
|
57
57
|
| `Proxmox.CephDaemon` ⁶ | `nodes/{node}/ceph/{mon,mgr,mds}` (whole list, by `name`) | read | shared path, and no `updateForm`: GET, GET — no write |
|
|
58
58
|
| `Proxmox.CephFs` ⁶ | `nodes/{node}/ceph/fs` (whole list, by `name`) | read | own: GET, present so returned untouched — no write |
|
|
59
59
|
| `Proxmox.CephOsd` ⁶ | `nodes/{node}/ceph/osd` (the CRUSH tree) | read | own: GET, host/class asserted; POSTs only with `dev` declared — no write |
|
|
60
|
+
| `Proxmox.ApiToken` ⁷ | `access/users/{userid}/token/{tokenid}` | provision ¹ | shared path — PUT skipped on `matches`; POST refused (`reconcile` dies rather than mint one) |
|
|
61
|
+
| `Proxmox.ZfsPool` ⁷ | `nodes/{node}/disks/zfs/{name}` | read | own: GET; no PUT exists on this family; POST only with `devices` AND `raidlevel` both declared |
|
|
60
62
|
|
|
61
63
|
¹ These object reads are gated on an allocate privilege, so the family reads with the provision
|
|
62
64
|
lease (`readRole`, resource.ts). That is still a read.
|
|
@@ -83,6 +85,13 @@ declared flag reasserts a maintenance toggle on every deploy). Pinned by
|
|
|
83
85
|
`src/proxmox/ceph-adopt.test.ts`, whose mutation check (retain flipped to destroy) shows nine
|
|
84
86
|
DELETEs, one per mon, mgr and mds.
|
|
85
87
|
|
|
88
|
+
⁷ Exported from the barrel 2026-09-23, decision 9. Both refuse to CREATE what they do not already
|
|
89
|
+
have: ApiToken's `reconcile` dies by name rather than mint a secret it cannot store (api-token.ts);
|
|
90
|
+
ZfsPool's `createPool` dies by name when `devices`/`raidlevel` are undeclared and the pool is not
|
|
91
|
+
already there (zfs-pool-write.ts) — the adopt-only shape a stripe pool such as n1's `speed` needs,
|
|
92
|
+
since PVE's own `raidlevel` enum has none. Pinned by `src/proxmox/api-token-adopt.test.ts` and
|
|
93
|
+
`src/proxmox/zfs-pool-adopt.test.ts`, whose mutation rows show one PUT and one DELETE respectively.
|
|
94
|
+
|
|
86
95
|
## Before the fix: `Proxmox.CephPool`
|
|
87
96
|
|
|
88
97
|
`reconcile` read the pool and, whenever it existed, PUT the declared `size`, `min_size`,
|
|
@@ -103,6 +112,11 @@ list shows six such tasks (2026-09-13 and 2026-09-20), one per declared pool per
|
|
|
103
112
|
- `src/proxmox/ceph-adopt.test.ts` adopts a reference cluster's nine daemons, one CephFS and six
|
|
104
113
|
OSDs (GETs only), undeclares all sixteen (no DELETE), and refuses an OSD on the wrong host
|
|
105
114
|
without a write.
|
|
115
|
+
- `src/proxmox/api-token-adopt.test.ts` adopts four token shapes read-only, refuses an absent
|
|
116
|
+
token, and shows a `privsep` flip PUTs exactly `comment`/`expire`/`privsep` once.
|
|
117
|
+
- `src/proxmox/zfs-pool-adopt.test.ts` adopts five adopt-only pools read-only, refuses an absent
|
|
118
|
+
or vanished one before any POST, and shows `RemovalPolicy.destroy()` then undeclaring sends
|
|
119
|
+
exactly one DELETE.
|
|
106
120
|
- `src/verify/verify.test.ts` pins Alchemy's own behaviour: `adopted` for a match and for a
|
|
107
121
|
drift, and a reconcile for both. If an Alchemy upgrade changes that, the test fails and this
|
|
108
122
|
page needs re-reading.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homeflare/alchemy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo, Talos, launchd and Caddy, and a verifier that proves adoptions are no-ops.",
|
|
5
5
|
"homepage": "https://github.com/taslabs-net/homeflare-kit/tree/main/packages/alchemy#readme",
|
|
6
6
|
"bugs": "https://github.com/taslabs-net/homeflare-kit/issues",
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Proxmox.ApiToken` adopted, undeclared and drifted through Alchemy's own Plan and Apply, over a
|
|
3
|
+
* fake cluster — pinning the metadata-only shape decision 9 (2026-09-23) builds: adopt and manage
|
|
4
|
+
* `comment`/`expire`/`privsep`, never mint a token this resource cannot hand anybody the secret
|
|
5
|
+
* for (api-token.ts's header).
|
|
6
|
+
*
|
|
7
|
+
* ★ FOUR LIVE SHAPES: `metrics@pve!exporter` and `iac@pve!apply` are the no-comment / commented
|
|
8
|
+
* shapes named in api-token.ts's own header; `svc@pve!lease1` and `svc@pve!lease2` are
|
|
9
|
+
* PLACEHOLDERS (never an estate mint-user account — see the ⛔ on declaring under one in
|
|
10
|
+
* api-token.ts's header) exercising a real non-zero `expire` offered as BOTH a JSON number and
|
|
11
|
+
* a JSON string, which PVE does depending on path — `int()` (values.ts) must not care which.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, expect, test } from 'bun:test';
|
|
14
|
+
import * as Effect from 'effect/Effect';
|
|
15
|
+
import * as Layer from 'effect/Layer';
|
|
16
|
+
import { engineOver } from '../verify/fake-engine.ts';
|
|
17
|
+
import { type ApiTokenProps, ProxmoxApiToken, ProxmoxApiTokenProvider } from './api-token.ts';
|
|
18
|
+
import { shape } from './api-token-form.ts';
|
|
19
|
+
import { FAKE_TARGET, type PveCall, fakePve, withoutBao } from './fake-pve.ts';
|
|
20
|
+
import type {
|
|
21
|
+
AccessUsersUseridTokenTokenidPostParams,
|
|
22
|
+
AccessUsersUseridTokenTokenidPutParams,
|
|
23
|
+
} from './generated/pve.ts';
|
|
24
|
+
|
|
25
|
+
type Live = { comment?: string; expire: number | string; privsep: number };
|
|
26
|
+
|
|
27
|
+
/** `access/users/{userid}/token/{tokenid}`, mutable so the PUT test can watch it change. */
|
|
28
|
+
const tokens = new Map<string, Live>([
|
|
29
|
+
['access/users/metrics@pve/token/exporter', { expire: 0, privsep: 0 }],
|
|
30
|
+
['access/users/iac@pve/token/apply', { comment: 'automation', expire: 0, privsep: 0 }],
|
|
31
|
+
['access/users/svc@pve/token/lease1', { expire: 1789327175, privsep: 0 }],
|
|
32
|
+
['access/users/svc@pve/token/lease2', { comment: 'lease', expire: '1789327175', privsep: 0 }],
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const cluster = () =>
|
|
36
|
+
fakePve((call: PveCall) => {
|
|
37
|
+
if (call.method === 'GET') return tokens.get(call.path);
|
|
38
|
+
if (call.method === 'PUT') {
|
|
39
|
+
const live = tokens.get(call.path);
|
|
40
|
+
if (live !== undefined) tokens.set(call.path, { ...live, ...call.form });
|
|
41
|
+
return call.form;
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/** The declared props matching a live fixture exactly — `expire` always as a `number` prop. */
|
|
47
|
+
const propsFor = (
|
|
48
|
+
userid: string,
|
|
49
|
+
tokenid: string,
|
|
50
|
+
over: { comment?: string; privsep?: boolean },
|
|
51
|
+
): ApiTokenProps => ({
|
|
52
|
+
expire: Number(tokens.get(`access/users/${userid}/token/${tokenid}`)?.expire ?? 0),
|
|
53
|
+
privsep: over.privsep ?? false,
|
|
54
|
+
target: FAKE_TARGET,
|
|
55
|
+
tokenid,
|
|
56
|
+
userid,
|
|
57
|
+
...(over.comment === undefined ? {} : { comment: over.comment }),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const token = (userid: string, tokenid: string, over: { comment?: string; privsep?: boolean }) =>
|
|
61
|
+
ProxmoxApiToken(`${userid.replace('@', '-')}-${tokenid}`, propsFor(userid, tokenid, over));
|
|
62
|
+
|
|
63
|
+
const engineFor = (fake: ReturnType<typeof cluster>) =>
|
|
64
|
+
engineOver(ProxmoxApiTokenProvider().pipe(Layer.provideMerge(fake.layer)));
|
|
65
|
+
|
|
66
|
+
const declared = () =>
|
|
67
|
+
Effect.all([
|
|
68
|
+
token('metrics@pve', 'exporter', {}),
|
|
69
|
+
token('iac@pve', 'apply', { comment: 'automation' }),
|
|
70
|
+
token('svc@pve', 'lease1', {}),
|
|
71
|
+
token('svc@pve', 'lease2', { comment: 'lease' }),
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
describe('adopting a metadata-only ApiToken declaration', () => {
|
|
75
|
+
test('no comment, commented, and a real expire as both a number and a string: noop, adopted, no writes', async () => {
|
|
76
|
+
const fake = cluster();
|
|
77
|
+
await withoutBao(async () => {
|
|
78
|
+
const engine = engineFor(fake);
|
|
79
|
+
const report = await engine.verify(declared());
|
|
80
|
+
expect(report.rows).toHaveLength(4);
|
|
81
|
+
for (const row of report.rows) {
|
|
82
|
+
expect(row).toMatchObject({ changed: [], diff: 'noop', ok: true, planned: 'adopted' });
|
|
83
|
+
}
|
|
84
|
+
const planned = await engine.deploy(declared());
|
|
85
|
+
expect(new Set(Object.values(planned))).toEqual(new Set(['adopted']));
|
|
86
|
+
expect(Object.keys(planned)).toHaveLength(4);
|
|
87
|
+
});
|
|
88
|
+
expect(fake.writes()).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('undeclaring all four sends no DELETE (retain is the default)', async () => {
|
|
92
|
+
const fake = cluster();
|
|
93
|
+
await withoutBao(async () => {
|
|
94
|
+
const engine = engineFor(fake);
|
|
95
|
+
await engine.deploy(declared());
|
|
96
|
+
expect(await engine.deploy(Effect.void)).toEqual({});
|
|
97
|
+
});
|
|
98
|
+
expect(fake.writes()).toEqual([]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('an absent token: the deploy refuses with the mint-elsewhere message, and writes nothing', async () => {
|
|
102
|
+
const fake = cluster();
|
|
103
|
+
const gone = () =>
|
|
104
|
+
ProxmoxApiToken('gone', {
|
|
105
|
+
expire: 0,
|
|
106
|
+
privsep: false,
|
|
107
|
+
target: FAKE_TARGET,
|
|
108
|
+
tokenid: 'missing',
|
|
109
|
+
userid: 'nope@pve',
|
|
110
|
+
});
|
|
111
|
+
await withoutBao(async () => {
|
|
112
|
+
const engine = engineFor(fake);
|
|
113
|
+
await expect(engine.deploy(gone())).rejects.toBeDefined();
|
|
114
|
+
});
|
|
115
|
+
expect(fake.writes()).toEqual([]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('a privsep flip: verify shows changed, and the deploy sends exactly one PUT of comment/expire/privsep', async () => {
|
|
119
|
+
const fake = cluster();
|
|
120
|
+
const flipped = () => token('iac@pve', 'apply', { comment: 'automation', privsep: true });
|
|
121
|
+
await withoutBao(async () => {
|
|
122
|
+
const engine = engineFor(fake);
|
|
123
|
+
const report = await engine.verify(flipped());
|
|
124
|
+
expect(report.rows[0]).toMatchObject({ changed: ['privsep'], diff: 'update', ok: false });
|
|
125
|
+
expect(await engine.deploy(flipped())).toEqual({ 'iac-pve-apply': 'adopted' });
|
|
126
|
+
});
|
|
127
|
+
const put = fake.calls.find((call) => call.method === 'PUT');
|
|
128
|
+
expect(fake.writes()).toEqual(['PUT access/users/iac@pve/token/apply']);
|
|
129
|
+
expect(put?.pairs.map(([name]) => name)).toEqual(['comment', 'expire', 'privsep']);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('the form keys typecheck against both generated PVE params, with no cast', () => {
|
|
133
|
+
const props = propsFor('iac@pve', 'apply', { comment: 'automation' });
|
|
134
|
+
const post: AccessUsersUseridTokenTokenidPostParams = shape(props);
|
|
135
|
+
const put: AccessUsersUseridTokenTokenidPutParams = shape(props);
|
|
136
|
+
expect(post).toEqual(put);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -16,6 +16,10 @@ import type { ApiTokenAttributes, ApiTokenProps } from './api-token.ts';
|
|
|
16
16
|
* that costs a forever-diff: the schema gives `expire` the default "same as user", and the code
|
|
17
17
|
* never implements it. api-token.ts records what that means.
|
|
18
18
|
*/
|
|
19
|
+
import type {
|
|
20
|
+
AccessUsersUseridTokenTokenidPostParams,
|
|
21
|
+
AccessUsersUseridTokenTokenidPutParams,
|
|
22
|
+
} from './generated/pve.ts';
|
|
19
23
|
import type { PveSpec } from './resource.ts';
|
|
20
24
|
import { bool, int, text } from './values.ts';
|
|
21
25
|
|
|
@@ -56,10 +60,23 @@ export const fullTokenid = (props: ApiTokenProps) => `${props.userid}!${props.to
|
|
|
56
60
|
* required prop precisely so that it is never undeclared, so there is nothing for it to do and
|
|
57
61
|
* its `string | undefined` return would have to be asserted away to fit `Record<string,string>`.
|
|
58
62
|
* An assertion here would be a claim about the prop that the type already makes properly.
|
|
63
|
+
*
|
|
64
|
+
* ★ TYPED AGAINST THE GENERATED PUT AND POST PARAMS, NOT `Record<string, string>`, since
|
|
65
|
+
* 2026-09-23 — the `notification-matcher-form.ts` pattern: `Pick` rather than the whole type,
|
|
66
|
+
* because neither `AccessUsersUseridTokenTokenidPutParams` nor …`PostParams` (generated/pve.ts)
|
|
67
|
+
* carries `userid`/`tokenid` (they are path segments — see the ⛔ above) and Put alone also
|
|
68
|
+
* carries `delete`/`regenerate`, which this family never sends. A schema drift on any of the
|
|
69
|
+
* three fields this function DOES send fails `tsc` here rather than surfacing as a 400 on a live
|
|
70
|
+
* cluster; api-token.test.ts pins the assignment both ways (Put and Post) without a cast.
|
|
71
|
+
* `expire` is written as a template literal, not `String(...)`, because the generated type is
|
|
72
|
+
* `` `${number}` ``, not `string` — `String(n)` widens to plain `string` and would not typecheck.
|
|
59
73
|
*/
|
|
60
|
-
export const shape = (
|
|
74
|
+
export const shape = (
|
|
75
|
+
props: ApiTokenProps,
|
|
76
|
+
): Pick<AccessUsersUseridTokenTokenidPutParams, 'comment' | 'expire' | 'privsep'> &
|
|
77
|
+
Pick<AccessUsersUseridTokenTokenidPostParams, 'comment' | 'expire' | 'privsep'> => ({
|
|
61
78
|
comment: props.comment ?? '',
|
|
62
|
-
expire:
|
|
79
|
+
expire: `${props.expire}`,
|
|
63
80
|
privsep: props.privsep ? '1' : '0',
|
|
64
81
|
});
|
|
65
82
|
|
package/src/proxmox/api-token.ts
CHANGED
|
@@ -20,9 +20,14 @@
|
|
|
20
20
|
* whole of a token's policy, and declaring them is real work: it is how `expire` stops being
|
|
21
21
|
* whatever somebody typed in 2024. Mint NEW tokens where the secret can be caught — OpenBao's
|
|
22
22
|
* `proxmox-c1` mount, or a human at `pveum user token add`.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* ⛔ ★ CORRECTED 2026-09-23 — THIS PARAGRAPH PREVIOUSLY SAID "THE CREATE PATH IS LEFT REACHABLE
|
|
24
|
+
* RATHER THAN STUBBED… the honest arrangement is a create that works". THAT CONTRADICTED THE
|
|
25
|
+
* `reconcile` BELOW, WHICH REFUSES: `handlers.reconcile` calls `Effect.die` by name when `read`
|
|
26
|
+
* answers absent, before any POST — metadata-only per decision 9 (2026-09-23), exported on the
|
|
27
|
+
* barrel as an adopt-and-manage-only Resource. A create that silently did nothing WOULD be the
|
|
28
|
+
* lie resource.ts's ★ describes for a stubbed delete; refusing loudly, by name, with the two
|
|
29
|
+
* ways to get a usable token, is the honest arrangement instead — see the ★ further down and
|
|
30
|
+
* api-token-adopt.test.ts's absent-token row, which pins the refusal and zero writes.
|
|
26
31
|
*
|
|
27
32
|
* ⛔ THERE IS NO `regenerate` PROP, AND IT IS NOT AN OVERSIGHT. PVE's own description: "All users
|
|
28
33
|
* of the previous secret will lose access after this operation." A prop for it would revoke a
|
|
@@ -29,7 +29,7 @@ export type ClusterSdnFabricsFabricGetReturn = readonly ({
|
|
|
29
29
|
'lock-token'?: string;
|
|
30
30
|
persistent_keepalive?: number;
|
|
31
31
|
protocol: 'openfabric' | 'ospf' | 'wireguard' | 'bgp';
|
|
32
|
-
redistribute
|
|
32
|
+
redistribute?: readonly string[];
|
|
33
33
|
route_filter?: string;
|
|
34
34
|
} & Record<string, unknown>)[];
|
|
35
35
|
|
|
@@ -45,7 +45,7 @@ export type ClusterSdnFabricsFabricPostParams = {
|
|
|
45
45
|
'lock-token'?: string;
|
|
46
46
|
persistent_keepalive?: `${number}`;
|
|
47
47
|
protocol: 'openfabric' | 'ospf' | 'wireguard' | 'bgp';
|
|
48
|
-
redistribute
|
|
48
|
+
redistribute?: readonly string[];
|
|
49
49
|
route_filter?: string;
|
|
50
50
|
};
|
|
51
51
|
/** POST /cluster/sdn/fabrics/fabric — `data` payload after client unwrap. */
|
|
@@ -63,7 +63,7 @@ export type ClusterSdnFabricsFabricIdGetReturn = {
|
|
|
63
63
|
'lock-token'?: string;
|
|
64
64
|
persistent_keepalive?: number;
|
|
65
65
|
protocol: 'openfabric' | 'ospf' | 'wireguard' | 'bgp';
|
|
66
|
-
redistribute
|
|
66
|
+
redistribute?: readonly string[];
|
|
67
67
|
route_filter?: string;
|
|
68
68
|
} & Record<string, unknown>;
|
|
69
69
|
|
|
@@ -71,7 +71,17 @@ export type ClusterSdnFabricsFabricIdGetReturn = {
|
|
|
71
71
|
export type ClusterSdnFabricsFabricIdPutParams = {
|
|
72
72
|
area?: string;
|
|
73
73
|
csnp_interval?: `${number}`;
|
|
74
|
-
delete
|
|
74
|
+
delete?: readonly (
|
|
75
|
+
| 'ip_prefix'
|
|
76
|
+
| 'ip6_prefix'
|
|
77
|
+
| 'hello_interval'
|
|
78
|
+
| 'csnp_interval'
|
|
79
|
+
| 'route_filter'
|
|
80
|
+
| 'redistribute'
|
|
81
|
+
| 'route_map_in'
|
|
82
|
+
| 'route_map_out'
|
|
83
|
+
| 'area'
|
|
84
|
+
| 'persistent_keepalive')[];
|
|
75
85
|
digest?: string;
|
|
76
86
|
hello_interval?: `${number}`;
|
|
77
87
|
ip6_prefix?: string;
|
|
@@ -79,7 +89,7 @@ export type ClusterSdnFabricsFabricIdPutParams = {
|
|
|
79
89
|
'lock-token'?: string;
|
|
80
90
|
persistent_keepalive?: `${number}`;
|
|
81
91
|
protocol: 'openfabric' | 'ospf' | 'wireguard' | 'bgp';
|
|
82
|
-
redistribute
|
|
92
|
+
redistribute?: readonly string[];
|
|
83
93
|
route_filter?: string;
|
|
84
94
|
};
|
|
85
95
|
/** PUT /cluster/sdn/fabrics/fabric/{id} — `data` payload after client unwrap. */
|
|
@@ -96,7 +106,7 @@ export type ClusterSdnFabricsNodeGetReturn = readonly ({
|
|
|
96
106
|
digest?: string;
|
|
97
107
|
endpoint?: string;
|
|
98
108
|
fabric_id: string;
|
|
99
|
-
interfaces
|
|
109
|
+
interfaces?: readonly string[];
|
|
100
110
|
ip?: string;
|
|
101
111
|
ip6?: string;
|
|
102
112
|
'lock-token'?: string;
|
|
@@ -115,7 +125,7 @@ export type ClusterSdnFabricsNodeFabric_idGetReturn = readonly ({
|
|
|
115
125
|
digest?: string;
|
|
116
126
|
endpoint?: string;
|
|
117
127
|
fabric_id: string;
|
|
118
|
-
interfaces
|
|
128
|
+
interfaces?: readonly string[];
|
|
119
129
|
ip?: string;
|
|
120
130
|
ip6?: string;
|
|
121
131
|
'lock-token'?: string;
|
|
@@ -131,7 +141,7 @@ export type ClusterSdnFabricsNodeFabric_idPostParams = {
|
|
|
131
141
|
allowed_ips?: readonly string[];
|
|
132
142
|
digest?: string;
|
|
133
143
|
endpoint?: string;
|
|
134
|
-
interfaces
|
|
144
|
+
interfaces?: readonly string[];
|
|
135
145
|
ip?: string;
|
|
136
146
|
ip6?: string;
|
|
137
147
|
'lock-token'?: string;
|
|
@@ -150,7 +160,7 @@ export type ClusterSdnFabricsNodeFabric_idNode_idGetReturn = {
|
|
|
150
160
|
digest?: string;
|
|
151
161
|
endpoint?: string;
|
|
152
162
|
fabric_id: string;
|
|
153
|
-
interfaces
|
|
163
|
+
interfaces?: readonly string[];
|
|
154
164
|
ip?: string;
|
|
155
165
|
ip6?: string;
|
|
156
166
|
'lock-token'?: string;
|
|
@@ -164,10 +174,10 @@ export type ClusterSdnFabricsNodeFabric_idNode_idGetReturn = {
|
|
|
164
174
|
/** PUT /cluster/sdn/fabrics/node/{fabric_id}/{node_id} — form/query parameters (path segments omitted). */
|
|
165
175
|
export type ClusterSdnFabricsNodeFabric_idNode_idPutParams = {
|
|
166
176
|
allowed_ips?: readonly string[];
|
|
167
|
-
delete
|
|
177
|
+
delete?: readonly ('interfaces' | 'ip' | 'ip6' | 'allowed_ips' | 'endpoint' | 'peers')[];
|
|
168
178
|
digest?: string;
|
|
169
179
|
endpoint?: string;
|
|
170
|
-
interfaces
|
|
180
|
+
interfaces?: readonly string[];
|
|
171
181
|
ip?: string;
|
|
172
182
|
ip6?: string;
|
|
173
183
|
'lock-token'?: string;
|
|
@@ -197,7 +207,7 @@ export type ClusterSdnFabricsAllGetReturn = {
|
|
|
197
207
|
'lock-token'?: string;
|
|
198
208
|
persistent_keepalive?: number;
|
|
199
209
|
protocol: 'openfabric' | 'ospf' | 'wireguard' | 'bgp';
|
|
200
|
-
redistribute
|
|
210
|
+
redistribute?: readonly string[];
|
|
201
211
|
route_filter?: string;
|
|
202
212
|
} & Record<string, unknown>)[];
|
|
203
213
|
nodes: readonly ({
|
|
@@ -205,7 +215,7 @@ export type ClusterSdnFabricsAllGetReturn = {
|
|
|
205
215
|
digest?: string;
|
|
206
216
|
endpoint?: string;
|
|
207
217
|
fabric_id: string;
|
|
208
|
-
interfaces
|
|
218
|
+
interfaces?: readonly string[];
|
|
209
219
|
ip?: string;
|
|
210
220
|
ip6?: string;
|
|
211
221
|
'lock-token'?: string;
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* ★ CephDaemon, CephFs and CephOsd are Resources since 2026-09-22: adopt-only by shape and retain
|
|
8
8
|
* on destroy (ceph-adopt.test.ts). ⛔ CephFlag stays Provider-only: a declared flag reasserts a
|
|
9
9
|
* maintenance toggle on every deploy (ceph-flag.ts).
|
|
10
|
+
* ★ ApiToken and ZfsPool are Resources since 2026-09-23, decision 9: ApiToken metadata-only,
|
|
11
|
+
* ZfsPool adopt-only when `devices`/`raidlevel` are left undeclared (api-token-adopt.test.ts,
|
|
12
|
+
* zfs-pool-adopt.test.ts).
|
|
10
13
|
*/
|
|
11
14
|
import { expect, test } from 'bun:test';
|
|
12
15
|
import { PbsDatastore, ProxmoxStorage } from './index.ts';
|
|
@@ -21,6 +24,7 @@ const resourceExports = [
|
|
|
21
24
|
'PbsSyncJob',
|
|
22
25
|
'PbsVerifyJob',
|
|
23
26
|
'ProxmoxAcl',
|
|
27
|
+
'ProxmoxApiToken',
|
|
24
28
|
'ProxmoxBackupJob',
|
|
25
29
|
'ProxmoxCephDaemon',
|
|
26
30
|
'ProxmoxCephFs',
|
|
@@ -39,6 +43,7 @@ const resourceExports = [
|
|
|
39
43
|
'ProxmoxSdnZone',
|
|
40
44
|
'ProxmoxStorage',
|
|
41
45
|
'ProxmoxUser',
|
|
46
|
+
'ProxmoxZfsPool',
|
|
42
47
|
] as const;
|
|
43
48
|
|
|
44
49
|
test('control-plane Resource constructors are on the public barrel', () => {
|
|
@@ -60,7 +65,5 @@ test('QEMU, NIC apply and the Ceph flag stay Provider-only', () => {
|
|
|
60
65
|
expect(src).not.toMatch(/export \{ ProxmoxVm[, }]/);
|
|
61
66
|
expect(src).not.toMatch(/export \{ ProxmoxNodeNetwork[, }]/);
|
|
62
67
|
expect(src).not.toMatch(/export \{ ProxmoxNetworkApply[, }]/);
|
|
63
|
-
expect(src).not.toMatch(/export \{ ProxmoxApiToken[, }]/);
|
|
64
|
-
expect(src).not.toMatch(/export \{ ProxmoxZfsPool[, }]/);
|
|
65
68
|
expect(src).not.toMatch(/export \{ ProxmoxCephFlag[, }]/);
|
|
66
69
|
});
|