@homeflare/alchemy 0.38.0 → 0.39.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/README.md +22 -0
- package/dist/forgejo/index.js +4 -2
- package/dist/forgejo/index.js.map +3 -3
- package/dist/forgejo/team-member.d.ts +25 -0
- package/dist/forgejo/team-member.d.ts.map +1 -1
- package/dist/proxmox/generated/constraints/index.d.ts +1 -1
- package/dist/proxmox/generated/constraints/index.d.ts.map +1 -1
- package/dist/proxmox/generated/constraints/pve-access.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-backup.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-ceph.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-firewall.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-ha.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-metrics.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-notifications.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-replication.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-cluster-sdn.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-ceph.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-disks.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-lxc.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-network.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-qemu.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-nodes-storage.d.ts +23 -0
- package/dist/proxmox/generated/constraints/pve-nodes-storage.d.ts.map +1 -0
- package/dist/proxmox/generated/constraints/pve-pools.d.ts +1 -1
- package/dist/proxmox/generated/constraints/pve-storage.d.ts +1 -1
- package/dist/proxmox/index.d.ts +14 -3
- package/dist/proxmox/index.d.ts.map +1 -1
- package/dist/proxmox/index.js +565 -133
- package/dist/proxmox/index.js.map +33 -23
- package/dist/proxmox/qemu-form.d.ts +27 -18
- package/dist/proxmox/qemu-form.d.ts.map +1 -1
- package/dist/proxmox/qemu-judge.d.ts +30 -0
- package/dist/proxmox/qemu-judge.d.ts.map +1 -0
- package/dist/proxmox/qemu-lifecycle.d.ts +16 -2
- package/dist/proxmox/qemu-lifecycle.d.ts.map +1 -1
- package/dist/proxmox/qemu-net.d.ts +9 -0
- package/dist/proxmox/qemu-net.d.ts.map +1 -0
- package/dist/proxmox/qemu-props.d.ts +112 -0
- package/dist/proxmox/qemu-props.d.ts.map +1 -0
- package/dist/proxmox/qemu-volume.d.ts +17 -0
- package/dist/proxmox/qemu-volume.d.ts.map +1 -0
- package/dist/proxmox/qemu.d.ts +14 -27
- package/dist/proxmox/qemu.d.ts.map +1 -1
- package/dist/proxmox/storage-download-errors.d.ts +5 -0
- package/dist/proxmox/storage-download-errors.d.ts.map +1 -0
- package/dist/proxmox/storage-download-identity.d.ts +27 -0
- package/dist/proxmox/storage-download-identity.d.ts.map +1 -0
- package/dist/proxmox/storage-download-lifecycle.d.ts +97 -0
- package/dist/proxmox/storage-download-lifecycle.d.ts.map +1 -0
- package/dist/proxmox/storage-download-props.d.ts +36 -0
- package/dist/proxmox/storage-download-props.d.ts.map +1 -0
- package/dist/proxmox/storage-download-task.d.ts +17 -0
- package/dist/proxmox/storage-download-task.d.ts.map +1 -0
- package/dist/proxmox/storage-download.d.ts +44 -0
- package/dist/proxmox/storage-download.d.ts.map +1 -0
- package/dist/telemetry/fake-collector.d.ts +36 -0
- package/dist/telemetry/fake-collector.d.ts.map +1 -0
- package/dist/telemetry/index.d.ts +11 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +223 -0
- package/dist/telemetry/index.js.map +12 -0
- package/dist/telemetry/layer.d.ts +86 -0
- package/dist/telemetry/layer.d.ts.map +1 -0
- package/dist/telemetry/redact-serialization.d.ts +21 -0
- package/dist/telemetry/redact-serialization.d.ts.map +1 -0
- package/dist/telemetry/redact.d.ts +46 -0
- package/dist/telemetry/redact.d.ts.map +1 -0
- package/dist/unifi/drift.d.ts +27 -0
- package/dist/unifi/drift.d.ts.map +1 -0
- package/dist/unifi/fake-unifi.d.ts +5 -1
- package/dist/unifi/fake-unifi.d.ts.map +1 -1
- package/dist/unifi/firewall-zone-form.d.ts +6 -0
- package/dist/unifi/firewall-zone-form.d.ts.map +1 -1
- package/dist/unifi/firewall-zone.d.ts +1 -1
- package/dist/unifi/firewall-zone.d.ts.map +1 -1
- package/dist/unifi/index.d.ts +4 -3
- package/dist/unifi/index.d.ts.map +1 -1
- package/dist/unifi/index.js +138 -58
- package/dist/unifi/index.js.map +10 -8
- package/dist/unifi/network-drift.d.ts +26 -0
- package/dist/unifi/network-drift.d.ts.map +1 -0
- package/dist/unifi/network-form.d.ts +40 -7
- package/dist/unifi/network-form.d.ts.map +1 -1
- package/dist/unifi/network.d.ts +1 -0
- package/dist/unifi/network.d.ts.map +1 -1
- package/dist/unifi/paginate.d.ts +36 -0
- package/dist/unifi/paginate.d.ts.map +1 -0
- package/dist/unifi/resource.d.ts +36 -1
- package/dist/unifi/resource.d.ts.map +1 -1
- package/docs/telemetry-spike.md +94 -0
- package/docs/telemetry.md +109 -0
- package/docs/unifi.md +56 -0
- package/package.json +6 -2
- package/src/forgejo/team-member.test.ts +14 -0
- package/src/forgejo/team-member.ts +28 -1
- package/src/proxmox/constraints-required.test.ts +1 -0
- package/src/proxmox/generated/constraints/index.ts +3 -1
- package/src/proxmox/generated/constraints/pve-access.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-backup.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-ceph.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-firewall.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-ha.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-metrics.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-notifications.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-replication.ts +1 -1
- package/src/proxmox/generated/constraints/pve-cluster-sdn.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-ceph.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-disks.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-lxc.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-network.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-qemu.ts +1 -1
- package/src/proxmox/generated/constraints/pve-nodes-storage.ts +30 -0
- package/src/proxmox/generated/constraints/pve-pools.ts +1 -1
- package/src/proxmox/generated/constraints/pve-storage.ts +1 -1
- package/src/proxmox/index.test.ts +11 -6
- package/src/proxmox/index.ts +19 -3
- package/src/proxmox/qemu-form.ts +49 -13
- package/src/proxmox/qemu-identity.test.ts +30 -0
- package/src/proxmox/qemu-judge.test.ts +65 -0
- package/src/proxmox/qemu-judge.ts +84 -0
- package/src/proxmox/qemu-lifecycle.test.ts +78 -19
- package/src/proxmox/qemu-lifecycle.ts +131 -32
- package/src/proxmox/qemu-net.test.ts +48 -0
- package/src/proxmox/qemu-net.ts +78 -0
- package/src/proxmox/qemu-ownership.test.ts +44 -0
- package/src/proxmox/qemu-props.test.ts +87 -0
- package/src/proxmox/qemu-props.ts +157 -0
- package/src/proxmox/qemu-volume.test.ts +80 -0
- package/src/proxmox/qemu-volume.ts +121 -0
- package/src/proxmox/qemu.ts +15 -29
- package/src/proxmox/storage-download-errors.ts +7 -0
- package/src/proxmox/storage-download-identity.test.ts +54 -0
- package/src/proxmox/storage-download-identity.ts +50 -0
- package/src/proxmox/storage-download-lifecycle.ts +226 -0
- package/src/proxmox/storage-download-ownership.test.ts +50 -0
- package/src/proxmox/storage-download-props.ts +38 -0
- package/src/proxmox/storage-download-task.ts +72 -0
- package/src/proxmox/storage-download.test.ts +216 -0
- package/src/proxmox/storage-download.ts +58 -0
- package/src/telemetry/fake-collector.ts +59 -0
- package/src/telemetry/http-client-sealing.test.ts +82 -0
- package/src/telemetry/index.ts +10 -0
- package/src/telemetry/layer.test.ts +204 -0
- package/src/telemetry/layer.ts +152 -0
- package/src/telemetry/redact-serialization.ts +178 -0
- package/src/telemetry/redact.test.ts +124 -0
- package/src/telemetry/redact.ts +147 -0
- package/src/unifi/drift.test.ts +78 -0
- package/src/unifi/drift.ts +56 -0
- package/src/unifi/errors-and-secrets.test.ts +127 -0
- package/src/unifi/fake-unifi.ts +5 -1
- package/src/unifi/firewall-zone-form.ts +31 -2
- package/src/unifi/firewall-zone.test.ts +21 -1
- package/src/unifi/firewall-zone.ts +1 -1
- package/src/unifi/get-only-guard.test.ts +165 -0
- package/src/unifi/index.ts +15 -1
- package/src/unifi/network-drift.test.ts +207 -0
- package/src/unifi/network-drift.ts +80 -0
- package/src/unifi/network-form.ts +11 -39
- package/src/unifi/network.test.ts +7 -82
- package/src/unifi/network.ts +6 -6
- package/src/unifi/paginate.test.ts +228 -0
- package/src/unifi/paginate.ts +129 -0
- package/src/unifi/resource.ts +73 -3
- package/src/unifi/write-op-reference.test.ts +236 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated pve-manager parameter constraints for `/nodes/{node}/storage` — DO NOT EDIT BY HAND.
|
|
3
|
+
*
|
|
4
|
+
* Run: bun codegen/constraints.ts
|
|
5
|
+
* Manifest entry: `pve-apidoc` — pve-manager 9.2.11/f6997e698c7933ea
|
|
6
|
+
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
|
+
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
|
+
*
|
|
9
|
+
* 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
|
+
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
|
+
* and therefore unchecked at plan time.
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ A `patternSource` with no `pattern` beside it is a rule that could NOT be carried into a
|
|
14
|
+
* JavaScript RegExp faithfully (codegen/pattern.ts). It is recorded and NOT enforced.
|
|
15
|
+
* ⚠️ `pattern` IS NOT THE VENDOR'S SPELLING. For PVE it is anchored, because PVE applies
|
|
16
|
+
* `m/^$pattern$/` itself (JSONSchema.pm); for PBS it is the vendor's own, which already
|
|
17
|
+
* carries its anchors. `patternSource` is the spelling to quote at a human — param-rules.ts.
|
|
18
|
+
* ⚠️ `each: true` means the value rules describe every ELEMENT of a repeated key, because the
|
|
19
|
+
* parameter is an array and stated its limits on `items`.
|
|
20
|
+
*/
|
|
21
|
+
import type { EndpointConstraints } from '../../constraints.ts';
|
|
22
|
+
|
|
23
|
+
export const PVE_NODES_STORAGE_CONSTRAINTS: Readonly<Record<string, EndpointConstraints>> = {
|
|
24
|
+
"pve:POST /nodes/{node}/storage/{storage}/download-url": {
|
|
25
|
+
"checksum-algorithm": {"enum":["md5","sha1","sha224","sha256","sha384","sha512"],"type":"string"},
|
|
26
|
+
"content": {"enum":["iso","vztmpl","import"],"format":"pve-storage-content","required":true,"type":"string"},
|
|
27
|
+
"filename": {"maxLength":255,"required":true,"type":"string"},
|
|
28
|
+
"url": {"pattern":"^https?://.*\\n?$","patternSource":"https?://.*","required":true,"type":"string"},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* sha256 9def8f13611184ee, read on a PVE cluster node from
|
|
7
7
|
* /usr/share/pve-docs/api-viewer/apidoc.js
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 59 of this product's 258 POST/PUT endpoints are tabled across all areas: the ones
|
|
10
10
|
* this package writes to, named in its own source. Every other vendor write endpoint is UNTABLED
|
|
11
11
|
* and therefore unchecked at plan time.
|
|
12
12
|
*
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Public barrel: control-plane Resource constructors and `ProxmoxLxc`, not
|
|
2
|
+
* Public barrel: control-plane Resource constructors and `ProxmoxLxc`, not NIC apply.
|
|
3
3
|
*
|
|
4
|
-
* ⛔
|
|
5
|
-
*
|
|
6
|
-
* retain on destroy (lxc.ts).
|
|
4
|
+
* ⛔ NodeNetwork/NetworkApply stay off the barrel — they stage interfaces.new. ★ Lxc is a
|
|
5
|
+
* Resource since 2026-09-21: adopt-first, refuse-not-replace, retain on destroy (lxc.ts).
|
|
7
6
|
* ★ CephDaemon, CephFs and CephOsd are Resources since 2026-09-22: adopt-only by shape and retain
|
|
8
7
|
* on destroy (ceph-adopt.test.ts). ⛔ CephFlag stays Provider-only: a declared flag reasserts a
|
|
9
8
|
* maintenance toggle on every deploy (ceph-flag.ts).
|
|
10
9
|
* ★ ApiToken and ZfsPool are Resources since 2026-09-23, decision 9: ApiToken metadata-only,
|
|
11
10
|
* ZfsPool adopt-only when `devices`/`raidlevel` are left undeclared (api-token-adopt.test.ts,
|
|
12
11
|
* zfs-pool-adopt.test.ts).
|
|
12
|
+
* ★ Vm and StorageDownload are Resources since 2026-09-26, for Talos: Vm's declared-keys model
|
|
13
|
+
* (qemu-props.ts) and retain-by-default; StorageDownload's checksum-pinned, create-oriented
|
|
14
|
+
* `download-url` (storage-download.ts). Neither was Provider-only for a design reason that
|
|
15
|
+
* changed — Vm's old "5-default PUT" is what kept it off this barrel until qemu-props.ts fixed
|
|
16
|
+
* it, per qemu-lifecycle.test.ts's own regression test.
|
|
13
17
|
*/
|
|
14
18
|
import { expect, test } from 'bun:test';
|
|
15
19
|
import { PbsDatastore, ProxmoxStorage } from './index.ts';
|
|
@@ -42,7 +46,9 @@ const resourceExports = [
|
|
|
42
46
|
'ProxmoxSdnVnet',
|
|
43
47
|
'ProxmoxSdnZone',
|
|
44
48
|
'ProxmoxStorage',
|
|
49
|
+
'ProxmoxStorageDownload',
|
|
45
50
|
'ProxmoxUser',
|
|
51
|
+
'ProxmoxVm',
|
|
46
52
|
'ProxmoxZfsPool',
|
|
47
53
|
] as const;
|
|
48
54
|
|
|
@@ -61,8 +67,7 @@ test('the provisioning baseline is on the barrel: the list, the declaration, the
|
|
|
61
67
|
expect(barrel.provisionBootstrap()).toStartWith('#!/bin/sh\n');
|
|
62
68
|
});
|
|
63
69
|
|
|
64
|
-
test('
|
|
65
|
-
expect(src).not.toMatch(/export \{ ProxmoxVm[, }]/);
|
|
70
|
+
test('NIC apply and the Ceph flag stay Provider-only', () => {
|
|
66
71
|
expect(src).not.toMatch(/export \{ ProxmoxNodeNetwork[, }]/);
|
|
67
72
|
expect(src).not.toMatch(/export \{ ProxmoxNetworkApply[, }]/);
|
|
68
73
|
expect(src).not.toMatch(/export \{ ProxmoxCephFlag[, }]/);
|
package/src/proxmox/index.ts
CHANGED
|
@@ -6,8 +6,18 @@
|
|
|
6
6
|
* provider needs but a consumer should not depend on. An `export *` here would publish
|
|
7
7
|
* every helper as API and make the next refactor a breaking change.
|
|
8
8
|
* ★ Control-plane Resource constructors (Storage, SDN, access, HA, backup,
|
|
9
|
-
* metrics, PBS) sit next to their Providers. NIC apply
|
|
10
|
-
*
|
|
9
|
+
* metrics, PBS) sit next to their Providers. NIC apply stays Provider-only
|
|
10
|
+
* so a stack cannot declare it by accident.
|
|
11
|
+
* ★ `ProxmoxVm` JOINED THEM 2026-09-26, FOR TALOS. Its declared-keys model (qemu-props.ts) is
|
|
12
|
+
* what let it join safely: an undeclared field is unmanaged, so it is never sent and never
|
|
13
|
+
* compared — the fix for the "5-default PUT" a Provider-only `Proxmox.Vm` never exercised.
|
|
14
|
+
* `cipassword` and `machine` are typed `never` and refused if smuggled past the types
|
|
15
|
+
* (qemu-form.ts's `formRefusals`); `defaultRemovalPolicy: 'retain'` means dropping a
|
|
16
|
+
* declaration never destroys a guest's disks. Node-pinned semantics (qemu-read.ts's "A
|
|
17
|
+
* migration is not an update") are unchanged by the export.
|
|
18
|
+
* ★ `Proxmox.StorageDownload` JOINED 2026-09-26 TOO, ALONGSIDE IT: `download-url` (content
|
|
19
|
+
* `import`, checksum-pinned) as a CREATE-oriented family — no adoption, no update, a task-poll
|
|
20
|
+
* failure (a checksum mismatch included) refuses rather than reports success.
|
|
11
21
|
* ★ `ProxmoxLxc` JOINED THEM 2026-09-21, AND IT WAS KEPT OFF UNTIL IT COULD NOT HURT A GUEST.
|
|
12
22
|
* The provider-only version diffed four fields and would have planned a create over any read
|
|
13
23
|
* failure. The Resource now adopts from the live config, refuses every change PVE cannot make
|
|
@@ -102,7 +112,13 @@ export {
|
|
|
102
112
|
} from './provision-baseline.ts';
|
|
103
113
|
export { provisionBootstrap } from './provision-bootstrap.ts';
|
|
104
114
|
export { type DeclareProvisionOptions, declareProvisionBaseline } from './provision-declare.ts';
|
|
105
|
-
export { ProxmoxVmProvider } from './qemu.ts';
|
|
115
|
+
export { ProxmoxVm, ProxmoxVmProvider, type VmAttributes, type VmProps } from './qemu.ts';
|
|
116
|
+
export {
|
|
117
|
+
ProxmoxStorageDownload,
|
|
118
|
+
ProxmoxStorageDownloadProvider,
|
|
119
|
+
type StorageDownloadAttributes,
|
|
120
|
+
type StorageDownloadProps,
|
|
121
|
+
} from './storage-download.ts';
|
|
106
122
|
export { ProxmoxReplicationJobProvider } from './replication-job.ts';
|
|
107
123
|
export { ProxmoxRole, ProxmoxRoleProvider } from './role.ts';
|
|
108
124
|
export { ProxmoxSdnApply, ProxmoxSdnApplyProvider } from './sdn-apply.ts';
|
package/src/proxmox/qemu-form.ts
CHANGED
|
@@ -1,15 +1,51 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
/**
|
|
2
|
+
* QEMU create and update forms — declared keys only. Path identity stays out of the body.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ NO DEFAULT IS EVER SUBSTITUTED FOR AN UNDECLARED FIELD. See qemu-props.ts's header for the
|
|
5
|
+
* "5-default PUT" this replaces: the old `shape()` sent `cores`/`memory`/`name`/`onboot`/
|
|
6
|
+
* `sockets` on every write, defaults included for whichever the declaration left out, so
|
|
7
|
+
* editing one field on an adopted VM silently reset every other to its factory value.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
type VmProps,
|
|
11
|
+
declaredKeys,
|
|
12
|
+
declaredValue,
|
|
13
|
+
isManagedKey,
|
|
14
|
+
wireValue,
|
|
15
|
+
} from './qemu-props.ts';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Why a declared key cannot go on the wire, checked before every create and every update.
|
|
19
|
+
* ★ THE SAME RULE `lxc-create-form.ts` ENFORCES FOR `Proxmox.Lxc`: a key smuggled past the types
|
|
20
|
+
* (a cast, a JS caller) is refused here rather than silently dropped or silently sent.
|
|
21
|
+
*/
|
|
22
|
+
export const formRefusals = (props: VmProps): string[] =>
|
|
23
|
+
declaredKeys(props)
|
|
24
|
+
.filter((key) => !isManagedKey(key))
|
|
25
|
+
.map((key) => `${key}: not a config key this resource manages (vmid ${String(props.vmid)}).`);
|
|
26
|
+
|
|
27
|
+
/** The declared config, wire-spelled — every key `formRefusals` did not already refuse. */
|
|
28
|
+
export const form = (props: VmProps): Record<string, string> => {
|
|
29
|
+
const out: Record<string, string> = {};
|
|
30
|
+
for (const key of declaredKeys(props)) {
|
|
31
|
+
if (!isManagedKey(key)) continue;
|
|
32
|
+
out[key] = wireValue(declaredValue(props, key));
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The create body: the declared form plus the vmid a create needs.
|
|
39
|
+
* ⚠️ NO RETURN-TYPE ANNOTATION, ON PURPOSE. Annotating this `Record<string, string>` erases the
|
|
40
|
+
* object literal's own inferred shape, and with it the one thing `nodes.createNodeQemu({
|
|
41
|
+
* ...createForm(news), node: news.node })` (qemu-lifecycle.ts) needs: TypeScript only accepts a
|
|
42
|
+
* spread as satisfying `CreateNodeQemuRequest`'s required `vmid` when the spread's inferred type
|
|
43
|
+
* still carries `vmid` as a literal key, which an index-signature-only type cannot prove.
|
|
44
|
+
*/
|
|
45
|
+
export const createForm = (props: VmProps) => ({
|
|
46
|
+
...form(props),
|
|
47
|
+
vmid: String(props.vmid),
|
|
12
48
|
});
|
|
13
49
|
|
|
14
|
-
/**
|
|
15
|
-
export const
|
|
50
|
+
/** The update body — identical construction to `createForm`, minus `vmid`, which PUT never sends. */
|
|
51
|
+
export const updateForm = form;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Proxmox.Vm` identity — red-team I1 on PR 297, through the real engine over a stub fetch.
|
|
3
|
+
* ⛔ WHAT THIS PINS: a changed `vmid` on an already-managed VM is refused as a different machine,
|
|
4
|
+
* never planned as an `update` (which used to POST a second VM and leave the first one running
|
|
5
|
+
* and untracked).
|
|
6
|
+
*/
|
|
7
|
+
import { expect, test } from 'bun:test';
|
|
8
|
+
import * as Layer from 'effect/Layer';
|
|
9
|
+
import { engineOver } from '../verify/fake-engine.ts';
|
|
10
|
+
import { FAKE_TARGET, fakePve, withoutBao } from './fake-pve.ts';
|
|
11
|
+
import { ProxmoxVm, ProxmoxVmProvider } from './qemu.ts';
|
|
12
|
+
|
|
13
|
+
const NODE = 'pve1';
|
|
14
|
+
const props = { node: NODE, target: FAKE_TARGET, vmid: 150 };
|
|
15
|
+
const live = { digest: 'a'.repeat(40), memory: '512' };
|
|
16
|
+
|
|
17
|
+
const engine = (fake: ReturnType<typeof fakePve>) =>
|
|
18
|
+
engineOver(ProxmoxVmProvider().pipe(Layer.provideMerge(fake.layer)));
|
|
19
|
+
|
|
20
|
+
test('a changed vmid is refused as a different VM, never planned as an update', async () => {
|
|
21
|
+
const fake = fakePve((call) => (call.method === 'GET' ? live : null));
|
|
22
|
+
await withoutBao(async () => {
|
|
23
|
+
const stack = engine(fake);
|
|
24
|
+
await stack.deploy(ProxmoxVm('row', props));
|
|
25
|
+
await expect(stack.deploy(ProxmoxVm('row', { ...props, vmid: 151 }))).rejects.toThrow(
|
|
26
|
+
/vmid: 150 -> 151 is a different VM/,
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
expect(fake.writes()).toEqual([]);
|
|
30
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure unit tests for `judge`/`identityRefusal` — red-team C1/I1 on PR 297. No network, no fake PVE.
|
|
3
|
+
* ⛔ WHAT THESE PIN: only drifted keys are ever PUT (an unrelated field's drift does not drag the
|
|
4
|
+
* disk/NIC keys along, the failure mode a whole-form resend had); a vmid change is refused, never
|
|
5
|
+
* built as a second VM.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, test } from 'bun:test';
|
|
8
|
+
import { FAKE_TARGET } from './fake-pve.ts';
|
|
9
|
+
import { identityRefusal, judge } from './qemu-judge.ts';
|
|
10
|
+
import type { VmProps } from './qemu-props.ts';
|
|
11
|
+
|
|
12
|
+
const base = { node: 'pve1', target: FAKE_TARGET, vmid: 101 } as const;
|
|
13
|
+
|
|
14
|
+
describe('judge', () => {
|
|
15
|
+
test('a fresh-spelled disk and a MAC-less NIC both settle against live, drifting nothing', () => {
|
|
16
|
+
const props: VmProps = {
|
|
17
|
+
...base,
|
|
18
|
+
ide2: 'cephtb4:cloudinit',
|
|
19
|
+
net0: 'virtio,bridge=vmbr0,tag=20',
|
|
20
|
+
scsi0: 'cephtb4:32',
|
|
21
|
+
};
|
|
22
|
+
const config = {
|
|
23
|
+
ide2: 'cephtb4:vm-101-cloudinit,media=cdrom',
|
|
24
|
+
net0: 'virtio=AA:BB:CC:DD:EE:FF,bridge=vmbr0,tag=20',
|
|
25
|
+
scsi0: 'cephtb4:vm-101-disk-0,size=32G',
|
|
26
|
+
};
|
|
27
|
+
expect(judge(props, config)).toEqual({ drift: [], put: {}, refuse: [] });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('an unrelated field drifting does not drag the settled disk/NIC keys into the PUT', () => {
|
|
31
|
+
const props: VmProps = {
|
|
32
|
+
...base,
|
|
33
|
+
memory: 4096,
|
|
34
|
+
net0: 'virtio,bridge=vmbr0,tag=20',
|
|
35
|
+
scsi0: 'cephtb4:32',
|
|
36
|
+
};
|
|
37
|
+
const config = {
|
|
38
|
+
memory: '2048',
|
|
39
|
+
net0: 'virtio=AA:BB:CC:DD:EE:FF,bridge=vmbr0,tag=20',
|
|
40
|
+
scsi0: 'cephtb4:vm-101-disk-0,size=32G',
|
|
41
|
+
};
|
|
42
|
+
const change = judge(props, config);
|
|
43
|
+
expect(change.drift).toEqual(['memory']);
|
|
44
|
+
expect(change.put).toEqual({ memory: '4096' });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('a scalar key not yet on the live config is put as declared', () => {
|
|
48
|
+
const change = judge({ ...base, agent: true }, {});
|
|
49
|
+
expect(change.put).toEqual({ agent: '1' });
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('identityRefusal', () => {
|
|
54
|
+
test('no prior vmid: never refused (a fresh create)', () => {
|
|
55
|
+
expect(identityRefusal(101, undefined)).toBeUndefined();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('the same vmid: never refused', () => {
|
|
59
|
+
expect(identityRefusal(101, 101)).toBeUndefined();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('a changed vmid: refused, naming both -- a new resource, not an update', () => {
|
|
63
|
+
expect(identityRefusal(151, 150)).toMatch(/vmid: 150 -> 151 is a different VM/);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declared against live: what a `Proxmox.Vm` deploy would write, per key — the QEMU sibling of
|
|
3
|
+
* lxc-judge.ts, added 2026-09-26 (red-team C1/I1 on PR 297).
|
|
4
|
+
*
|
|
5
|
+
* ⛔ REPLACES A WHOLE-STRING, WHOLE-FORM COMPARISON. Before this, `matches` compared each declared
|
|
6
|
+
* key to live BYTE FOR BYTE and, on any drift, `formToSend` re-PUT every declared key at once
|
|
7
|
+
* (qemu-lifecycle.ts). A `scsi0`/`ide2`/`net0` declared in the "new" or MAC-less spelling then
|
|
8
|
+
* NEVER matched its own live volume/NIC (qemu-volume.ts / qemu-net.ts explain why), so every
|
|
9
|
+
* deploy re-sent all three -- reallocating the disk, re-importing it, and handing out a fresh MAC
|
|
10
|
+
* each time, even when nothing else changed. `judge` below decides per key, on the LIVE volume id
|
|
11
|
+
* and LIVE MAC, so an unrelated field's drift no longer drags the disk/NIC keys along with it.
|
|
12
|
+
*/
|
|
13
|
+
import { type VmProps, declaredKeys, declaredValue, indexedKey, wireValue } from './qemu-props.ts';
|
|
14
|
+
import { judgeDisk } from './qemu-volume.ts';
|
|
15
|
+
import { netWrite, sameNet } from './qemu-net.ts';
|
|
16
|
+
|
|
17
|
+
export type VmChange = {
|
|
18
|
+
/** Keys to `PUT …/config`, wire-spelled and built on live ids where one exists. */
|
|
19
|
+
readonly put: Record<string, string>;
|
|
20
|
+
/** Why a key cannot be written as an update. Non-empty means nothing is written. */
|
|
21
|
+
readonly refuse: readonly string[];
|
|
22
|
+
/** Every declared key that differs from live, refused or not. */
|
|
23
|
+
readonly drift: readonly string[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const judge = (props: VmProps, config: Record<string, string>): VmChange => {
|
|
27
|
+
const put: Record<string, string> = {};
|
|
28
|
+
const refuse: string[] = [];
|
|
29
|
+
const drift = new Set<string>();
|
|
30
|
+
|
|
31
|
+
for (const key of declaredKeys(props)) {
|
|
32
|
+
const want = wireValue(declaredValue(props, key));
|
|
33
|
+
const have = config[key];
|
|
34
|
+
const family = indexedKey(key)?.[0];
|
|
35
|
+
|
|
36
|
+
if (family === 'scsi' || family === 'ide') {
|
|
37
|
+
if (have === undefined) {
|
|
38
|
+
drift.add(key);
|
|
39
|
+
put[key] = want;
|
|
40
|
+
} else {
|
|
41
|
+
const verdict = judgeDisk(key, want, have);
|
|
42
|
+
if (verdict.refuse !== undefined) {
|
|
43
|
+
drift.add(key);
|
|
44
|
+
refuse.push(verdict.refuse);
|
|
45
|
+
} else if (verdict.put !== undefined) {
|
|
46
|
+
drift.add(key);
|
|
47
|
+
put[key] = verdict.put;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (family === 'net') {
|
|
54
|
+
if (have === undefined) {
|
|
55
|
+
drift.add(key);
|
|
56
|
+
put[key] = want;
|
|
57
|
+
} else if (!sameNet(want, have)) {
|
|
58
|
+
drift.add(key);
|
|
59
|
+
put[key] = netWrite(want, have);
|
|
60
|
+
}
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Scalars, `ipconfigN`, `serialN`: a plain string compare, as the old whole-form model did.
|
|
65
|
+
// ⚠️ No "new spelling" or MAC ambiguity applies to these -- see qemu-props.ts's own doc per key.
|
|
66
|
+
if (want !== (have ?? '')) {
|
|
67
|
+
drift.add(key);
|
|
68
|
+
put[key] = want;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { drift: [...drift].sort(), put, refuse };
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* What makes a `Proxmox.Vm` a DIFFERENT machine: a new vmid (red-team I1). Never planned as a
|
|
76
|
+
* replace -- see qemu.ts's header on why nothing here replaces a running VM.
|
|
77
|
+
* ⚠️ `was` PREFERS THE RECORDED ATTRIBUTE, so a `node` change (HA failover, a hand `qm migrate`)
|
|
78
|
+
* never reads as a vmid change: only `vmid` is judged here, mirroring lxc-identity.ts.
|
|
79
|
+
*/
|
|
80
|
+
export const identityRefusal = (vmid: number, was: number | undefined): string | undefined =>
|
|
81
|
+
was === undefined || vmid === was
|
|
82
|
+
? undefined
|
|
83
|
+
: `vmid: ${String(was)} -> ${String(vmid)} is a different VM. Declare it as a new resource; ` +
|
|
84
|
+
'this one is retained when its declaration goes.';
|
|
@@ -5,7 +5,7 @@ import * as Effect from 'effect/Effect';
|
|
|
5
5
|
import * as Layer from 'effect/Layer';
|
|
6
6
|
import { engineOver } from '../verify/fake-engine.ts';
|
|
7
7
|
import { FAKE_TARGET, type FakePve, fakePve, withoutBao } from './fake-pve.ts';
|
|
8
|
-
import { ProxmoxVm, ProxmoxVmProvider } from './qemu.ts';
|
|
8
|
+
import { ProxmoxVm, ProxmoxVmProvider, type VmProps } from './qemu.ts';
|
|
9
9
|
|
|
10
10
|
const NODE = 'pve1';
|
|
11
11
|
const VMID = 150;
|
|
@@ -42,24 +42,27 @@ describe('Proxmox.Vm distilled transport', () => {
|
|
|
42
42
|
});
|
|
43
43
|
|
|
44
44
|
test('absence creates on the collection route, waits, then settles', async () => {
|
|
45
|
-
let
|
|
45
|
+
let posted: Record<string, string> | undefined;
|
|
46
46
|
const fake = fakePve((call) => {
|
|
47
47
|
if (call.path.includes('/tasks/') && call.path.endsWith('/status')) {
|
|
48
48
|
return { status: 'stopped', exitstatus: 'OK' };
|
|
49
49
|
}
|
|
50
50
|
if (isIndex(call)) return [];
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
// ⚠️ `net0` IS NOW A MANAGED, COMPARED KEY (qemu-props.ts) — the old fixture answered the
|
|
52
|
+
// SAME static `live` regardless of what was posted, which only worked because the old
|
|
53
|
+
// `matches` never looked at `net0` at all. Echoing the create form back is what proves
|
|
54
|
+
// the declared value is what a real PVE config would report.
|
|
55
|
+
if (call.method === 'GET') return posted === undefined ? missing() : { ...live, ...posted };
|
|
56
|
+
posted = call.form;
|
|
53
57
|
return UPID;
|
|
54
58
|
});
|
|
59
|
+
const declared = { ...props, net0: 'virtio=AA:BB:CC:DD:EE:FF,bridge=vmbr0' };
|
|
55
60
|
await withoutBao(async () => {
|
|
56
61
|
const stack = engine(fake);
|
|
57
|
-
await stack.deploy(
|
|
58
|
-
|
|
62
|
+
await stack.deploy(ProxmoxVm('row', declared));
|
|
63
|
+
expect((await stack.verify(ProxmoxVm('row', declared), { all: true })).rows[0]).toMatchObject(
|
|
64
|
+
{ diff: 'noop' },
|
|
59
65
|
);
|
|
60
|
-
expect((await stack.verify(ProxmoxVm('row', props), { all: true })).rows[0]).toMatchObject({
|
|
61
|
-
diff: 'noop',
|
|
62
|
-
});
|
|
63
66
|
});
|
|
64
67
|
expect(fake.writes()).toEqual([`POST nodes/${NODE}/qemu`]);
|
|
65
68
|
expect(fake.calls.find((call) => call.method === 'POST')?.form['net0']).toBe(
|
|
@@ -68,6 +71,41 @@ describe('Proxmox.Vm distilled transport', () => {
|
|
|
68
71
|
expect(fake.calls.some((call) => call.path.includes('/tasks/'))).toBe(true);
|
|
69
72
|
});
|
|
70
73
|
|
|
74
|
+
test('C1: a "fresh" disk/cloudinit and a MAC-less NIC do not re-drift after PVE allocates them', async () => {
|
|
75
|
+
// ⛔ THE MEASURED REGRESSION: before qemu-judge.ts, every deploy after the create re-sent
|
|
76
|
+
// scsi0/ide2/net0 in their DECLARED (fresh) spelling, because a plain string compare never
|
|
77
|
+
// equals what PVE reports back for an allocated volume or a generated MAC -- reallocating the
|
|
78
|
+
// disk and handing out a new MAC on every single deploy.
|
|
79
|
+
const declared = {
|
|
80
|
+
...props,
|
|
81
|
+
ide2: 'cephtb4:cloudinit',
|
|
82
|
+
net0: 'virtio,bridge=vmbr0,tag=20',
|
|
83
|
+
scsi0: 'cephtb4:32',
|
|
84
|
+
};
|
|
85
|
+
const allocated = {
|
|
86
|
+
ide2: 'cephtb4:vm-150-cloudinit,media=cdrom',
|
|
87
|
+
net0: 'virtio=AA:BB:CC:DD:EE:FF,bridge=vmbr0,tag=20',
|
|
88
|
+
scsi0: 'cephtb4:vm-150-disk-0,size=32G',
|
|
89
|
+
};
|
|
90
|
+
let created = false;
|
|
91
|
+
const fake = fakePve((call) => {
|
|
92
|
+
if (call.path.includes('/tasks/') && call.path.endsWith('/status')) {
|
|
93
|
+
return { status: 'stopped', exitstatus: 'OK' };
|
|
94
|
+
}
|
|
95
|
+
if (isIndex(call)) return [];
|
|
96
|
+
if (call.method === 'GET') return created ? { ...live, ...allocated } : missing();
|
|
97
|
+
created = true;
|
|
98
|
+
return UPID;
|
|
99
|
+
});
|
|
100
|
+
await withoutBao(async () => {
|
|
101
|
+
const stack = engine(fake);
|
|
102
|
+
await stack.deploy(ProxmoxVm('row', declared));
|
|
103
|
+
await stack.deploy(ProxmoxVm('row', declared));
|
|
104
|
+
await stack.deploy(ProxmoxVm('row', declared));
|
|
105
|
+
});
|
|
106
|
+
expect(fake.writes()).toEqual([`POST nodes/${NODE}/qemu`]);
|
|
107
|
+
});
|
|
108
|
+
|
|
71
109
|
test('a transitional task status keeps polling instead of aborting the create', async () => {
|
|
72
110
|
// ⚠️ `GetNodeTaskStatusResponseStatus` types `status` as the closed union
|
|
73
111
|
// "running" | "stopped", but the runtime validator is `S.String` (distilled-proxmox's
|
|
@@ -113,24 +151,45 @@ describe('Proxmox.Vm distilled transport', () => {
|
|
|
113
151
|
expect(fake.writes()).toEqual([]);
|
|
114
152
|
});
|
|
115
153
|
|
|
116
|
-
test('drift updates the config route and
|
|
117
|
-
|
|
154
|
+
test('drift on a declared field updates the config route, and only that field', async () => {
|
|
155
|
+
// ⛔ THE "5-DEFAULT PUT" REGRESSION TEST. `name` is deliberately left UNDECLARED and deliberately
|
|
156
|
+
// NOT what qemu-form.ts's old default would have produced (`vm150`) -- the old `shape()` sent
|
|
157
|
+
// it (and `cores`/`sockets`/`onboot`) on every write regardless, silently resetting an
|
|
158
|
+
// undeclared field to its factory value. Only `memory` is declared here, so only `memory`
|
|
159
|
+
// may appear in the PUT body.
|
|
160
|
+
const declared = { ...props, memory: 512 };
|
|
161
|
+
let current: unknown = { ...live, memory: '256', name: 'a-name-nobody-declared' };
|
|
118
162
|
const fake = fakePve((call) => {
|
|
119
163
|
if (call.method === 'GET') return current;
|
|
120
|
-
current =
|
|
164
|
+
current = { ...(current as Record<string, unknown>), memory: '512' };
|
|
121
165
|
return null;
|
|
122
166
|
});
|
|
123
167
|
await withoutBao(async () => {
|
|
124
168
|
const stack = engine(fake);
|
|
125
|
-
expect((await stack.verify(ProxmoxVm('row',
|
|
126
|
-
diff: 'update',
|
|
127
|
-
|
|
128
|
-
await stack.deploy(ProxmoxVm('row',
|
|
129
|
-
expect((await stack.verify(ProxmoxVm('row',
|
|
130
|
-
diff: 'noop',
|
|
131
|
-
|
|
169
|
+
expect((await stack.verify(ProxmoxVm('row', declared), { all: true })).rows[0]).toMatchObject(
|
|
170
|
+
{ diff: 'update' },
|
|
171
|
+
);
|
|
172
|
+
await stack.deploy(ProxmoxVm('row', declared));
|
|
173
|
+
expect((await stack.verify(ProxmoxVm('row', declared), { all: true })).rows[0]).toMatchObject(
|
|
174
|
+
{ diff: 'noop' },
|
|
175
|
+
);
|
|
132
176
|
});
|
|
133
177
|
expect(fake.writes()).toEqual([`PUT nodes/${NODE}/qemu/${String(VMID)}/config`]);
|
|
178
|
+
const put = fake.calls.find((call) => call.method === 'PUT');
|
|
179
|
+
expect(put?.form).toEqual({ memory: '512' });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('an unmanaged key smuggled past the types is refused before any write', async () => {
|
|
183
|
+
const fake = fakePve((call) => (call.method === 'GET' ? live : null));
|
|
184
|
+
// ⛔ `cipassword` IS TYPED `never` (qemu-props.ts): a cast is the only way in, exactly the
|
|
185
|
+
// scenario the type cannot guard against by itself. `formRefusals` is the runtime backstop.
|
|
186
|
+
const smuggled = { ...props, cipassword: 'not-a-real-secret' } as unknown as VmProps;
|
|
187
|
+
await withoutBao(async () => {
|
|
188
|
+
await expect(engine(fake).deploy(ProxmoxVm('row', smuggled))).rejects.toThrow(
|
|
189
|
+
/cipassword: not a config key this resource manages/,
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
expect(fake.writes()).toEqual([]);
|
|
134
193
|
});
|
|
135
194
|
|
|
136
195
|
test('unrelated read failures and a digest-less body do not write', async () => {
|