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((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((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(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 { 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'], 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(reqArg: http.IncomingMessage): Promise { 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)); } }