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
+51 -27
View File
@@ -2,6 +2,12 @@ import * as plugins from './plugins.js';
import type { TFirecrackerArch } from './interfaces/index.js';
import { SmartVMError } from './interfaces/index.js';
type TShellExecResult = Awaited<ReturnType<InstanceType<typeof plugins.smartshell.Smartshell>['execSpawn']>>;
function getErrorMessage(err: unknown): string {
return err instanceof Error ? err.message : String(err);
}
/**
* Helper to check if a file or directory exists.
*/
@@ -21,10 +27,21 @@ async function pathExists(filePath: string): Promise<boolean> {
export class ImageManager {
private dataDir: string;
private arch: TFirecrackerArch;
private shell: InstanceType<typeof plugins.smartshell.Smartshell>;
constructor(dataDir: string, arch: TFirecrackerArch = 'x86_64') {
this.dataDir = dataDir;
this.arch = arch;
this.shell = new plugins.smartshell.Smartshell({ executor: 'bash' });
}
private async runChecked(command: string, args: string[]): Promise<TShellExecResult> {
const result = await this.shell.execSpawn(command, args, { silent: true });
if (result.exitCode !== 0) {
const output = (result.stderr || result.stdout || '').trim();
throw new Error(`${command} ${args.join(' ')} exited with code ${result.exitCode}${output ? `: ${output}` : ''}`);
}
return result;
}
/**
@@ -89,14 +106,22 @@ export class ImageManager {
*/
public async getLatestVersion(): Promise<string> {
try {
const response = await plugins.SmartRequest.create()
.url('https://api.github.com/repos/firecracker-microvm/firecracker/releases/latest')
.get();
const data = await response.json() as { tag_name: string };
return data.tag_name;
const result = await this.runChecked('curl', [
'-fsSLI',
'-o',
'/dev/null',
'-w',
'%{url_effective}',
'https://github.com/firecracker-microvm/firecracker/releases/latest',
]);
const match = result.stdout.trim().match(/\/releases\/tag\/(v\d+\.\d+\.\d+)$/);
if (!match) {
throw new Error(`Could not parse latest Firecracker release URL: ${result.stdout.trim()}`);
}
return match[1];
} catch (err) {
throw new SmartVMError(
`Failed to fetch latest Firecracker version: ${(err as Error).message}`,
`Failed to fetch latest Firecracker version: ${getErrorMessage(err)}`,
'VERSION_FETCH_FAILED',
);
}
@@ -119,11 +144,10 @@ export class ImageManager {
try {
// Download the archive
const shell = new plugins.smartshell.Smartshell({ executor: 'bash' });
await shell.exec(`curl -fSL -o "${archivePath}" "${downloadUrl}"`);
await this.runChecked('curl', ['-fSL', '-o', archivePath, downloadUrl]);
// Extract the archive
await shell.exec(`tar -xzf "${archivePath}" -C "${targetDir}"`);
await this.runChecked('tar', ['-xzf', archivePath, '-C', targetDir]);
// Firecracker archives contain a directory like release-v1.5.0-x86_64/
// with binaries named like firecracker-v1.5.0-x86_64
@@ -134,21 +158,25 @@ export class ImageManager {
const jailerDst = this.getJailerPath(version);
// Move binaries to expected paths
await shell.exec(`mv "${firecrackerSrc}" "${firecrackerDst}"`);
await plugins.fs.promises.rename(firecrackerSrc, firecrackerDst);
if (await pathExists(jailerSrc)) {
await shell.exec(`mv "${jailerSrc}" "${jailerDst}"`);
await plugins.fs.promises.rename(jailerSrc, jailerDst);
}
// Make executable
await shell.exec(`chmod +x "${firecrackerDst}"`);
await plugins.fs.promises.chmod(firecrackerDst, 0o755);
if (await pathExists(jailerDst)) {
await plugins.fs.promises.chmod(jailerDst, 0o755);
}
// Clean up
await shell.exec(`rm -rf "${archivePath}" "${extractedDir}"`);
await plugins.fs.promises.rm(archivePath, { force: true });
await plugins.fs.promises.rm(extractedDir, { recursive: true, force: true });
return firecrackerDst;
} catch (err) {
throw new SmartVMError(
`Failed to download Firecracker ${version}: ${(err as Error).message}`,
`Failed to download Firecracker ${version}: ${getErrorMessage(err)}`,
'DOWNLOAD_FAILED',
);
}
@@ -163,12 +191,11 @@ export class ImageManager {
const kernelPath = plugins.path.join(kernelsDir, name);
try {
const shell = new plugins.smartshell.Smartshell({ executor: 'bash' });
await shell.exec(`curl -fSL -o "${kernelPath}" "${url}"`);
await this.runChecked('curl', ['-fSL', '-o', kernelPath, url]);
return kernelPath;
} catch (err) {
throw new SmartVMError(
`Failed to download kernel from ${url}: ${(err as Error).message}`,
`Failed to download kernel from ${url}: ${getErrorMessage(err)}`,
'DOWNLOAD_FAILED',
);
}
@@ -183,12 +210,11 @@ export class ImageManager {
const rootfsPath = plugins.path.join(rootfsDir, name);
try {
const shell = new plugins.smartshell.Smartshell({ executor: 'bash' });
await shell.exec(`curl -fSL -o "${rootfsPath}" "${url}"`);
await this.runChecked('curl', ['-fSL', '-o', rootfsPath, url]);
return rootfsPath;
} catch (err) {
throw new SmartVMError(
`Failed to download rootfs from ${url}: ${(err as Error).message}`,
`Failed to download rootfs from ${url}: ${getErrorMessage(err)}`,
'DOWNLOAD_FAILED',
);
}
@@ -203,13 +229,12 @@ export class ImageManager {
const rootfsPath = plugins.path.join(rootfsDir, name);
try {
const shell = new plugins.smartshell.Smartshell({ executor: 'bash' });
await shell.exec(`dd if=/dev/zero of="${rootfsPath}" bs=1M count=${sizeMib}`);
await shell.exec(`mkfs.ext4 "${rootfsPath}"`);
await this.runChecked('dd', ['if=/dev/zero', `of=${rootfsPath}`, 'bs=1M', `count=${sizeMib}`]);
await this.runChecked('mkfs.ext4', [rootfsPath]);
return rootfsPath;
} catch (err) {
throw new SmartVMError(
`Failed to create blank rootfs: ${(err as Error).message}`,
`Failed to create blank rootfs: ${getErrorMessage(err)}`,
'ROOTFS_CREATE_FAILED',
);
}
@@ -224,12 +249,11 @@ export class ImageManager {
const targetPath = plugins.path.join(rootfsDir, targetName);
try {
const shell = new plugins.smartshell.Smartshell({ executor: 'bash' });
await shell.exec(`cp "${sourcePath}" "${targetPath}"`);
await plugins.fs.promises.copyFile(sourcePath, targetPath);
return targetPath;
} catch (err) {
throw new SmartVMError(
`Failed to clone rootfs: ${(err as Error).message}`,
`Failed to clone rootfs: ${getErrorMessage(err)}`,
'ROOTFS_CLONE_FAILED',
);
}