@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,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Proxmox.Lxc` — a container, declared. This is what the estate's provision credential exists for.
|
|
3
|
+
*
|
|
4
|
+
* ★ `hf-provision@pve` HOLDS EXACTLY THE ROLE THIS NEEDS, and nothing more: `LXCProvisioner` on `/`
|
|
5
|
+
* grants VM.Allocate, VM.Audit, VM.Config.{CPU,Disk,HWType,Memory,Network,Options},
|
|
6
|
+
* VM.PowerMgmt, Datastore.AllocateSpace, Datastore.Audit, SDN.Use, Sys.Audit. Read that list as a
|
|
7
|
+
* specification: the credential was scoped to provision containers and to do nothing else.
|
|
8
|
+
*
|
|
9
|
+
* ⛔ `vmid` IS THE PRIMARY KEY AND IT IS CLUSTER-WIDE, not per node. Two resources declaring the
|
|
10
|
+
* same vmid on different nodes are the same object, and PVE will refuse the second with
|
|
11
|
+
* "already exists". It is required rather than allocated here: letting the provider pick the next
|
|
12
|
+
* free id would make the identity of a container depend on the order plans happened to run in.
|
|
13
|
+
*
|
|
14
|
+
* ⚠️ MOST FIELDS ARE CREATE-TIME. `ostemplate` and `storage` are consumed when the rootfs is built
|
|
15
|
+
* and are not readable back afterwards, so they are deliberately NOT compared in `matches` --
|
|
16
|
+
* a provider that diffed them would report an update on every plan, forever. What is compared is
|
|
17
|
+
* what PVE will actually tell you about a running container.
|
|
18
|
+
*/
|
|
19
|
+
import { Resource } from 'alchemy';
|
|
20
|
+
import * as Provider from 'alchemy/Provider';
|
|
21
|
+
import * as Effect from 'effect/Effect';
|
|
22
|
+
import { type PveRequirements, type WithTarget, pveHandlers } from './resource.ts';
|
|
23
|
+
import { num } from './values.ts';
|
|
24
|
+
|
|
25
|
+
export interface LxcProps extends WithTarget {
|
|
26
|
+
/** Which node hosts it. Moving a container between nodes is a migration, not an update. */
|
|
27
|
+
node: string;
|
|
28
|
+
/** ⛔ CLUSTER-WIDE primary key. Required, never auto-allocated — see the ⛔ above. */
|
|
29
|
+
vmid: number;
|
|
30
|
+
/** e.g. `local:vztmpl/debian-13-standard_13.0-1_amd64.tar.zst`. Create-time only. */
|
|
31
|
+
ostemplate: string;
|
|
32
|
+
/** Storage for the rootfs, e.g. `local-zfs`. Create-time only. */
|
|
33
|
+
storage: string;
|
|
34
|
+
hostname?: string;
|
|
35
|
+
/** MiB. Mutable. */
|
|
36
|
+
memory?: number;
|
|
37
|
+
cores?: number;
|
|
38
|
+
/** e.g. `name=eth0,bridge=vmbr0,ip=dhcp`. Mutable. */
|
|
39
|
+
net0?: string;
|
|
40
|
+
/** Start on boot. Mutable. */
|
|
41
|
+
onboot?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface LxcAttributes {
|
|
45
|
+
vmid: number;
|
|
46
|
+
node: string;
|
|
47
|
+
hostname: string;
|
|
48
|
+
memory: number;
|
|
49
|
+
cores: number;
|
|
50
|
+
onboot: boolean;
|
|
51
|
+
/** `running` | `stopped`. Reported, never declared — power state is not configuration. */
|
|
52
|
+
status: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ProxmoxLxc extends Resource<
|
|
56
|
+
'Proxmox.Lxc',
|
|
57
|
+
LxcProps,
|
|
58
|
+
LxcAttributes,
|
|
59
|
+
never,
|
|
60
|
+
PveRequirements
|
|
61
|
+
> {}
|
|
62
|
+
|
|
63
|
+
export const ProxmoxLxc = Resource<ProxmoxLxc>('Proxmox.Lxc');
|
|
64
|
+
|
|
65
|
+
const handlers = pveHandlers<LxcProps, LxcAttributes>({
|
|
66
|
+
/**
|
|
67
|
+
* ⚠️ READ FROM `config`, NOT FROM THE GUEST LIST. `GET /nodes/{n}/lxc` reports runtime shape
|
|
68
|
+
* (status, uptime, current memory), while `/config` reports DECLARED shape. Diffing against
|
|
69
|
+
* runtime memory would report an update every time a container ballooned.
|
|
70
|
+
*/
|
|
71
|
+
attributes: (live, props) => ({
|
|
72
|
+
cores: num(live['cores'], 1),
|
|
73
|
+
hostname: typeof live['hostname'] === 'string' ? live['hostname'] : '',
|
|
74
|
+
memory: num(live['memory'], 512),
|
|
75
|
+
node: props.node,
|
|
76
|
+
onboot: live['onboot'] === 1 || live['onboot'] === true,
|
|
77
|
+
status: typeof live['status'] === 'string' ? live['status'] : 'unknown',
|
|
78
|
+
vmid: props.vmid,
|
|
79
|
+
}),
|
|
80
|
+
collection: (props) => `nodes/${props.node}/lxc`,
|
|
81
|
+
createForm: (props) => ({
|
|
82
|
+
cores: String(props.cores ?? 1),
|
|
83
|
+
hostname: props.hostname ?? `ct${String(props.vmid)}`,
|
|
84
|
+
memory: String(props.memory ?? 512),
|
|
85
|
+
onboot: props.onboot === true ? '1' : '0',
|
|
86
|
+
ostemplate: props.ostemplate,
|
|
87
|
+
storage: props.storage,
|
|
88
|
+
vmid: String(props.vmid),
|
|
89
|
+
...(props.net0 === undefined ? {} : { net0: props.net0 }),
|
|
90
|
+
}),
|
|
91
|
+
matches: (attributes, props) =>
|
|
92
|
+
attributes.hostname === (props.hostname ?? `ct${String(props.vmid)}`) &&
|
|
93
|
+
attributes.memory === (props.memory ?? 512) &&
|
|
94
|
+
attributes.cores === (props.cores ?? 1) &&
|
|
95
|
+
attributes.onboot === (props.onboot === true),
|
|
96
|
+
path: (props) => `nodes/${props.node}/lxc/${String(props.vmid)}/config`,
|
|
97
|
+
/**
|
|
98
|
+
* ⚠️ PVE UPDATES A CONTAINER THROUGH `PUT .../config`, THE SAME PATH IT IS READ FROM, which is
|
|
99
|
+
* why `path` ends in `/config` while `collection` does not. Create POSTs to the collection;
|
|
100
|
+
* read and update both use the config document.
|
|
101
|
+
*/
|
|
102
|
+
updateForm: (props) => ({
|
|
103
|
+
cores: String(props.cores ?? 1),
|
|
104
|
+
hostname: props.hostname ?? `ct${String(props.vmid)}`,
|
|
105
|
+
memory: String(props.memory ?? 512),
|
|
106
|
+
onboot: props.onboot === true ? '1' : '0',
|
|
107
|
+
...(props.net0 === undefined ? {} : { net0: props.net0 }),
|
|
108
|
+
}),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* ⛔ PVE REFUSES TO DELETE A RUNNING CONTAINER, and that refusal is kept — `handlers.delete`
|
|
113
|
+
* passes it straight through. Stopping a guest so that a `destroy` can proceed is a decision an
|
|
114
|
+
* operator makes, not one a plan makes on their behalf while they are reading the diff.
|
|
115
|
+
*/
|
|
116
|
+
export const ProxmoxLxcProvider = () =>
|
|
117
|
+
Provider.effect(ProxmoxLxc, Effect.succeed(ProxmoxLxc.Provider.of(handlers)));
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which transport failures prove a request never left this machine: the rule that decides whether
|
|
3
|
+
* a WRITE may go on to a second cluster member.
|
|
4
|
+
*
|
|
5
|
+
* ⛔ BOTH DIRECTIONS ARE PINNED. Too narrow and a write never fails over (Bun's refusal code was
|
|
6
|
+
* missing from the first draft); too wide and a write that already landed is sent to a second
|
|
7
|
+
* node (the first draft also trusted message text that mentions TLS).
|
|
8
|
+
*/
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import { describe, it } from 'node:test';
|
|
11
|
+
import * as HttpClientError from 'effect/unstable/http/HttpClientError';
|
|
12
|
+
import { isPreSendTransport, isTransportFailure } from './members.ts';
|
|
13
|
+
|
|
14
|
+
const transport = (cause: Error) =>
|
|
15
|
+
new HttpClientError.HttpClientError({
|
|
16
|
+
reason: new HttpClientError.TransportError({
|
|
17
|
+
cause,
|
|
18
|
+
request: { method: 'PUT', url: 'https://n2.mgmt.example.com:8006/api2/json/x' } as never,
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const coded = (code: string, message = 'transport') => Object.assign(new Error(message), { code });
|
|
23
|
+
|
|
24
|
+
describe('transport classification', () => {
|
|
25
|
+
it('treats timeout TransportError as transport but not pre-send', () => {
|
|
26
|
+
const timeout = transport(coded('AbortError', 'aborted'));
|
|
27
|
+
assert.equal(isTransportFailure(timeout), true);
|
|
28
|
+
assert.equal(isPreSendTransport(timeout), false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// ★ Bun's spelling first — MEASURED 2026-09-14 through FetchHttpClient; see members.ts.
|
|
32
|
+
it('treats refused, unresolvable and refused-certificate as pre-send', () => {
|
|
33
|
+
for (const code of [
|
|
34
|
+
'ConnectionRefused',
|
|
35
|
+
'ECONNREFUSED',
|
|
36
|
+
'ENOTFOUND',
|
|
37
|
+
'ERR_TLS_CERT_ALTNAME_INVALID',
|
|
38
|
+
]) {
|
|
39
|
+
assert.equal(isPreSendTransport(transport(coded(code))), true, code);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ⛔ A message that merely mentions TLS proves nothing about whether a write was sent.
|
|
44
|
+
it('never takes message text, a mid-stream TLS error or a reset as proof of pre-send', () => {
|
|
45
|
+
assert.equal(isPreSendTransport(transport(new Error('TLS connection closed by peer'))), false);
|
|
46
|
+
assert.equal(isPreSendTransport(transport(coded('ERR_SSL_DECRYPTION_FAILED'))), false);
|
|
47
|
+
assert.equal(isPreSendTransport(transport(coded('ECONNRESET'))), false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('never treats an HTTP answer as a transport failure', () => {
|
|
51
|
+
assert.equal(isTransportFailure(new Error('500')), false);
|
|
52
|
+
assert.equal(isPreSendTransport(new Error('ConnectionRefused')), false);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TB4 cluster members: ordered failover, last-good preference, transport classification.
|
|
3
|
+
*
|
|
4
|
+
* ★ ANY MEMBER'S :8006 API MANAGES THE WHOLE CLUSTER — PVE proxies node paths itself — so the
|
|
5
|
+
* provider reaches the cluster through whichever member is up, not through one pinned node.
|
|
6
|
+
*
|
|
7
|
+
* ⛔ NEVER cluster-tb4.example.com: it resolves to 10.20.10.250, which nothing serves.
|
|
8
|
+
*/
|
|
9
|
+
import * as Effect from 'effect/Effect';
|
|
10
|
+
import * as Result from 'effect/Result';
|
|
11
|
+
import * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
12
|
+
import * as HttpClientError from 'effect/unstable/http/HttpClientError';
|
|
13
|
+
import type * as HttpClientRequest from 'effect/unstable/http/HttpClientRequest';
|
|
14
|
+
import type * as HttpClientResponse from 'effect/unstable/http/HttpClientResponse';
|
|
15
|
+
import type { PveTarget } from './credentials.ts';
|
|
16
|
+
|
|
17
|
+
export const PVE_API_PORT = 8006;
|
|
18
|
+
|
|
19
|
+
/** Strict TLS, always :8006 — each node's certificate carries its mgmt name as a SAN. */
|
|
20
|
+
export const pveApiBase = (member: string) => `https://${member}:${String(PVE_API_PORT)}/api2/json`;
|
|
21
|
+
|
|
22
|
+
/** Process-scoped last member that answered, keyed by OpenBao mount (= one PVE cluster). */
|
|
23
|
+
const lastGoodByMount = new Map<string, string>();
|
|
24
|
+
|
|
25
|
+
/** ⚠️ TESTS ONLY — resets sticky member choice between cases. */
|
|
26
|
+
export const resetLastGoodForTest = () => lastGoodByMount.clear();
|
|
27
|
+
|
|
28
|
+
export const noteGoodMember = (target: PveTarget, member: string) => {
|
|
29
|
+
lastGoodByMount.set(target.mount, member);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Last-good first, then the rest in configuration order. */
|
|
33
|
+
export const orderedMembers = (target: PveTarget): readonly string[] => {
|
|
34
|
+
const preferred = lastGoodByMount.get(target.mount);
|
|
35
|
+
if (preferred === undefined || !target.members.includes(preferred)) return target.members;
|
|
36
|
+
return [preferred, ...target.members.filter((member) => member !== preferred)];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const errorCode = (cause: unknown): string | undefined => {
|
|
40
|
+
if (cause !== null && typeof cause === 'object') {
|
|
41
|
+
if ('code' in cause && typeof cause.code === 'string') return cause.code;
|
|
42
|
+
if ('cause' in cause) return errorCode((cause as { cause: unknown }).cause);
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const isTimeoutCause = (cause: unknown): boolean => {
|
|
48
|
+
const code = errorCode(cause);
|
|
49
|
+
if (code === 'AbortError' || code === 'ETIMEDOUT' || code === 'UND_ERR_CONNECT_TIMEOUT') {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return cause instanceof Error && /timed out|timeout/i.test(cause.message);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Error codes that PROVE the request never left this machine.
|
|
57
|
+
*
|
|
58
|
+
* ★ MEASURED 2026-09-14 through Effect's FetchHttpClient on Bun 1.4. The codes are Bun's, not
|
|
59
|
+
* Node's, which made the first draft's Node-only list dead code for the commonest case:
|
|
60
|
+
* connection refused cause.code = "ConnectionRefused" (Node says ECONNREFUSED)
|
|
61
|
+
* name does not resolve cause.code = "ENOTFOUND"
|
|
62
|
+
* wrong certificate name cause.code = "ERR_TLS_CERT_ALTNAME_INVALID"
|
|
63
|
+
* no answer after connect Effect's TimeoutError — not an HttpClientError at all
|
|
64
|
+
* ⛔ CODES ONLY, NEVER MESSAGE TEXT. The first draft also matched /TLS|SSL|certificate/ in the
|
|
65
|
+
* message, and a connection that drops mid-response can mention TLS too — which would have sent a
|
|
66
|
+
* write that had already landed to a second node.
|
|
67
|
+
* ⚠️ A CERTIFICATE failure is pre-send: the handshake has to finish before a byte of the request is
|
|
68
|
+
* written. A generic ERR_SSL_* is deliberately absent, because it can happen mid-stream.
|
|
69
|
+
*/
|
|
70
|
+
const PRE_SEND_CODES = new Set(['ConnectionRefused', 'ECONNREFUSED', 'ENOTFOUND', 'EAI_AGAIN']);
|
|
71
|
+
|
|
72
|
+
const isCertificateCode = (code: string) =>
|
|
73
|
+
/^(ERR_TLS_CERT_|CERT_|UNABLE_TO_VERIFY_|DEPTH_ZERO_SELF_SIGNED|SELF_SIGNED_CERT)/.test(code);
|
|
74
|
+
|
|
75
|
+
const isPreSendCause = (cause: unknown): boolean => {
|
|
76
|
+
const code = errorCode(cause);
|
|
77
|
+
return code !== undefined && (PRE_SEND_CODES.has(code) || isCertificateCode(code));
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Effect HttpClientError.reason._tag — see effect HttpClientError.ts RequestError vs ResponseError.
|
|
82
|
+
* ★ TransportError | InvalidUrlError | EncodeError occur before a response exists; StatusCodeError
|
|
83
|
+
* and kin include a response. client.execute does NOT filter status, so HTTP answers are never
|
|
84
|
+
* TransportError — only the transport layer is.
|
|
85
|
+
*/
|
|
86
|
+
export const isTransportFailure = (cause: unknown): boolean => {
|
|
87
|
+
if (!HttpClientError.isHttpClientError(cause)) return false;
|
|
88
|
+
const tag = cause.reason._tag;
|
|
89
|
+
return tag === 'TransportError' || tag === 'InvalidUrlError' || tag === 'EncodeError';
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** Pre-send only: connection refused, DNS, a certificate refused — never a timeout after connect. */
|
|
93
|
+
export const isPreSendTransport = (cause: unknown): boolean => {
|
|
94
|
+
if (!HttpClientError.isHttpClientError(cause)) return false;
|
|
95
|
+
const { reason } = cause;
|
|
96
|
+
if (reason._tag === 'InvalidUrlError' || reason._tag === 'EncodeError') return true;
|
|
97
|
+
if (reason._tag === 'TransportError') {
|
|
98
|
+
if (isTimeoutCause(reason.cause)) return false;
|
|
99
|
+
return isPreSendCause(reason.cause);
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const describeTransport = (cause: unknown): string => {
|
|
105
|
+
if (HttpClientError.isHttpClientError(cause)) return cause.message;
|
|
106
|
+
return String(cause);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export type MemberAttempt = { readonly member: string; readonly why: string };
|
|
110
|
+
|
|
111
|
+
export type PveMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Run one request against cluster members with failover.
|
|
115
|
+
*
|
|
116
|
+
* ⛔ A POST/PUT/DELETE WHOSE REQUEST MAY HAVE REACHED A MEMBER MUST NOT BE SENT AGAIN TO ANOTHER.
|
|
117
|
+
* Only fail over a write on a provably pre-send transport failure — connection refused, DNS or a
|
|
118
|
+
* refused certificate. A timeout after sending is NOT pre-send: fail it, never repeat it. An HTTP
|
|
119
|
+
* status of any kind, including 5xx and PVE's 596, is the answer from a live member — retrying
|
|
120
|
+
* elsewhere can repeat a write.
|
|
121
|
+
*
|
|
122
|
+
* ★ Reads fail over on any transport failure, including timeout — no side effect to duplicate.
|
|
123
|
+
* MEASURED 2026-09-14: full TB4 plans stayed all-noop with the first member refused (127.0.0.1)
|
|
124
|
+
* and with it unresolvable (n9.invalid).
|
|
125
|
+
*/
|
|
126
|
+
export const executeOnCluster = (
|
|
127
|
+
target: PveTarget,
|
|
128
|
+
method: PveMethod,
|
|
129
|
+
path: string,
|
|
130
|
+
build: (apiBase: string) => HttpClientRequest.HttpClientRequest,
|
|
131
|
+
): Effect.Effect<
|
|
132
|
+
HttpClientResponse.HttpClientResponse,
|
|
133
|
+
{
|
|
134
|
+
readonly tag: 'cluster';
|
|
135
|
+
readonly method: PveMethod;
|
|
136
|
+
readonly path: string;
|
|
137
|
+
readonly attempts: readonly MemberAttempt[];
|
|
138
|
+
},
|
|
139
|
+
HttpClient.HttpClient
|
|
140
|
+
> =>
|
|
141
|
+
Effect.gen(function* () {
|
|
142
|
+
/**
|
|
143
|
+
* ★ `HttpClient`, NOT RAW `fetch`, BECAUSE THAT IS WHAT ALCHEMY'S OWN PROVIDERS USE. Its
|
|
144
|
+
* Hetzner provider builds on `FetchHttpClient.layer` and the `HttpClient` service
|
|
145
|
+
* (src/Hetzner/Providers.ts), and there is no bare `fetch` anywhere in its Docker,
|
|
146
|
+
* Kubernetes or GitHub providers. Going through the service rather than the global buys
|
|
147
|
+
* the things a provider actually needs: a typed error channel instead of a thrown promise,
|
|
148
|
+
* interruption when a plan is cancelled, whatever tracing the runtime has installed, and a
|
|
149
|
+
* client that can be swapped for a stub in a test without monkey-patching a global.
|
|
150
|
+
*
|
|
151
|
+
* ⚠️ THE REQUIREMENT IS REAL AND IT PROPAGATES. Every operation carries `HttpClient`, so every
|
|
152
|
+
* resource's Requirements type names it (PveRequirements in resource.ts) and the stack
|
|
153
|
+
* provides `FetchHttpClient.layer`. That is the cost of the above, paid once.
|
|
154
|
+
*/
|
|
155
|
+
const client = yield* HttpClient.HttpClient;
|
|
156
|
+
const isWrite = method !== 'GET';
|
|
157
|
+
const attempts: MemberAttempt[] = [];
|
|
158
|
+
for (const member of orderedMembers(target)) {
|
|
159
|
+
const outcome = yield* client.execute(build(pveApiBase(member))).pipe(Effect.result);
|
|
160
|
+
if (Result.isSuccess(outcome)) {
|
|
161
|
+
noteGoodMember(target, member);
|
|
162
|
+
return outcome.success;
|
|
163
|
+
}
|
|
164
|
+
const why = describeTransport(outcome.failure);
|
|
165
|
+
attempts.push({ member, why });
|
|
166
|
+
const mayRetry = isWrite
|
|
167
|
+
? isPreSendTransport(outcome.failure)
|
|
168
|
+
: isTransportFailure(outcome.failure);
|
|
169
|
+
if (!mayRetry) {
|
|
170
|
+
return yield* Effect.fail({ attempts, method, path, tag: 'cluster' as const });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return yield* Effect.fail({ attempts, method, path, tag: 'cluster' as const });
|
|
174
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A metric server's props, as the form PVE wants.
|
|
3
|
+
*
|
|
4
|
+
* ★ SPLIT OUT OF metric-server.ts TO KEEP BOTH FILES UNDER THE 250-LINE CAP, and the seam is a
|
|
5
|
+
* real one rather than a convenient line number: this file answers "how does a declaration
|
|
6
|
+
* become a PVE form", and metric-server.ts answers "what is a metric server and when has it
|
|
7
|
+
* changed". Nothing here reads the cluster and nothing here decides a diff.
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ THE `import type` BACK TO metric-server.ts IS A CYCLE ON PAPER ONLY. It is type-only, so it
|
|
10
|
+
* is erased before anything runs — `MetricServerProps` stays the resource's public shape, in the
|
|
11
|
+
* file that declares the resource, rather than being moved somewhere odd to dodge the arrow.
|
|
12
|
+
* metric-server-otel.ts imports `UNSET` from here and this file imports nothing back from it,
|
|
13
|
+
* so no runtime cycle exists either.
|
|
14
|
+
*/
|
|
15
|
+
import type { MetricServerProps } from './metric-server.ts';
|
|
16
|
+
import { withClears } from './values.ts';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* ⚠️ "NOT SET" CANNOT BE 0 HERE, WHICH IS WHY IT IS -1. `timeout` has a minimum of 0 in PVE's
|
|
20
|
+
* schema, so a deliberate `timeout 0` exists; a provider using 0 as its absent-marker reads that
|
|
21
|
+
* zero as "unset" and never clears it. -1 is outside every one of these fields' ranges.
|
|
22
|
+
*/
|
|
23
|
+
export const UNSET = -1;
|
|
24
|
+
|
|
25
|
+
const str = (value: number | undefined) => (value === undefined ? undefined : String(value));
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Every managed optional, per type: the value to send, or undefined for "clear it".
|
|
29
|
+
*
|
|
30
|
+
* ⛔ ONE TABLE FEEDS BOTH HALVES OF AN UPDATE, AND THAT IS THE POINT. A PUT THAT OMITS A FIELD DOES
|
|
31
|
+
* NOT CLEAR IT — PVE merges the form into the existing section — so a managed optional needs a
|
|
32
|
+
* matching `delete=` the moment its prop goes away, or `matches` reports an update on every plan
|
|
33
|
+
* forever. Deriving the form and the delete list from one map makes "managed but not clearable"
|
|
34
|
+
* impossible to write here, which a separate clear-list would not.
|
|
35
|
+
* ⛔ NOTHING SET OUT OF BAND MAY BE LISTED HERE, `token` FIRST AMONG THEM: a field in this map is
|
|
36
|
+
* CLEARED whenever it is undeclared, so adding the token would strip a working target's
|
|
37
|
+
* credential on the next plan — metrics stop, nothing errors, the graph goes flat. `otel-headers`
|
|
38
|
+
* is absent for the same reason: it is where an OTLP server's bearer token lives.
|
|
39
|
+
* ⚠️ AND THE BRANCHES CANNOT BE ONE SHARED MAP. PVE validates a section against that plugin's own
|
|
40
|
+
* option set, so `path` on an influxdb server is a hard 400 and `delete=path` there answers "no
|
|
41
|
+
* such option" — neither is an ignored hint. The opentelemetry plugin has no `mtu` and no
|
|
42
|
+
* `timeout` at all (its option list is `server`, `port`, `disable` and the `otel-*` family), so
|
|
43
|
+
* that branch carries neither.
|
|
44
|
+
* ⚠️ graphite's `proto` (udp or tcp) is deliberately absent: a transport chosen by hand stays put
|
|
45
|
+
* and no plan diffs it. Managing it means adding it HERE, where it is clearable by construction.
|
|
46
|
+
* `otel-resource-attributes` is absent for the reason metric-server-otel.ts gives.
|
|
47
|
+
*/
|
|
48
|
+
export const optional = (props: MetricServerProps): Record<string, string | undefined> => {
|
|
49
|
+
switch (props.type) {
|
|
50
|
+
case 'graphite':
|
|
51
|
+
return { mtu: str(props.mtu), path: props.path, timeout: str(props.timeout) };
|
|
52
|
+
case 'influxdb':
|
|
53
|
+
return {
|
|
54
|
+
'api-path-prefix': props['api-path-prefix'],
|
|
55
|
+
bucket: props.bucket,
|
|
56
|
+
influxdbproto: props.influxdbproto,
|
|
57
|
+
'max-body-size': str(props['max-body-size']),
|
|
58
|
+
mtu: str(props.mtu),
|
|
59
|
+
organization: props.organization,
|
|
60
|
+
timeout: str(props.timeout),
|
|
61
|
+
};
|
|
62
|
+
case 'opentelemetry':
|
|
63
|
+
return {
|
|
64
|
+
'otel-compression': props['otel-compression'],
|
|
65
|
+
'otel-max-body-size': str(props['otel-max-body-size']),
|
|
66
|
+
'otel-path': props['otel-path'],
|
|
67
|
+
'otel-protocol': props['otel-protocol'],
|
|
68
|
+
'otel-timeout': str(props['otel-timeout']),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The certificate-verification flag, for the one plugin that owns it.
|
|
75
|
+
*
|
|
76
|
+
* ⛔ EACH FLAG GOES ONLY TO ITS OWN PLUGIN. `verify-certificate` is influxdb's, `otel-verify-ssl` is
|
|
77
|
+
* opentelemetry's, graphite has neither — and PVE refuses an option outside the section's plugin
|
|
78
|
+
* rather than ignoring it, the same way it refuses `path` on influxdb.
|
|
79
|
+
*/
|
|
80
|
+
const verification = (props: MetricServerProps): Record<string, string> => {
|
|
81
|
+
switch (props.type) {
|
|
82
|
+
case 'graphite':
|
|
83
|
+
return {};
|
|
84
|
+
case 'influxdb':
|
|
85
|
+
return { 'verify-certificate': props['verify-certificate'] === false ? '0' : '1' };
|
|
86
|
+
case 'opentelemetry':
|
|
87
|
+
return { 'otel-verify-ssl': props['otel-verify-ssl'] === false ? '0' : '1' };
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The fields sent on EVERY write, create and update alike.
|
|
93
|
+
*
|
|
94
|
+
* ⚠️ `server` AND `port` ARE REQUIRED BY THE PUT TOO, not only by the POST: a PUT that omits either
|
|
95
|
+
* fails parameter verification rather than leaving them alone. PVE also refuses to clear them —
|
|
96
|
+
* "unable to delete required option" — which is why they are here and not in `optional`.
|
|
97
|
+
* ⚠️ `disable` AND THE VERIFY FLAG ARE SENT WITH THEIR DEFAULTS rather than left out. Writing the
|
|
98
|
+
* value PVE would have assumed makes the read-back match on the first plan after the create,
|
|
99
|
+
* whether or not that PVE materialises defaults into the file.
|
|
100
|
+
*/
|
|
101
|
+
const required = (props: MetricServerProps): Record<string, string> => ({
|
|
102
|
+
disable: props.disable === true ? '1' : '0',
|
|
103
|
+
port: String(props.port),
|
|
104
|
+
server: props.server,
|
|
105
|
+
...verification(props),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
export const body = (props: MetricServerProps) => {
|
|
109
|
+
const fields = required(props);
|
|
110
|
+
for (const [option, value] of Object.entries(optional(props))) {
|
|
111
|
+
if (value !== undefined) fields[option] = value;
|
|
112
|
+
}
|
|
113
|
+
return fields;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* An update's form: everything `body` sends, plus a `delete=` naming each managed optional that is
|
|
118
|
+
* no longer declared.
|
|
119
|
+
*
|
|
120
|
+
* ★ MOVED HERE FROM metric-server.ts UNCHANGED, so the test beside this file can reach the form an
|
|
121
|
+
* update really sends instead of re-deriving it and testing its own copy.
|
|
122
|
+
*/
|
|
123
|
+
export const update = (props: MetricServerProps) => {
|
|
124
|
+
const clear = Object.entries(optional(props))
|
|
125
|
+
.filter(([, value]) => value === undefined)
|
|
126
|
+
.map(([option]) => option);
|
|
127
|
+
return withClears(body(props), clear);
|
|
128
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `opentelemetry` plugin's option set: what a metric server of that type accepts, how it reads
|
|
3
|
+
* back, and when it has changed.
|
|
4
|
+
*
|
|
5
|
+
* ★ SPLIT OUT OF metric-server.ts TO KEEP IT UNDER THE 250-LINE CAP, AND THE SEAM IS THE PLUGIN.
|
|
6
|
+
* PVE validates a section against ONE plugin's option set, so these fields mean nothing to a
|
|
7
|
+
* graphite or influxdb section and theirs mean nothing here. This file answers "what is an OTLP
|
|
8
|
+
* metric server"; metric-server-form.ts still builds every form, so the clear-list keeps coming
|
|
9
|
+
* from the one table that also builds the body.
|
|
10
|
+
*
|
|
11
|
+
* ⛔ PVE'S OTLP EXPORTER SPEAKS JSON AND NOTHING ELSE. `PVE::Status::OpenTelemetry::send` sets
|
|
12
|
+
* `Content-Type: application/json` (pve-manager PVE/Status/OpenTelemetry.pm:643) and gzips by
|
|
13
|
+
* default; there is no protobuf option. vector's opentelemetry source and VictoriaMetrics both
|
|
14
|
+
* take protobuf only — measured 2026-09-14 — so a server pointed straight at either fails every
|
|
15
|
+
* push. That is why TB4's declaration points at each node's own vector bridge
|
|
16
|
+
* (hosts/pve/vector/vector.yaml in homeflare-config) rather than at the estate's intake.
|
|
17
|
+
*
|
|
18
|
+
* ⛔ `otel-headers` IS `never`, FOR THE REASON `token` IS. It is where an OTLP bearer token goes —
|
|
19
|
+
* base64 of a JSON object of arbitrary HTTP headers — and Alchemy persists attributes UNENCRYPTED.
|
|
20
|
+
* It is also absent from `optional` in metric-server-form.ts, so an undeclared one is never
|
|
21
|
+
* cleared off a working target on the next plan.
|
|
22
|
+
*
|
|
23
|
+
* ⚠️ `otel-resource-attributes` IS NOT MANAGED AT ALL: not a prop, not read, not compared, not
|
|
24
|
+
* cleared. It is base64 of a JSON object whose key order nothing canonicalises, so comparing it
|
|
25
|
+
* means decoding both sides — work with no consumer yet, since nothing sets it and PVE already
|
|
26
|
+
* stamps `service.name`, `proxmox.cluster` and `proxmox.node` on every request
|
|
27
|
+
* (`_build_otlp_metrics`, lines 207-228). A hand-set one therefore survives every plan and never
|
|
28
|
+
* shows as drift. The day one matters, it goes into `optional` and here together.
|
|
29
|
+
*/
|
|
30
|
+
import { UNSET } from './metric-server-form.ts';
|
|
31
|
+
import { bool, int, text } from './values.ts';
|
|
32
|
+
|
|
33
|
+
export interface MetricServerOtelProps {
|
|
34
|
+
/**
|
|
35
|
+
* ⚠️ PVE'S DEFAULT IS `https`. `http` is for a receiver on the node itself, where the plaintext
|
|
36
|
+
* never leaves the host; anything further away belongs on `https`.
|
|
37
|
+
*/
|
|
38
|
+
'otel-protocol'?: 'http' | 'https';
|
|
39
|
+
/** Unset is PVE's `/v1/metrics`. */
|
|
40
|
+
'otel-path'?: string;
|
|
41
|
+
/** Unset is PVE's `gzip`, sent as `Content-Encoding: gzip`. */
|
|
42
|
+
'otel-compression'?: 'none' | 'gzip';
|
|
43
|
+
/**
|
|
44
|
+
* Per-request HTTP timeout, 1-10 seconds; unset is PVE's 5.
|
|
45
|
+
* ⚠️ pvestatd WAITS THIS LONG, PER REQUEST, AGAINST A RECEIVER THAT HANGS — the send is inline in
|
|
46
|
+
* its update loop (`send`, line 635) — so this is also how long a broken receiver can stretch
|
|
47
|
+
* one status cycle.
|
|
48
|
+
*/
|
|
49
|
+
'otel-timeout'?: number;
|
|
50
|
+
/** Minimum 1024; unset is PVE's 10000000. pvestatd splits one flush into requests under it. */
|
|
51
|
+
'otel-max-body-size'?: number;
|
|
52
|
+
/** ⚠️ An unset `otel-verify-ssl` means VERIFY — PVE's default, and the safe one. */
|
|
53
|
+
'otel-verify-ssl'?: boolean;
|
|
54
|
+
/** ⛔ `never` ON PURPOSE — see the ⛔ in the header. The compile error is the feature. */
|
|
55
|
+
'otel-headers'?: never;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* ⚠️ THE SAME RULES AS THE REST OF `MetricServerAttributes`. The strings report `''` when the
|
|
60
|
+
* section carries none — PVE's default is then what the target uses — the numbers report `UNSET`
|
|
61
|
+
* for the same reason, and `otel-verify-ssl` reports its default directly, because "absent"
|
|
62
|
+
* there means the target verifies.
|
|
63
|
+
*/
|
|
64
|
+
export interface MetricServerOtelAttributes {
|
|
65
|
+
'otel-compression': string;
|
|
66
|
+
'otel-max-body-size': number;
|
|
67
|
+
'otel-path': string;
|
|
68
|
+
'otel-protocol': string;
|
|
69
|
+
'otel-timeout': number;
|
|
70
|
+
'otel-verify-ssl': boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The `otel-*` half of a live section. Read for every type; compared only for this one. */
|
|
74
|
+
export const otelAttributes = (live: Record<string, unknown>): MetricServerOtelAttributes => ({
|
|
75
|
+
'otel-compression': text(live['otel-compression']),
|
|
76
|
+
'otel-max-body-size': int(live['otel-max-body-size'], UNSET),
|
|
77
|
+
'otel-path': text(live['otel-path']),
|
|
78
|
+
'otel-protocol': text(live['otel-protocol']),
|
|
79
|
+
'otel-timeout': int(live['otel-timeout'], UNSET),
|
|
80
|
+
'otel-verify-ssl': bool(live['otel-verify-ssl'], true),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* True when every managed `otel-*` field already says what the declaration says.
|
|
85
|
+
*
|
|
86
|
+
* ⛔ EVERY FIELD `optional` SENDS OR CLEARS IS COMPARED HERE, AND NOTHING ELSE IS. A field sent but
|
|
87
|
+
* not compared is drift a plan can never show; a field compared but not sent is an update the
|
|
88
|
+
* PUT it triggers can never satisfy, reported forever.
|
|
89
|
+
*/
|
|
90
|
+
export const otelMatches = (
|
|
91
|
+
attributes: MetricServerOtelAttributes,
|
|
92
|
+
props: MetricServerOtelProps,
|
|
93
|
+
): boolean =>
|
|
94
|
+
attributes['otel-compression'] === (props['otel-compression'] ?? '') &&
|
|
95
|
+
attributes['otel-max-body-size'] === (props['otel-max-body-size'] ?? UNSET) &&
|
|
96
|
+
attributes['otel-path'] === (props['otel-path'] ?? '') &&
|
|
97
|
+
attributes['otel-protocol'] === (props['otel-protocol'] ?? '') &&
|
|
98
|
+
attributes['otel-timeout'] === (props['otel-timeout'] ?? UNSET) &&
|
|
99
|
+
attributes['otel-verify-ssl'] === (props['otel-verify-ssl'] !== false);
|