This commit is contained in:
@@ -23,16 +23,31 @@ afterEach(async () => {
|
||||
await fixture.cleanup();
|
||||
});
|
||||
|
||||
async function cache(images: string[]) {
|
||||
async function reportImages(
|
||||
preparedImages: string[] | undefined,
|
||||
cachedImages = preparedImages ?? []
|
||||
) {
|
||||
const hosts = await import('../../src/lib/server/hosts/index.js');
|
||||
const host = hosts.getHost(prepared.host.id)!;
|
||||
hosts.heartbeat(host.id, { ...host.inventory, runtimeImages: images });
|
||||
hosts.heartbeat(host.id, {
|
||||
...host.inventory,
|
||||
runtimeImages: cachedImages,
|
||||
preparedRuntimeImages: preparedImages
|
||||
});
|
||||
}
|
||||
|
||||
describe('runtime image launch policy', () => {
|
||||
it.each([false, true])(
|
||||
'rejects an uncached custom image at POST /api/servers (automatic: %s)',
|
||||
async (automatic) => {
|
||||
it.each([
|
||||
{ automatic: false, cached: false, legacy: false },
|
||||
{ automatic: true, cached: false, legacy: false },
|
||||
{ automatic: false, cached: true, legacy: false },
|
||||
{ automatic: true, cached: true, legacy: false },
|
||||
{ automatic: false, cached: true, legacy: true },
|
||||
{ automatic: true, cached: true, legacy: true }
|
||||
])(
|
||||
'rejects an unprepared custom image at POST /api/servers ($automatic, cached: $cached, legacy: $legacy)',
|
||||
async ({ automatic, cached, legacy }) => {
|
||||
await reportImages(legacy ? undefined : [], cached ? [custom] : []);
|
||||
const { POST } = await import('../../src/routes/api/servers/+server.js');
|
||||
const response = await POST({
|
||||
request: new Request('https://controller.test/api/servers', {
|
||||
@@ -57,13 +72,88 @@ describe('runtime image launch policy', () => {
|
||||
);
|
||||
|
||||
it('allows the configured default before it is cached', async () => {
|
||||
await cache([]);
|
||||
await reportImages([]);
|
||||
const { queueNativeCreation } = await import('../../src/lib/server/provisioning/native.js');
|
||||
expect(queueNativeCreation(launch).ok).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps the cached configured default eligible with a legacy agent', async () => {
|
||||
const { config } = await import('../../src/lib/server/config.js');
|
||||
await reportImages(undefined, [config.defaultRuntimeImage, custom]);
|
||||
const { nativeSelection } = await import('../../src/lib/server/provisioning/native.js');
|
||||
expect(nativeSelection({}, 't5').hostId).toBe(prepared.host.id);
|
||||
const { getHost } = await import('../../src/lib/server/hosts/index.js');
|
||||
expect(getHost(prepared.host.id)!.inventory.preparedRuntimeImages).toEqual([
|
||||
config.defaultRuntimeImage
|
||||
]);
|
||||
});
|
||||
|
||||
it('ignores malformed preparation fields already stored by an older controller', async () => {
|
||||
const { config } = await import('../../src/lib/server/config.js');
|
||||
const { db } = await import('../../src/lib/server/db/index.js');
|
||||
const { getHost } = await import('../../src/lib/server/hosts/index.js');
|
||||
const inventory = getHost(prepared.host.id)!.inventory;
|
||||
for (const reported of [{}, 42, custom]) {
|
||||
db()
|
||||
.prepare('UPDATE ec_hosts SET inventory=? WHERE id=?')
|
||||
.run(
|
||||
JSON.stringify({
|
||||
...inventory,
|
||||
runtimeImages: [config.defaultRuntimeImage, custom],
|
||||
preparedRuntimeImages: reported
|
||||
}),
|
||||
prepared.host.id
|
||||
);
|
||||
expect(getHost(prepared.host.id)).toMatchObject({
|
||||
readiness: 'ready',
|
||||
inventory: { preparedRuntimeImages: [config.defaultRuntimeImage] }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('hides unrelated cached images from host list, detail, and page responses', async () => {
|
||||
const { config } = await import('../../src/lib/server/config.js');
|
||||
await reportImages(undefined, [config.defaultRuntimeImage, custom]);
|
||||
const list = await import('../../src/routes/api/hosts/+server.js');
|
||||
const detail = await import('../../src/routes/api/hosts/[id]/+server.js');
|
||||
const { load } = await import('../../src/routes/hosts/+page.server.js');
|
||||
const request = new Request('https://controller.test/api/hosts');
|
||||
const bodies = [
|
||||
await list.GET({ request }).json(),
|
||||
await detail.GET({ request, params: { id: prepared.host.id } }).json(),
|
||||
load()
|
||||
];
|
||||
for (const body of bodies) {
|
||||
const host = 'hosts' in body ? body.hosts[0] : body.host;
|
||||
expect(host.inventory.runtimeImages).toBeUndefined();
|
||||
expect(host.inventory.preparedRuntimeImages).toEqual([config.defaultRuntimeImage]);
|
||||
expect(JSON.stringify(body)).not.toContain(custom);
|
||||
}
|
||||
});
|
||||
|
||||
it('does not place an accepted runtime on another host that only caches it', async () => {
|
||||
await reportImages([custom]);
|
||||
const other = await prepareNativeHost();
|
||||
const hosts = await import('../../src/lib/server/hosts/index.js');
|
||||
hosts.heartbeat(other.host.id, {
|
||||
...hosts.getHost(other.host.id)!.inventory,
|
||||
runtimeImages: [custom]
|
||||
});
|
||||
expect(() => hosts.requireReadyHost(other.host.id, { image: custom })).toThrow(
|
||||
/runtime image is not prepared/
|
||||
);
|
||||
const { choosePlacement } = await import('../../src/lib/server/hosts/capacity.js');
|
||||
expect(() =>
|
||||
choosePlacement({
|
||||
hostId: other.host.id,
|
||||
libraryId: other.versions.get('t5')!,
|
||||
image: custom
|
||||
})
|
||||
).toThrow(/runtime image is not prepared/);
|
||||
});
|
||||
|
||||
it('accepts prepared images only on hosts that have them', async () => {
|
||||
await cache([custom]);
|
||||
await reportImages([custom]);
|
||||
const hosts = await import('../../src/lib/server/hosts/index.js');
|
||||
const other = hosts.createHost({ name: 'Unprepared host' });
|
||||
const { nativeLaunchSelection, nativeSelection } =
|
||||
@@ -81,7 +171,7 @@ describe('runtime image launch policy', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('requires exact image references and does not treat a queued admin pull as cached', async () => {
|
||||
it('requires exact prepared references and does not treat a queued admin pull as preparation', async () => {
|
||||
const hosts = await import('../../src/lib/server/hosts/index.js');
|
||||
hosts.queueHostOperation(prepared.host.id, 'runtime.pull', { image: custom });
|
||||
const { nativeLaunchSelection } =
|
||||
@@ -89,25 +179,25 @@ describe('runtime image launch policy', () => {
|
||||
expect(() => nativeLaunchSelection({ image: custom }, 't5')).toThrow(
|
||||
/prepared by an admin/
|
||||
);
|
||||
await cache([custom + '-other']);
|
||||
await reportImages([custom + '-other']);
|
||||
expect(() => nativeLaunchSelection({ image: custom }, 't5')).toThrow(
|
||||
/prepared by an admin/
|
||||
);
|
||||
});
|
||||
|
||||
it('rechecks cached images when submitting a saved single-server preview', async () => {
|
||||
await cache([custom]);
|
||||
it('rechecks preparation when submitting a saved preview even if the image is still cached', async () => {
|
||||
await reportImages([custom]);
|
||||
const { previewNativeCreation, queueNativeCreation } =
|
||||
await import('../../src/lib/server/provisioning/native.js');
|
||||
const preview = previewNativeCreation({ ...launch, image: custom });
|
||||
await cache([]);
|
||||
await reportImages([], [custom]);
|
||||
expect(queueNativeCreation({ ...launch, previewId: preview.id })).toMatchObject({
|
||||
ok: false,
|
||||
message: expect.stringContaining('prepared by an admin')
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects uncached collection images and rechecks saved previews before writing any jobs', async () => {
|
||||
it('rejects unprepared collection images and rechecks saved previews before writing any jobs', async () => {
|
||||
const { saveBlueprint } = await import('../../src/lib/server/fleet/launch-recipes.js');
|
||||
const { previewCollection, queueCollection } =
|
||||
await import('../../src/lib/server/fleet/collections.js');
|
||||
@@ -115,15 +205,16 @@ describe('runtime image launch policy', () => {
|
||||
name: 'Image policy',
|
||||
entries: [{ game: 't5', mapSlug: 'theater', count: 2 }]
|
||||
});
|
||||
await reportImages([], [custom]);
|
||||
expect(() => previewCollection(blueprint.id, 'fill', { image: custom })).toThrow(
|
||||
/prepared by an admin/
|
||||
);
|
||||
await cache([custom]);
|
||||
await reportImages([custom]);
|
||||
const preview = previewCollection(blueprint.id, 'fill', { image: custom });
|
||||
const keys = Object.fromEntries(
|
||||
preview.servers.map((server) => [server.name!, 'key-' + server.name])
|
||||
);
|
||||
await cache([]);
|
||||
await reportImages([], [custom]);
|
||||
expect(() => queueCollection({ previewId: preview.id, keys })).toThrow(
|
||||
/prepared by an admin/
|
||||
);
|
||||
@@ -136,7 +227,7 @@ describe('runtime image launch policy', () => {
|
||||
]) {
|
||||
expect(db().prepare(`SELECT * FROM ${table}`).all()).toEqual([]);
|
||||
}
|
||||
await cache([custom]);
|
||||
await reportImages([custom]);
|
||||
expect(queueCollection({ previewId: preview.id, keys }).ok).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user