@homeflare/alchemy 0.1.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/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/cloudflare/client.d.ts +45 -0
- package/dist/cloudflare/client.d.ts.map +1 -0
- package/dist/cloudflare/index.d.ts +11 -0
- package/dist/cloudflare/index.d.ts.map +1 -0
- package/dist/cloudflare/index.js +129 -0
- package/dist/cloudflare/index.js.map +13 -0
- package/dist/cloudflare/lock-rules.d.ts +50 -0
- package/dist/cloudflare/lock-rules.d.ts.map +1 -0
- package/dist/cloudflare/providers.d.ts +19 -0
- package/dist/cloudflare/providers.d.ts.map +1 -0
- package/dist/cloudflare/r2-bucket-lock.d.ts +100 -0
- package/dist/cloudflare/r2-bucket-lock.d.ts.map +1 -0
- package/dist/forgejo/branch-protection-form.d.ts +19 -0
- package/dist/forgejo/branch-protection-form.d.ts.map +1 -0
- package/dist/forgejo/branch-protection.d.ts +47 -0
- package/dist/forgejo/branch-protection.d.ts.map +1 -0
- package/dist/forgejo/client.d.ts +37 -0
- package/dist/forgejo/client.d.ts.map +1 -0
- package/dist/forgejo/index.d.ts +18 -0
- package/dist/forgejo/index.d.ts.map +1 -0
- package/dist/forgejo/index.js +479 -0
- package/dist/forgejo/index.js.map +21 -0
- package/dist/forgejo/org-actions-secrets.d.ts +45 -0
- package/dist/forgejo/org-actions-secrets.d.ts.map +1 -0
- package/dist/forgejo/org-label.d.ts +46 -0
- package/dist/forgejo/org-label.d.ts.map +1 -0
- package/dist/forgejo/org-team.d.ts +42 -0
- package/dist/forgejo/org-team.d.ts.map +1 -0
- package/dist/forgejo/repo-webhook-form.d.ts +27 -0
- package/dist/forgejo/repo-webhook-form.d.ts.map +1 -0
- package/dist/forgejo/repo-webhook.d.ts +46 -0
- package/dist/forgejo/repo-webhook.d.ts.map +1 -0
- package/dist/forgejo/repository.d.ts +45 -0
- package/dist/forgejo/repository.d.ts.map +1 -0
- package/dist/forgejo/resource.d.ts +83 -0
- package/dist/forgejo/resource.d.ts.map +1 -0
- package/dist/forgejo/team-member.d.ts +39 -0
- package/dist/forgejo/team-member.d.ts.map +1 -0
- package/dist/forgejo/values.d.ts +24 -0
- package/dist/forgejo/values.d.ts.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +9 -0
- package/dist/openbao/auth-role-form.d.ts +38 -0
- package/dist/openbao/auth-role-form.d.ts.map +1 -0
- package/dist/openbao/auth-role.d.ts +20 -0
- package/dist/openbao/auth-role.d.ts.map +1 -0
- package/dist/openbao/bao-address.d.ts +72 -0
- package/dist/openbao/bao-address.d.ts.map +1 -0
- package/dist/openbao/bao-http.d.ts +71 -0
- package/dist/openbao/bao-http.d.ts.map +1 -0
- package/dist/openbao/bao-status.d.ts +74 -0
- package/dist/openbao/bao-status.d.ts.map +1 -0
- package/dist/openbao/cloudflare-group-scope.d.ts +73 -0
- package/dist/openbao/cloudflare-group-scope.d.ts.map +1 -0
- package/dist/openbao/cloudflare-parity-catalog.d.ts +28 -0
- package/dist/openbao/cloudflare-parity-catalog.d.ts.map +1 -0
- package/dist/openbao/cloudflare-parity-snapshot.d.ts +31 -0
- package/dist/openbao/cloudflare-parity-snapshot.d.ts.map +1 -0
- package/dist/openbao/cloudflare-parity.d.ts +14 -0
- package/dist/openbao/cloudflare-parity.d.ts.map +1 -0
- package/dist/openbao/cloudflare-permission-groups-solve.d.ts +85 -0
- package/dist/openbao/cloudflare-permission-groups-solve.d.ts.map +1 -0
- package/dist/openbao/cloudflare-permission-groups.d.ts +87 -0
- package/dist/openbao/cloudflare-permission-groups.d.ts.map +1 -0
- package/dist/openbao/cloudflare-policy.d.ts +65 -0
- package/dist/openbao/cloudflare-policy.d.ts.map +1 -0
- package/dist/openbao/cloudflare-role-form.d.ts +110 -0
- package/dist/openbao/cloudflare-role-form.d.ts.map +1 -0
- package/dist/openbao/cloudflare-role-wire.d.ts +27 -0
- package/dist/openbao/cloudflare-role-wire.d.ts.map +1 -0
- package/dist/openbao/cloudflare-role.d.ts +35 -0
- package/dist/openbao/cloudflare-role.d.ts.map +1 -0
- package/dist/openbao/cloudflare-roles-config.d.ts +67 -0
- package/dist/openbao/cloudflare-roles-config.d.ts.map +1 -0
- package/dist/openbao/cloudflare-roles-expand.d.ts +76 -0
- package/dist/openbao/cloudflare-roles-expand.d.ts.map +1 -0
- package/dist/openbao/digest.d.ts +24 -0
- package/dist/openbao/digest.d.ts.map +1 -0
- package/dist/openbao/fake-bao.d.ts +40 -0
- package/dist/openbao/fake-bao.d.ts.map +1 -0
- package/dist/openbao/forgejo-bootstrap-parse.d.ts +46 -0
- package/dist/openbao/forgejo-bootstrap-parse.d.ts.map +1 -0
- package/dist/openbao/forgejo-bootstrap.d.ts +29 -0
- package/dist/openbao/forgejo-bootstrap.d.ts.map +1 -0
- package/dist/openbao/index.d.ts +18 -0
- package/dist/openbao/index.d.ts.map +1 -0
- package/dist/openbao/index.js +1158 -0
- package/dist/openbao/index.js.map +33 -0
- package/dist/openbao/mount-form.d.ts +60 -0
- package/dist/openbao/mount-form.d.ts.map +1 -0
- package/dist/openbao/mount-wire.d.ts +24 -0
- package/dist/openbao/mount-wire.d.ts.map +1 -0
- package/dist/openbao/mount.d.ts +20 -0
- package/dist/openbao/mount.d.ts.map +1 -0
- package/dist/openbao/pki-role-form.d.ts +118 -0
- package/dist/openbao/pki-role-form.d.ts.map +1 -0
- package/dist/openbao/pki-role.d.ts +50 -0
- package/dist/openbao/pki-role.d.ts.map +1 -0
- package/dist/openbao/policy-assembly.d.ts +13 -0
- package/dist/openbao/policy-assembly.d.ts.map +1 -0
- package/dist/openbao/policy-wire.d.ts +41 -0
- package/dist/openbao/policy-wire.d.ts.map +1 -0
- package/dist/openbao/policy.d.ts +83 -0
- package/dist/openbao/policy.d.ts.map +1 -0
- package/dist/openbao/proxmox-role-form.d.ts +92 -0
- package/dist/openbao/proxmox-role-form.d.ts.map +1 -0
- package/dist/openbao/proxmox-role.d.ts +47 -0
- package/dist/openbao/proxmox-role.d.ts.map +1 -0
- package/dist/openbao/ssh-role-form.d.ts +99 -0
- package/dist/openbao/ssh-role-form.d.ts.map +1 -0
- package/dist/openbao/ssh-role.d.ts +43 -0
- package/dist/openbao/ssh-role.d.ts.map +1 -0
- package/dist/proxmox/acl.d.ts +72 -0
- package/dist/proxmox/acl.d.ts.map +1 -0
- package/dist/proxmox/api-token-form.d.ts +59 -0
- package/dist/proxmox/api-token-form.d.ts.map +1 -0
- package/dist/proxmox/api-token.d.ts +159 -0
- package/dist/proxmox/api-token.d.ts.map +1 -0
- package/dist/proxmox/backup-job.d.ts +112 -0
- package/dist/proxmox/backup-job.d.ts.map +1 -0
- package/dist/proxmox/ceph-daemon-form.d.ts +70 -0
- package/dist/proxmox/ceph-daemon-form.d.ts.map +1 -0
- package/dist/proxmox/ceph-daemon.d.ts +162 -0
- package/dist/proxmox/ceph-daemon.d.ts.map +1 -0
- package/dist/proxmox/ceph-flag.d.ts +131 -0
- package/dist/proxmox/ceph-flag.d.ts.map +1 -0
- package/dist/proxmox/ceph-fs-wire.d.ts +106 -0
- package/dist/proxmox/ceph-fs-wire.d.ts.map +1 -0
- package/dist/proxmox/ceph-fs.d.ts +124 -0
- package/dist/proxmox/ceph-fs.d.ts.map +1 -0
- package/dist/proxmox/ceph-osd-tree.d.ts +154 -0
- package/dist/proxmox/ceph-osd-tree.d.ts.map +1 -0
- package/dist/proxmox/ceph-osd-write.d.ts +36 -0
- package/dist/proxmox/ceph-osd-write.d.ts.map +1 -0
- package/dist/proxmox/ceph-osd.d.ts +102 -0
- package/dist/proxmox/ceph-osd.d.ts.map +1 -0
- package/dist/proxmox/ceph-pool-form.d.ts +217 -0
- package/dist/proxmox/ceph-pool-form.d.ts.map +1 -0
- package/dist/proxmox/ceph-pool-settle.d.ts +39 -0
- package/dist/proxmox/ceph-pool-settle.d.ts.map +1 -0
- package/dist/proxmox/ceph-pool.d.ts +48 -0
- package/dist/proxmox/ceph-pool.d.ts.map +1 -0
- package/dist/proxmox/client.d.ts +64 -0
- package/dist/proxmox/client.d.ts.map +1 -0
- package/dist/proxmox/credentials.d.ts +153 -0
- package/dist/proxmox/credentials.d.ts.map +1 -0
- package/dist/proxmox/firewall-alias-form.d.ts +98 -0
- package/dist/proxmox/firewall-alias-form.d.ts.map +1 -0
- package/dist/proxmox/firewall-alias.d.ts +141 -0
- package/dist/proxmox/firewall-alias.d.ts.map +1 -0
- package/dist/proxmox/generated/pbs.d.ts +388 -0
- package/dist/proxmox/generated/pbs.d.ts.map +1 -0
- package/dist/proxmox/generated/pve.d.ts +4713 -0
- package/dist/proxmox/generated/pve.d.ts.map +1 -0
- package/dist/proxmox/group.d.ts +104 -0
- package/dist/proxmox/group.d.ts.map +1 -0
- package/dist/proxmox/ha-resource.d.ts +104 -0
- package/dist/proxmox/ha-resource.d.ts.map +1 -0
- package/dist/proxmox/ha-rule-form.d.ts +103 -0
- package/dist/proxmox/ha-rule-form.d.ts.map +1 -0
- package/dist/proxmox/ha-rule.d.ts +156 -0
- package/dist/proxmox/ha-rule.d.ts.map +1 -0
- package/dist/proxmox/index.d.ts +43 -0
- package/dist/proxmox/index.d.ts.map +1 -0
- package/dist/proxmox/index.js +2322 -0
- package/dist/proxmox/index.js.map +70 -0
- package/dist/proxmox/lease-cache.d.ts +98 -0
- package/dist/proxmox/lease-cache.d.ts.map +1 -0
- package/dist/proxmox/lxc.d.ts +59 -0
- package/dist/proxmox/lxc.d.ts.map +1 -0
- package/dist/proxmox/members.d.ts +56 -0
- package/dist/proxmox/members.d.ts.map +1 -0
- package/dist/proxmox/metric-server-form.d.ts +53 -0
- package/dist/proxmox/metric-server-form.d.ts.map +1 -0
- package/dist/proxmox/metric-server-otel.d.ts +49 -0
- package/dist/proxmox/metric-server-otel.d.ts.map +1 -0
- package/dist/proxmox/metric-server.d.ts +123 -0
- package/dist/proxmox/metric-server.d.ts.map +1 -0
- package/dist/proxmox/network-apply-read.d.ts +93 -0
- package/dist/proxmox/network-apply-read.d.ts.map +1 -0
- package/dist/proxmox/network-apply.d.ts +102 -0
- package/dist/proxmox/network-apply.d.ts.map +1 -0
- package/dist/proxmox/node-network-form.d.ts +128 -0
- package/dist/proxmox/node-network-form.d.ts.map +1 -0
- package/dist/proxmox/node-network.d.ts +173 -0
- package/dist/proxmox/node-network.d.ts.map +1 -0
- package/dist/proxmox/notification-target-form.d.ts +44 -0
- package/dist/proxmox/notification-target-form.d.ts.map +1 -0
- package/dist/proxmox/notification-target.d.ts +146 -0
- package/dist/proxmox/notification-target.d.ts.map +1 -0
- package/dist/proxmox/pbs-datastore-form.d.ts +147 -0
- package/dist/proxmox/pbs-datastore-form.d.ts.map +1 -0
- package/dist/proxmox/pbs-datastore-guard.d.ts +55 -0
- package/dist/proxmox/pbs-datastore-guard.d.ts.map +1 -0
- package/dist/proxmox/pbs-datastore.d.ts +140 -0
- package/dist/proxmox/pbs-datastore.d.ts.map +1 -0
- package/dist/proxmox/pbs-prune-job-form.d.ts +97 -0
- package/dist/proxmox/pbs-prune-job-form.d.ts.map +1 -0
- package/dist/proxmox/pbs-prune-job.d.ts +168 -0
- package/dist/proxmox/pbs-prune-job.d.ts.map +1 -0
- package/dist/proxmox/pbs-sync-job-form.d.ts +83 -0
- package/dist/proxmox/pbs-sync-job-form.d.ts.map +1 -0
- package/dist/proxmox/pbs-sync-job.d.ts +246 -0
- package/dist/proxmox/pbs-sync-job.d.ts.map +1 -0
- package/dist/proxmox/pbs-verify-job-form.d.ts +114 -0
- package/dist/proxmox/pbs-verify-job-form.d.ts.map +1 -0
- package/dist/proxmox/pbs-verify-job.d.ts +145 -0
- package/dist/proxmox/pbs-verify-job.d.ts.map +1 -0
- package/dist/proxmox/pool.d.ts +42 -0
- package/dist/proxmox/pool.d.ts.map +1 -0
- package/dist/proxmox/qemu.d.ts +46 -0
- package/dist/proxmox/qemu.d.ts.map +1 -0
- package/dist/proxmox/replication-job-form.d.ts +71 -0
- package/dist/proxmox/replication-job-form.d.ts.map +1 -0
- package/dist/proxmox/replication-job.d.ts +162 -0
- package/dist/proxmox/replication-job.d.ts.map +1 -0
- package/dist/proxmox/resource.d.ts +157 -0
- package/dist/proxmox/resource.d.ts.map +1 -0
- package/dist/proxmox/role.d.ts +97 -0
- package/dist/proxmox/role.d.ts.map +1 -0
- package/dist/proxmox/sdn-apply-read.d.ts +80 -0
- package/dist/proxmox/sdn-apply-read.d.ts.map +1 -0
- package/dist/proxmox/sdn-apply.d.ts +63 -0
- package/dist/proxmox/sdn-apply.d.ts.map +1 -0
- package/dist/proxmox/sdn-subnet-form.d.ts +111 -0
- package/dist/proxmox/sdn-subnet-form.d.ts.map +1 -0
- package/dist/proxmox/sdn-subnet.d.ts +161 -0
- package/dist/proxmox/sdn-subnet.d.ts.map +1 -0
- package/dist/proxmox/sdn-vnet.d.ts +113 -0
- package/dist/proxmox/sdn-vnet.d.ts.map +1 -0
- package/dist/proxmox/sdn-zone.d.ts +126 -0
- package/dist/proxmox/sdn-zone.d.ts.map +1 -0
- package/dist/proxmox/storage.d.ts +120 -0
- package/dist/proxmox/storage.d.ts.map +1 -0
- package/dist/proxmox/user.d.ts +107 -0
- package/dist/proxmox/user.d.ts.map +1 -0
- package/dist/proxmox/values.d.ts +134 -0
- package/dist/proxmox/values.d.ts.map +1 -0
- package/dist/proxmox/zfs-pool-write.d.ts +76 -0
- package/dist/proxmox/zfs-pool-write.d.ts.map +1 -0
- package/dist/proxmox/zfs-pool.d.ts +146 -0
- package/dist/proxmox/zfs-pool.d.ts.map +1 -0
- package/dist/talos/credentials.d.ts +30 -0
- package/dist/talos/credentials.d.ts.map +1 -0
- package/dist/talos/index.d.ts +15 -0
- package/dist/talos/index.d.ts.map +1 -0
- package/dist/talos/index.js +392 -0
- package/dist/talos/index.js.map +16 -0
- package/dist/talos/kubeconfig.d.ts +53 -0
- package/dist/talos/kubeconfig.d.ts.map +1 -0
- package/dist/talos/resource.d.ts +32 -0
- package/dist/talos/resource.d.ts.map +1 -0
- package/dist/talos/talos-bootstrap.d.ts +27 -0
- package/dist/talos/talos-bootstrap.d.ts.map +1 -0
- package/dist/talos/talos-cluster-health.d.ts +36 -0
- package/dist/talos/talos-cluster-health.d.ts.map +1 -0
- package/dist/talos/talos-machine-config.d.ts +41 -0
- package/dist/talos/talos-machine-config.d.ts.map +1 -0
- package/dist/talos/talosctl.d.ts +29 -0
- package/dist/talos/talosctl.d.ts.map +1 -0
- package/dist/talos/values.d.ts +20 -0
- package/dist/talos/values.d.ts.map +1 -0
- package/package.json +57 -0
- package/src/cloudflare/client.ts +76 -0
- package/src/cloudflare/index.ts +10 -0
- package/src/cloudflare/lock-rules.ts +60 -0
- package/src/cloudflare/providers.ts +23 -0
- package/src/cloudflare/r2-bucket-lock.ts +220 -0
- package/src/forgejo/branch-protection-form.ts +29 -0
- package/src/forgejo/branch-protection.ts +101 -0
- package/src/forgejo/client.ts +136 -0
- package/src/forgejo/index.ts +17 -0
- package/src/forgejo/org-actions-secrets.ts +99 -0
- package/src/forgejo/org-label.ts +100 -0
- package/src/forgejo/org-team.ts +105 -0
- package/src/forgejo/repo-webhook-form.ts +47 -0
- package/src/forgejo/repo-webhook.ts +118 -0
- package/src/forgejo/repository.ts +126 -0
- package/src/forgejo/resource.ts +171 -0
- package/src/forgejo/team-member.ts +97 -0
- package/src/forgejo/values.test.ts +48 -0
- package/src/forgejo/values.ts +61 -0
- package/src/index.ts +18 -0
- package/src/openbao/auth-role-form.ts +112 -0
- package/src/openbao/auth-role.ts +101 -0
- package/src/openbao/bao-address.test.ts +89 -0
- package/src/openbao/bao-address.ts +97 -0
- package/src/openbao/bao-http-gate.test.ts +88 -0
- package/src/openbao/bao-http-unix.test.ts +70 -0
- package/src/openbao/bao-http.test.ts +161 -0
- package/src/openbao/bao-http.ts +185 -0
- package/src/openbao/bao-status.test.ts +114 -0
- package/src/openbao/bao-status.ts +137 -0
- package/src/openbao/cloudflare-group-scope.test.ts +103 -0
- package/src/openbao/cloudflare-group-scope.ts +83 -0
- package/src/openbao/cloudflare-parity-catalog.ts +104 -0
- package/src/openbao/cloudflare-parity-snapshot.ts +84 -0
- package/src/openbao/cloudflare-parity.ts +152 -0
- package/src/openbao/cloudflare-permission-groups-solve.test.ts +171 -0
- package/src/openbao/cloudflare-permission-groups-solve.ts +197 -0
- package/src/openbao/cloudflare-permission-groups.test.ts +369 -0
- package/src/openbao/cloudflare-permission-groups.ts +299 -0
- package/src/openbao/cloudflare-policy.ts +180 -0
- package/src/openbao/cloudflare-role-form.test.ts +177 -0
- package/src/openbao/cloudflare-role-form.ts +183 -0
- package/src/openbao/cloudflare-role-wire.test.ts +132 -0
- package/src/openbao/cloudflare-role-wire.ts +41 -0
- package/src/openbao/cloudflare-role.ts +189 -0
- package/src/openbao/cloudflare-roles-config.ts +230 -0
- package/src/openbao/cloudflare-roles-expand-bucket.test.ts +134 -0
- package/src/openbao/cloudflare-roles-expand.ts +231 -0
- package/src/openbao/digest.ts +26 -0
- package/src/openbao/fake-bao.ts +84 -0
- package/src/openbao/forgejo-bootstrap-parse.test.ts +76 -0
- package/src/openbao/forgejo-bootstrap-parse.ts +90 -0
- package/src/openbao/forgejo-bootstrap.ts +181 -0
- package/src/openbao/index.ts +17 -0
- package/src/openbao/mount-form.ts +147 -0
- package/src/openbao/mount-wire.test.ts +108 -0
- package/src/openbao/mount-wire.ts +79 -0
- package/src/openbao/mount.ts +132 -0
- package/src/openbao/pki-role-form.ts +248 -0
- package/src/openbao/pki-role.ts +202 -0
- package/src/openbao/policy-assembly.test.ts +28 -0
- package/src/openbao/policy-assembly.ts +13 -0
- package/src/openbao/policy-wire.test.ts +90 -0
- package/src/openbao/policy-wire.ts +53 -0
- package/src/openbao/policy.ts +212 -0
- package/src/openbao/proxmox-role-form.ts +174 -0
- package/src/openbao/proxmox-role.ts +214 -0
- package/src/openbao/ssh-role-form.ts +248 -0
- package/src/openbao/ssh-role.ts +240 -0
- package/src/proxmox/acl.ts +267 -0
- package/src/proxmox/api-token-form.ts +122 -0
- package/src/proxmox/api-token.ts +222 -0
- package/src/proxmox/backup-job.ts +213 -0
- package/src/proxmox/ceph-daemon-form.ts +84 -0
- package/src/proxmox/ceph-daemon.ts +243 -0
- package/src/proxmox/ceph-flag.ts +249 -0
- package/src/proxmox/ceph-fs-wire.ts +246 -0
- package/src/proxmox/ceph-fs.ts +249 -0
- package/src/proxmox/ceph-osd-tree.ts +193 -0
- package/src/proxmox/ceph-osd-write.ts +76 -0
- package/src/proxmox/ceph-osd.ts +244 -0
- package/src/proxmox/ceph-pool-form.ts +249 -0
- package/src/proxmox/ceph-pool-settle.ts +70 -0
- package/src/proxmox/ceph-pool.ts +212 -0
- package/src/proxmox/client.test.ts +185 -0
- package/src/proxmox/client.ts +240 -0
- package/src/proxmox/credentials.test.ts +144 -0
- package/src/proxmox/credentials.ts +254 -0
- package/src/proxmox/firewall-alias-form.ts +112 -0
- package/src/proxmox/firewall-alias.ts +208 -0
- package/src/proxmox/generated/pbs.ts +461 -0
- package/src/proxmox/generated/pve.ts +7344 -0
- package/src/proxmox/group.ts +199 -0
- package/src/proxmox/ha-resource.ts +169 -0
- package/src/proxmox/ha-rule-form.ts +185 -0
- package/src/proxmox/ha-rule.ts +240 -0
- package/src/proxmox/index.ts +42 -0
- package/src/proxmox/lease-cache.test.ts +163 -0
- package/src/proxmox/lease-cache.ts +131 -0
- package/src/proxmox/lxc.ts +117 -0
- package/src/proxmox/members.test.ts +54 -0
- package/src/proxmox/members.ts +174 -0
- package/src/proxmox/metric-server-form.ts +128 -0
- package/src/proxmox/metric-server-otel.ts +99 -0
- package/src/proxmox/metric-server.ts +214 -0
- package/src/proxmox/network-apply-read.ts +228 -0
- package/src/proxmox/network-apply.ts +244 -0
- package/src/proxmox/node-network-form.ts +213 -0
- package/src/proxmox/node-network.ts +243 -0
- package/src/proxmox/notification-target-form.ts +104 -0
- package/src/proxmox/notification-target.ts +222 -0
- package/src/proxmox/pbs-datastore-form.ts +248 -0
- package/src/proxmox/pbs-datastore-guard.ts +105 -0
- package/src/proxmox/pbs-datastore.ts +271 -0
- package/src/proxmox/pbs-prune-job-form.ts +208 -0
- package/src/proxmox/pbs-prune-job.ts +227 -0
- package/src/proxmox/pbs-sync-job-form.ts +234 -0
- package/src/proxmox/pbs-sync-job.ts +258 -0
- package/src/proxmox/pbs-verify-job-form.ts +131 -0
- package/src/proxmox/pbs-verify-job.ts +227 -0
- package/src/proxmox/pool.test.ts +42 -0
- package/src/proxmox/pool.ts +80 -0
- package/src/proxmox/qemu.ts +89 -0
- package/src/proxmox/replication-job-form.ts +89 -0
- package/src/proxmox/replication-job.ts +248 -0
- package/src/proxmox/resource.ts +257 -0
- package/src/proxmox/role.ts +161 -0
- package/src/proxmox/sdn-apply-read.test.ts +152 -0
- package/src/proxmox/sdn-apply-read.ts +189 -0
- package/src/proxmox/sdn-apply.ts +154 -0
- package/src/proxmox/sdn-subnet-form.ts +156 -0
- package/src/proxmox/sdn-subnet.ts +238 -0
- package/src/proxmox/sdn-vnet.ts +228 -0
- package/src/proxmox/sdn-zone.ts +246 -0
- package/src/proxmox/storage.ts +245 -0
- package/src/proxmox/user.ts +214 -0
- package/src/proxmox/values.test.ts +193 -0
- package/src/proxmox/values.ts +203 -0
- package/src/proxmox/zfs-pool-write.ts +130 -0
- package/src/proxmox/zfs-pool.ts +253 -0
- package/src/talos/credentials.ts +136 -0
- package/src/talos/index.ts +14 -0
- package/src/talos/kubeconfig.ts +170 -0
- package/src/talos/resource.ts +31 -0
- package/src/talos/talos-bootstrap.ts +109 -0
- package/src/talos/talos-cluster-health.ts +119 -0
- package/src/talos/talos-machine-config.ts +146 -0
- package/src/talos/talosctl.ts +94 -0
- package/src/talos/values.test.ts +80 -0
- package/src/talos/values.ts +70 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Proxmox.HaRule` — where the CRM may place a guest, and which guests must not share a node.
|
|
3
|
+
*
|
|
4
|
+
* ★ THIS IS THE PVE 9 SUCCESSOR TO HA GROUPS, AND THE GROUPS ENDPOINT IS GONE. MEASURED on n2
|
|
5
|
+
* (pve-manager/9.2.11, 2026-09-13): `pvesh get /cluster/ha/groups` answers `cannot index groups:
|
|
6
|
+
* ha groups have been migrated to rules`, and `/etc/pve/ha/` holds `rules.cfg` with no
|
|
7
|
+
* `groups.cfg` beside it. The published schema still documents groups and still offers a `group`
|
|
8
|
+
* parameter on `Proxmox.HaResource`; both are dead. Do not port them back.
|
|
9
|
+
*
|
|
10
|
+
* ★ ONE RESOURCE FOR BOTH RULE TYPES, ON THE ceph-daemon.ts ARGUMENT AND AFTER READING BOTH
|
|
11
|
+
* PLUGINS. `PVE::HA::Rules::NodeAffinity` and `::ResourceAffinity` share `resources`, `affinity`,
|
|
12
|
+
* `disable` and `comment`; node-affinity adds `nodes` and `strict` and nothing else diverges. The
|
|
13
|
+
* endpoint, the path, the create/update/delete calls, the `order` and `digest` traps below and
|
|
14
|
+
* the list normalisation are identical for both. Two files would have carried every one of those
|
|
15
|
+
* traps twice and let them drift.
|
|
16
|
+
* ⛔ `type` IS CREATE-ONLY. MEASURED in `/usr/share/perl5/PVE/API2/HA/Rules.pm`: `update_rule`
|
|
17
|
+
* takes the plugin from the STORED rule (`my $type = $rule->{type};`) and never rewrites it. So
|
|
18
|
+
* `attributes` reports a rule of the other type as ABSENT, which sends reconcile down the
|
|
19
|
+
* create branch and makes PVE refuse with `HA rule '<id>' already defined` — a loud, accurate
|
|
20
|
+
* error rather than a PUT pushing node-affinity fields at a resource-affinity rule.
|
|
21
|
+
* ⛔ AND YET IT IS SENT ON THE UPDATE ANYWAY, because the PUT is validated against a `oneOf`
|
|
22
|
+
* keyed on it and refuses the whole body without it. That one is worth reading in full: it
|
|
23
|
+
* is the ⛔ on `required` in ha-rule-form.ts.
|
|
24
|
+
*
|
|
25
|
+
* ⛔ THREE FIELDS PVE RETURNS AND NO WRITE ACCEPTS, EACH ONE A FOREVER-UPDATE IF COMPARED.
|
|
26
|
+
* `order` is an ORDINAL PVE ASSIGNS ITSELF (`$rules->{order}->{$ruleid} =
|
|
27
|
+
* PVE::HA::Rules::get_next_ordinal($rules)`) and it appears in no create or update schema at all;
|
|
28
|
+
* it is reported in attributes and never compared. `digest` is the digest of the WHOLE rules
|
|
29
|
+
* file, so it moves when a rule written for somebody else's guests moves — it is not even an
|
|
30
|
+
* attribute, for the reason ha-resource.ts gives. `errors` is PVE's own feasibility verdict,
|
|
31
|
+
* recomputed across every rule on every read; reported, never compared. See `problems` in
|
|
32
|
+
* ha-rule-form.ts.
|
|
33
|
+
*
|
|
34
|
+
* ⚠️ IT COMPOSES WITH `Proxmox.HaResource`, AND THE EDGE IS REAL RATHER THAN TIDY. MEASURED:
|
|
35
|
+
* `create_rule` runs `$assert_valid_resources_param`, which calls
|
|
36
|
+
* `PVE::HA::Config::service_is_configured($resource)` for every sid and dies `cannot use
|
|
37
|
+
* unmanaged resource(s) <sid>` if one is not already an HA resource. A rule naming a sid nothing
|
|
38
|
+
* has put under HA is a HARD 400, not a warning — so `resources` should be written as the
|
|
39
|
+
* HaResource's own attribute (`resources: [web.sid, db.sid]`) and let Alchemy order the deploy,
|
|
40
|
+
* not as a bare string that happens to match. `$assert_valid_nodes_param` refuses a non-existent
|
|
41
|
+
* node the same way.
|
|
42
|
+
* ⛔ AND THE EDGE RUNS BACK THE OTHER WAY, WHERE ALCHEMY CANNOT SEE IT. Destroying an
|
|
43
|
+
* `HaResource` deletes with PVE's default `purge=1`, which strips that sid out of every rule
|
|
44
|
+
* and DELETES any rule left with no members. So a plan that only removes a guest can remove
|
|
45
|
+
* this resource's object from under it; the next plan then reads absent and recreates it, or
|
|
46
|
+
* fails on the `unmanaged resource` check if the guest is gone for good.
|
|
47
|
+
*
|
|
48
|
+
* ⚠️ BOTH LANES ALREADY HAVE THE PRIVILEGES, WHICH IS NEW. MEASURED 2026-09-13:
|
|
49
|
+
* GET (collection and item) checks `Sys.Audit` on `/` and the built-in `PVEAuditor` holds it, so
|
|
50
|
+
* `readRole` stays the default `read` lease — unlike storage.ts and the SDN families. POST, PUT
|
|
51
|
+
* and DELETE all check `Sys.Console` on `/`, and `pvesh get /access/roles/LXCProvisioner` now
|
|
52
|
+
* returns `Sys.Console` among its 27 privileges. The warning in ha-resource.ts's header — that
|
|
53
|
+
* `Sys.Console` is missing — is stale; the role was widened, and the price it named still stands:
|
|
54
|
+
* `Sys.Console` is also what opens a root shell on every node.
|
|
55
|
+
*
|
|
56
|
+
* ⚠️ REMOVAL IS THE PLAIN DEFAULT, DELIBERATELY, AND `delete` IS THE FACTORY'S. An HA rule is
|
|
57
|
+
* exactly a line of TypeScript — nothing in it is irreplaceable the way a Ceph pool or a ZFS
|
|
58
|
+
* vdev is — so the `retain` default that resource.ts explains does not apply. Read the plan for
|
|
59
|
+
* what it is, though: while a negative resource-affinity rule is absent, nothing stops the CRM
|
|
60
|
+
* putting both halves of a pair on one node.
|
|
61
|
+
*
|
|
62
|
+
* ⚠️ A READ GOES THROUGH THE GROUPS MIGRATION. Every GET calls `migrate_groups_to_rules`, so on a
|
|
63
|
+
* cluster that still has `groups.cfg` the answer includes SYNTHETIC rules nobody declared. This
|
|
64
|
+
* cluster has none, so nothing is injected here; a rule id colliding with an old group name on a
|
|
65
|
+
* cluster that does would read back something this provider never wrote.
|
|
66
|
+
*
|
|
67
|
+
* ⚠️ A CHANGED `type` READS AS `update` IN THE PLAN AND FAILS AS "already defined" ON THE APPLY,
|
|
68
|
+
* AND THOSE TWO WORDS POINT AWAY FROM THE CAUSE. `attributes` answers undefined when the live
|
|
69
|
+
* rule's type disagrees, so `diff` takes resource.ts's `live === undefined` branch and says
|
|
70
|
+
* update; reconcile then reads undefined, POSTs, and PVE refuses because the rule is plainly
|
|
71
|
+
* there. The cause is that `type` is create-only. metric-server.ts has the same shape, so this
|
|
72
|
+
* is the package's consistent behaviour rather than a quirk of this file — but it is worth
|
|
73
|
+
* knowing before reading the error. Delete the declaration and write a new one to change a type.
|
|
74
|
+
*/
|
|
75
|
+
import { Resource } from 'alchemy';
|
|
76
|
+
import * as Provider from 'alchemy/Provider';
|
|
77
|
+
import * as Effect from 'effect/Effect';
|
|
78
|
+
import { body, clearList, commentText, nodeList, problems } from './ha-rule-form.ts';
|
|
79
|
+
import { type PveRequirements, type WithTarget, pveHandlers } from './resource.ts';
|
|
80
|
+
import { bool, csv, int, text, withClears } from './values.ts';
|
|
81
|
+
|
|
82
|
+
export type HaRuleType = 'node-affinity' | 'resource-affinity';
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* ⛔ REQUIRED ON BOTH TYPES, AND THAT IS A DELIBERATE NARROWING OF PVE'S SCHEMA. PVE makes it
|
|
86
|
+
* optional for node-affinity (defaulting to `positive`) and mandatory for resource-affinity. A
|
|
87
|
+
* provider that mirrored that has two bad options when a declaration omits it: guess `positive`,
|
|
88
|
+
* which silently flips a live `negative` rule — the one keeping two guests APART — into one that
|
|
89
|
+
* pins them together; or leave it out of the write, which makes `matches` disagree with a live
|
|
90
|
+
* `negative` rule forever while every PUT changes nothing. Requiring one word from the caller
|
|
91
|
+
* removes both.
|
|
92
|
+
*/
|
|
93
|
+
export type HaAffinity = 'positive' | 'negative';
|
|
94
|
+
|
|
95
|
+
interface HaRuleBase extends WithTarget {
|
|
96
|
+
/** A `pve-configid`: a letter, then letters, digits, `-` and `_`. Identity — renaming is a new
|
|
97
|
+
* rule, and the old one stays behind exactly as `sid` does in ha-resource.ts. */
|
|
98
|
+
rule: string;
|
|
99
|
+
/**
|
|
100
|
+
* The HA resources this rule constrains, as prefixed sids — `ct:101`, `vm:100`.
|
|
101
|
+
* ⚠️ EVERY ONE MUST ALREADY BE UNDER HA. See the composition ⚠️ in the header: this is an
|
|
102
|
+
* ordering edge, and the honest way to express it is the HaResource's own `sid` attribute.
|
|
103
|
+
*/
|
|
104
|
+
resources: readonly string[] | string;
|
|
105
|
+
affinity: HaAffinity;
|
|
106
|
+
/** Keep the rule in the file but stop the CRM applying it. ⛔ Clearing it needs `delete=disable`
|
|
107
|
+
* — see the ⛔ in `optional` in ha-rule-form.ts. */
|
|
108
|
+
disable?: boolean;
|
|
109
|
+
/** Free text shown in the HA panel. Max 4096 characters. */
|
|
110
|
+
comment?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface NodeAffinityRuleProps extends HaRuleBase {
|
|
114
|
+
type: 'node-affinity';
|
|
115
|
+
/**
|
|
116
|
+
* Cluster nodes, each optionally `<node>:<priority>`. Higher priority wins; the numbers are
|
|
117
|
+
* relative and nothing else. ⚠️ REQUIRED BY THE TYPE, not merely by PVE — an optional `nodes`
|
|
118
|
+
* would let a declaration that cannot describe any live rule sit in a permanent update loop.
|
|
119
|
+
*/
|
|
120
|
+
nodes: readonly string[] | string;
|
|
121
|
+
/** `false` (PVE's default) makes the nodes a PREFERENCE; `true` makes them the only nodes the
|
|
122
|
+
* resources may run on, and the resources STOP when none of them is available. */
|
|
123
|
+
strict?: boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface ResourceAffinityRuleProps extends HaRuleBase {
|
|
127
|
+
type: 'resource-affinity';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* ⚠️ A UNION, NOT ONE FLAT INTERFACE WITH OPTIONAL EXTRAS, because `nodes` and `strict` are not
|
|
132
|
+
* merely unused on a resource-affinity rule — PVE's oneOf declares `additionalProperties: 0`, so
|
|
133
|
+
* sending either is a 400, and comparing either would be a diff no write could ever settle. The
|
|
134
|
+
* union makes both unwritable rather than ignored.
|
|
135
|
+
*/
|
|
136
|
+
export type HaRuleProps = NodeAffinityRuleProps | ResourceAffinityRuleProps;
|
|
137
|
+
|
|
138
|
+
export interface HaRuleAttributes {
|
|
139
|
+
rule: string;
|
|
140
|
+
type: HaRuleType;
|
|
141
|
+
/** ⚠️ NORMALISED, NOT RAW. PVE returns `join(',', sort keys %$hash)` — a re-sorted SET. */
|
|
142
|
+
resources: string;
|
|
143
|
+
/** ⚠️ Normalised by `nodeList`; always `''` on a resource-affinity rule, which has no nodes. */
|
|
144
|
+
nodes: string;
|
|
145
|
+
affinity: string;
|
|
146
|
+
strict: boolean;
|
|
147
|
+
disable: boolean;
|
|
148
|
+
comment: string;
|
|
149
|
+
/** ⛔ PVE-ASSIGNED AND UNWRITABLE. Reported so a plan can show it; never compared. */
|
|
150
|
+
order: number;
|
|
151
|
+
/** ⛔ PVE's feasibility verdict for this rule. Reported; never compared. */
|
|
152
|
+
errors: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface ProxmoxHaRule extends Resource<
|
|
156
|
+
'Proxmox.HaRule',
|
|
157
|
+
HaRuleProps,
|
|
158
|
+
HaRuleAttributes,
|
|
159
|
+
never,
|
|
160
|
+
PveRequirements
|
|
161
|
+
> {}
|
|
162
|
+
|
|
163
|
+
export const ProxmoxHaRule = Resource<ProxmoxHaRule>('Proxmox.HaRule');
|
|
164
|
+
|
|
165
|
+
const handlers = pveHandlers<HaRuleProps, HaRuleAttributes>({
|
|
166
|
+
attributes: (live, props) => {
|
|
167
|
+
/**
|
|
168
|
+
* ⛔ A RULE OF THE OTHER TYPE IS ANOTHER OBJECT, AND "ABSENT" IS THE HONEST ANSWER — the same
|
|
169
|
+
* move metric-server.ts makes, for the same reason: `type` cannot be changed by a PUT, so
|
|
170
|
+
* reporting the foreign rule as missing makes reconcile POST and PVE refuse because the id
|
|
171
|
+
* is taken. An older PVE that omitted `type` from the read would fall through and be trusted.
|
|
172
|
+
*/
|
|
173
|
+
const liveType = text(live['type']);
|
|
174
|
+
if (liveType !== '' && liveType !== props.type) return undefined;
|
|
175
|
+
return {
|
|
176
|
+
affinity: text(live['affinity']),
|
|
177
|
+
comment: text(live['comment']),
|
|
178
|
+
/**
|
|
179
|
+
* ⚠️ ABSENT WHEN OFF, AND THAT IS NOT THE SAME AS `false` BY ACCIDENT. MEASURED in
|
|
180
|
+
* `PVE::HA::Rules::set_rule_defaults`: a default is materialised into the read only when it
|
|
181
|
+
* is TRUTHY, so `disable` (no default) and `strict` (default 0) are simply missing from an
|
|
182
|
+
* enabled, non-strict rule while `affinity` (default `positive`) is always present.
|
|
183
|
+
*/
|
|
184
|
+
disable: bool(live['disable'], false),
|
|
185
|
+
errors: problems(live['errors']),
|
|
186
|
+
nodes: nodeList(typeof live['nodes'] === 'string' ? live['nodes'] : undefined),
|
|
187
|
+
order: int(live['order'], 0),
|
|
188
|
+
resources: csv(typeof live['resources'] === 'string' ? live['resources'] : undefined),
|
|
189
|
+
rule: text(live['rule'], props.rule),
|
|
190
|
+
strict: bool(live['strict'], false),
|
|
191
|
+
type: props.type,
|
|
192
|
+
};
|
|
193
|
+
},
|
|
194
|
+
collection: () => 'cluster/ha/rules',
|
|
195
|
+
/** ⚠️ ONLY `rule` IS ADDED HERE: the POST is to the collection, so the id is not in the path.
|
|
196
|
+
* `type` is already in `body` — the PUT needs it too, for the reason ha-rule-form.ts gives. */
|
|
197
|
+
createForm: (props) => ({ ...body(props), rule: props.rule }),
|
|
198
|
+
/**
|
|
199
|
+
* ⛔ ONLY FIELDS A WRITE CAN ACTUALLY SET ARE COMPARED. `order`, `errors` and `digest` are out
|
|
200
|
+
* for the reason the header gives at length — each would report an update that the PUT it
|
|
201
|
+
* triggers cannot satisfy, on every plan, forever. `rule` and `type` are out too: one is the
|
|
202
|
+
* key the object was read by, the other is refused rather than updated.
|
|
203
|
+
* ⚠️ `nodes` AND `strict` ARE COMPARED ONLY ON A NODE-AFFINITY RULE. They do not exist on the
|
|
204
|
+
* other plugin, so `attributes.nodes` is `''` and `attributes.strict` is `false` there by
|
|
205
|
+
* construction — comparing them would be comparing a declaration against nothing.
|
|
206
|
+
* ⚠️ THE COMMENT IS COMPARED AS PVE WILL STORE IT, not as it was declared — `commentText` and
|
|
207
|
+
* its ⛔ explain why a `%XX` in a comment does not survive the round trip.
|
|
208
|
+
*/
|
|
209
|
+
matches: (attributes, props) =>
|
|
210
|
+
attributes.resources === csv(props.resources) &&
|
|
211
|
+
attributes.affinity === props.affinity &&
|
|
212
|
+
attributes.comment === commentText(props.comment ?? '') &&
|
|
213
|
+
attributes.disable === (props.disable === true) &&
|
|
214
|
+
(props.type !== 'node-affinity' ||
|
|
215
|
+
(attributes.nodes === nodeList(props.nodes) &&
|
|
216
|
+
attributes.strict === (props.strict === true))),
|
|
217
|
+
/** ⚠️ A `pve-configid` has no `/` or `:` in it, so there is nothing here to encode. */
|
|
218
|
+
path: (props) => `cluster/ha/rules/${props.rule}`,
|
|
219
|
+
/**
|
|
220
|
+
* ⛔ THE `delete=` HALF IS WHAT MAKES THIS SETTLE. A PUT merges into the stored rule and a
|
|
221
|
+
* `disable=0` is thrown away before the plugin sees it, so without the clear list a rule
|
|
222
|
+
* disabled by hand could never be re-enabled by a declaration and every plan would report the
|
|
223
|
+
* same update. `clearList` and `body` come off one table — see ha-rule-form.ts.
|
|
224
|
+
*/
|
|
225
|
+
updateForm: (props) => {
|
|
226
|
+
const clear = clearList(props);
|
|
227
|
+
const fields = body(props);
|
|
228
|
+
return withClears(fields, clear);
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* ⛔ THE EMPTY `list` FROM `pveHandlers` STANDS, AND IT COSTS NOTHING HERE TODAY.
|
|
234
|
+
* `GET /cluster/ha/rules` MEASURED `[]` on this cluster on 2026-09-13 — but the same call on a
|
|
235
|
+
* cluster somebody has used returns every rule an operator ever clicked, plus the synthetic ones
|
|
236
|
+
* the groups migration invents on read. Adopting either is how a later `alchemy destroy` removes
|
|
237
|
+
* a separation rule nobody declared. Adoption stays an explicit act.
|
|
238
|
+
*/
|
|
239
|
+
export const ProxmoxHaRuleProvider = () =>
|
|
240
|
+
Provider.effect(ProxmoxHaRule, Effect.succeed(ProxmoxHaRule.Provider.of(handlers)));
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxmox providers for Alchemy.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ THIS BARREL IS THE PUBLIC API, AND IT IS DELIBERATELY SMALLER THAN THE DIRECTORY.
|
|
5
|
+
* These are the symbols a real stack consumes; the rest of the files are internals a
|
|
6
|
+
* provider needs but a consumer should not depend on. An `export *` here would publish
|
|
7
|
+
* every helper as API and make the next refactor a breaking change.
|
|
8
|
+
* ★ Anything unlisted is still reachable by path if you genuinely need it — that is a
|
|
9
|
+
* deliberate, visible act rather than an accident of barrelling.
|
|
10
|
+
*/
|
|
11
|
+
export { ProxmoxAclProvider } from './acl.ts';
|
|
12
|
+
export { ProxmoxApiTokenProvider } from './api-token.ts';
|
|
13
|
+
export { ProxmoxBackupJobProvider } from './backup-job.ts';
|
|
14
|
+
export { ProxmoxCephDaemonProvider } from './ceph-daemon.ts';
|
|
15
|
+
export { ProxmoxCephFlagProvider } from './ceph-flag.ts';
|
|
16
|
+
export { ProxmoxCephFsProvider } from './ceph-fs.ts';
|
|
17
|
+
export { ProxmoxCephOsdProvider } from './ceph-osd.ts';
|
|
18
|
+
export { ProxmoxCephPool, ProxmoxCephPoolProvider } from './ceph-pool.ts';
|
|
19
|
+
export { ProxmoxFirewallAliasProvider } from './firewall-alias.ts';
|
|
20
|
+
export { ProxmoxGroupProvider } from './group.ts';
|
|
21
|
+
export { ProxmoxHaResourceProvider } from './ha-resource.ts';
|
|
22
|
+
export { ProxmoxHaRuleProvider } from './ha-rule.ts';
|
|
23
|
+
export { ProxmoxLxcProvider } from './lxc.ts';
|
|
24
|
+
export { ProxmoxMetricServerProvider } from './metric-server.ts';
|
|
25
|
+
export { ProxmoxNetworkApplyProvider } from './network-apply.ts';
|
|
26
|
+
export { ProxmoxNodeNetworkProvider } from './node-network.ts';
|
|
27
|
+
export { ProxmoxNotificationTargetProvider } from './notification-target.ts';
|
|
28
|
+
export { PbsDatastoreProvider } from './pbs-datastore.ts';
|
|
29
|
+
export { PbsPruneJobProvider } from './pbs-prune-job.ts';
|
|
30
|
+
export { PbsSyncJobProvider } from './pbs-sync-job.ts';
|
|
31
|
+
export { PbsVerifyJobProvider } from './pbs-verify-job.ts';
|
|
32
|
+
export { ProxmoxPool, ProxmoxPoolProvider } from './pool.ts';
|
|
33
|
+
export { ProxmoxVmProvider } from './qemu.ts';
|
|
34
|
+
export { ProxmoxReplicationJobProvider } from './replication-job.ts';
|
|
35
|
+
export { ProxmoxRoleProvider } from './role.ts';
|
|
36
|
+
export { ProxmoxSdnApplyProvider } from './sdn-apply.ts';
|
|
37
|
+
export { ProxmoxSdnSubnetProvider } from './sdn-subnet.ts';
|
|
38
|
+
export { ProxmoxSdnVnetProvider } from './sdn-vnet.ts';
|
|
39
|
+
export { ProxmoxSdnZoneProvider } from './sdn-zone.ts';
|
|
40
|
+
export { ProxmoxStorageProvider } from './storage.ts';
|
|
41
|
+
export { ProxmoxUserProvider } from './user.ts';
|
|
42
|
+
export { ProxmoxZfsPoolProvider } from './zfs-pool.ts';
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ★ BOTH DIRECTIONS ARE PINNED. A cache that never serves a hit is a slow no-op nobody notices; a
|
|
3
|
+
* cache that serves an expiring credential turns a 401 into "the object is absent", which is how
|
|
4
|
+
* a plan comes to say CREATE for something that exists. The margin is the whole safety property,
|
|
5
|
+
* so it is tested from both sides of its edge.
|
|
6
|
+
*
|
|
7
|
+
* ⛔ AND THE CONCURRENCY IS PINNED, BECAUSE THAT IS WHAT THE MEASUREMENT WAS ABOUT. 98 resources
|
|
8
|
+
* reached an empty cache in the same instant, and the hand-written cache this replaced was wrong
|
|
9
|
+
* only on the path where a mint fails — so that path is tested, not assumed.
|
|
10
|
+
*/
|
|
11
|
+
import assert from 'node:assert/strict';
|
|
12
|
+
import { describe, it } from 'node:test';
|
|
13
|
+
import * as Cache from 'effect/Cache';
|
|
14
|
+
import * as Duration from 'effect/Duration';
|
|
15
|
+
import * as Effect from 'effect/Effect';
|
|
16
|
+
import * as Exit from 'effect/Exit';
|
|
17
|
+
import type { PveCredential } from './credentials.ts';
|
|
18
|
+
import { type LeaseKey, makeLeases, timeToLive } from './lease-cache.ts';
|
|
19
|
+
|
|
20
|
+
const TB4: LeaseKey = {
|
|
21
|
+
mount: 'proxmox-tb4',
|
|
22
|
+
role: 'read',
|
|
23
|
+
scheme: 'pve',
|
|
24
|
+
};
|
|
25
|
+
/** ⚠️ Same role as TB4, DIFFERENT mount — the collision the key has to survive. */
|
|
26
|
+
const OPS: LeaseKey = { ...TB4, mount: 'proxmox-ops' };
|
|
27
|
+
|
|
28
|
+
const cred = (tokenId: string, leaseSeconds: number): PveCredential => ({
|
|
29
|
+
leaseSeconds,
|
|
30
|
+
secret: 'not-a-real-secret',
|
|
31
|
+
tokenId,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const keptSeconds = (exit: Exit.Exit<PveCredential, unknown>) =>
|
|
35
|
+
Duration.toMillis(timeToLive(exit)) / 1000;
|
|
36
|
+
|
|
37
|
+
/** A mint that counts itself, takes a moment — so callers genuinely overlap — and does as told. */
|
|
38
|
+
const countingMint = (outcome: (call: number) => PveCredential | Error) => {
|
|
39
|
+
let calls = 0;
|
|
40
|
+
const mintFor = () =>
|
|
41
|
+
Effect.gen(function* () {
|
|
42
|
+
calls += 1;
|
|
43
|
+
const result = outcome(calls);
|
|
44
|
+
yield* Effect.sleep('10 millis');
|
|
45
|
+
if (result instanceof Error) return yield* Effect.fail(result);
|
|
46
|
+
return result;
|
|
47
|
+
});
|
|
48
|
+
return { calls: () => calls, mintFor };
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const concurrently = <A, E>(times: number, effect: Effect.Effect<A, E>) =>
|
|
52
|
+
Effect.all(
|
|
53
|
+
Array.from({ length: times }, () => Effect.exit(effect)),
|
|
54
|
+
{ concurrency: 'unbounded' },
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
describe('how long a lease is kept', () => {
|
|
58
|
+
it('keeps a 300s provision lease for 240s — the last minute is the margin', () => {
|
|
59
|
+
assert.equal(keptSeconds(Exit.succeed(cred('hf-provision@pve!a', 300))), 240);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('keeps a lease one second past the margin for one second', () => {
|
|
63
|
+
assert.equal(keptSeconds(Exit.succeed(cred('hf-read@pve!a', 61))), 1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('never keeps a lease inside the margin', () => {
|
|
67
|
+
assert.equal(keptSeconds(Exit.succeed(cred('hf-read@pve!brief', 60))), 0);
|
|
68
|
+
assert.equal(keptSeconds(Exit.succeed(cred('hf-read@pve!brief', 30))), 0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('never keeps a lease with no reported duration', () => {
|
|
72
|
+
// ⛔ `lease_duration` absent reads as 0. Keeping a credential of unknown lifetime is the one
|
|
73
|
+
// thing this file must not do — not keeping is always safe.
|
|
74
|
+
assert.equal(keptSeconds(Exit.succeed(cred('hf-read@pve!unknown', 0))), 0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('never keeps a failure', () => {
|
|
78
|
+
assert.equal(keptSeconds(Exit.fail(new Error('vault sealed'))), 0);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('lease cache under concurrency', () => {
|
|
83
|
+
it('mints once for many concurrent callers, and serves the next from the cache', async () => {
|
|
84
|
+
const mint = countingMint(() => cred('hf-read@pve!a', 3600));
|
|
85
|
+
await Effect.runPromise(
|
|
86
|
+
Effect.gen(function* () {
|
|
87
|
+
const leases = yield* makeLeases(mint.mintFor);
|
|
88
|
+
const exits = yield* concurrently(20, Cache.get(leases, TB4));
|
|
89
|
+
assert.equal(exits.filter(Exit.isSuccess).length, 20);
|
|
90
|
+
// An equal but separately built key is the same entry.
|
|
91
|
+
yield* Cache.get(leases, { ...TB4 });
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
assert.equal(mint.calls(), 1);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ⛔ THE HAND-WRITTEN CACHE'S BUG LIVED HERE: a failed mint let every waiter claim at once.
|
|
98
|
+
it('mints ONCE more after a failure, not once per waiter', async () => {
|
|
99
|
+
const mint = countingMint((call) =>
|
|
100
|
+
call === 1 ? new Error('vault sealed') : cred('hf-read@pve!b', 3600),
|
|
101
|
+
);
|
|
102
|
+
await Effect.runPromise(
|
|
103
|
+
Effect.gen(function* () {
|
|
104
|
+
const leases = yield* makeLeases(mint.mintFor);
|
|
105
|
+
const first = yield* concurrently(20, Cache.get(leases, TB4));
|
|
106
|
+
assert.equal(first.filter(Exit.isFailure).length, 20, 'the waiters share the failure');
|
|
107
|
+
const second = yield* concurrently(20, Cache.get(leases, TB4));
|
|
108
|
+
assert.equal(second.filter(Exit.isSuccess).length, 20);
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
assert.equal(mint.calls(), 2);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('shares an uncacheable lease with its waiters and mints again for the next caller', async () => {
|
|
115
|
+
const mint = countingMint((call) => cred(`hf-read@pve!brief${String(call)}`, 30));
|
|
116
|
+
await Effect.runPromise(
|
|
117
|
+
Effect.gen(function* () {
|
|
118
|
+
const leases = yield* makeLeases(mint.mintFor);
|
|
119
|
+
yield* concurrently(20, Cache.get(leases, TB4));
|
|
120
|
+
yield* Cache.get(leases, TB4);
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
assert.equal(mint.calls(), 2);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('does not serve one cluster the credential of another with the same mount and role', async () => {
|
|
127
|
+
const mint = countingMint((call) => cred(`hf-read@pve!${String(call)}`, 3600));
|
|
128
|
+
const [tb4, ops] = await Effect.runPromise(
|
|
129
|
+
Effect.gen(function* () {
|
|
130
|
+
const leases = yield* makeLeases(mint.mintFor);
|
|
131
|
+
return [yield* Cache.get(leases, TB4), yield* Cache.get(leases, OPS)] as const;
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
assert.notEqual(tb4.tokenId, ops.tokenId);
|
|
135
|
+
assert.equal(mint.calls(), 2);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('shares one lease across two members of the same cluster mount', async () => {
|
|
139
|
+
const mint = countingMint((call) => cred(`hf-read@pve!${String(call)}`, 3600));
|
|
140
|
+
const N2: LeaseKey = { mount: 'proxmox-tb4', role: 'read', scheme: 'pve' };
|
|
141
|
+
const N3: LeaseKey = { mount: 'proxmox-tb4', role: 'read', scheme: 'pve' };
|
|
142
|
+
await Effect.runPromise(
|
|
143
|
+
Effect.gen(function* () {
|
|
144
|
+
const leases = yield* makeLeases(mint.mintFor);
|
|
145
|
+
yield* Cache.get(leases, N2);
|
|
146
|
+
yield* Cache.get(leases, N3);
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
assert.equal(mint.calls(), 1);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('keeps read and provision apart', async () => {
|
|
153
|
+
const mint = countingMint((call) => cred(`hf@pve!${String(call)}`, 3600));
|
|
154
|
+
const PROVISION: LeaseKey = { ...TB4, role: 'provision' };
|
|
155
|
+
const [read, provision] = await Effect.runPromise(
|
|
156
|
+
Effect.gen(function* () {
|
|
157
|
+
const leases = yield* makeLeases(mint.mintFor);
|
|
158
|
+
return [yield* Cache.get(leases, TB4), yield* Cache.get(leases, PROVISION)] as const;
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
assert.notEqual(read.tokenId, provision.tokenId);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One credential per (target, role) for the life of a run, instead of one per API call.
|
|
3
|
+
*
|
|
4
|
+
* 🔴 THE MEASUREMENT THAT MADE THIS NECESSARY. A single `alchemy plan` plus `deploy` of the TB4
|
|
5
|
+
* stack — 98 resources — left **760 API token entries in `/etc/pve/user.cfg`**, counted on n2
|
|
6
|
+
* immediately afterwards. `mint()` asked OpenBao for a new credential on EVERY call, and each
|
|
7
|
+
* mint creates a real Proxmox token: read, diff, reconcile and read-back each got their own. They
|
|
8
|
+
* drain as the leases expire (760 down to 712 over five minutes), so nothing was broken — but
|
|
9
|
+
* `/etc/pve` is pmxcfs, a REPLICATED cluster filesystem, and every one of those mints is a write
|
|
10
|
+
* to it that all three nodes must agree on. Two plans in quick succession stack thousands.
|
|
11
|
+
*
|
|
12
|
+
* ⛔ AND ON PBS THE COST IS SECONDS, NOT BYTES. `pbs_grant.go` waits for PBS to honour a freshly
|
|
13
|
+
* granted ACL before handing the credential over — measured at about 4.5s, and unavoidable,
|
|
14
|
+
* because PBS caches its parsed ACL config. Per mint. The ten PBS resources in pbs.ts would pay
|
|
15
|
+
* that ten times over for credentials identical to each other.
|
|
16
|
+
*
|
|
17
|
+
* ★ SO THE FIX IS TO STOP THROWING AWAY A CREDENTIAL THAT IS STILL VALID. This is strictly FEWER
|
|
18
|
+
* credentials in existence at once, not more: the lease TTL is untouched, and a token that would
|
|
19
|
+
* have been minted and abandoned is simply never minted. It is the same reasoning
|
|
20
|
+
* `network-apply.ts` already applies by hand when it threads one lease through `pveWith` for its
|
|
21
|
+
* apply and its task poll.
|
|
22
|
+
*
|
|
23
|
+
* ⚠️ THE CACHE IS PROCESS-SCOPED AND THAT IS THE INTENDED LIFETIME. A plan or a deploy is one
|
|
24
|
+
* process; the next one starts cold. Nothing is written to disk, and a credential never outlives
|
|
25
|
+
* the run that minted it.
|
|
26
|
+
*
|
|
27
|
+
* ⛔ IT IS NOT A PERFORMANCE CACHE, SO IT MUST NEVER SERVE A CREDENTIAL THAT COULD DIE MID-CALL.
|
|
28
|
+
* The provision lease is 300s and a deploy can run longer than that. A credential handed out at
|
|
29
|
+
* t+299 would fail the call it was handed to, and the failure surfaces as a 401 — which `read`
|
|
30
|
+
* folds into "absent", which is how a plan comes to say CREATE for an object that exists. That is
|
|
31
|
+
* the exact class of bug this package found three times in one day, so the margin below is
|
|
32
|
+
* deliberately generous rather than tuned.
|
|
33
|
+
*
|
|
34
|
+
* ★ THE MECHANISM IS EFFECT'S OWN `Cache`, AND THE HAND-WRITTEN ONE IT REPLACED WAS WRONG IN
|
|
35
|
+
* EXACTLY THE PLACE A LIBRARY IS NOT. A TTL map alone still minted 89 credentials for 98
|
|
36
|
+
* resources, because Alchemy plans concurrently and every resource missed in the same instant;
|
|
37
|
+
* the in-flight map added to fix that got the first miss right and the failure path wrong. A
|
|
38
|
+
* waiter woken by a FAILED or uncacheable mint claimed the slot without looking again, the
|
|
39
|
+
* waiters overwrote each other's claims, and each one's cleanup deleted somebody else's — the
|
|
40
|
+
* thundering herd back through the error door (found in review 2026-09-14, confirmed by
|
|
41
|
+
* reading). `Cache.get` shares one pending lookup among concurrent callers by design. MEASURED
|
|
42
|
+
* with a probe before this was written: ten concurrent gets on equal-but-distinct key objects ran
|
|
43
|
+
* the lookup once; a zero time-to-live ran it again for the next caller; a failure reached all
|
|
44
|
+
* ten waiters and was not kept.
|
|
45
|
+
*/
|
|
46
|
+
import * as Cache from 'effect/Cache';
|
|
47
|
+
import * as Duration from 'effect/Duration';
|
|
48
|
+
import * as Effect from 'effect/Effect';
|
|
49
|
+
import * as Exit from 'effect/Exit';
|
|
50
|
+
import { type ApiTarget, type PveCredential, type PveRole, mint } from './credentials.ts';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Re-mint once a lease is inside this much of its end, in seconds.
|
|
54
|
+
*
|
|
55
|
+
* ⚠️ 60s AGAINST A 300s PROVISION LEASE MEANS A CACHED CREDENTIAL IS ONLY EVER HANDED OUT WITH AT
|
|
56
|
+
* LEAST A MINUTE LEFT. The slowest single call in this package is `network-apply`'s reload poll,
|
|
57
|
+
* and that mints its own lease deliberately — see the ⚠️ on `pveWith` there — so nothing using
|
|
58
|
+
* this cache comes near a minute.
|
|
59
|
+
* ⚠️ A MOUNT REPORTING NO `lease_duration` IS NEVER CACHED: `leaseSeconds` is `0` and
|
|
60
|
+
* `timeToLive` answers zero. Not caching is always safe; caching a credential whose lifetime is
|
|
61
|
+
* unknown is not.
|
|
62
|
+
*/
|
|
63
|
+
const REMINT_MARGIN_SECONDS = 60;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* What a credential is cached under.
|
|
67
|
+
*
|
|
68
|
+
* ⛔ KEYED ON THE OPENBAO MOUNT AND ROLE, NOT ON A MEMBER HOSTNAME. A PVE API token is
|
|
69
|
+
* cluster-wide — `user.cfg` is replicated — so n2, n3 and n4 share one credential. Keying on
|
|
70
|
+
* `api` would mint three identical tokens when failover rotates members.
|
|
71
|
+
* ★ THE MOUNT CANNOT COLLIDE ACROSS CLUSTERS: `proxmox-tb4`, `proxmox-ops` and `pbs-tb4` are
|
|
72
|
+
* distinct OpenBao mounts with distinct policies; two estates never share a mount name.
|
|
73
|
+
* ★ A PLAIN OBJECT IS A SAFE KEY BECAUSE EFFECT COMPARES KEYS STRUCTURALLY — two separately built
|
|
74
|
+
* literals with the same fields are one entry, which the probe above measured.
|
|
75
|
+
*/
|
|
76
|
+
export type LeaseKey = {
|
|
77
|
+
readonly mount: string;
|
|
78
|
+
readonly role: PveRole;
|
|
79
|
+
readonly scheme: ApiTarget['scheme'];
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const keyOf = (target: ApiTarget, role: PveRole): LeaseKey => ({
|
|
83
|
+
mount: target.mount,
|
|
84
|
+
role,
|
|
85
|
+
scheme: target.scheme,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* How long a freshly minted credential may be handed out again.
|
|
90
|
+
*
|
|
91
|
+
* ⛔ ZERO FOR A FAILURE, FOR AN UNKNOWN LIFETIME AND FOR A LEASE INSIDE THE MARGIN. Zero means "do
|
|
92
|
+
* not keep": the callers already waiting on THIS mint still receive its result, and the next
|
|
93
|
+
* caller mints again. Keeping a failure would turn one refused mint into a refused run.
|
|
94
|
+
* ⚠️ EXPORTED FOR THE TEST. The margin is the whole safety property, so it is pinned from both
|
|
95
|
+
* sides of its edge without waiting four minutes of wall clock.
|
|
96
|
+
*/
|
|
97
|
+
export const timeToLive = (exit: Exit.Exit<PveCredential, unknown>): Duration.Duration =>
|
|
98
|
+
Exit.isSuccess(exit) && exit.value.leaseSeconds > REMINT_MARGIN_SECONDS
|
|
99
|
+
? Duration.seconds(exit.value.leaseSeconds - REMINT_MARGIN_SECONDS)
|
|
100
|
+
: Duration.zero;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A lease cache over any mint.
|
|
104
|
+
*
|
|
105
|
+
* ⚠️ ONE BEHAVIOUR CHANGED WITH THE MOVE TO `Cache`, DELIBERATELY. Callers waiting on a mint that
|
|
106
|
+
* FAILS now receive that failure, where the hand-written version had each of them mint again.
|
|
107
|
+
* They asked in the same instant for the same credential, and a plan with one failed resource
|
|
108
|
+
* fails either way — so the change is in how many errors the output lists, not in the outcome,
|
|
109
|
+
* and a sealed vault is asked once rather than ninety-eight times.
|
|
110
|
+
* ★ `requireServicesAt: 'lookup'` IS WHAT LETS IT BE BUILT ONCE, AT IMPORT. The mint needs an
|
|
111
|
+
* `HttpClient`; that requirement moves to each `get`, where every caller already has one.
|
|
112
|
+
* ⚠️ EXPORTED FOR THE TEST, which hands it a counting mint instead of calling OpenBao.
|
|
113
|
+
*/
|
|
114
|
+
export const makeLeases = <E, R>(mintFor: (key: LeaseKey) => Effect.Effect<PveCredential, E, R>) =>
|
|
115
|
+
Cache.makeWith(mintFor, { capacity: 64, requireServicesAt: 'lookup', timeToLive });
|
|
116
|
+
|
|
117
|
+
const targetForKey = (key: LeaseKey): ApiTarget =>
|
|
118
|
+
key.scheme === 'pbs'
|
|
119
|
+
? { api: 'https://pbs.invalid/api2/json', mount: key.mount, scheme: 'pbs' }
|
|
120
|
+
: { members: [], mount: key.mount, scheme: 'pve' };
|
|
121
|
+
|
|
122
|
+
const leases = Effect.runSync(makeLeases((key) => mint(targetForKey(key), key.role)));
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A credential for `role` on `target`: a still-valid lease when one is kept, one shared mint when
|
|
126
|
+
* not.
|
|
127
|
+
*
|
|
128
|
+
* ★ client.ts is the one place that decides to use the cache. `mint` stays exported and unchanged
|
|
129
|
+
* for the caller that reasons about identity rather than cost — network-apply.ts.
|
|
130
|
+
*/
|
|
131
|
+
export const leased = (target: ApiTarget, role: PveRole) => Cache.get(leases, keyOf(target, role));
|