@celilo/terraform-fake
v0.3.1
Published
Fake infrastructure backends you can point a real Terraform provider at. Ships a Proxmox /api2/json fake built from an observed provider lifecycle — including the response types that panic telmate/proxmox if you get them wrong — with provisioning left to
Downloads
572
Maintainers
Readme
@celilo/terraform-fake
A fake Proxmox API you can point a real Terraform provider at.
Not a mock that returns 200 OK. It holds state, so a terraform apply creates
something, a second apply reports no changes, and a destroy removes it —
which is the part that actually catches bugs, because a fake that always says yes
lets a broken plan look like a working one.
bun add -d @celilo/terraform-fake # or npm / pnpmWhy this exists
Testing Terraform against a real hypervisor is slow, expensive, and hard to get into a known state. Testing it against nothing at all is the usual alternative, and it means the provider interaction — the part most likely to break — is the one part never exercised.
The endpoint list here is not a reading of the Proxmox API docs. It is what
telmate/proxmox and one production consumer were observed to call across a
full create → re-apply → destroy cycle, captured by pointing the real provider at
a logging server and implementing whatever it asked for next.
Usage
import { createProxmoxFake } from '@celilo/terraform-fake';
const fake = createProxmoxFake({
tls: { key: myKeyPem, cert: myCertPem }, // self-signed is fine
nodes: [{ name: 'pve1', cores: 8, memoryBytes: 16 * 1024 ** 3, diskBytes: 500 * 1024 ** 3 }],
});
const port = await fake.listen(8006);
// ... run terraform against https://127.0.0.1:8006/api2/json ...
await fake.close();Point Terraform at it:
provider "proxmox" {
pm_api_url = "https://127.0.0.1:8006/api2/json"
pm_api_token_id = "you@pve!testing"
pm_api_token_secret = "any-value"
pm_tls_insecure = true # the cert is self-signed
}The token's user does not need to exist in any config you write — the fake reports whoever your token says you are.
Making the guests real
By default nothing is provisioned: the fake is bookkeeping, which is all you need to exercise a provider. If you want a create to produce an actual machine — a container, a VM, a process — supply a provisioner:
const fake = createProxmoxFake({
tls,
provisioner: {
async createGuest(guest) {
// guest.vmid, guest.hostname, guest.config (the raw create parameters,
// including net0 with the address Terraform asked for)
await startSomethingReal(guest);
},
async destroyGuest(guest) {
await removeIt(guest.vmid);
},
},
});This seam is deliberate. Everything environment-specific — Docker, networks, SSH keys, naming conventions — belongs to you, not to this package.
Asserting on what happened
fake.state is the state store, so a test can check what a plan actually did:
const guest = fake.state.findGuest(203);
expect(guest?.config.net0).toContain('ip=10.0.20.13/24');Things that will bite you if you build this yourself
Each of these was found by a provider crash, and each is handled here.
The config read-back must be re-typed. A create arrives form-encoded, so every value is a string. Echo it back and the provider panics — it type-asserts without the comma-ok form, so a mismatch takes down the plugin rather than producing an error:
panic: interface conversion: interface {} is string, not float64cpulimit is the trap. The obvious repair — coerce anything numeric-looking
— panics the other way, because cpulimit is semantically a number and read as
a string. The exact field list lives in config-types.ts.
/cluster/resources must honour ?type=. The provider fetches ?type=vm
and then reads vmid as a float on every row returned. A node row has no
vmid, so answering an unfiltered list crashes it.
State must outlive a single apply. Restart the fake between applies and its guests vanish, so the provider concludes the resource was deleted and recreates it. Correct provider behaviour; a badly misleading test.
The first call is /access/users. Not /version. A fake that omits it fails
before doing anything, with an error naming neither.
Scope
Supported: the proxmox_lxc lifecycle (create, read, update, delete, start,
stop), cluster resources, nodes, storage, tasks, access and pools.
proxmox_vm_qemu is supported for clone-from-template, which is the only way
a VM gets created in practice: create and destroy are clean, and a re-apply has
two known residual diffs (disk.format and a synthesized startup_shutdown
block) that are provider-schema artifacts rather than read-back errors.
It did have its own type map and its own cpulimit-shaped surprise, exactly as
predicted: agent reads as a STRING even though its value is 1, because
Proxmox packs options into it. Coercing it panics in
config__qemu__guestagent.go the same way not coercing cores panics in
config__qemu__cpu.go.
Two more VM-only behaviours worth knowing if you build this yourself:
- A VM's address is discovered through the guest agent
(
/agent/network-get-interfaces), not fromnet0. The provider polls it after start and blocks until it answers. - Proxmox reads disks back differently than they are written.
local-lvm:20on the way in is an allocation request; on the way out it islocal-lvm:vm-9200-disk-0,size=20G. Echo the request form and the provider reads a 20GB disk as0Tand proposes a change on every plan.
Verified against telmate/[email protected] and Terraform 1.7.1. Newer provider
versions may assert different types — re-extract rather than assume.
License
MIT
