feat(base-images): add managed base image bundles with cache retention, hosted manifests, and opt-in integration boot testing

This commit is contained in:
2026-05-01 13:30:51 +00:00
parent 0ace928886
commit 9d0a57c5de
19 changed files with 2015 additions and 148 deletions
+47 -36
View File
@@ -2,6 +2,10 @@ import * as plugins from './plugins.js';
import type { ISocketClientOptions, IApiResponse } from './interfaces/index.js';
import { SmartVMError } from './interfaces/index.js';
function getErrorMessage(err: unknown): string {
return err instanceof Error ? err.message : String(err);
}
/**
* HTTP client that communicates with Firecracker over a Unix domain socket.
* Uses @push.rocks/smartrequest with the `http://unix:<socket>:<path>` URL format.
@@ -20,6 +24,22 @@ export class SocketClient {
return `http://unix:${this.socketPath}:${apiPath}`;
}
private async parseResponseBody<T>(response: any): Promise<T> {
try {
const text = await response.text();
if (!text) {
return undefined as T;
}
try {
return JSON.parse(text) as T;
} catch {
return text as T;
}
} catch {
return undefined as T;
}
}
/**
* Perform a GET request.
*/
@@ -31,12 +51,7 @@ export class SocketClient {
.get();
const statusCode = response.status;
let body: T;
try {
body = await response.json() as T;
} catch {
body = undefined as any;
}
const body = await this.parseResponseBody<T>(response);
return {
statusCode,
body,
@@ -44,7 +59,7 @@ export class SocketClient {
};
} catch (err) {
throw new SmartVMError(
`GET ${apiPath} failed: ${(err as Error).message}`,
`GET ${apiPath} failed: ${getErrorMessage(err)}`,
'SOCKET_REQUEST_FAILED',
);
}
@@ -54,21 +69,19 @@ export class SocketClient {
* Perform a PUT request with a JSON body.
*/
public async put<T = any>(apiPath: string, body?: Record<string, any>): Promise<IApiResponse<T>> {
const url = this.buildUrl(apiPath);
try {
let request = plugins.SmartRequest.create().url(url);
if (body !== undefined) {
request = request.json(body);
}
const url = this.buildUrl(apiPath);
try {
let request = plugins.SmartRequest.create().url(url);
if (body !== undefined) {
const bodyBuffer = Buffer.from(JSON.stringify(body));
request = request
.buffer(bodyBuffer, 'application/json')
.header('Content-Length', String(bodyBuffer.length));
}
const response = await request.put();
const statusCode = response.status;
let responseBody: T;
try {
responseBody = await response.json() as T;
} catch {
responseBody = undefined as any;
}
const responseBody = await this.parseResponseBody<T>(response);
return {
statusCode,
body: responseBody,
@@ -76,7 +89,7 @@ export class SocketClient {
};
} catch (err) {
throw new SmartVMError(
`PUT ${apiPath} failed: ${(err as Error).message}`,
`PUT ${apiPath} failed: ${getErrorMessage(err)}`,
'SOCKET_REQUEST_FAILED',
);
}
@@ -86,21 +99,19 @@ export class SocketClient {
* Perform a PATCH request with a JSON body.
*/
public async patch<T = any>(apiPath: string, body?: Record<string, any>): Promise<IApiResponse<T>> {
const url = this.buildUrl(apiPath);
try {
let request = plugins.SmartRequest.create().url(url);
if (body !== undefined) {
request = request.json(body);
}
const url = this.buildUrl(apiPath);
try {
let request = plugins.SmartRequest.create().url(url);
if (body !== undefined) {
const bodyBuffer = Buffer.from(JSON.stringify(body));
request = request
.buffer(bodyBuffer, 'application/json')
.header('Content-Length', String(bodyBuffer.length));
}
const response = await request.patch();
const statusCode = response.status;
let responseBody: T;
try {
responseBody = await response.json() as T;
} catch {
responseBody = undefined as any;
}
const responseBody = await this.parseResponseBody<T>(response);
return {
statusCode,
body: responseBody,
@@ -108,21 +119,21 @@ export class SocketClient {
};
} catch (err) {
throw new SmartVMError(
`PATCH ${apiPath} failed: ${(err as Error).message}`,
`PATCH ${apiPath} failed: ${getErrorMessage(err)}`,
'SOCKET_REQUEST_FAILED',
);
}
}
/**
* Check if the Firecracker API socket is ready by polling GET /.
* Check if the Firecracker API socket is ready by polling GET /version.
*/
public async isReady(timeoutMs: number = 5000): Promise<boolean> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
const response = await this.get('/');
if (response.ok || response.statusCode === 200 || response.statusCode === 400) {
const response = await this.get('/version');
if (response.ok) {
return true;
}
} catch {