feat: harden baseos image builds

This commit is contained in:
2026-05-07 19:04:12 +00:00
parent 1792ea89e1
commit c55eb5b832
+124 -6
View File
@@ -115,9 +115,16 @@ interface ICoreBuildBaseOsImageResponse {
errorText?: string;
}
interface ICoreBuildCapabilitiesResponse {
workerId: string;
supportedBuildTypes: string[];
supportedArchitectures: TBaseOsImageArchitecture[];
}
export class CloudlyBaseOsManager {
public cloudlyRef: Cloudly;
public typedRouter = new plugins.typedrequest.TypedRouter();
private cleanupInterval?: ReturnType<typeof setInterval>;
public get db() {
return this.cloudlyRef.mongodbConnector.smartdataDb;
@@ -205,10 +212,27 @@ export class CloudlyBaseOsManager {
}
public async start() {
await this.cleanupExpiredImageBuilds().catch((error) => {
logger.log('error', `BaseOS image cleanup failed: ${(error as Error).message}`);
});
const cleanupIntervalMs = Number(
process.env.CLOUDLY_BASEOS_IMAGE_CLEANUP_INTERVAL_MS || 1000 * 60 * 60 * 12,
);
if (Number.isFinite(cleanupIntervalMs) && cleanupIntervalMs > 0) {
this.cleanupInterval = setInterval(() => {
this.cleanupExpiredImageBuilds().catch((error) => {
logger.log('error', `BaseOS image cleanup failed: ${(error as Error).message}`);
});
}, cleanupIntervalMs);
}
logger.log('info', 'BaseOS manager started');
}
public async stop() {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = undefined;
}
logger.log('info', 'BaseOS manager stopped');
}
@@ -267,11 +291,7 @@ export class CloudlyBaseOsManager {
}
const artifact = build.data.artifact;
const smartbucket = new plugins.smartbucket.SmartBucket({
...this.cloudlyRef.config.data.s3Descriptor,
port: Number((this.cloudlyRef.config.data.s3Descriptor as any).port || 443),
} as any);
const bucket = await smartbucket.getBucketByName(artifact.bucketName);
const bucket = await this.getArtifactBucket(artifact.bucketName);
const artifactStream = await bucket.fastGetStream({ path: artifact.key }, 'nodestream');
resArg.status(200);
resArg.setHeader('Content-Type', artifact.contentType || 'application/octet-stream');
@@ -388,6 +408,7 @@ export class CloudlyBaseOsManager {
if (!workerUrl) {
throw new plugins.typedrequest.TypedResponseError('corebuildWorkerUrl is not configured in Cloudly settings');
}
await this.assertWorkerSupportsImageBuild(workerUrl, workerToken, buildRequestArg.architecture);
const now = Date.now();
const buildId = await this.CBaseOsImageBuild.getNewId();
@@ -448,6 +469,56 @@ export class CloudlyBaseOsManager {
};
}
public async cleanupExpiredImageBuilds() {
const now = Date.now();
const builds = await this.CBaseOsImageBuild.getInstances({});
let deletedBuilds = 0;
let deletedArtifacts = 0;
let clearedDownloadTokens = 0;
for (const build of builds) {
let shouldSave = false;
if (build.downloadTokenExpiresAt && build.downloadTokenExpiresAt <= now) {
build.downloadTokenHash = undefined;
build.downloadTokenExpiresAt = undefined;
clearedDownloadTokens++;
shouldSave = true;
}
const isExpired = Boolean(build.data.expiresAt && build.data.expiresAt <= now);
const canDeleteBuild = build.data.status === 'ready'
|| build.data.status === 'failed'
|| build.data.status === 'cancelled';
if (isExpired && canDeleteBuild) {
if (build.data.artifact) {
await this.deleteImageArtifact(build.data.artifact);
deletedArtifacts++;
}
await build.delete();
deletedBuilds++;
continue;
}
if (shouldSave) {
build.data.updatedAt = now;
await build.save();
}
}
if (deletedBuilds > 0 || deletedArtifacts > 0 || clearedDownloadTokens > 0) {
logger.log(
'info',
`BaseOS image cleanup completed: ${deletedBuilds} builds, ${deletedArtifacts} artifacts, ${clearedDownloadTokens} download tokens`,
);
}
return {
deletedBuilds,
deletedArtifacts,
clearedDownloadTokens,
};
}
private async upsertNode(statusArg: IBaseOsRuntimeInfo, nodeTokenArg: string) {
const now = Date.now();
let node = await this.CBaseOsNode.getInstance({
@@ -498,7 +569,7 @@ export class CloudlyBaseOsManager {
: 'baseos-rpi.img';
const artifactKey = `corebuild/baseos/${buildArg.id}/${artifactFilename}`;
const response = await fetch(
new URL('/corebuild/v1/jobs/baseos-image', workerUrlArg.endsWith('/') ? workerUrlArg : `${workerUrlArg}/`),
this.getCoreBuildUrl(workerUrlArg, '/corebuild/v1/jobs/baseos-image'),
{
method: 'POST',
headers: {
@@ -546,6 +617,53 @@ export class CloudlyBaseOsManager {
await buildArg.save();
}
private async assertWorkerSupportsImageBuild(
workerUrlArg: string,
workerTokenArg: string | undefined,
architectureArg: TBaseOsImageArchitecture,
) {
const response = await fetch(
this.getCoreBuildUrl(workerUrlArg, '/corebuild/v1/capabilities'),
{
method: 'GET',
headers: workerTokenArg ? { authorization: `Bearer ${workerTokenArg}` } : {},
},
);
if (!response.ok) {
throw new plugins.typedrequest.TypedResponseError(
`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`,
);
}
if (!capabilities.supportedArchitectures?.includes(architectureArg)) {
throw new plugins.typedrequest.TypedResponseError(
`CoreBuild worker ${capabilities.workerId} does not support BaseOS ${architectureArg} builds`,
);
}
}
private getCoreBuildUrl(workerUrlArg: string, pathArg: string) {
return new URL(pathArg, workerUrlArg.endsWith('/') ? workerUrlArg : `${workerUrlArg}/`);
}
private async deleteImageArtifact(artifactArg: IBaseOsImageArtifact) {
const bucket = await this.getArtifactBucket(artifactArg.bucketName);
await bucket.fastRemove({ path: artifactArg.key });
}
private async getArtifactBucket(bucketNameArg: string) {
const smartbucket = new plugins.smartbucket.SmartBucket({
...this.cloudlyRef.config.data.s3Descriptor,
port: Number((this.cloudlyRef.config.data.s3Descriptor as any).port || 443),
} as any);
return await smartbucket.getBucketByName(bucketNameArg);
}
private async getImageBuildById(buildIdArg: string) {
const build = await this.CBaseOsImageBuild.getInstance({ id: buildIdArg });
if (!build) {