Rebuild beta and campaign file activations
This commit is contained in:
@@ -18,6 +18,7 @@ import { db } from '../db/index.js';
|
||||
import { unsettledCancelledCommand } from './cancellation.js';
|
||||
import { config, relDestForKind, serverDir } from '../config.js';
|
||||
import {
|
||||
runtimePasswordForServer,
|
||||
nativeInstance,
|
||||
nativeRequest,
|
||||
unplacedNativeCreation,
|
||||
@@ -55,6 +56,7 @@ import {
|
||||
availableServerKeyCount
|
||||
} from '../fleet/server-keys.js';
|
||||
import { activeBetaRemovalBlocker, getServer } from '../fleet/servers.js';
|
||||
import { applyActivationFiles } from './activation-files.js';
|
||||
import { nextLobbyNumber, type CreateServerInput, type CreateServerResult } from './instances.js';
|
||||
import {
|
||||
beginDeployment,
|
||||
@@ -916,7 +918,10 @@ function queueUnhealthyRecovery(job: Job, instance: NativeInstance): void {
|
||||
}
|
||||
let payload: Record<string, unknown> = { unhealthyRecovery: true };
|
||||
if (step === 'rebuild') {
|
||||
const beta = activeBetaRemovalBlocker(job.server);
|
||||
const beta = activeBetaRemovalBlocker(
|
||||
job.server,
|
||||
job.action === 'rebuild' ? job.id : undefined
|
||||
);
|
||||
if (beta) {
|
||||
hold(job, beta);
|
||||
return;
|
||||
@@ -952,6 +957,7 @@ function queueUnhealthyRecovery(job: Job, instance: NativeInstance): void {
|
||||
unhealthyRecovery: true
|
||||
};
|
||||
delete payload.safetyOverride;
|
||||
delete payload.stageId;
|
||||
}
|
||||
const operation = queueHostOperationSerialized(
|
||||
instance.host_id,
|
||||
@@ -1701,6 +1707,17 @@ async function processJob(job: Job, eligibility: (job: Job) => string | null) {
|
||||
hold(job, 'Waiting for current health evidence before completing verification.');
|
||||
return;
|
||||
}
|
||||
// A queued activation from a previous controller release may still say restart.
|
||||
// Upgrade only intent that has never submitted a host command.
|
||||
if (
|
||||
job.action === 'restart' &&
|
||||
job.state === 'waiting' &&
|
||||
!job.submitted_at &&
|
||||
filesFor(job).length
|
||||
) {
|
||||
db().prepare("UPDATE server_deployments SET action='rebuild' WHERE id=?").run(job.id);
|
||||
job.action = 'rebuild';
|
||||
}
|
||||
const reason = eligibility(job);
|
||||
if (reason) {
|
||||
hold(job, reason);
|
||||
@@ -1712,13 +1729,58 @@ async function processJob(job: Job, eligibility: (job: Job) => string | null) {
|
||||
return;
|
||||
}
|
||||
if (job.action === 'destroy' || job.action === 'rebuild') {
|
||||
const beta = activeBetaRemovalBlocker(job.server);
|
||||
const beta = activeBetaRemovalBlocker(
|
||||
job.server,
|
||||
job.action === 'rebuild' ? job.id : undefined
|
||||
);
|
||||
if (beta) {
|
||||
hold(job, beta);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const files = job.stage_operation_id ? storedFiles(job) : filesFor(job);
|
||||
if (
|
||||
job.action === 'rebuild' &&
|
||||
files.length &&
|
||||
!host.inventory.capabilities?.includes('instance.rebuild-files')
|
||||
) {
|
||||
hold(job, 'Update the host agent to support beta and campaign file rebuilds.');
|
||||
return;
|
||||
}
|
||||
if (job.action === 'rebuild' && !recipeSummaryForDeployment(job.id)) {
|
||||
const server = getServer(job.server);
|
||||
if (!server?.map_slug) throw new Error('A saved map is required before rebuilding.');
|
||||
const recipe: NativeRecipe = {
|
||||
...captureRecipePlan(server.game as 't5' | 't6', server.map_slug),
|
||||
image: instance.image,
|
||||
native: {
|
||||
hostId: instance.host_id,
|
||||
libraryId: instance.library_id,
|
||||
image: instance.image,
|
||||
bindAddress: instance.bind_address
|
||||
}
|
||||
};
|
||||
const stored = recipeStorage(recipe);
|
||||
db().transaction(() => {
|
||||
db()
|
||||
.prepare(
|
||||
'INSERT INTO server_creation_jobs(deployment_id,server,port,game_mode,map_slug,expected_map) VALUES(?,?,?,?,?,?)'
|
||||
)
|
||||
.run(
|
||||
job.id,
|
||||
job.server,
|
||||
instance!.port,
|
||||
'zombie',
|
||||
recipe.mapSlug,
|
||||
recipe.expectedMap
|
||||
);
|
||||
db()
|
||||
.prepare(
|
||||
'INSERT INTO server_launch_recipes(deployment_id,recipe,recipe_summary) VALUES(?,?,?)'
|
||||
)
|
||||
.run(job.id, stored.recipe, stored.recipeSummary);
|
||||
})();
|
||||
}
|
||||
if (files.length && !job.stage_operation_id) {
|
||||
db().transaction(() => {
|
||||
const stage = queueHostOperationSerialized(
|
||||
@@ -1838,7 +1900,7 @@ async function processJob(job: Job, eligibility: (job: Job) => string | null) {
|
||||
'instance.inspect'
|
||||
);
|
||||
if (!stillActive(job)) return false;
|
||||
if (job.action === 'rebuild' && health.state === 'unhealthy') {
|
||||
if (job.action === 'rebuild' && !files.length && health.state === 'unhealthy') {
|
||||
if (
|
||||
!getHost(instance.host_id)?.inventory.capabilities?.includes(
|
||||
unhealthyRecoveryCapability
|
||||
@@ -1929,7 +1991,10 @@ async function processJob(job: Job, eligibility: (job: Job) => string | null) {
|
||||
phase: 'prepare',
|
||||
checkpoint: { key: 'startup-queue', label: 'Startup capacity available' }
|
||||
});
|
||||
preparedPassword = config.rcon.password || randomBytes(32).toString('hex');
|
||||
preparedPassword =
|
||||
(files.length ? runtimePasswordForServer(job.server) : undefined) ||
|
||||
config.rcon.password ||
|
||||
randomBytes(32).toString('hex');
|
||||
prepared = await prepareNativeLaunch({
|
||||
game: prior.game,
|
||||
mapSlug: prior.mapSlug,
|
||||
@@ -1940,9 +2005,10 @@ async function processJob(job: Job, eligibility: (job: Job) => string | null) {
|
||||
bankToken: bankCredential(instance.id),
|
||||
rebuild: job.action === 'rebuild' && !migration,
|
||||
safetyOverride: overrideSafety,
|
||||
unhealthyRecovery: host.inventory.capabilities?.includes(
|
||||
unhealthyRecoveryCapability
|
||||
),
|
||||
unhealthyRecovery:
|
||||
!files.length &&
|
||||
host.inventory.capabilities?.includes(unhealthyRecoveryCapability),
|
||||
activationId: files.length ? job.id : undefined,
|
||||
frozenRecipeId
|
||||
});
|
||||
if (job.action === 'rebuild' && !migration && !(await checkOccupancy())) return;
|
||||
@@ -1977,7 +2043,10 @@ async function processJob(job: Job, eligibility: (job: Job) => string | null) {
|
||||
return;
|
||||
}
|
||||
if (job.action === 'destroy' || job.action === 'rebuild') {
|
||||
const beta = activeBetaRemovalBlocker(job.server);
|
||||
const beta = activeBetaRemovalBlocker(
|
||||
job.server,
|
||||
job.action === 'rebuild' ? job.id : undefined
|
||||
);
|
||||
if (beta) {
|
||||
hold(job, beta);
|
||||
return;
|
||||
@@ -2032,6 +2101,7 @@ async function processJob(job: Job, eligibility: (job: Job) => string | null) {
|
||||
prepared.recipe.mapSlug,
|
||||
preparedPassword
|
||||
);
|
||||
if (files.length) applyActivationFiles(expected, job.id);
|
||||
if (
|
||||
getServerKey(job.server) !== preparedKey ||
|
||||
!sameLaunchFiles(prepared.recipe, expected)
|
||||
@@ -2119,7 +2189,14 @@ async function processJob(job: Job, eligibility: (job: Job) => string | null) {
|
||||
recipe.image,
|
||||
instance!.port
|
||||
);
|
||||
for (const file of recipe.files)
|
||||
for (const file of recipe.files.filter(
|
||||
(file) =>
|
||||
!files.some(
|
||||
(change) =>
|
||||
change.path ===
|
||||
posix.join(relDestForKind(file.kind, recipe.game), file.path)
|
||||
)
|
||||
))
|
||||
recordScript(
|
||||
job.server,
|
||||
file.path,
|
||||
|
||||
Reference in New Issue
Block a user