142 lines
4.5 KiB
TypeScript
142 lines
4.5 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'],
|
|
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));
|
|
}
|
|
}
|