@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,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Proxmox.SdnZone` — the network a declared guest sits on, finally declared too.
|
|
3
|
+
*
|
|
4
|
+
* ★ WHY THIS ONE MATTERS MORE THAN IT LOOKS. Every guest NIC here names a bridge —
|
|
5
|
+
* `net0=name=eth0,bridge=vmbr0` — that nothing in this package declares: a hand-built interface,
|
|
6
|
+
* repeated per node, that a container silently depends on. A zone is the first piece of that
|
|
7
|
+
* dependency PVE will let a plan own.
|
|
8
|
+
*
|
|
9
|
+
* ⛔ SDN WRITES ARE STAGED, NOT APPLIED, AND THAT DEFEATS THE FACTORY'S READ-BACK GUARANTEE.
|
|
10
|
+
* POST and PUT under `cluster/sdn/zones` edit `/etc/pve/sdn/zones.cfg` and change NOTHING on any
|
|
11
|
+
* node. The plain GET reads that same staged file back, so `pveOperations` sees its own write,
|
|
12
|
+
* finds the object present, and reports a success in which no node gained an interface. The
|
|
13
|
+
* applied state is a different document — `.running-config` beside it — and `?pending=1` is the
|
|
14
|
+
* read that tells them apart: it returns a merged view with a `state` of new/changed/deleted.
|
|
15
|
+
*
|
|
16
|
+
* ⛔ THE APPLY CANNOT LIVE IN THIS RECONCILE, so it is absent rather than forgotten.
|
|
17
|
+
* `PUT /cluster/sdn` is the atomic pending-to-apply and it applies the WHOLE staged config —
|
|
18
|
+
* every zone, vnet and subnet, on every node. Called from a per-resource reconcile it would push
|
|
19
|
+
* some OTHER resource's half-written zone out to the cluster at whatever point the plan ordering
|
|
20
|
+
* reached this one, and PVE has no pre-apply hook to veto that. The apply belongs to ONE object
|
|
21
|
+
* that depends on all of them — a sibling `Proxmox.SdnApply` over `cluster/sdn`, or a stack-level
|
|
22
|
+
* hook that runs last — so that "declared" and "live on the nodes" stay two visible steps.
|
|
23
|
+
*
|
|
24
|
+
* ⚠️ SO THESE ATTRIBUTES DESCRIBE THE STAGED ZONE, and there is deliberately no `applied` flag:
|
|
25
|
+
* the factory uses ONE path for GET, PUT and DELETE, and PVE's parameter schemas are closed, so
|
|
26
|
+
* an unexpected `pending` on the PUT is a 400 rather than an ignored hint. Reading `?pending=1`
|
|
27
|
+
* means re-implementing the operations this file exists to share, and an `applied: true` that
|
|
28
|
+
* was never measured is worse than an absent one. Deploying this resource does not change the
|
|
29
|
+
* network; it changes what the next apply will do.
|
|
30
|
+
*
|
|
31
|
+
* ⛔ THE READ THIS RESOURCE PERFORMS NEEDS SDN.Allocate, NOT SDN.Audit, AND AN EARLIER DRAFT OF
|
|
32
|
+
* THIS COMMENT SAID OTHERWISE. Read off the cluster's own schema on 2026-09-13:
|
|
33
|
+
*
|
|
34
|
+
* GET /cluster/sdn/zones -> "list entries where you have SDN.Audit or SDN.Allocate"
|
|
35
|
+
* GET /cluster/sdn/zones/{zone} -> {"check": ["perm", "/sdn/zones/{zone}", ["SDN.Allocate"]]}
|
|
36
|
+
*
|
|
37
|
+
* `pveOperations.read` calls the ITEM endpoint, so an auditor-shaped `read` lease is REFUSED —
|
|
38
|
+
* and the refusal does not surface as a 403. `read` folds every failure into `undefined`, which
|
|
39
|
+
* the factory reads as "absent", so the plan says create, the POST goes out, and PVE answers
|
|
40
|
+
* that the zone already exists. This is the same class as Proxmox.Storage; see
|
|
41
|
+
* docs/privileges.md, "The failure that does not look like a permission problem".
|
|
42
|
+
*
|
|
43
|
+
* ⚠️ THE REST OF THE PRIVILEGES. Writes need SDN.Allocate (`/sdn/zones` to create,
|
|
44
|
+
* `/sdn/zones/{zone}` to update or delete); the apply — `Proxmox.SdnApply`, which is what
|
|
45
|
+
* actually publishes any of this — needs SDN.Allocate on `/sdn` itself. SDN.Use is NOT among
|
|
46
|
+
* them: it permits attaching a guest to an existing vnet and nothing else.
|
|
47
|
+
*/
|
|
48
|
+
import { Resource } from 'alchemy';
|
|
49
|
+
import * as Provider from 'alchemy/Provider';
|
|
50
|
+
import * as Effect from 'effect/Effect';
|
|
51
|
+
import { type PveRequirements, type WithTarget, pveHandlers } from './resource.ts';
|
|
52
|
+
import { csv, int, text, withClears } from './values.ts';
|
|
53
|
+
|
|
54
|
+
/** PVE's zone plugins. Which one a zone is decides which fields below it will even accept. */
|
|
55
|
+
export type SdnZoneType = 'evpn' | 'qinq' | 'simple' | 'vlan' | 'vxlan';
|
|
56
|
+
|
|
57
|
+
export interface SdnZoneProps extends WithTarget {
|
|
58
|
+
/**
|
|
59
|
+
* PVE's primary key. ⚠️ AT MOST EIGHT LOWERCASE ALPHANUMERICS, first one a letter: the id lands
|
|
60
|
+
* in generated interface names, so PVE enforces it with a regex whose rejection reads as
|
|
61
|
+
* "value does not match the regex pattern" rather than as "your name is too long".
|
|
62
|
+
*/
|
|
63
|
+
zone: string;
|
|
64
|
+
/**
|
|
65
|
+
* ⛔ CREATE-TIME ONLY. `PUT /cluster/sdn/zones/{zone}` has no `type` parameter at all — a
|
|
66
|
+
* section's type is fixed when it is written. Changing it here is not an update PVE can
|
|
67
|
+
* perform; the ⛔ in `attributes` turns that edit into a refusal instead of a silent no-op.
|
|
68
|
+
*/
|
|
69
|
+
type: SdnZoneType;
|
|
70
|
+
/** Accepted by every zone type. Unset means the zone inherits the underlying bridge's MTU. */
|
|
71
|
+
mtu?: number;
|
|
72
|
+
/** ⚠️ WHICH NODES THE ZONE IS DEPLOYED TO. Unset means EVERY node, not none. */
|
|
73
|
+
nodes?: string[];
|
|
74
|
+
/** IPAM plugin id. Unset is the built-in `pve` one — the two are one zone, see `sameIpam`. */
|
|
75
|
+
ipam?: string;
|
|
76
|
+
/** DNS plugin id used to register guest records. */
|
|
77
|
+
dns?: string;
|
|
78
|
+
/** The domain those records are registered under, e.g. `example.internal`. */
|
|
79
|
+
dnszone?: string;
|
|
80
|
+
/** vxlan only, and REQUIRED there: the peer addresses the tunnel mesh is built from. */
|
|
81
|
+
peers?: string[];
|
|
82
|
+
/** evpn only, and required there. PVE's own hyphenated name, so no mapping to get wrong. */
|
|
83
|
+
'vrf-vxlan'?: number;
|
|
84
|
+
/** qinq only, and required there: the service VLAN tag the zone's vnets are stacked inside. */
|
|
85
|
+
tag?: number;
|
|
86
|
+
/** vlan and qinq only, and required in both: the existing bridge the zone is carved out of. */
|
|
87
|
+
bridge?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* ⛔ NO `digest` HERE, ON PURPOSE. PVE returns one, but it is the digest of the WHOLE zones.cfg
|
|
92
|
+
* file rather than of this section — declaring a SECOND zone would rewrite this one's stored
|
|
93
|
+
* attributes, and comparing it would report an update on a zone nobody touched.
|
|
94
|
+
*/
|
|
95
|
+
export interface SdnZoneAttributes {
|
|
96
|
+
zone: string;
|
|
97
|
+
type: SdnZoneType;
|
|
98
|
+
/** 0 when unset, which is PVE's "inherit from the bridge" rather than an MTU of zero. */
|
|
99
|
+
mtu: number;
|
|
100
|
+
/** Normalised: split, sorted, rejoined. Never compare a raw PVE string to it — see `csv`. */
|
|
101
|
+
nodes: string;
|
|
102
|
+
/** The EFFECTIVE ipam: an absent one is reported as `pve`, because that is what will be used. */
|
|
103
|
+
ipam: string;
|
|
104
|
+
dns: string;
|
|
105
|
+
dnszone: string;
|
|
106
|
+
peers: string;
|
|
107
|
+
'vrf-vxlan': number;
|
|
108
|
+
tag: number;
|
|
109
|
+
bridge: string;
|
|
110
|
+
/** ⚠️ THE REMINDER IN THE STATE ITSELF: staged config. No value here says a node has the zone. */
|
|
111
|
+
readonly staged: true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface ProxmoxSdnZone extends Resource<
|
|
115
|
+
'Proxmox.SdnZone',
|
|
116
|
+
SdnZoneProps,
|
|
117
|
+
SdnZoneAttributes,
|
|
118
|
+
never,
|
|
119
|
+
PveRequirements
|
|
120
|
+
> {}
|
|
121
|
+
|
|
122
|
+
export const ProxmoxSdnZone = Resource<ProxmoxSdnZone>('Proxmox.SdnZone');
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* ⚠️ AN ABSENT `ipam` AND `ipam pve` ARE THE SAME ZONE. `pve` is the built-in default, and whether
|
|
126
|
+
* PVE materialises that word into zones.cfg on create is version-dependent — so comparing the
|
|
127
|
+
* raw values reports an update on the plan immediately AFTER the create, on a zone this provider
|
|
128
|
+
* just wrote itself. Normalising both sides is loop-proof under either behaviour.
|
|
129
|
+
*/
|
|
130
|
+
const sameIpam = (live: string, declared: string | undefined) =>
|
|
131
|
+
(live === '' ? 'pve' : live) === (declared === undefined || declared === '' ? 'pve' : declared);
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The mutable fields, spread in only when defined.
|
|
135
|
+
*
|
|
136
|
+
* ⚠️ AN EMPTY STRING IS NOT "UNSET" FOR AN INTEGER FIELD. `mtu=` fails PVE's parameter check
|
|
137
|
+
* outright instead of clearing the value, so the pool.ts habit of sending `''` for an absent
|
|
138
|
+
* optional does not carry over here. Clearing is a separate verb — see `clearable`.
|
|
139
|
+
*/
|
|
140
|
+
const body = (props: SdnZoneProps): Record<string, string> => ({
|
|
141
|
+
...(props.bridge === undefined ? {} : { bridge: props.bridge }),
|
|
142
|
+
...(props.dns === undefined ? {} : { dns: props.dns }),
|
|
143
|
+
...(props.dnszone === undefined ? {} : { dnszone: props.dnszone }),
|
|
144
|
+
...(props.ipam === undefined ? {} : { ipam: props.ipam }),
|
|
145
|
+
...(props.mtu === undefined ? {} : { mtu: String(props.mtu) }),
|
|
146
|
+
...(props.nodes === undefined ? {} : { nodes: csv(props.nodes) }),
|
|
147
|
+
...(props.peers === undefined ? {} : { peers: csv(props.peers) }),
|
|
148
|
+
...(props.tag === undefined ? {} : { tag: String(props.tag) }),
|
|
149
|
+
...(props['vrf-vxlan'] === undefined ? {} : { 'vrf-vxlan': String(props['vrf-vxlan']) }),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Which managed fields PVE will let `delete=` clear, per zone type.
|
|
154
|
+
*
|
|
155
|
+
* ⛔ A PUT THAT SIMPLY OMITS A FIELD DOES NOT CLEAR IT. PVE merges the form into the existing
|
|
156
|
+
* section, so dropping `mtu` from a declaration leaves the old MTU in place and `matches`
|
|
157
|
+
* reports an update on every plan forever — the exact bug this table exists to prevent.
|
|
158
|
+
* ⛔ AND THE LIST CANNOT BE THE SAME FOR EVERY TYPE. `delete=<opt>` dies with "no such option"
|
|
159
|
+
* when the option is not in that plugin's schema (`peers` on a simple zone) and with "unable to
|
|
160
|
+
* delete required option" when it is required there. `bridge`, `tag`, `peers` and `vrf-vxlan`
|
|
161
|
+
* are required by the only types that accept them, so none of the four is ever clearable: a
|
|
162
|
+
* zone that lost one is a replace, not an update.
|
|
163
|
+
* ⚠️ `ipam` IS OMITTED FOR evpn DELIBERATELY. It is optional on the other four; on evpn its
|
|
164
|
+
* optionality has moved between releases, and a wrong entry here is a hard 400 on EVERY update
|
|
165
|
+
* rather than a cosmetic diff. Dropping `ipam` from an evpn declaration therefore reports an
|
|
166
|
+
* update that does not converge — visible, unlike a wrong delete that breaks the zone.
|
|
167
|
+
*/
|
|
168
|
+
const clearable: Record<SdnZoneType, readonly string[]> = {
|
|
169
|
+
evpn: ['dns', 'dnszone', 'mtu', 'nodes'],
|
|
170
|
+
qinq: ['dns', 'dnszone', 'ipam', 'mtu', 'nodes'],
|
|
171
|
+
simple: ['dns', 'dnszone', 'ipam', 'mtu', 'nodes'],
|
|
172
|
+
vlan: ['dns', 'dnszone', 'ipam', 'mtu', 'nodes'],
|
|
173
|
+
vxlan: ['dns', 'dnszone', 'ipam', 'mtu', 'nodes'],
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const handlers = pveHandlers<SdnZoneProps, SdnZoneAttributes>({
|
|
177
|
+
// ⛔ PVE gates this family's ITEM read on the allocate privilege, not the audit one — see
|
|
178
|
+
// `readRole` in resource.ts. The auditor-shaped lease reads 403, which `read` turns into
|
|
179
|
+
// "absent", and the plan then says create for an object that is plainly there.
|
|
180
|
+
readRole: 'provision',
|
|
181
|
+
attributes: (live, props) => {
|
|
182
|
+
/**
|
|
183
|
+
* ⛔ A ZONE OF ANOTHER TYPE IS ANOTHER OBJECT, AND "ABSENT" IS THE HONEST ANSWER. A PUT cannot
|
|
184
|
+
* change `type`, and the factory only calls a change `replace` for a resource with no update
|
|
185
|
+
* path — which this is not. Reporting the foreign zone as missing makes reconcile POST, and
|
|
186
|
+
* PVE refuses with "sdn zone ID 'x' already defined": a loud, accurate error instead of a
|
|
187
|
+
* silent noop over somebody else's zone. An older PVE that omits `type` falls through.
|
|
188
|
+
*/
|
|
189
|
+
const liveType = text(live['type']);
|
|
190
|
+
if (liveType !== '' && liveType !== props.type) return undefined;
|
|
191
|
+
return {
|
|
192
|
+
bridge: text(live['bridge']),
|
|
193
|
+
dns: text(live['dns']),
|
|
194
|
+
dnszone: text(live['dnszone']),
|
|
195
|
+
ipam: text(live['ipam']) === '' ? 'pve' : text(live['ipam']),
|
|
196
|
+
mtu: int(live['mtu'], 0),
|
|
197
|
+
nodes: csv(text(live['nodes'])),
|
|
198
|
+
peers: csv(text(live['peers'])),
|
|
199
|
+
staged: true,
|
|
200
|
+
tag: int(live['tag'], 0),
|
|
201
|
+
type: props.type,
|
|
202
|
+
'vrf-vxlan': int(live['vrf-vxlan'], 0),
|
|
203
|
+
zone: props.zone,
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
collection: () => 'cluster/sdn/zones',
|
|
207
|
+
createForm: (props) => ({ ...body(props), type: props.type, zone: props.zone }),
|
|
208
|
+
/**
|
|
209
|
+
* ⚠️ `type` AND `zone` ARE NOT COMPARED. `zone` is the key the object was read by, and `type` is
|
|
210
|
+
* handled above by refusing to recognise a zone of the wrong one. Comparing either here would
|
|
211
|
+
* produce a diff whose update can never satisfy it.
|
|
212
|
+
*/
|
|
213
|
+
matches: (attributes, props) =>
|
|
214
|
+
attributes.mtu === (props.mtu ?? 0) &&
|
|
215
|
+
attributes.nodes === csv(props.nodes) &&
|
|
216
|
+
sameIpam(attributes.ipam, props.ipam) &&
|
|
217
|
+
attributes.dns === (props.dns ?? '') &&
|
|
218
|
+
attributes.dnszone === (props.dnszone ?? '') &&
|
|
219
|
+
attributes.peers === csv(props.peers) &&
|
|
220
|
+
attributes['vrf-vxlan'] === (props['vrf-vxlan'] ?? 0) &&
|
|
221
|
+
attributes.tag === (props.tag ?? 0) &&
|
|
222
|
+
attributes.bridge === (props.bridge ?? ''),
|
|
223
|
+
path: (props) => `cluster/sdn/zones/${props.zone}`,
|
|
224
|
+
updateForm: (props) => {
|
|
225
|
+
const fields = body(props);
|
|
226
|
+
const clear = clearable[props.type].filter((option) => fields[option] === undefined);
|
|
227
|
+
return withClears(fields, clear);
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* ⛔ Empty for the same reason as every other resource here, and with extra force for SDN:
|
|
233
|
+
* `GET /cluster/sdn/zones` answers with the zones the cluster's whole network already
|
|
234
|
+
* runs on. Adopting those would make a later `alchemy destroy` a cluster-wide outage.
|
|
235
|
+
|
|
236
|
+
*
|
|
237
|
+
* ⛔ A DELETE IS STAGED TOO, AND THAT IS THE HALF PEOPLE FORGET. The section leaves
|
|
238
|
+
* zones.cfg and Alchemy drops the resource, but the zone KEEPS RUNNING on every node
|
|
239
|
+
* until something applies `PUT /cluster/sdn`: a destroy nobody applies reads as complete
|
|
240
|
+
* in the plan and has not happened on the cluster.
|
|
241
|
+
* ⚠️ PVE also refuses to remove a zone that still holds vnets. That refusal is kept — it is
|
|
242
|
+
* the cluster declining to orphan a network because a line left a file.
|
|
243
|
+
|
|
244
|
+
*/
|
|
245
|
+
export const ProxmoxSdnZoneProvider = () =>
|
|
246
|
+
Provider.effect(ProxmoxSdnZone, Effect.succeed(ProxmoxSdnZone.Provider.of(handlers)));
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Proxmox.Storage` — one entry in `/etc/pve/storage.cfg`, declared. Cluster-scoped like a pool,
|
|
3
|
+
* which is why `path` and `collection` carry no node segment though a storage is used per node.
|
|
4
|
+
*
|
|
5
|
+
* ★ THIS IS THE OBJECT EVERY CONTAINER ALREADY DEPENDS ON AND NOTHING DECLARES. `LxcProps.storage`
|
|
6
|
+
* and the prefix of `LxcProps.ostemplate` (`local:vztmpl/...`) are bare strings, so a container's
|
|
7
|
+
* rootfs points at something outside the graph: rebuild a node and the plan still reads `noop`
|
|
8
|
+
* right up to the create that fails because the storage is not there. Passing this resource's
|
|
9
|
+
* `storage` attribute into `LxcProps.storage` makes the dependency real — Alchemy orders by data
|
|
10
|
+
* flow, so the storage reconciles first because the container reads a value out of it.
|
|
11
|
+
*
|
|
12
|
+
* ⛔ ONE PRIVILEGE COVERS THIS WHOLE FAMILY AND IT IS `Datastore.Allocate` ON `/storage`. Create,
|
|
13
|
+
* update, delete AND the single-object read all check it — `Datastore.Audit` is not enough for
|
|
14
|
+
* any of them. MEASURED: `GET /storage/local` under the auditor-scoped `read` lease answers 403
|
|
15
|
+
* "Permission check failed (/storage/local, Datastore.Allocate)". `pveOperations.read` folds
|
|
16
|
+
* every failure into `undefined`, i.e. "absent", because a 404 is a legitimate answer there and
|
|
17
|
+
* it cannot tell the two apart — so under that lease this resource NEVER CONVERGES, in two
|
|
18
|
+
* directions: reconcile reads "absent" and POSTs a create over a storage that is already defined,
|
|
19
|
+
* and where a create does land the read-back is 403 too, so reconcile dies with "the write
|
|
20
|
+
* returned no error but the object is still absent" about an object it has just built. Both
|
|
21
|
+
* messages point away from the cause.
|
|
22
|
+
* ★ SO WIDEN OR RE-POINT BEFORE THE FIRST PLAN, outside this file: give the credential mount's
|
|
23
|
+
* `read` role `Datastore.Allocate` on `/storage`, or read this family with `provision`. The
|
|
24
|
+
* provision role in the estate this was written for holds `Datastore.AllocateSpace` and
|
|
25
|
+
* `Datastore.Audit`, and NEITHER IS IT — AllocateSpace writes volumes INTO a storage, Allocate
|
|
26
|
+
* DEFINES one — so it 403s here until widened the way `Pool.Allocate` was for `Proxmox.Pool`.
|
|
27
|
+
*
|
|
28
|
+
* ⛔ THERE IS NO `password` PROP AND THERE MUST NEVER BE ONE. PBS and CIFS storages take one at
|
|
29
|
+
* create time, and Alchemy persists attributes UNENCRYPTED — a secret in props is one careless
|
|
30
|
+
* line away from being an attribute, and from there it is in the state store's nightly dump. So
|
|
31
|
+
* `StorageLocator` types `password`, `keyring` and `encryption-key` as `never`: declaring one is
|
|
32
|
+
* a COMPILE ERROR rather than a leak somebody finds in a backup months later. Set it once out of
|
|
33
|
+
* band (`pvesm set <id> --password`); PVE never returns it on read, so nothing here could diff it
|
|
34
|
+
* anyway.
|
|
35
|
+
*/
|
|
36
|
+
import { Resource } from 'alchemy';
|
|
37
|
+
import * as Provider from 'alchemy/Provider';
|
|
38
|
+
import * as Effect from 'effect/Effect';
|
|
39
|
+
import { type PveRequirements, type WithTarget, pveHandlers } from './resource.ts';
|
|
40
|
+
import { bool, flag } from './values.ts';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The create-time, plugin-specific half of a storage: `dir` wants `path`, `nfs` `server`+`export`,
|
|
44
|
+
* `cifs` `server`+`share`, `pbs` `server`+`datastore`, `rbd` `pool`+`monhost`, `lvmthin`
|
|
45
|
+
* `vgname`+`thinpool`, `zfspool` `pool`. A bag rather than two dozen named fields, because PVE
|
|
46
|
+
* ships about that many plugins and each brings its own locator. ⚠️ Never diffed — see `matches`.
|
|
47
|
+
*
|
|
48
|
+
* ⛔ `password`, `keyring` AND `encryption-key` ARE `never` ON PURPOSE — see the ⛔ in the header.
|
|
49
|
+
* POSITIVE-CONTROLLED rather than assumed: all three are a compile error, `TS2322: Type 'string'
|
|
50
|
+
* is not assignable to type 'undefined'`.
|
|
51
|
+
*/
|
|
52
|
+
export type StorageLocator = Record<string, string> & {
|
|
53
|
+
password?: never;
|
|
54
|
+
keyring?: never;
|
|
55
|
+
'encryption-key'?: never;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export interface StorageProps extends WithTarget {
|
|
59
|
+
/** PVE's primary key, cluster-wide — the string an LXC's `storage` and `ostemplate` name. */
|
|
60
|
+
storage: string;
|
|
61
|
+
/**
|
|
62
|
+
* `dir` | `nfs` | `cifs` | `pbs` | `zfspool` | `lvmthin` | `rbd` | … ⚠️ CREATE-ONLY, so a changed
|
|
63
|
+
* `type` plans as `noop` (see `matches`). PVE cannot retype a storage: remove and redeclare.
|
|
64
|
+
*/
|
|
65
|
+
type: string;
|
|
66
|
+
/** ⚠️ Create-only, one shape per plugin. See `StorageLocator`. */
|
|
67
|
+
locator?: StorageLocator;
|
|
68
|
+
/** `images,rootdir,vztmpl,iso,backup,snippets`. PVE reports `none` for an empty set. */
|
|
69
|
+
content?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Node restriction, e.g. `n1,n2`. ⚠️ A STORAGE WITH A `nodes` LIST DOES NOT EXIST ANYWHERE ELSE:
|
|
72
|
+
* an LXC declared on a node outside it fails at create with "storage not available on node".
|
|
73
|
+
*/
|
|
74
|
+
nodes?: string;
|
|
75
|
+
/** Keeps the definition but stops PVE using it. */
|
|
76
|
+
disable?: boolean;
|
|
77
|
+
/** Tells PVE the same volumes are visible from every node — a claim, not a mechanism. */
|
|
78
|
+
shared?: boolean;
|
|
79
|
+
/** `off` | `metadata` | `falloc` | `full`. File-based plugins only. */
|
|
80
|
+
preallocation?: string;
|
|
81
|
+
/**
|
|
82
|
+
* `keep-last=3,keep-daily=7,…`. ⚠️ THE WIRE NAME IS KEPT, HYPHEN AND ALL: a `pruneBackups` that
|
|
83
|
+
* quietly becomes `prune-backups` in the form body is a second name for one thing.
|
|
84
|
+
*/
|
|
85
|
+
'prune-backups'?: string;
|
|
86
|
+
comment?: string;
|
|
87
|
+
/**
|
|
88
|
+
* ⚠️ THE DEPRECATED PREDECESSOR OF `prune-backups`, and the two do not mix — declare one or the
|
|
89
|
+
* other. `0` means UNLIMITED to PVE rather than "unset", hence the `-1` in the attribute.
|
|
90
|
+
*/
|
|
91
|
+
maxfiles?: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface StorageAttributes {
|
|
95
|
+
storage: string;
|
|
96
|
+
/** Reported so a plan can show what it points at; never diffed — see `type` above. */
|
|
97
|
+
type: string;
|
|
98
|
+
content: string;
|
|
99
|
+
nodes: string;
|
|
100
|
+
disable: boolean;
|
|
101
|
+
shared: boolean;
|
|
102
|
+
preallocation: string;
|
|
103
|
+
'prune-backups': string;
|
|
104
|
+
comment: string;
|
|
105
|
+
/** ⚠️ `-1` means "no maxfiles in storage.cfg". `0` is a real setting meaning unlimited. */
|
|
106
|
+
maxfiles: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface ProxmoxStorage extends Resource<
|
|
110
|
+
'Proxmox.Storage',
|
|
111
|
+
StorageProps,
|
|
112
|
+
StorageAttributes,
|
|
113
|
+
never,
|
|
114
|
+
PveRequirements
|
|
115
|
+
> {}
|
|
116
|
+
|
|
117
|
+
/** ★ `retain` by default — a storage holding volumes cannot be rebuilt. See the ★ in resource.ts. */
|
|
118
|
+
export const ProxmoxStorage = Resource<ProxmoxStorage>('Proxmox.Storage', {
|
|
119
|
+
defaultRemovalPolicy: 'retain',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const str = (value: unknown, fallback = '') => (typeof value === 'string' ? value : fallback);
|
|
123
|
+
|
|
124
|
+
const field = (name: string, value: string | undefined): Record<string, string> =>
|
|
125
|
+
value === undefined ? {} : { [name]: value };
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* ⚠️ THE CAST DROPS THE THREE `never` GUARDS AND NOTHING ELSE, and it is load-bearing rather than
|
|
129
|
+
* lazy: an intersection carrying OPTIONAL properties is not assignable to `Record<string,string>`
|
|
130
|
+
* at all, so the guards and the form body cannot both exist without one conversion. At runtime a
|
|
131
|
+
* locator is a plain string map; the guards only ever existed to fail a declaration.
|
|
132
|
+
*/
|
|
133
|
+
const locatorForm = (locator: StorageLocator | undefined) =>
|
|
134
|
+
({ ...locator }) as Record<string, string>;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* ⚠️ PVE RE-SERIALISES ITS LIST FIELDS IN ITS OWN ORDER — `content` and `nodes` are parsed into a
|
|
138
|
+
* set and written back sorted, `prune-backups` is a property string that comes back in schema
|
|
139
|
+
* order. Comparing raw strings would report an update for `iso,backup` against `backup,iso` on
|
|
140
|
+
* every plan, and the PUT would "fix" it into the identical string. Trimming matters too: a
|
|
141
|
+
* declared `iso, backup` would otherwise never equal what PVE stores.
|
|
142
|
+
*/
|
|
143
|
+
const tokens = (value: string) =>
|
|
144
|
+
value
|
|
145
|
+
.split(',')
|
|
146
|
+
.map((part) => part.trim())
|
|
147
|
+
.filter((part) => part !== '')
|
|
148
|
+
.sort()
|
|
149
|
+
.join(',');
|
|
150
|
+
|
|
151
|
+
const sameList = (declared: string | undefined, live: string) =>
|
|
152
|
+
declared === undefined || tokens(declared) === tokens(live);
|
|
153
|
+
|
|
154
|
+
const same = <T>(declared: T | undefined, live: T) => declared === undefined || declared === live;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The mutable half, for create and update alike.
|
|
158
|
+
*
|
|
159
|
+
* ⚠️ AN UNDECLARED FIELD IS NEITHER SENT NOR COMPARED: undeclared means UNMANAGED here, unlike
|
|
160
|
+
* `Proxmox.Pool` where an absent comment means `''`. A storage has no single default to fall back
|
|
161
|
+
* on — `content` defaults per plugin, and clearing a field needs an explicit `delete=` parameter
|
|
162
|
+
* rather than an empty value — so a guessed default would rewrite a storage somebody tuned by
|
|
163
|
+
* hand, and comparing one would report drift nobody declared.
|
|
164
|
+
*
|
|
165
|
+
* ⚠️ RECONCILE PUTs UNCONDITIONALLY whenever the object exists (resource.ts), so this form must be
|
|
166
|
+
* safe to re-apply to a storage that already matches. It is: the declared set and nothing else,
|
|
167
|
+
* and PVE leaves absent parameters alone.
|
|
168
|
+
*/
|
|
169
|
+
const mutable = (props: StorageProps): Record<string, string> => ({
|
|
170
|
+
...field('comment', props.comment),
|
|
171
|
+
...field('content', props.content),
|
|
172
|
+
...field('disable', flag(props.disable)),
|
|
173
|
+
...field('maxfiles', props.maxfiles === undefined ? undefined : String(props.maxfiles)),
|
|
174
|
+
...field('nodes', props.nodes),
|
|
175
|
+
...field('preallocation', props.preallocation),
|
|
176
|
+
...field('prune-backups', props['prune-backups']),
|
|
177
|
+
...field('shared', flag(props.shared)),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const handlers = pveHandlers<StorageProps, StorageAttributes>({
|
|
181
|
+
// ⛔ PVE gates this family's ITEM read on the allocate privilege, not the audit one — see
|
|
182
|
+
// `readRole` in resource.ts. The auditor-shaped lease reads 403, which `read` turns into
|
|
183
|
+
// "absent", and the plan then says create for an object that is plainly there.
|
|
184
|
+
readRole: 'provision',
|
|
185
|
+
/**
|
|
186
|
+
* ⚠️ `digest` IS DELIBERATELY NOT AN ATTRIBUTE. PVE returns one on every storage read and it
|
|
187
|
+
* covers storage.cfg as a FILE, not this section of it, so keeping it would rewrite this
|
|
188
|
+
* resource's state whenever an unrelated storage was edited — churn that reads like drift.
|
|
189
|
+
*/
|
|
190
|
+
attributes: (live, props) => ({
|
|
191
|
+
comment: str(live['comment']),
|
|
192
|
+
content: str(live['content']),
|
|
193
|
+
disable: bool(live['disable']),
|
|
194
|
+
maxfiles: typeof live['maxfiles'] === 'number' ? live['maxfiles'] : -1,
|
|
195
|
+
nodes: str(live['nodes']),
|
|
196
|
+
preallocation: str(live['preallocation']),
|
|
197
|
+
'prune-backups': str(live['prune-backups']),
|
|
198
|
+
shared: bool(live['shared']),
|
|
199
|
+
storage: props.storage,
|
|
200
|
+
type: str(live['type'], props.type),
|
|
201
|
+
}),
|
|
202
|
+
collection: () => 'storage',
|
|
203
|
+
/**
|
|
204
|
+
* ⚠️ THE BAG IS SPREAD FIRST SO NOTHING IN IT CAN SHADOW A FIELD THIS RESOURCE MANAGES. A stray
|
|
205
|
+
* `storage` or `type` in a locator would otherwise rename the object being created, and PVE
|
|
206
|
+
* would build the wrong thing under a name Alchemy then records as the declared one.
|
|
207
|
+
*/
|
|
208
|
+
createForm: (props) => ({
|
|
209
|
+
...locatorForm(props.locator),
|
|
210
|
+
...mutable(props),
|
|
211
|
+
storage: props.storage,
|
|
212
|
+
type: props.type,
|
|
213
|
+
}),
|
|
214
|
+
/**
|
|
215
|
+
* ⚠️ `type` AND THE LOCATOR ARE NEVER COMPARED, AND AN UNDECLARED FIELD IS NOT COMPARED EITHER.
|
|
216
|
+
* PVE returns the create-only fields on read but refuses them on PUT, so diffing one could only
|
|
217
|
+
* plan an update that no write can apply: a plan that reports work on every run, forever.
|
|
218
|
+
*/
|
|
219
|
+
matches: (attributes, props) =>
|
|
220
|
+
sameList(props.content, attributes.content) &&
|
|
221
|
+
sameList(props.nodes, attributes.nodes) &&
|
|
222
|
+
sameList(props['prune-backups'], attributes['prune-backups']) &&
|
|
223
|
+
same(props.disable, attributes.disable) &&
|
|
224
|
+
same(props.shared, attributes.shared) &&
|
|
225
|
+
same(props.preallocation, attributes.preallocation) &&
|
|
226
|
+
same(props.comment, attributes.comment) &&
|
|
227
|
+
same(props.maxfiles, attributes.maxfiles),
|
|
228
|
+
path: (props) => `storage/${props.storage}`,
|
|
229
|
+
updateForm: mutable,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* ⛔ EMPTY, AND HERE IT MATTERS MORE THAN ANYWHERE ELSE IN THIS PACKAGE. `GET /storage`
|
|
234
|
+
* returns every definition on the cluster, `local` and `local-lvm`/`local-zfs` included:
|
|
235
|
+
* the installer's own, that every guest's rootfs sits on. Adopting those would put
|
|
236
|
+
* Alchemy one `delete` away from the cluster's disks. Adoption is explicit, always.
|
|
237
|
+
|
|
238
|
+
*
|
|
239
|
+
* ⚠️ THIS REMOVES THE DEFINITION, NOT THE DATA. PVE drops the section from storage.cfg and
|
|
240
|
+
* leaves the volumes on disk untouched — so the loss is not the bytes, it is that guests
|
|
241
|
+
* whose config still names this storage can no longer resolve their own disks.
|
|
242
|
+
|
|
243
|
+
*/
|
|
244
|
+
export const ProxmoxStorageProvider = () =>
|
|
245
|
+
Provider.effect(ProxmoxStorage, Effect.succeed(ProxmoxStorage.Provider.of(handlers)));
|