This commit is contained in:
@@ -156,7 +156,10 @@ export interface HostInventory {
|
||||
dockerVersion?: string;
|
||||
dockerRunning?: boolean;
|
||||
directoriesWritable?: boolean;
|
||||
/** Agent-reported Docker cache; removed from presented host inventory. */
|
||||
runtimeImages?: string[];
|
||||
/** Prepared, locally available image references eligible for launches. */
|
||||
preparedRuntimeImages?: string[];
|
||||
libraries?: HostLibrary[];
|
||||
currentOperation?: string | null;
|
||||
[key: string]: unknown;
|
||||
|
||||
@@ -198,7 +198,7 @@ export function launchAvailability(selections: NativePlacement[]): {
|
||||
);
|
||||
const eligible = (candidate: HostAvailability, selection: NativePlacement) =>
|
||||
(!selection.hostId || selection.automatic || candidate.host.id === selection.hostId) &&
|
||||
!!candidate.host.inventory.runtimeImages?.includes(selection.image ?? '') &&
|
||||
!!candidate.host.inventory.preparedRuntimeImages?.includes(selection.image ?? '') &&
|
||||
!!candidate.host.inventory.libraries?.some(
|
||||
(library) =>
|
||||
library.id === selection.libraryId && ['ready', 'outdated'].includes(library.state)
|
||||
|
||||
@@ -66,7 +66,7 @@ export async function readCapacityCurrent(query: CapacityQuery): Promise<Capacit
|
||||
if (!host.credentialActive && host.enrolled)
|
||||
reasons.push('Host credentials are inactive.');
|
||||
if (host.readiness !== 'ready') reasons.push(...host.readinessReasons);
|
||||
if (!host.inventory.runtimeImages?.includes(config.defaultRuntimeImage))
|
||||
if (!host.inventory.preparedRuntimeImages?.includes(config.defaultRuntimeImage))
|
||||
reasons.push('The default runtime is not ready.');
|
||||
if (!requestedGames.some((game) => games.includes(game)))
|
||||
reasons.push('Latest game files for this selection are not ready.');
|
||||
|
||||
@@ -140,7 +140,7 @@ function hostFromRow(row: HostRow): Host {
|
||||
)
|
||||
reasons.push('Insufficient available game storage.');
|
||||
const infrastructureReady = reasons.length === 0;
|
||||
const runtimeReady = !!inventory.runtimeImages?.length;
|
||||
const runtimeReady = !!inventory.preparedRuntimeImages?.length;
|
||||
const libraryReady = !!inventory.libraries?.some((library) => library.state === 'ready');
|
||||
if (!runtimeReady) reasons.push('The Plutainer runtime image is not ready yet.');
|
||||
if (!libraryReady) reasons.push('No verified game-file library is available yet.');
|
||||
@@ -676,12 +676,14 @@ export function validateInventory(value: unknown): HostInventory {
|
||||
for (const key of ['dockerRunning', 'directoriesWritable'])
|
||||
if (inventory[key] !== undefined && typeof inventory[key] !== 'boolean')
|
||||
throw new Error(`Invalid inventory ${key}.`);
|
||||
if (
|
||||
inventory.runtimeImages !== undefined &&
|
||||
(!Array.isArray(inventory.runtimeImages) ||
|
||||
inventory.runtimeImages.some((image) => typeof image !== 'string'))
|
||||
)
|
||||
throw new Error('Invalid runtime image inventory.');
|
||||
for (const key of ['runtimeImages', 'preparedRuntimeImages'] as const) {
|
||||
const images = inventory[key];
|
||||
if (
|
||||
images !== undefined &&
|
||||
(!Array.isArray(images) || images.some((image) => typeof image !== 'string'))
|
||||
)
|
||||
throw new Error('Invalid runtime image inventory.');
|
||||
}
|
||||
if (
|
||||
inventory.capabilities !== undefined &&
|
||||
(!Array.isArray(inventory.capabilities) ||
|
||||
@@ -952,8 +954,8 @@ export function requireReadyHost(
|
||||
)
|
||||
)
|
||||
reasons.push('The selected game library is not ready on this host.');
|
||||
if (options.image && !host.inventory.runtimeImages?.includes(options.image))
|
||||
reasons.push('The selected runtime image is not cached on this host.');
|
||||
if (options.image && !host.inventory.preparedRuntimeImages?.includes(options.image))
|
||||
reasons.push('The selected runtime image is not prepared on this host.');
|
||||
if (
|
||||
options.port !== undefined &&
|
||||
!hostPortAvailable(hostId, options.port, options.bindAddress, options.protocol)
|
||||
|
||||
@@ -190,7 +190,7 @@ function targets(host: Host): Target[] {
|
||||
const result: Target[] = [config.defaultRuntimeImage].map((image) => {
|
||||
const operation = latestOperation(host.id, 'runtime.pull', 'image', image);
|
||||
const runtimeReady =
|
||||
host.inventory.runtimeImages?.includes(image) && verifiedThisEnable(operation);
|
||||
host.inventory.preparedRuntimeImages?.includes(image) && verifiedThisEnable(operation);
|
||||
return {
|
||||
image,
|
||||
requiredBytes: host.minimumFreeBytes,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AsyncLocalStorage } from 'node:async_hooks';
|
||||
import { config } from '../config.js';
|
||||
import type { HostInventory } from '../../hosts.js';
|
||||
|
||||
type InventoryCache = Map<string, { json: string; inventory: HostInventory }>;
|
||||
@@ -21,7 +22,20 @@ export function parseHostInventory(hostId: string, json: string): HostInventory
|
||||
const cache = requests.getStore()?.inventories;
|
||||
const saved = cache?.get(hostId);
|
||||
if (saved?.json === json) return saved.inventory;
|
||||
const inventory: HostInventory = JSON.parse(json);
|
||||
// Full Docker inventory can include unrelated workloads. Keep it out of
|
||||
// every presented host, including inventory persisted by older agents.
|
||||
const { runtimeImages, ...inventory }: HostInventory = JSON.parse(json);
|
||||
// The configured default is explicitly trusted by the controller. Older
|
||||
// agents may prove it is cached, but cannot authorize any custom runtime.
|
||||
const reported = inventory.preparedRuntimeImages;
|
||||
const prepared = new Set(
|
||||
Array.isArray(reported) && reported.every((image) => typeof image === 'string')
|
||||
? reported
|
||||
: []
|
||||
);
|
||||
if (runtimeImages?.includes(config.defaultRuntimeImage))
|
||||
prepared.add(config.defaultRuntimeImage);
|
||||
inventory.preparedRuntimeImages = [...prepared];
|
||||
// Compare the stored JSON so heartbeats and transaction rollbacks within
|
||||
// a request remain visible. Other host state is always read afresh.
|
||||
cache?.set(hostId, { json, inventory });
|
||||
|
||||
@@ -136,7 +136,7 @@ export function nativeLaunchSelection(
|
||||
throw new Error('The selected host is being removed. Choose another host.');
|
||||
}
|
||||
// Only the controller-configured default may be prepared automatically.
|
||||
// Custom runtimes must already be present in trusted host inventory; a
|
||||
// Custom runtimes must be reported as admin-prepared by the host agent; a
|
||||
// launch request must never authorize a registry download.
|
||||
if (
|
||||
image !== config.defaultRuntimeImage &&
|
||||
@@ -144,7 +144,7 @@ export function nativeLaunchSelection(
|
||||
(host) =>
|
||||
!host.decommission &&
|
||||
(automatic ? host.mode === 'active' : host.id === input.hostId) &&
|
||||
host.inventory.runtimeImages?.includes(image)
|
||||
host.inventory.preparedRuntimeImages?.includes(image)
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
hosts.filter(
|
||||
(host) =>
|
||||
host.readiness === 'ready' &&
|
||||
host.inventory.runtimeImages?.includes(selection.image)
|
||||
host.inventory.preparedRuntimeImages?.includes(selection.image)
|
||||
)
|
||||
);
|
||||
const latest = $derived(latestGameFiles(libraries));
|
||||
|
||||
Reference in New Issue
Block a user