feat: add corebuild worker selection
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user