feat: add corebuild worker selection

This commit is contained in:
2026-05-07 19:49:56 +00:00
parent c55eb5b832
commit d9dcc5b048
5 changed files with 171 additions and 36 deletions
+147 -31
View File
@@ -14,6 +14,7 @@ import {
type IBaseOsImageArtifact,
type IBaseOsImageBuildPublic,
type TBaseOsImageArchitecture,
type TBaseOsImageKind,
} from './classes.baseosimagebuild.js';
interface IBaseOsRegisterRequest {
@@ -52,6 +53,7 @@ interface IRequestGetBaseOsNodes {
interface IBaseOsImageBuildRequest {
architecture: TBaseOsImageArchitecture;
imageKind?: TBaseOsImageKind;
cloudlyUrl?: string;
sourceImageUrl?: string;
ubuntuVersion?: string;
@@ -119,6 +121,17 @@ interface ICoreBuildCapabilitiesResponse {
workerId: string;
supportedBuildTypes: string[];
supportedArchitectures: TBaseOsImageArchitecture[];
supportedImageKinds?: TBaseOsImageKind[];
}
interface ICoreBuildWorkerSetting {
id?: string;
url: string;
token?: string;
}
interface ISelectedCoreBuildWorker extends ICoreBuildWorkerSetting {
capabilities: ICoreBuildCapabilitiesResponse;
}
export class CloudlyBaseOsManager {
@@ -403,12 +416,11 @@ export class CloudlyBaseOsManager {
throw new plugins.typedrequest.TypedResponseError('Cloudly S3 storage is required for BaseOS image builds');
}
const workerUrl = await this.cloudlyRef.settingsManager.getSetting('corebuildWorkerUrl');
const workerToken = await this.cloudlyRef.settingsManager.getSetting('corebuildWorkerToken');
if (!workerUrl) {
throw new plugins.typedrequest.TypedResponseError('corebuildWorkerUrl is not configured in Cloudly settings');
const imageKind = this.getImageKind(buildRequestArg);
if (imageKind === 'balena-raw' && !buildRequestArg.sourceImageUrl) {
throw new plugins.typedrequest.TypedResponseError('sourceImageUrl is required for balena-raw BaseOS image builds');
}
await this.assertWorkerSupportsImageBuild(workerUrl, workerToken, buildRequestArg.architecture);
const worker = await this.selectCoreBuildWorker(buildRequestArg.architecture, imageKind);
const now = Date.now();
const buildId = await this.CBaseOsImageBuild.getNewId();
@@ -420,6 +432,7 @@ export class CloudlyBaseOsManager {
data: {
status: 'queued',
architecture: buildRequestArg.architecture,
imageKind,
cloudlyUrl: buildRequestArg.cloudlyUrl || this.getPublicCloudlyUrl(),
sourceImageUrl: buildRequestArg.sourceImageUrl,
ubuntuVersion: buildRequestArg.ubuntuVersion || '24.04',
@@ -434,7 +447,7 @@ export class CloudlyBaseOsManager {
});
await build.save();
this.executeImageBuild(build, provisioningToken, buildRequestArg, workerUrl, workerToken).catch(async (error) => {
this.executeImageBuild(build, provisioningToken, buildRequestArg, worker).catch(async (error) => {
build.data.status = 'failed';
build.data.errorText = (error as Error).message;
build.data.updatedAt = Date.now();
@@ -554,33 +567,32 @@ export class CloudlyBaseOsManager {
buildArg: BaseOsImageBuild,
provisioningTokenArg: string,
buildRequestArg: IBaseOsImageBuildRequest,
workerUrlArg: string,
workerTokenArg?: string,
workerArg: ISelectedCoreBuildWorker,
) {
buildArg.data.status = 'building';
buildArg.data.startedAt = Date.now();
buildArg.data.updatedAt = Date.now();
await buildArg.save();
const artifactFilename = buildArg.data.architecture === 'amd64'
? 'baseos.iso'
: buildArg.data.architecture === 'arm64'
? 'baseos-arm64.iso'
: 'baseos-rpi.img';
const artifactFilename = this.getArtifactFilename(
buildArg.data.architecture,
buildArg.data.imageKind || 'ubuntu-iso',
);
const artifactKey = `corebuild/baseos/${buildArg.id}/${artifactFilename}`;
const response = await fetch(
this.getCoreBuildUrl(workerUrlArg, '/corebuild/v1/jobs/baseos-image'),
this.getCoreBuildUrl(workerArg.url, '/corebuild/v1/jobs/baseos-image'),
{
method: 'POST',
headers: {
'content-type': 'application/json',
...(workerTokenArg ? { authorization: `Bearer ${workerTokenArg}` } : {}),
...(workerArg.token ? { authorization: `Bearer ${workerArg.token}` } : {}),
},
body: JSON.stringify({
apiToken: workerTokenArg,
apiToken: workerArg.token,
job: {
id: buildArg.id,
architecture: buildArg.data.architecture,
imageKind: buildArg.data.imageKind,
cloudlyUrl: buildArg.data.cloudlyUrl,
provisioningToken: provisioningTokenArg,
sourceImageUrl: buildArg.data.sourceImageUrl,
@@ -617,16 +629,98 @@ export class CloudlyBaseOsManager {
await buildArg.save();
}
private async assertWorkerSupportsImageBuild(
workerUrlArg: string,
workerTokenArg: string | undefined,
private async selectCoreBuildWorker(
architectureArg: TBaseOsImageArchitecture,
) {
imageKindArg: TBaseOsImageKind,
): Promise<ISelectedCoreBuildWorker> {
const workers = await this.getConfiguredCoreBuildWorkers();
if (workers.length === 0) {
throw new plugins.typedrequest.TypedResponseError('No CoreBuild workers are configured in Cloudly settings');
}
const rejectionReasons: string[] = [];
for (const worker of workers) {
try {
const capabilities = await this.fetchWorkerCapabilities(worker);
const workerLabel = capabilities.workerId || worker.id || worker.url;
const supportedImageKinds = capabilities.supportedImageKinds || ['ubuntu-iso'];
if (!capabilities.supportedBuildTypes?.includes('baseos-image')) {
rejectionReasons.push(`${workerLabel}: missing baseos-image support`);
continue;
}
if (!capabilities.supportedArchitectures?.includes(architectureArg)) {
rejectionReasons.push(`${workerLabel}: missing ${architectureArg} support`);
continue;
}
if (!supportedImageKinds.includes(imageKindArg)) {
rejectionReasons.push(`${workerLabel}: missing ${imageKindArg} support`);
continue;
}
return {
...worker,
capabilities,
};
} catch (error) {
rejectionReasons.push(`${worker.id || worker.url}: ${(error as Error).message}`);
}
}
throw new plugins.typedrequest.TypedResponseError(
`No CoreBuild worker supports BaseOS ${architectureArg} ${imageKindArg} builds. ${rejectionReasons.join('; ')}`,
);
}
private async getConfiguredCoreBuildWorkers(): Promise<ICoreBuildWorkerSetting[]> {
const workers: ICoreBuildWorkerSetting[] = [];
const workersJson = await this.cloudlyRef.settingsManager.getSetting('corebuildWorkersJson');
if (workersJson) {
try {
const parsedWorkers = JSON.parse(workersJson) as unknown;
if (!Array.isArray(parsedWorkers)) {
throw new Error('corebuildWorkersJson must be a JSON array');
}
for (const worker of parsedWorkers) {
if (typeof worker === 'string' && worker) {
workers.push({ url: worker });
} else if (this.isCoreBuildWorkerSetting(worker)) {
workers.push(worker);
} else {
throw new Error('Each CoreBuild worker must be a URL string or object with a url field');
}
}
} catch (error) {
throw new plugins.typedrequest.TypedResponseError(
`corebuildWorkersJson is invalid: ${(error as Error).message}`,
);
}
}
const legacyWorkerUrl = await this.cloudlyRef.settingsManager.getSetting('corebuildWorkerUrl');
const legacyWorkerToken = await this.cloudlyRef.settingsManager.getSetting('corebuildWorkerToken');
if (legacyWorkerUrl) {
workers.push({
id: 'default',
url: legacyWorkerUrl,
token: legacyWorkerToken,
});
}
const seenUrls = new Set<string>();
return workers.filter((workerArg) => {
if (seenUrls.has(workerArg.url)) {
return false;
}
seenUrls.add(workerArg.url);
return true;
});
}
private async fetchWorkerCapabilities(workerArg: ICoreBuildWorkerSetting) {
const response = await fetch(
this.getCoreBuildUrl(workerUrlArg, '/corebuild/v1/capabilities'),
this.getCoreBuildUrl(workerArg.url, '/corebuild/v1/capabilities'),
{
method: 'GET',
headers: workerTokenArg ? { authorization: `Bearer ${workerTokenArg}` } : {},
headers: workerArg.token ? { authorization: `Bearer ${workerArg.token}` } : {},
},
);
if (!response.ok) {
@@ -634,17 +728,39 @@ export class CloudlyBaseOsManager {
`CoreBuild capabilities request failed with HTTP ${response.status}`,
);
}
const capabilities = await response.json() as ICoreBuildCapabilitiesResponse;
if (!capabilities.supportedBuildTypes?.includes('baseos-image')) {
throw new plugins.typedrequest.TypedResponseError(
`CoreBuild worker ${capabilities.workerId} does not support BaseOS image builds`,
);
return await response.json() as ICoreBuildCapabilitiesResponse;
}
private getImageKind(buildRequestArg: IBaseOsImageBuildRequest): TBaseOsImageKind {
if (buildRequestArg.imageKind) {
return buildRequestArg.imageKind;
}
if (!capabilities.supportedArchitectures?.includes(architectureArg)) {
throw new plugins.typedrequest.TypedResponseError(
`CoreBuild worker ${capabilities.workerId} does not support BaseOS ${architectureArg} builds`,
);
if (buildRequestArg.architecture === 'rpi') {
return 'balena-raw';
}
if (buildRequestArg.sourceImageUrl && /\.(img|img\.xz|zip)(\?|$)/i.test(buildRequestArg.sourceImageUrl)) {
return 'balena-raw';
}
return 'ubuntu-iso';
}
private getArtifactFilename(
architectureArg: TBaseOsImageArchitecture,
imageKindArg: TBaseOsImageKind,
) {
const architectureSuffix = architectureArg === 'amd64' ? '' : `-${architectureArg}`;
if (imageKindArg === 'balena-raw') {
return `baseos${architectureSuffix}.img.xz`;
}
return `baseos${architectureSuffix}.iso`;
}
private isCoreBuildWorkerSetting(valueArg: unknown): valueArg is ICoreBuildWorkerSetting {
return Boolean(valueArg)
&& typeof valueArg === 'object'
&& typeof (valueArg as ICoreBuildWorkerSetting).url === 'string'
&& (!(valueArg as ICoreBuildWorkerSetting).token || typeof (valueArg as ICoreBuildWorkerSetting).token === 'string')
&& (!(valueArg as ICoreBuildWorkerSetting).id || typeof (valueArg as ICoreBuildWorkerSetting).id === 'string');
}
private getCoreBuildUrl(workerUrlArg: string, pathArg: string) {