Add safe manual instance rebalancing
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import { db } from '../db/index.js';
|
||||
import { nativeInstance, runtimePasswordForServer } from '../backends/native.js';
|
||||
import { getServer } from '../fleet/servers.js';
|
||||
import { listServerScripts } from '../fleet/inventory.js';
|
||||
import { recipeStorage, recipeSummaryForDeployment, type LaunchRecipePlan } from '../fleet/launch-recipes.js';
|
||||
|
||||
/** Preserve the recorded deployed inventory, including changes made after launch. */
|
||||
export function captureMoveRecipe(server: string): LaunchRecipePlan {
|
||||
const instance = nativeInstance(server);
|
||||
const row = getServer(server);
|
||||
if (!instance || !row?.map_slug) throw new Error('A live native instance and saved map are required before moving.');
|
||||
if (!runtimePasswordForServer(server)) throw new Error('The deployed runtime credentials are unavailable.');
|
||||
const launch = db().prepare(`SELECT d.id FROM server_deployments d
|
||||
JOIN native_provisioning n ON n.deployment_id=d.id
|
||||
JOIN server_launch_recipes r ON r.deployment_id=d.id
|
||||
WHERE n.instance_id=? AND d.state IN ('done','warning') AND d.server_deleted_at IS NULL
|
||||
ORDER BY d.rowid DESC LIMIT 1`).get(instance.id) as { id: string } | undefined;
|
||||
const previous = launch && recipeSummaryForDeployment(launch.id)?.recipe;
|
||||
if (!previous || previous.game !== row.game || previous.mapSlug !== row.map_slug) {
|
||||
throw new Error('The deployed launch recipe is unavailable or differs from the saved map.');
|
||||
}
|
||||
const inventory = listServerScripts(server);
|
||||
if (!inventory.length) throw new Error('The deployed file inventory is unavailable.');
|
||||
const files = inventory.map(file => {
|
||||
const before = previous.files.find(item => item.path === file.rel_path);
|
||||
const version = file.script_version_id === null ? undefined : db().prepare(`SELECT v.version,v.kind,s.name
|
||||
FROM script_versions v JOIN scripts s ON s.id=v.script_id WHERE v.id=?`).get(file.script_version_id) as
|
||||
{ version: number; kind: string; name: string } | undefined;
|
||||
const kind = version?.kind === 'baseline' ? 'config' : version?.kind ?? before?.kind;
|
||||
if (!['script', 'plugin', 'config'].includes(kind ?? '')) throw new Error(`The deployed file type is unavailable for ${file.rel_path}.`);
|
||||
if (!db().prepare('SELECT 1 FROM file_blobs WHERE sha256=? AND size=?').get(file.sha256, file.size)) {
|
||||
throw new Error(`The deployed bytes are unavailable for ${file.rel_path}.`);
|
||||
}
|
||||
return { name: version?.name ?? before?.name ?? file.rel_path, kind: kind as 'script' | 'plugin' | 'config',
|
||||
path: file.rel_path, versionId: file.script_version_id, version: version?.version ?? null,
|
||||
sha256: file.sha256, size: file.size };
|
||||
});
|
||||
if (!files.some(file => file.path === 'electric_cherry_map.cfg' && file.kind === 'config')) {
|
||||
throw new Error('The deployed map configuration is unavailable.');
|
||||
}
|
||||
return { format: 1, game: previous.game, mapSlug: previous.mapSlug, mapName: previous.mapName,
|
||||
expectedMap: previous.expectedMap, configFile: previous.configFile,
|
||||
configSource: 'Preserved deployed files', capturedAt: new Date().toISOString(), image: instance.image,
|
||||
native: { libraryId: instance.library_id, image: instance.image }, files };
|
||||
}
|
||||
|
||||
/** Immutable hash references keep deployed bytes alive without decoding plugins on the controller. */
|
||||
export function moveRecipeStorage(recipe: LaunchRecipePlan): ReturnType<typeof recipeStorage> {
|
||||
const metadata = { ...recipe, files: recipe.files.map(({ content: _content, ...file }) => file) };
|
||||
const stored = recipeStorage(metadata);
|
||||
return { recipeSummary: stored.recipeSummary,
|
||||
recipe: JSON.stringify({ ...metadata, files: metadata.files.map(file => ({ ...file, contentHash: file.sha256 })) }) };
|
||||
}
|
||||
|
||||
export function moveRecipeMatches(server: string, recipe: LaunchRecipePlan): boolean {
|
||||
const inventory = listServerScripts(server);
|
||||
return inventory.length === recipe.files.length && inventory.every(file => recipe.files.some(saved =>
|
||||
saved.path === file.rel_path && saved.sha256 === file.sha256 && saved.size === file.size));
|
||||
}
|
||||
Reference in New Issue
Block a user