Account for runtime RCON passwords in config drift

This commit is contained in:
2026-10-08 03:07:49 -04:00
parent 8de9247ff2
commit f8942c732a
13 changed files with 299 additions and 19 deletions
+41
View File
@@ -26,6 +26,8 @@ async function setup(capable = true) {
VALUES(?,'t5','zombie_theater','zombie','theater','ui')`).run(server);
database.prepare(`INSERT INTO native_instances(id,server,host_id,library_id,image,port,created_at)
VALUES(?,?,?,'library','image',28960,?)`).run(randomUUID(), server, host.id, new Date().toISOString());
database.prepare(`INSERT INTO ec_host_operations(id,host_id,instance_id,generation,kind,payload,state,created_at,updated_at)
SELECT 'launch',host_id,id,generation,'instance.create','{}','completed',created_at,created_at FROM native_instances WHERE server=?`).run(server);
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
const { recordScript } = await import('../../src/lib/server/fleet/inventory.js');
@@ -94,6 +96,45 @@ function expectOneManifest(request: Awaited<ReturnType<typeof setup>>['request']
}
describe('native drift manifests', () => {
it.each([true, false])('reconciles verified startup password changes without writing server files (manifest: %s)', async capable => {
const api = await setup(capable);
const { applyRuntimePassword } = await import('../../src/lib/server/fleet/runtime-config.js');
const { getServerScript, recordScript } = await import('../../src/lib/server/fleet/inventory.js');
const promoted = Buffer.from('set rcon_password "password"\nset sv_maprotation "map zombie_theater"\n');
const { uploadNewVersion } = await import('../../src/lib/server/fleet/scripts.js');
// Replace the promoted baseline bytes, retaining a legacy inventory hash.
const next = uploadNewVersion(api.baseline.script.id, promoted);
api.database.prepare('UPDATE scripts SET current_version_id=? WHERE id=?').run(next.id, api.baseline.script.id);
const password = 'a'.repeat(64);
api.database.prepare('UPDATE ec_host_operations SET payload=? WHERE id=?').run(JSON.stringify({ rconPassword: password }), 'launch');
recordScript(server, 'dedicated_sp.cfg', null, hash(promoted), promoted.length, 'native-create');
const effective = Buffer.from(applyRuntimePassword(promoted.toString(), password));
expect(effective.length - promoted.length).toBe(56);
api.contents.set('configs/dedicated_sp.cfg', effective);
expect(await api.scanServerDrift(server)).toEqual([]);
expect(getServerScript(server, 'dedicated_sp.cfg')).toMatchObject({ sha256: hash(effective), size: effective.length,
source: 'runtime-config-reconciliation' });
if (capable) expectOneManifest(api.request);
expect((await api.prepareDriftCampaign(server)).plan.files).toEqual([]);
// A wrong password of the same length is still real drift.
api.contents.set('configs/dedicated_sp.cfg', Buffer.from(applyRuntimePassword(promoted.toString(), 'b'.repeat(64))));
expect(await api.scanServerDrift(server)).toEqual([expect.objectContaining({ relPath: 'dedicated_sp.cfg', driftKind: 'replace' })]);
if (!capable) {
const repair = await api.prepareDriftCampaign(server);
expect(repair.files[0].after).toEqual(effective);
}
api.contents.set('configs/dedicated_sp.cfg', Buffer.concat([effective, Buffer.from('set sv_voice "0"\n')]));
expect(await api.scanServerDrift(server)).toHaveLength(1);
});
it('reports unavailable credentials as a scan failure rather than scheduling a destructive repair', async () => {
const api = await setup();
api.database.prepare("UPDATE ec_host_operations SET history_expired_at='expired',payload='{}' WHERE id='launch'").run();
await expect(api.scanServerDrift(server)).rejects.toThrow('launch credentials are unavailable');
});
it('checks managed files, composed config, generated config, and untracked trees in one request', async () => {
const api = await setup();
expect(await api.scanServerDrift(server)).toEqual([]);