refactor: make provisioning own instance lifecycle operations
This commit is contained in:
@@ -2,6 +2,7 @@ import { json } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db/index.js';
|
||||
import { emit, type BetaStage } from '$lib/server/pipeline/bus.js';
|
||||
import { storedPlan } from '$lib/server/fleet/beta-tests.js';
|
||||
import { releaseRestartRequest } from '$lib/server/provisioning/index.js';
|
||||
|
||||
function parseId(raw: string): number | null {
|
||||
const n = Number.parseInt(raw, 10);
|
||||
@@ -28,6 +29,7 @@ export async function POST({ params, request }: { params: { id: string }; reques
|
||||
// Best-effort resume point: if we already got past restart, go back to
|
||||
// health check; otherwise back to waiting for the server to be empty.
|
||||
const nextStage: BetaStage = row.restarted_at ? 'awaiting-health' : 'awaiting-empty';
|
||||
if (!row.restarted_at) releaseRestartRequest(body.server, { kind: 'beta', id: String(betaId) });
|
||||
const now = new Date().toISOString();
|
||||
db()
|
||||
.prepare('UPDATE beta_servers SET stage = ?, error = NULL, updated_at = ? WHERE beta_id = ? AND server = ?')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { releaseRestartRequest } from '$lib/server/provisioning/index.js';
|
||||
import { db } from '$lib/server/db/index.js';
|
||||
import { getBeta } from '$lib/server/fleet/beta.js';
|
||||
import { activity, currentRun, evidence, readMetrics, saveMetrics, setOutcome, storedPlan } from '$lib/server/fleet/beta-tests.js';
|
||||
@@ -50,6 +51,8 @@ export async function POST({ params, request }: { params: { id: string }; reques
|
||||
if (stored.outcome === 'inconclusive') throw new Error('The testing window ended. Roll back and open a new beta with a longer deadline');
|
||||
if (db().prepare("SELECT 1 FROM beta_servers WHERE beta_id = ? AND error LIKE 'deploy:%'").get(id)) throw new Error('Deployment was incomplete. Roll back before opening a fresh beta');
|
||||
db().transaction(() => {
|
||||
const servers = db().prepare('SELECT server FROM beta_servers WHERE beta_id = ?').all(id) as { server: string }[];
|
||||
for (const { server } of servers) releaseRestartRequest(server, { kind: 'beta', id: String(id) });
|
||||
db().prepare('UPDATE beta_test_runs SET retired_at = ? WHERE beta_id = ? AND retired_at IS NULL').run(new Date().toISOString(), id);
|
||||
db().prepare("UPDATE beta_servers SET stage = 'awaiting-empty', error = NULL, restarted_at = NULL, healthy_at = NULL WHERE beta_id = ? AND deployed_at IS NOT NULL AND error NOT LIKE 'deploy:%'").run(id);
|
||||
// SQL NULL does not satisfy NOT LIKE.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { listDeployments } from '$lib/server/fleet/deployments.js';
|
||||
import { listDeployments } from '$lib/server/provisioning/index.js';
|
||||
export function GET() { return json({ deployments: listDeployments() }); }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { json, error } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db/index.js';
|
||||
import { getDeployment } from '$lib/server/fleet/deployments.js';
|
||||
import { getDeployment } from '$lib/server/provisioning/index.js';
|
||||
|
||||
export async function GET({ params }: { params: { id: string } }) {
|
||||
const lifecycle = getDeployment(params.id);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { cancelRemoval } from '$lib/server/fleet/deployments.js';
|
||||
import { cancelRemoval } from '$lib/server/provisioning/index.js';
|
||||
|
||||
export function POST({ params }: { params: { id: string } }) {
|
||||
if (!cancelRemoval(params.id)) return json({ error: 'Removal has already begun or this request is no longer waiting. Refresh to see its current state.' }, { status: 409 });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { resumeVerification, resumeCleanup, resumeRebuild } from '$lib/server/fleet/deployments.js';
|
||||
import { resumeVerification, resumeCleanup, resumeRebuild } from '$lib/server/provisioning/index.js';
|
||||
import { kick } from '$lib/server/pipeline/worker.js';
|
||||
|
||||
export function POST({ params }: { params: { id: string } }) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { previewCreation } from '$lib/server/fleet/create.js';
|
||||
import { previewCreation } from '$lib/server/provisioning/index.js';
|
||||
|
||||
export async function POST({ request }: { request: Request }) {
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { listDeployments } from '$lib/server/fleet/deployments.js';
|
||||
import { requestLifecycle } from '$lib/server/fleet/lifecycle.js';
|
||||
import { listDeployments } from '$lib/server/provisioning/index.js';
|
||||
import { requestLifecycle } from '$lib/server/provisioning/index.js';
|
||||
import { runtimeActions, type RuntimeAction } from '$lib/provisioning.js';
|
||||
import { kick } from '$lib/server/pipeline/worker.js';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { json } from '@sveltejs/kit';
|
||||
import { listServers, heldByBeta } from '$lib/server/fleet/servers.js';
|
||||
import { classifyHealth } from '$lib/server/fleet/health.js';
|
||||
import { isProvisioning } from '$lib/server/fleet/provisioning.js';
|
||||
import { queueCreateServer } from '$lib/server/fleet/create.js';
|
||||
import { queueCreateServer } from '$lib/server/provisioning/index.js';
|
||||
import { config, komodoConfigured, rconConfigured } from '$lib/server/config.js';
|
||||
import type { RconPlayer } from '$lib/server/integrations/rcon.js';
|
||||
import type { GameMode, ZombieGame } from '$lib/server/fleet/map-aliases.js';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { kick } from '$lib/server/pipeline/worker.js';
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { destroyServer } from '$lib/server/fleet/create.js';
|
||||
import { destroyServer } from '$lib/server/provisioning/index.js';
|
||||
|
||||
export async function DELETE({ params }: { params: { name: string } }) {
|
||||
const name = params.name;
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
import EmptyState from '$ui/components/primitives/EmptyState.svelte';
|
||||
|
||||
interface BetaRow { id:number; state:string; opened_at:string; closed_at:string|null; wait_for_health:number; wait_for_match:number; notes?:string|null; }
|
||||
interface BetaServer { server:string; stage:string; error:string|null; deployed_at:string|null; restarted_at:string|null; healthy_at:string|null; match_seen_at:string|null; }
|
||||
interface BetaServer { server:string; stage:string; error:string|null; deployed_at:string|null; restarted_at:string|null; healthy_at:string|null; match_seen_at:string|null; provisioning_id:string|null; }
|
||||
interface BetaFile { id:number; name:string; kind:string; target_rel_path:string; version:{id:number; version:number; release_version:string|null; sha256:string}|null; }
|
||||
interface Promotion { id:string; pending:number; failed:number; }
|
||||
let test = $state<BetaTestView|null>(null);
|
||||
@@ -152,7 +152,7 @@
|
||||
{#if test}<div id="test-results"><Card padding="lg"><BetaTestProgress {test} betaId={id} {closed} deadline={deadline?.toISOString()} retryBlocked={servers.some(server=>server.error?.startsWith('deploy:')) ? 'Deployment was incomplete. Roll back before opening a fresh beta.' : undefined} onUpdate={load} /></Card></div>{/if}
|
||||
<details class="ui-panel"><summary>Files and versions ({scripts.length})</summary><div class="file-list">{#each scripts as file}<div><a href={`/${file.kind === 'baseline' ? 'configs' : file.kind + 's'}/${file.id}`}>{file.name}</a><strong>{artifactVersionLabel(file.version)}</strong><code>{file.target_rel_path}</code><span>{file.version?.sha256.slice(0,12)}</span></div>{/each}</div></details>
|
||||
<details class="ui-panel"><summary>Deployment progress ({servers.length} servers)</summary><div class="server-list">
|
||||
{#each ranked as server}<div class="server-row"><a href={`/servers/${encodeURIComponent(server.server)}`}><ServerIdentity name={server.server}/></a><StatusBadge state={server.stage}/><p>Files {server.deployed_at ? 'installed' : 'pending'} · Restart {server.restarted_at ? 'completed' : 'pending'}{#if beta.wait_for_health} · Health {server.healthy_at ? 'verified' : 'pending'}{/if}{#if beta.wait_for_match} · Match {server.match_seen_at ? 'observed' : 'pending'}{/if}</p>{#if server.error}<p class="ui-error">{server.error}</p>{/if}{#if server.stage === 'failed' && !test && !closed}<Button size="sm" onclick={() => retry(server.server)} disabled={acting !== null} loading={acting === `retry:${server.server}`}>Retry server</Button>{/if}</div>{/each}
|
||||
{#each ranked as server}<div class="server-row"><a href={`/servers/${encodeURIComponent(server.server)}`}><ServerIdentity name={server.server}/></a><StatusBadge state={server.stage}/><p>Files {server.deployed_at ? 'installed' : 'pending'} · Restart {server.restarted_at ? 'submitted' : 'pending'}{#if beta.wait_for_health} · Health {server.healthy_at ? 'verified' : 'pending'}{/if}{#if beta.wait_for_match} · Match {server.match_seen_at ? 'observed' : 'pending'}{/if}</p>{#if server.provisioning_id}<a href={`/provisioning/${encodeURIComponent(server.provisioning_id)}`}>View provisioning</a>{/if}{#if server.error}<p class="ui-error">{server.error}</p>{/if}{#if server.stage === 'failed' && !test && !closed}<Button size="sm" onclick={() => retry(server.server)} disabled={acting !== null} loading={acting === `retry:${server.server}`}>Retry server</Button>{/if}</div>{/each}
|
||||
</div></details>
|
||||
<OperationHistory {events} active={!closed} showOverview={false}/>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
interface ServerRow {
|
||||
deployment_id: string; server: string; files_changed: number;
|
||||
restart_state: string; players: number | null;
|
||||
checked_at: string | null; restarted_at: string | null;
|
||||
checked_at: string | null; restarted_at: string | null; provisioning_id: string | null;
|
||||
attempts: number; error: string | null;
|
||||
}
|
||||
interface Detail {
|
||||
@@ -148,6 +148,7 @@
|
||||
</div>
|
||||
<div class="srv-r">
|
||||
<StatusBadge state={s.restart_state} />
|
||||
{#if s.provisioning_id}<a href="/provisioning/{encodeURIComponent(s.provisioning_id)}">View provisioning</a>{/if}
|
||||
{#if s.players !== null && s.players > 0}
|
||||
<span class="players">{s.players} players at last check</span>
|
||||
{/if}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { redirect, error } from '@sveltejs/kit';
|
||||
import { getDeployment } from '$lib/server/fleet/deployments.js';
|
||||
import { getDeployment } from '$lib/server/provisioning/index.js';
|
||||
import { db } from '$lib/server/db/index.js';
|
||||
export function load({ params }: { params: { id: string } }) {
|
||||
const detail = getDeployment(params.id);
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
stage: string; started_at: string; finished_at: string | null;
|
||||
triggered_by: string; error: string | null; warnings: string; server_exists: number;
|
||||
}
|
||||
interface Detail { deployment: Deployment; events: { id: number; created_at: string; message: string }[]; recipe: LaunchRecipeSummary | null; blueprintName: string | null; canResumeVerification: boolean; canResumeCleanup: boolean; canResumeRebuild: boolean; canCancelRemoval: boolean; collection: { id: string; name: string } | null }
|
||||
interface Detail { deployment: Deployment; events: { id: number; created_at: string; message: string }[]; sources: { kind: 'campaign' | 'beta' | 'maintenance'; source_id: string }[]; recipe: LaunchRecipeSummary | null; blueprintName: string | null; canResumeVerification: boolean; canResumeCleanup: boolean; canResumeRebuild: boolean; canCancelRemoval: boolean; collection: { id: string; name: string } | null }
|
||||
let detail = $state<Detail | null>(null);
|
||||
let error = $state('');
|
||||
let refreshing = false;
|
||||
@@ -152,6 +152,7 @@
|
||||
{#each warnings as warning}<p class="warning">{warning}</p>{/each}
|
||||
{#if deployment.expected_map}<p>Expected map: <strong>{deployment.expected_map}</strong></p>{/if}
|
||||
<dl><div><dt>Started</dt><dd>{fmt(deployment.started_at)}</dd></div><div><dt>Finished</dt><dd>{deployment.finished_at ? fmt(deployment.finished_at) : 'In progress'}</dd></div><div><dt>Requested by</dt><dd>{deployment.triggered_by}</dd></div></dl>
|
||||
{#if detail?.sources.length}<div class="recovery-links" aria-label="Requesting work">{#each detail.sources as source}<a href={source.kind === 'campaign' ? `/campaigns/${encodeURIComponent(source.source_id)}` : source.kind === 'beta' ? `/betas/${encodeURIComponent(source.source_id)}` : '/settings#plutonium'}>{source.kind === 'campaign' ? 'Campaign' : source.kind === 'beta' ? 'Beta' : 'Maintenance'} {source.source_id}</a>{/each}</div>{/if}
|
||||
</section>
|
||||
{#if detail?.recipe}<section class="saved-recipe"><div class="top"><h2>{detail.blueprintName ? `Launched from ${detail.blueprintName}` : 'Recorded launch files'}</h2><Button variant="ghost" onclick={() => open = true}>Launch another server →</Button></div><p class="muted">Launch this map with the latest promoted files using a fresh server ID and port. This does not repair the existing server.</p><LaunchRecipe recipe={detail.recipe} historical /></section><DeployServerDialog bind:open sourceDeploymentId={deployment.id} />{/if}
|
||||
<OperationHistory steps={steps.map((label,index): OperationStep => ({label,state:deployment.state === 'cancelled' ? 'pending' : index < currentStep ? 'done' : index === currentStep ? (!active && ['failed','interrupted'].includes(deployment.state) ? 'failed' : 'current') : 'pending'}))} events={detail?.events ?? []} {active} />
|
||||
|
||||
Reference in New Issue
Block a user