import { describe, expect, test } from 'bun:test'; /** * The provisioner's logic, with `docker` stubbed — no daemon involved. * * What is worth testing here is everything BEFORE the shell-out: which zone * network a guest lands on, that the container is named so the cleanup sweep * can find it, and that readiness is waited on rather than assumed. */ import type { GuestRecord } from '@celilo/terraform-fake'; import { CONTAINER_PREFIX, GUEST_PROJECT_LABEL, containerNameFor, createDockerProvisioner, parseNet, zoneForGateway, } from './proxmox-provisioner'; import { ZONE_GATEWAYS, zoneIp } from './types'; /** * Addresses are DERIVED from the zone plan, never written out. A literal here * pins an address to a zone role, and the two drift the moment the plan moves: * an earlier draft of this file paired dmz's gateway with an address from the * app range, and every assertion still passed because they were only ever * compared against each other. */ const DMZ_ADDRESS = zoneIp('dmz', 13); const NET0 = `name=eth0,bridge=vmbr0,gw=${ZONE_GATEWAYS.dmz},ip=${DMZ_ADDRESS}/24,tag=20`; const guest = (overrides: Record = {}): GuestRecord => ({ vmid: 203, node: 'pve1', kind: 'lxc', hostname: 'test-host', status: 'running', config: { net0: NET0, ...overrides }, }); /** Records argv instead of running docker; replies `active` to the readiness poll. */ function stubDocker() { const calls: string[][] = []; const runner = (args: string[]): string => { calls.push(args); return args.includes('is-active') ? 'active\n' : ''; }; return { calls, runner, find: (verb: string) => calls.find((c) => c[0] === verb) }; } const provisionerWith = (docker: (args: string[]) => string) => createDockerProvisioner({ project: 'celilo-e2e-test', docker, sleep: async () => {} }); describe('createGuest', () => { test('runs the container on the zone network at the address terraform was given', async () => { const { runner, find } = stubDocker(); await provisionerWith(runner).createGuest(guest()); const run = find('run') ?? []; expect(run).toContain('celilo-e2e-test_dmz'); expect(run[run.indexOf('--ip') + 1]).toBe(DMZ_ADDRESS); expect(run.at(-1)).toBe('celilo-e2e/target-machine'); }); test('names the container so the debris sweep can find it', async () => { // cele2e doctor keys off this prefix; an unprefixed container leaks // silently between runs. Project membership is a separate concern — the // per-test sweeps cannot match this name (no timestamp in it), so the // label test below is what teardown actually keys off. const { runner, find } = stubDocker(); await provisionerWith(runner).createGuest(guest()); const name = (find('run') ?? [])[(find('run') ?? []).indexOf('--name') + 1]; expect(name).toStartWith(CONTAINER_PREFIX); expect(name).toBe('celilo-e2e-lxc-203'); }); test('labels the guest with its compose project so teardown can find it (celilo#1247)', async () => { // The name cannot carry project membership (no timestamp), and compose's // own labels cannot be borrowed (the guest is not a compose service, and // compose down removes orphans only on the full label set — verified: // a bare com.docker.compose.project label is not enough). The explicit // label is what projectTeardownCommands lists guests by. const { runner, find } = stubDocker(); await provisionerWith(runner).createGuest(guest()); const run = find('run') ?? []; expect(run[run.indexOf('--label') + 1]).toBe(`${GUEST_PROJECT_LABEL}=celilo-e2e-test`); }); test('passes the gateway through, since target-setup routes from it', async () => { const { runner, find } = stubDocker(); await provisionerWith(runner).createGuest(guest()); expect(find('run')).toContain(`GATEWAY=${ZONE_GATEWAYS.dmz}`); }); test('mounts the ssh-keys volume the machine pool already uses', async () => { const { runner, find } = stubDocker(); await provisionerWith(runner).createGuest(guest()); expect(find('run')).toContain('celilo-e2e-test_ssh-keys:/ssh-keys:ro'); }); test('the app zone gets the dockerd-capable image', async () => { const { runner, find } = stubDocker(); const net0 = `name=eth0,gw=${ZONE_GATEWAYS.app},ip=${zoneIp('app', 13)}/24`; await provisionerWith(runner).createGuest(guest({ net0 })); expect((find('run') ?? []).at(-1)).toBe('celilo-e2e/target-machine-docker'); }); test('waits for target-setup before returning', async () => { // Returning early lets Ansible race sshd, which surfaces as an // intermittent connection failure several steps later. let ready = false; const calls: string[][] = []; const runner = (args: string[]): string => { calls.push(args); if (!args.includes('is-active')) return ''; const answer = ready ? 'active\n' : 'activating\n'; ready = true; return answer; }; await provisionerWith(runner).createGuest(guest()); expect(calls.filter((c) => c.includes('is-active')).length).toBe(2); }); test('gives up rather than hanging when the container never boots', async () => { const runner = (args: string[]): string => { if (args.includes('is-active')) throw new Error('container not running'); return ''; }; const provisioner = createDockerProvisioner({ project: 'p', docker: runner, readyTimeoutMs: 0, sleep: async () => {}, }); expect(provisioner.createGuest(guest())).rejects.toThrow('did not go active'); }); test('appends ssh_public_keys AFTER boot, since target-setup overwrites the file', async () => { const { runner, calls } = stubDocker(); await provisionerWith(runner).createGuest( guest({ ssh_public_keys: 'ssh-ed25519 AAAAC3 test' }), ); const append = calls.findIndex((c) => c.join(' ').includes('authorized_keys')); const ready = calls.findIndex((c) => c.includes('is-active')); expect(append).toBeGreaterThan(ready); }); test('refuses a gateway belonging to no zone rather than guessing one', async () => { const { runner } = stubDocker(); const net0 = 'name=eth0,gw=192.0.2.1,ip=192.0.2.10/24'; expect(provisionerWith(runner).createGuest(guest({ net0 }))).rejects.toThrow('no rig zone'); }); test('refuses a net0 with no address rather than starting an unreachable box', async () => { const { runner } = stubDocker(); expect(provisionerWith(runner).createGuest(guest({ net0: 'name=eth0' }))).rejects.toThrow( 'no gw/ip', ); }); }); describe('destroyGuest', () => { test('removes the container', () => { const { runner, find } = stubDocker(); provisionerWith(runner).destroyGuest(guest()); expect(find('rm')).toEqual(['rm', '-f', 'celilo-e2e-lxc-203']); }); }); describe('the containment that makes the mounted Docker socket acceptable', () => { test('nothing without the prefix is ever touched', () => { // The simulator can reach every container on the developer's machine. This // is the guard that keeps it to its own (D2). expect(containerNameFor(203)).toStartWith(CONTAINER_PREFIX); }); }); describe('parseNet', () => { test('unpacks the comma-packed net0 Proxmox uses', () => { expect(parseNet(NET0)).toMatchObject({ name: 'eth0', bridge: 'vmbr0', ip: `${DMZ_ADDRESS}/24`, tag: '20', }); }); test('an empty net0 yields nothing rather than throwing', () => { expect(parseNet('')).toEqual({}); }); }); describe('zoneForGateway', () => { test('every rig zone resolves from its own gateway', () => { for (const zone of Object.keys(ZONE_GATEWAYS) as Array) { expect(zoneForGateway(ZONE_GATEWAYS[zone])).toBe(zone); } }); test('an unknown gateway resolves to nothing', () => { expect(zoneForGateway('192.0.2.1')).toBeUndefined(); }); });