feat(base-images): add managed base image bundles with cache retention, hosted manifests, and opt-in integration boot testing
This commit is contained in:
+47
-36
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user