@homeflare/alchemy 0.32.0 → 0.34.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/grafana/alerting-provenance.d.ts +80 -0
- package/dist/grafana/alerting-provenance.d.ts.map +1 -0
- package/dist/grafana/contact-point-fixtures.d.ts +28 -0
- package/dist/grafana/contact-point-fixtures.d.ts.map +1 -0
- package/dist/grafana/contact-point.d.ts +99 -0
- package/dist/grafana/contact-point.d.ts.map +1 -0
- package/dist/grafana/dashboard-model.d.ts +47 -0
- package/dist/grafana/dashboard-model.d.ts.map +1 -0
- package/dist/grafana/dashboard.d.ts +98 -0
- package/dist/grafana/dashboard.d.ts.map +1 -0
- package/dist/grafana/datasource.d.ts +2 -9
- package/dist/grafana/datasource.d.ts.map +1 -1
- package/dist/grafana/folder.d.ts +100 -0
- package/dist/grafana/folder.d.ts.map +1 -0
- package/dist/grafana/index.d.ts +16 -9
- package/dist/grafana/index.d.ts.map +1 -1
- package/dist/grafana/index.js +325 -16
- package/dist/grafana/index.js.map +15 -5
- package/dist/grafana/message-template.d.ts +70 -0
- package/dist/grafana/message-template.d.ts.map +1 -0
- package/dist/grafana/mute-timing.d.ts +112 -0
- package/dist/grafana/mute-timing.d.ts.map +1 -0
- package/dist/grafana/providers.d.ts +1 -1
- package/dist/grafana/providers.d.ts.map +1 -1
- package/dist/grafana/provisioned.d.ts +11 -0
- package/dist/grafana/provisioned.d.ts.map +1 -0
- package/dist/grafana/secret-refs.d.ts +19 -0
- package/dist/grafana/secret-refs.d.ts.map +1 -0
- package/dist/grafana/subset-match.d.ts +2 -0
- package/dist/grafana/subset-match.d.ts.map +1 -0
- package/dist/linux/index.d.ts +1 -1
- package/dist/linux/index.d.ts.map +1 -1
- package/dist/linux/index.js +3 -1
- package/dist/linux/index.js.map +1 -1
- package/dist/proxmox/index.js +76 -45
- package/dist/proxmox/index.js.map +6 -6
- package/dist/proxmox/zfs-pool-form.d.ts +124 -0
- package/dist/proxmox/zfs-pool-form.d.ts.map +1 -0
- package/dist/proxmox/zfs-pool-wire.d.ts +78 -0
- package/dist/proxmox/zfs-pool-wire.d.ts.map +1 -0
- package/dist/proxmox/zfs-pool.d.ts +27 -52
- package/dist/proxmox/zfs-pool.d.ts.map +1 -1
- package/docs/grafana-alerting.md +145 -0
- package/docs/grafana-folder-dashboard.md +145 -0
- package/docs/grafana.md +12 -10
- package/docs/upstream-conformance.md +31 -4
- package/package.json +1 -1
- package/src/grafana/alerting-provenance.ts +109 -0
- package/src/grafana/contact-point-fixtures.ts +25 -0
- package/src/grafana/contact-point-provenance.test.ts +113 -0
- package/src/grafana/contact-point-secrets.test.ts +117 -0
- package/src/grafana/contact-point.test.ts +123 -0
- package/src/grafana/contact-point.ts +179 -0
- package/src/grafana/dashboard-model.test.ts +111 -0
- package/src/grafana/dashboard-model.ts +62 -0
- package/src/grafana/dashboard.test.ts +232 -0
- package/src/grafana/dashboard.ts +161 -0
- package/src/grafana/datasource.ts +5 -27
- package/src/grafana/folder.test.ts +212 -0
- package/src/grafana/folder.ts +161 -0
- package/src/grafana/index.ts +42 -9
- package/src/grafana/message-template.test.ts +212 -0
- package/src/grafana/message-template.ts +114 -0
- package/src/grafana/mute-timing.test.ts +227 -0
- package/src/grafana/mute-timing.ts +160 -0
- package/src/grafana/providers.ts +13 -1
- package/src/grafana/provisioned.ts +49 -0
- package/src/grafana/secret-refs.ts +48 -0
- package/src/grafana/subset-match.ts +71 -0
- package/src/linux/index-export.test.ts +25 -0
- package/src/linux/index.ts +15 -1
- package/src/proxmox/constraints-host-forms.test.ts +1 -1
- package/src/proxmox/zfs-pool-adopt.test.ts +26 -0
- package/src/proxmox/zfs-pool-form.ts +236 -0
- package/src/proxmox/zfs-pool-read-failure.test.ts +118 -0
- package/src/proxmox/zfs-pool-wire.ts +132 -0
- package/src/proxmox/zfs-pool.ts +106 -76
- package/dist/proxmox/zfs-pool-spec.d.ts +0 -27
- package/dist/proxmox/zfs-pool-spec.d.ts.map +0 -1
- package/dist/proxmox/zfs-pool-write.d.ts +0 -128
- package/dist/proxmox/zfs-pool-write.d.ts.map +0 -1
- package/src/proxmox/zfs-pool-spec.ts +0 -76
- package/src/proxmox/zfs-pool-write.ts +0 -196
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The create lane for `Proxmox.ZfsPool`: the form PVE wants, and the wait for the worker it forks.
|
|
3
|
-
*
|
|
4
|
-
* ★ SPLIT OUT OF zfs-pool.ts TO KEEP BOTH FILES UNDER THE 250-LINE CAP, and the seam is a real one
|
|
5
|
-
* rather than a convenient line number. This file answers "how does a declaration become a pool
|
|
6
|
-
* on disks, and how do we know it landed". zfs-pool.ts answers "what is a zpool, what can be
|
|
7
|
-
* said about a live one, and when has it changed" — and on this family the answer to the last of
|
|
8
|
-
* those is "never", which is why the two halves have so little to say to each other.
|
|
9
|
-
*
|
|
10
|
-
* ⚠️ THE `import type` BACK TO zfs-pool.ts IS A CYCLE ON PAPER ONLY — it is type-only, so it is
|
|
11
|
-
* erased before anything runs, and the resource's public shape stays in the file that declares
|
|
12
|
-
* the resource rather than being moved somewhere odd to dodge the arrow. Same as
|
|
13
|
-
* metric-server-form.ts.
|
|
14
|
-
*
|
|
15
|
-
* ⚠️ `createPool` TAKES THE READ AS A PARAMETER, GENERIC IN ITS ERROR AND REQUIREMENT CHANNELS,
|
|
16
|
-
* rather than importing `pveOperations` and rebuilding the spec here. Two `pveOperations` over
|
|
17
|
-
* one spec would be two closures nobody can prove are the same; passing the one the resource
|
|
18
|
-
* already built means this file cannot accidentally read the cluster differently from `diff`.
|
|
19
|
-
*/
|
|
20
|
-
import * as Effect from 'effect/Effect';
|
|
21
|
-
import { pve } from './client.ts';
|
|
22
|
-
import type { EndpointPair } from './resource-spec.ts';
|
|
23
|
-
import { text } from './values.ts';
|
|
24
|
-
import type { ZfsPoolAttributes, ZfsPoolProps } from './zfs-pool.ts';
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* The POST body. Every field in it is write-only — nothing here is ever read back.
|
|
28
|
-
*
|
|
29
|
-
* ⚠️ `node` IS NOT IN THE BODY: it is already the `nodes/{node}` segment of the URL being POSTed
|
|
30
|
-
* to, and a second copy can only disagree with it. Same reasoning as `id` in metric-server.ts.
|
|
31
|
-
* ⚠️ `devices` IS JOINED IN DECLARED ORDER AND NOT PUT THROUGH `csv()` FROM values.ts. `csv` sorts,
|
|
32
|
-
* and PVE walks this list two at a time to pair `raid10` mirrors — MEASURED in the node's own
|
|
33
|
-
* `PVE::API2::Disks::ZFS` — so sorting it builds a different pool from the one declared. The
|
|
34
|
-
* list is never compared against anything, so it needs no normalisation, only faithful order.
|
|
35
|
-
* ⚠️ AN OMITTED OPTIONAL IS NOT SENT. PVE's own defaults (ashift 12, compression on) are then
|
|
36
|
-
* applied by the node. Sending a guessed default would be indistinguishable here — the values
|
|
37
|
-
* are unreadable afterwards either way — but it would put a number in the request that no
|
|
38
|
-
* declaration asked for, and this is the one call that writes to physical disks.
|
|
39
|
-
*/
|
|
40
|
-
/**
|
|
41
|
-
* The vendor rules a FULL declaration's create form is checked against at plan time.
|
|
42
|
-
*
|
|
43
|
-
* ⛔ HERE RATHER THAN INLINE IN zfs-pool.ts BECAUSE THAT FILE IS ALREADY OVER THE 250-LINE CAP,
|
|
44
|
-
* and a `pve:POST …` literal has to live in a non-test source file for the generator to table
|
|
45
|
-
* it at all. zfs-pool-spec.ts imports it on the line it already imports `createForm` from.
|
|
46
|
-
* ⚠️ NO UPDATE KEY: PVE registers no PUT under `/nodes/{node}/disks/zfs`, which is exactly why
|
|
47
|
-
* the spec declares no `updateForm` and a changed prop is a REPLACE.
|
|
48
|
-
* ⚠️ `spec.endpoint` DOES NOT RETURN THIS CONSTANT DIRECTLY — see the ★ on `zfsPoolEndpoint`
|
|
49
|
-
* below, and the ⛔ on `devices`/`raidlevel` in zfs-pool.ts (decision 9, 2026-09-23).
|
|
50
|
-
*/
|
|
51
|
-
export const ZFS_POOL_ENDPOINT: EndpointPair = { create: 'pve:POST /nodes/{node}/disks/zfs' };
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* ★ `PveSpec['endpoint']` AS A FUNCTION OF PROPS, THE WAY notification-target.ts AND
|
|
55
|
-
* ceph-daemon.ts ALREADY DO IT FOR A DIFFERENT REASON (resource-spec.ts's ⚠️ on that field).
|
|
56
|
-
* Here the reason is decision 9's adopt-only mode: `guardCreate` runs on the FIRST plan of
|
|
57
|
-
* ANY declaration (resource.ts `ops.diff`, `output === undefined` branch) and checks the
|
|
58
|
-
* create form's REQUIRED fields whenever `endpoint.create` resolves to a key — and PVE marks
|
|
59
|
-
* both `devices` and `raidlevel` required (measured, codegen/constraints's `pve-nodes-disks`
|
|
60
|
-
* row). An adopt-only declaration deliberately omits both, so returning the real key there
|
|
61
|
-
* would refuse a MATCHING adoption at plan time, before `read` ever ran — exactly the gap
|
|
62
|
-
* `guardCreate`/`guardUpdate` being exported (resource.ts's ★) says this family must not fall
|
|
63
|
-
* through. Returning `{}` (no `create` key) makes `guardCreate` a no-op (`constraint-guard.ts`:
|
|
64
|
-
* `create === undefined ? Effect.void : …`) for an adopt-only declaration, while a FULL one
|
|
65
|
-
* (`devices` and `raidlevel` both present) is checked exactly as before.
|
|
66
|
-
* ⚠️ THE RUNTIME BACKSTOP IS `createPool` BELOW, NOT THIS FUNCTION. Skipping the plan-time guard
|
|
67
|
-
* for an adopt-only declaration does not skip the check that the pool is actually there —
|
|
68
|
-
* `createPool` refuses before any POST when the read comes back absent and either field is
|
|
69
|
-
* undefined, which is what a genuinely absent adopt-only declaration hits at apply time.
|
|
70
|
-
*/
|
|
71
|
-
export const zfsPoolEndpoint = (props: ZfsPoolProps): EndpointPair =>
|
|
72
|
-
props.devices === undefined || props.raidlevel === undefined ? {} : ZFS_POOL_ENDPOINT;
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* ⚠️ EVERY KEY IS OMITTED WHEN ABSENT, `devices` AND `raidlevel` INCLUDED — not just the
|
|
76
|
-
* optional ones. An adopt-only declaration (both undefined, decision 9) must produce a form
|
|
77
|
-
* `createPool`'s own guard can inspect for their absence, and one this function itself does
|
|
78
|
-
* not throw building: `props.devices.join(',')` on an undefined `devices` is exactly the
|
|
79
|
-
* `TypeError` the plan-time guard hit before this change, because `guardCreate` evaluates
|
|
80
|
-
* `spec.createForm(props)` eagerly (resource-guard.ts) — before it even looks at `presence`.
|
|
81
|
-
*/
|
|
82
|
-
export const createForm = (props: ZfsPoolProps): Record<string, string> => ({
|
|
83
|
-
...(props.devices === undefined ? {} : { devices: props.devices.join(',') }),
|
|
84
|
-
name: props.name,
|
|
85
|
-
...(props.raidlevel === undefined ? {} : { raidlevel: props.raidlevel }),
|
|
86
|
-
...(props.ashift === undefined ? {} : { ashift: String(props.ashift) }),
|
|
87
|
-
...(props.compression === undefined ? {} : { compression: props.compression }),
|
|
88
|
-
...(props['draid-config'] === undefined ? {} : { 'draid-config': props['draid-config'] }),
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
/** 30 reads, 2s apart. A create that has not landed in a minute has gone wrong, not gone slow. */
|
|
92
|
-
const SETTLE_ATTEMPTS = 30;
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Create the pool if it is absent, and otherwise leave it completely alone.
|
|
96
|
-
*
|
|
97
|
-
* ⛔ THE POST ONLY FORKS A WORKER. MEASURED in `/usr/share/perl5/PVE/API2/Disks/ZFS.pm` on node-b: the
|
|
98
|
-
* create handler ends in `$rpcenv->fork_worker('zfscreate', ...)` and its HTTP answer is a UPID
|
|
99
|
-
* returned the instant the worker is forked — before `zpool create` has run, let alone finished.
|
|
100
|
-
* A read-back taken immediately, as `pveOperations.reconcile` takes it, finds nothing and would
|
|
101
|
-
* report a successful create as the failure "the write returned no error but the object is still
|
|
102
|
-
* absent". THAT is why this function exists instead of the factory's reconcile.
|
|
103
|
-
*
|
|
104
|
-
* ⚠️ THERE IS NO UPDATE BRANCH BECAUSE PVE HAS NO PUT HERE. An existing pool is returned exactly as
|
|
105
|
-
* read, unwritten: the only write this resource ever makes is the create of a pool that is not
|
|
106
|
-
* there. Do not add a "repair" write; there is no field it could set.
|
|
107
|
-
*
|
|
108
|
-
* ⛔ AND IF THE READ IS WRONG, THE POST IS STILL SAFE — measured, not hoped for. `read` folds a 403
|
|
109
|
-
* or an unreachable node into "absent", so a too-narrow lease would send this down the create
|
|
110
|
-
* path over a live pool. PVE's handler calls `get_pool_data()` before it forks anything and dies
|
|
111
|
-
* with "pool '<name>' already exists on node '<node>'", and `assert_disk_unused` refuses every
|
|
112
|
-
* device that pool is holding. A create aimed at a pool that is really there fails loudly rather
|
|
113
|
-
* than wiping it.
|
|
114
|
-
*
|
|
115
|
-
* ⚠️ SHORT POLLS WITH A HARD CAP, NOT ONE LONG WAIT. `read` cannot tell "not yet" from "forbidden"
|
|
116
|
-
* — both are `undefined` — so the loop gives up with the UPID rather than retrying forever, and
|
|
117
|
-
* the UPID is the only thing that leads to the worker's real error.
|
|
118
|
-
*
|
|
119
|
-
* ⛔ THE ADOPT-ONLY GUARD, RIGHT AFTER THE READ AND BEFORE ANYTHING ELSE. Decision 9 (2026-09-23):
|
|
120
|
-
* omitting `devices` and `raidlevel` declares a pool this resource may only ADOPT, for a pool
|
|
121
|
-
* PVE's own schema cannot fully describe — n1's `speed` is a stripe, and `raidlevel`'s enum has
|
|
122
|
-
* no stripe (zfs-pool.ts). `zfsPoolEndpoint` above already keeps the vendor constraint guard
|
|
123
|
-
* from refusing such a declaration at PLAN time; this is the matching refusal at APPLY time, for
|
|
124
|
-
* the one case that guard cannot see — the pool genuinely is not there. MEASURED without it: a
|
|
125
|
-
* tolerant `createForm` sends `POST … {name: "rpool"}` with no `devices`/`raidlevel`, which PVE's
|
|
126
|
-
* own schema would 400 on eventually, but only after the request left this process; refusing
|
|
127
|
-
* here is the same `Effect.die` idiom as every other refusal in this package, and it costs
|
|
128
|
-
* nothing this pool did not already lack.
|
|
129
|
-
*/
|
|
130
|
-
export const createPool = <E, R>(
|
|
131
|
-
props: ZfsPoolProps,
|
|
132
|
-
collection: string,
|
|
133
|
-
path: string,
|
|
134
|
-
read: (props: ZfsPoolProps) => Effect.Effect<ZfsPoolAttributes | undefined, E, R>,
|
|
135
|
-
) =>
|
|
136
|
-
Effect.gen(function* () {
|
|
137
|
-
const existing = yield* read(props);
|
|
138
|
-
if (existing !== undefined) return existing;
|
|
139
|
-
|
|
140
|
-
if (props.devices === undefined || props.raidlevel === undefined) {
|
|
141
|
-
return yield* Effect.die(
|
|
142
|
-
new Error(
|
|
143
|
-
`${props.node}/${props.name}: this is an adopt-only declaration (no \`devices\` or ` +
|
|
144
|
-
'`raidlevel`) and the pool is not there to adopt. An adopt-only Proxmox.ZfsPool never ' +
|
|
145
|
-
'creates -- declare both fields for a pool this resource should build, or point this ' +
|
|
146
|
-
'declaration at a pool that already exists on this node.',
|
|
147
|
-
),
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const upid = yield* pve<string>(
|
|
152
|
-
props.target,
|
|
153
|
-
'provision',
|
|
154
|
-
'POST',
|
|
155
|
-
collection,
|
|
156
|
-
createForm(props),
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
const settle = (attempts: number): Effect.Effect<ZfsPoolAttributes | undefined, E, R> =>
|
|
160
|
-
Effect.gen(function* () {
|
|
161
|
-
const live = yield* read(props);
|
|
162
|
-
if (live !== undefined || attempts <= 0) return live;
|
|
163
|
-
yield* Effect.sleep('2 seconds');
|
|
164
|
-
return yield* settle(attempts - 1);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
const after = yield* settle(SETTLE_ATTEMPTS);
|
|
168
|
-
if (after === undefined) {
|
|
169
|
-
return yield* Effect.die(
|
|
170
|
-
new Error(
|
|
171
|
-
`${path}: POST returned ${text(upid, '(no UPID)')} but the pool has not appeared. That ` +
|
|
172
|
-
'POST only forks a `zfscreate` worker, so the task holds the real error -- read it ' +
|
|
173
|
-
`with \`pvesh get /nodes/${props.node}/tasks/<upid>/log\`. NOTHING WAS RETRIED AND ` +
|
|
174
|
-
'NOTHING WAS CLEANED UP: if the worker reached `zpool create`, the declared devices ' +
|
|
175
|
-
'have already been written to.',
|
|
176
|
-
),
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
return after;
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* ⛔ `DELETE /nodes/{node}/disks/zfs/{name}` RUNS `zpool destroy` AND THE DATASETS GO WITH IT. Per
|
|
184
|
-
* S14 (alchemy-provider-standard), it is implemented rather than stubbed — a `delete` that
|
|
185
|
-
* silently does nothing lies to whoever reads the plan — and what keeps an ordinary removed
|
|
186
|
-
* declaration from reaching it is the resource's `defaultRemovalPolicy: 'retain'` (zfs-pool.ts):
|
|
187
|
-
* an orphaned pool is forgotten, not destroyed, unless a caller opts in with
|
|
188
|
-
* `.pipe(RemovalPolicy.destroy())`. See the corrected header in zfs-pool.ts.
|
|
189
|
-
*
|
|
190
|
-
* ⚠️ `cleanup-config` IS NOT SENT. It would additionally remove the PVE storage entry pointing
|
|
191
|
-
* at this pool, which is a different object with its own resource (`Proxmox.Storage`); one
|
|
192
|
-
* resource reaching over to delete another's is how a stack ends up with state describing
|
|
193
|
-
* something that is gone.
|
|
194
|
-
*/
|
|
195
|
-
export const destroyPool = (olds: ZfsPoolProps, path: string) =>
|
|
196
|
-
pve(olds.target, 'provision', 'DELETE', path);
|