Files
corebuild/ts/classes.corebuildserver.ts
T
2026-05-07 20:33:14 +00:00

143 lines
4.6 KiB
TypeScript

import * as http from 'node:http';
import * as os from 'node:os';
import { BaseOsImageBuilder } from './classes.baseosimagebuilder.js';
import type {
IBaseOsImageJobRequest,
IBaseOsImageJobResponse,
ICoreBuildCapabilities,
} from './types.js';
export interface ICoreBuildServerOptions {
port: number;
token?: string;
workdir: string;
isoCreatorCommand: string;
workerId: string;
}
export class CoreBuildServer {
private server?: http.Server;
private builder: BaseOsImageBuilder;
public static fromEnv() {
return new CoreBuildServer({
port: Number(process.env.COREBUILD_PORT || '3060'),
token: process.env.COREBUILD_TOKEN,
workdir: process.env.COREBUILD_WORKDIR || BaseOsImageBuilder.getDefaultWorkdir(),
isoCreatorCommand: process.env.ISO_CREATOR_COMMAND || 'isocreator',
workerId: process.env.COREBUILD_WORKER_ID || BaseOsImageBuilder.getDefaultWorkerId(),
});
}
constructor(private options: ICoreBuildServerOptions) {
this.builder = new BaseOsImageBuilder({
workdir: options.workdir,
isoCreatorCommand: options.isoCreatorCommand,
});
}
public async start() {
this.server = http.createServer(async (req, res) => {
await this.handleRequest(req, res).catch((error) => {
this.sendJson(res, 500, {
success: false,
errorText: (error as Error).message,
});
});
});
await new Promise<void>((resolve) => this.server!.listen(this.options.port, resolve));
console.log(`corebuild listening on ${this.options.port}`);
}
public async stop() {
if (!this.server) {
return;
}
await new Promise<void>((resolve, reject) => {
this.server!.close((error) => error ? reject(error) : resolve());
});
}
private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse) {
const url = new URL(req.url || '/', 'http://localhost');
if (req.method === 'GET' && url.pathname === '/health') {
this.sendJson(res, 200, { ok: true });
return;
}
if (req.method === 'GET' && url.pathname === '/corebuild/v1/capabilities') {
this.sendJson(res, 200, this.getCapabilities());
return;
}
if (req.method === 'POST' && url.pathname === '/corebuild/v1/jobs/baseos-image') {
const requestBody = await this.readJson<IBaseOsImageJobRequest>(req);
this.validateToken(req, requestBody.apiToken);
const response = await this.handleBaseOsImageJob(requestBody);
this.sendJson(res, response.success ? 200 : 500, response);
return;
}
this.sendJson(res, 404, { success: false, errorText: 'not found' });
}
private async handleBaseOsImageJob(
requestArg: IBaseOsImageJobRequest,
): Promise<IBaseOsImageJobResponse> {
const logs: string[] = [];
try {
const result = await this.builder.build(requestArg.job);
return {
success: true,
artifact: result.artifact,
logs: result.logs,
};
} catch (error) {
logs.push((error as Error).message);
return {
success: false,
logs,
errorText: (error as Error).message,
};
}
}
private getCapabilities(): ICoreBuildCapabilities {
return {
workerId: this.options.workerId,
supportedBuildTypes: ['baseos-image'],
supportedArchitectures: ['amd64', 'arm64', 'rpi'],
supportedImageKinds: ['ubuntu-iso', 'balena-raw'],
supportedSourcePresets: ['balena-generic-amd64', 'balena-generic-aarch64', 'balena-raspberrypi4-64'],
cpuCores: os.cpus().length,
memoryGb: Math.round(os.totalmem() / 1024 / 1024 / 1024),
workdir: this.options.workdir,
};
}
private validateToken(reqArg: http.IncomingMessage, bodyTokenArg?: string) {
if (!this.options.token) {
return;
}
const authHeader = reqArg.headers.authorization;
const headerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice('Bearer '.length) : undefined;
const token = bodyTokenArg || headerToken || reqArg.headers['x-corebuild-token'];
if (token !== this.options.token) {
throw new Error('corebuild token is invalid');
}
}
private async readJson<T>(reqArg: http.IncomingMessage): Promise<T> {
const chunks: Buffer[] = [];
for await (const chunk of reqArg) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
const body = Buffer.concat(chunks).toString('utf8').trim();
return body ? JSON.parse(body) as T : {} as T;
}
private sendJson(resArg: http.ServerResponse, statusCodeArg: number, bodyArg: object) {
resArg.statusCode = statusCodeArg;
resArg.setHeader('Content-Type', 'application/json');
resArg.end(JSON.stringify(bodyArg));
}
}