Preserve runtime passwords when revalidating launch files

This commit is contained in:
2026-10-08 11:31:26 -04:00
parent f8942c732a
commit 8863e8a882
4 changed files with 76 additions and 10 deletions
+34 -5
View File
@@ -179,17 +179,23 @@ describe('asynchronous native launch preparation', () => {
expect(db().prepare('SELECT state FROM server_deployments WHERE id=?').get(job.deploymentId)).toEqual({ state: 'cancelled' });
});
it('discards prepared bytes when the promoted version changes before dispatch', async () => {
it.each(['baseline', 'config', 'script', 'plugin'] as const)('discards prepared bytes when the promoted %s changes before dispatch', async kind => {
const { createScript, uploadNewVersion } = await import('../../src/lib/server/fleet/scripts.js');
const { db } = await import('../../src/lib/server/db/index.js');
const { script, version } = createScript({ name: 'Launch plugin', kind: 'plugin', targetRelPath: 'fixture.dll',
scopeType: 'game', scopeValue: 't5', content: Buffer.from('before promotion') });
const baseline = createScript({ name: 'Launch baseline', kind: 'baseline', scopeType: 'game', scopeValue: 't5',
content: Buffer.from('set rcon_password "password"\nset sv_voice "1"\n') });
db().prepare('UPDATE scripts SET current_version_id=? WHERE id=?').run(baseline.version.id, baseline.script.id);
const isConfig = kind === 'baseline' || kind === 'config';
const { script, version } = kind === 'baseline' ? baseline : createScript({ name: `Launch ${kind}`, kind,
targetRelPath: kind === 'script' ? 'fixture.gsc' : kind === 'plugin' ? 'fixture.dll' : undefined,
scopeType: kind === 'config' ? 'map' : 'game', scopeValue: kind === 'config' ? 'zombie_theater' : 't5',
content: Buffer.from(isConfig ? 'set sv_voice "1"\n' : 'before promotion') });
db().prepare('UPDATE scripts SET current_version_id=? WHERE id=?').run(version.id, script.id);
const job = await queue();
const paused = await pausePreparation();
const pass = runPass();
await paused.ready;
const content = Buffer.from('newly promoted plugin');
const content = Buffer.from(isConfig ? 'set rcon_password "password"\nset sv_voice "0"\n' : 'newly promoted file');
const latest = uploadNewVersion(script.id, content);
db().prepare('UPDATE scripts SET current_version_id=? WHERE id=?').run(latest.id, script.id);
paused.release();
@@ -200,7 +206,30 @@ describe('asynchronous native launch preparation', () => {
expect(operation).toBeDefined();
const { getHostOperation } = await import('../../src/lib/server/hosts/index.js');
const payload = getHostOperation(operation!.id)!.payload as { files: { contentBase64: string; sha256: string }[] };
expect(payload.files).toContainEqual(expect.objectContaining({ contentBase64: content.toString('base64'), sha256: latest.sha256 }));
if (isConfig) {
const { getPromotedServerConfig } = await import('../../src/lib/server/fleet/server-config.js');
const expected = getPromotedServerConfig('t5', 'zombie_theater', 'theater', 'test-password')!;
expect(payload.files).toContainEqual(expect.objectContaining({ contentBase64: expected.toString('base64') }));
expect(expected.toString()).toContain('set sv_voice "0"');
} else expect(payload.files).toContainEqual(expect.objectContaining({ contentBase64: content.toString('base64'), sha256: latest.sha256 }));
});
it('discards prepared bytes when the server key changes before dispatch', async () => {
const job = await queue();
const paused = await pausePreparation();
const pass = runPass();
await paused.ready;
const { db } = await import('../../src/lib/server/db/index.js');
db().prepare(`UPDATE server_keys SET pluto_key='changed-key' WHERE id IN
(SELECT key_id FROM server_key_leases WHERE server_id=? AND is_current=1)`).run(job.name);
paused.release();
await pass;
expect(await operationFor(job.deploymentId)).toBeUndefined();
await runPass();
const operation = await operationFor(job.deploymentId);
expect(operation).toBeDefined();
const { getHostOperation } = await import('../../src/lib/server/hosts/index.js');
expect(getHostOperation(operation!.id)!.payload).toMatchObject({ env: { PLUTO_SERVER_KEY: 'changed-key' } });
});
it('prepares only one launch while the single startup slot is reserved', async () => {
+34
View File
@@ -84,6 +84,40 @@ async function observations(server: string) {
return [serverObservation(row), observationReader()(row), new MonitoringSnapshot().observe(row)];
}
describe('runtime configuration at dispatch', () => {
it.each([
['t5', 'shared'], ['t5', 'generated'], ['t6', 'shared'], ['t6', 'generated']
] as const)('dispatches %s with a promoted baseline and %s RCON password', async (game, source) => {
const { config, configFileForGame } = await import('../../src/lib/server/config.js');
if (source === 'generated') Object.assign(config.rcon, { password: '' });
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
const { db } = await import('../../src/lib/server/db/index.js');
const { script, version } = createScript({ name: 'Launch baseline', kind: 'baseline', scopeType: 'game', scopeValue: game,
content: Buffer.from('set rcon_password "password"\nset sv_voice "1"\n') });
db().prepare('UPDATE scripts SET current_version_id=? WHERE id=?').run(version.id, script.id);
const job = await queue(game);
await advance();
const operation = await operationFor(job.deploymentId);
expect(operation).toMatchObject({ kind: 'instance.create', state: 'pending' });
const payload = operation!.payload as { rconPassword: string; files: { path: string; contentBase64: string; sha256: string }[] };
expect(payload.rconPassword).toEqual(source === 'shared' ? 'test-password' : expect.stringMatching(/^[a-f0-9]{64}$/));
const configFile = configFileForGame(game);
const file = payload.files.find(file => file.path === `configs/${configFile}`)!;
const content = Buffer.from(file.contentBase64, 'base64');
expect(content.toString()).toContain(`set rcon_password "${payload.rconPassword}"`);
expect(content.toString()).not.toContain('set rcon_password "password"');
const hash = createHash('sha256').update(content).digest('hex');
expect(file.sha256).toBe(hash);
const { getServerScript } = await import('../../src/lib/server/fleet/inventory.js');
expect(getServerScript(job.name, configFile)).toMatchObject({ sha256: hash, size: content.length, source: 'native-create' });
const { recipeForDeployment } = await import('../../src/lib/server/fleet/launch-recipes.js');
expect(recipeForDeployment(job.deploymentId)!.recipe.files).toContainEqual(expect.objectContaining({
path: configFile, sha256: hash, size: content.length, content: content.toString('base64')
}));
});
});
describe('provisioning health handoff', () => {
it.each(['create', 'start', 'restart', 'rebuild'] as const)('%s publishes verified game health before completion becomes visible', async action => {
const job = await queue(); await advance();