import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; import { existsSync } from 'node:fs'; import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { createModuleDeployWorker, isInstanceRequestRefusedError } from '@celilo/capabilities'; import { type DbClient, createDbClient } from '../db/client'; import type { ModuleManifest } from '../manifest/schema'; import { createInstanceOps, refuseUndeclaredNetworks, validateInstanceConfig, } from './instance-ops'; import { deriveInstanceModuleId } from './module-instances'; const manifest = (over: Record = {}): ModuleManifest => ({ celilo_contract: '1.0', id: 'lab', name: 'BYOI Lab', version: '1.0.0', requires: { capabilities: [] }, provides: { capabilities: [] }, variables: { owns: [], imports: [] }, ...over, }) as unknown as ModuleManifest; const ownedVar = (over: Record) => ({ name: 'ssh_key', type: 'string', required: true, source: 'user', description: '', ...over, }); describe('validateInstanceConfig', () => { test('accepts a config supplying every required user variable', () => { const m = manifest({ variables: { owns: [ownedVar({})], imports: [] } }); expect(validateInstanceConfig(m, { ssh_key: 'ssh-ed25519 AAAA' })).toEqual([]); }); test('reports a required variable that was not supplied', () => { const m = manifest({ variables: { owns: [ownedVar({})], imports: [] } }); const problems = validateInstanceConfig(m, {}); expect(problems).toHaveLength(1); expect(problems[0]).toContain("'ssh_key' is required"); }); test('a required variable with a default need not be supplied', () => { const m = manifest({ variables: { owns: [ownedVar({ default: 'ssh-ed25519 DEFAULT' })], imports: [] }, }); expect(validateInstanceConfig(m, {})).toEqual([]); }); test('refuses a key the submodule does not declare, and lists what it does', () => { const m = manifest({ variables: { owns: [ownedVar({})], imports: [] } }); const problems = validateInstanceConfig(m, { ssh_key: 'k', nonsense: 1 }); expect(problems).toHaveLength(1); expect(problems[0]).toContain("'nonsense' is not declared"); expect(problems[0]).toContain('ssh_key'); }); // A derived value silently overwritten at generate time looks like celilo // ignoring the request, so it is refused rather than accepted and discarded. test('refuses a declared variable celilo derives rather than the caller supplying', () => { const m = manifest({ variables: { owns: [ownedVar({}), ownedVar({ name: 'container_ip', source: 'infrastructure' })], imports: [], }, }); const problems = validateInstanceConfig(m, { ssh_key: 'k', container_ip: '10.0.0.5' }); expect(problems).toHaveLength(1); expect(problems[0]).toContain('celilo derives it'); }); // Four failed instantiations to learn four missing keys is the experience // this avoids. test('reports every problem at once', () => { const m = manifest({ variables: { owns: [ownedVar({}), ownedVar({ name: 'owner_sub' })], imports: [] }, }); expect(validateInstanceConfig(m, { junk: 1 })).toHaveLength(3); }); }); describe('refuseUndeclaredNetworks', () => { test('passes when every insisted network is declared', () => { const m = manifest({ requires: { capabilities: [], networks: [{ name: 'quarantine' }] } }); expect(refuseUndeclaredNetworks(m, ['dmz', 'quarantine'])).toBeNull(); }); test('refuses and names the missing network', () => { const m = manifest({ requires: { capabilities: [], networks: [{ name: 'quarantine' }] } }); const refusal = refuseUndeclaredNetworks(m, ['dmz']); expect(refusal).toContain('quarantine'); expect(refusal).toContain('never creates a network'); }); test('a submodule insisting on nothing passes', () => { expect(refuseUndeclaredNetworks(manifest(), [])).toBeNull(); }); }); describe('createInstanceOps against a real database', () => { let root: string; let db: DbClient; let parentPath: string; const SUBMODULE_YAML = ` celilo_contract: "1.0" id: lab name: BYOI Lab version: 1.0.0 variables: owns: - name: ssh_key type: string required: true source: user description: Public key baked into authorized_keys `; async function setup(opts: { submoduleYaml?: string; declaredNetworks?: string[] } = {}) { parentPath = join(root, 'modules', 'byoi'); const submodulePath = join(parentPath, 'submodules', 'lab'); await mkdir(join(submodulePath, 'ansible'), { recursive: true }); await writeFile(join(parentPath, 'manifest.yml'), 'id: byoi\n'); await writeFile(join(submodulePath, 'manifest.yml'), opts.submoduleYaml ?? SUBMODULE_YAML); db.$client.run( "INSERT INTO modules (id,name,version,manifest_data,source_path) VALUES ('byoi','byoi','1.0.0',?,?)", [JSON.stringify({ id: 'byoi', submodules: ['lab'] }), parentPath], ); return createInstanceOps({ parentId: 'byoi', db, declaredNetworks: opts.declaredNetworks ?? ['quarantine'], }); } const worker = (ops: ReturnType) => createModuleDeployWorker({ moduleId: 'byoi', ops }); beforeEach(async () => { root = await mkdtemp(join(tmpdir(), 'celilo-instance-ops-')); db = createDbClient({ path: join(root, 'celilo.db') }); }); afterEach(async () => { db.$client.close(); await rm(root, { recursive: true, force: true }); }); test('records an instance, its config, and its symlink farm', async () => { const ops = await setup(); const moduleId = await ops.create({ submodule: 'lab', instanceKey: 'sub-abc', config: { ssh_key: 'ssh-ed25519 AAAA' }, label: 'Sally', }); expect(moduleId).toBe(deriveInstanceModuleId('byoi', 'lab', 'sub-abc')); // A flat peer of every other module, never nested under the parent. const instancePath = join(dirname(parentPath), moduleId); expect(dirname(instancePath)).toBe(dirname(parentPath)); expect(existsSync(join(instancePath, 'generated'))).toBe(true); expect(existsSync(join(instancePath, 'ansible'))).toBe(true); const row = db.$client .query('SELECT state, label, parent_id, submodule FROM module_instances') .get() as Record; expect(row).toMatchObject({ state: 'pending', label: 'Sally', parent_id: 'byoi', submodule: 'lab', }); const config = db.$client .query("SELECT value_json FROM module_configs WHERE module_id = ? AND key = 'ssh_key'") .get(moduleId) as { value_json: string }; expect(JSON.parse(config.value_json)).toBe('ssh-ed25519 AAAA'); }); // A reconcile loop that fires twice must not produce a second system. test('is idempotent on the caller key and repairs the farm on a repeat', async () => { const ops = await setup(); const first = await ops.create({ submodule: 'lab', instanceKey: 'sub-abc', config: { ssh_key: 'k1' }, }); const instancePath = join(dirname(parentPath), first); await rm(join(instancePath, 'ansible'), { force: true }); const second = await ops.create({ submodule: 'lab', instanceKey: 'sub-abc', config: { ssh_key: 'k2' }, }); expect(second).toBe(first); expect( (db.$client.query('SELECT count(*) n FROM module_instances').get() as { n: number }).n, ).toBe(1); // A retry REPAIRS rather than merely not-breaking. expect(existsSync(join(instancePath, 'ansible'))).toBe(true); }); test('refuses invalid config before writing anything', async () => { const ops = await setup(); let caught: unknown; try { await ops.create({ submodule: 'lab', instanceKey: 'sub-abc', config: {} }); } catch (error) { caught = error; } // Duck-typed, because the error crosses the identity boundary a module's // bundled copy of @celilo/capabilities creates (celilo#173). expect(isInstanceRequestRefusedError(caught)).toBe(true); expect( (db.$client.query('SELECT count(*) n FROM module_instances').get() as { n: number }).n, ).toBe(0); }); // D8, and at the REQUEST rather than three minutes into a terraform apply. test('refuses an undeclared network before provisioning', async () => { const ops = await setup({ submoduleYaml: `${SUBMODULE_YAML} requires: networks: - name: quarantine `, declaredNetworks: ['dmz'], }); await expect( ops.create({ submodule: 'lab', instanceKey: 'k', config: { ssh_key: 'x' } }), ).rejects.toThrow(/quarantine/); }); test('a framework refusal reaches the caller as accepted:false, not a throw', async () => { const capability = worker(await setup()); const result = await capability.instantiate({ submodule: 'lab', instanceKey: 'sub-abc', config: {}, }); expect(result.accepted).toBe(false); if (!result.accepted) expect(result.reason).toContain("'ssh_key' is required"); }); test('destroy marks the instance and reports its id', async () => { const ops = await setup(); const moduleId = await ops.create({ submodule: 'lab', instanceKey: 'sub-abc', config: { ssh_key: 'k' }, }); expect(await ops.markForDestruction('lab', 'sub-abc')).toBe(moduleId); const row = db.$client.query('SELECT state FROM module_instances').get() as { state: string }; expect(row.state).toBe('destroying'); }); test('destroying something that was never there reports null', async () => { const ops = await setup(); expect(await ops.markForDestruction('lab', 'never')).toBeNull(); }); test('list reports what was recorded, scoped to the caller', async () => { const ops = await setup(); await ops.create({ submodule: 'lab', instanceKey: 'a', config: { ssh_key: 'k' } }); await ops.create({ submodule: 'lab', instanceKey: 'b', config: { ssh_key: 'k' } }); const listed = await ops.list(); expect(listed.map((i) => i.instanceKey).sort()).toEqual(['a', 'b']); expect(listed.every((i) => i.state === 'pending')).toBe(true); expect(listed.every((i) => i.failureReason === null)).toBe(true); }); test('declaredSubmodules reads the parent manifest, so authority is not supplied', async () => { const ops = await setup(); expect(ops.declaredSubmodules()).toEqual(['lab']); const result = await worker(ops).instantiate({ submodule: 'not-mine', instanceKey: 'k' }); expect(result.accepted).toBe(false); }); });