2026-05-07 19:49:57 +00:00
import { createServer , type Server } from 'node:http' ;
import { execFile } from 'node:child_process' ;
import { createReadStream , createWriteStream , mkdirSync , rmSync } from 'node:fs' ;
import { dirname , join , resolve } from 'node:path' ;
import { pipeline } from 'node:stream/promises' ;
import { fileURLToPath } from 'node:url' ;
import { promisify } from 'node:util' ;
import { tapNodeTools } from '@git.zone/tstest/tapbundle_node' ;
import { SmartNetwork } from '@push.rocks/smartnetwork' ;
import { CoreBuildServer } from '../../../corebuild/ts/index.js' ;
import { smartbucket } from '../../../corebuild/ts/plugins.js' ;
const execFileAsync = promisify ( execFile ) ;
const scenarioDir = dirname ( fileURLToPath ( import . meta . url ) ) ;
const testingDir = resolve ( scenarioDir , '../..' ) ;
const repoRoot = resolve ( testingDir , '..' ) ;
const smokeId = ` baseos-image- ${ Date . now ( ) . toString ( 36 ) } ` ;
const buildDir = join ( testingDir , '.nogit' , 'baseos-image-pipeline' , smokeId ) ;
const bucketName = ` baseos-image- ${ Date . now ( ) . toString ( 36 ) } ` ;
const apiToken = 'corebuild-test-token' ;
const run = async ( commandArg : string , argsArg : string [ ] ) = > {
const { stdout , stderr } = await execFileAsync ( commandArg , argsArg , {
maxBuffer : 1024 * 1024 * 20 ,
} ) ;
if ( stdout . trim ( ) ) {
console . log ( stdout . trim ( ) ) ;
}
if ( stderr . trim ( ) ) {
console . log ( stderr . trim ( ) ) ;
}
} ;
const commandExists = async ( commandArg : string ) = > {
try {
await execFileAsync ( 'bash' , [ '-lc' , ` command -v ${ commandArg } ` ] ) ;
return true ;
} catch {
return false ;
}
} ;
const assert = ( conditionArg : unknown , messageArg : string ) = > {
if ( ! conditionArg ) {
throw new Error ( messageArg ) ;
}
} ;
const isLibguestfsUnavailable = ( errorArg : unknown ) = > {
const errorText = ` ${ ( errorArg as { message? : string ; stderr? : string } ).message || ''} \ n ${
( errorArg as { stderr? : string } ).stderr || ''
} ` ;
return errorText . includes ( 'libguestfs' ) && errorText . includes ( 'supermin exited with error status' ) ;
} ;
const ensureTools = async ( ) = > {
for ( const command of [ 'qemu-img' , 'guestfish' , 'xz' ] ) {
assert ( await commandExists ( command ) , ` Missing required command for BaseOS image scenario: ${ command } ` ) ;
}
} ;
const createSourceImage = async ( sourceImagePathArg : string ) = > {
await run ( 'qemu-img' , [ 'create' , '-f' , 'raw' , sourceImagePathArg , '64M' ] ) ;
await run ( 'guestfish' , [
'--rw' ,
'--format=raw' ,
'-a' ,
sourceImagePathArg ,
'run' ,
':' ,
'part-init' ,
'/dev/sda' ,
'mbr' ,
':' ,
'part-add' ,
'/dev/sda' ,
'p' ,
'2048' ,
'129024' ,
':' ,
'mkfs' ,
'vfat' ,
'/dev/sda1' ,
':' ,
'mount' ,
'/dev/sda1' ,
'/' ,
':' ,
'write' ,
'/config.json' ,
'{}\n' ,
] ) ;
} ;
const startSourceServer = async ( sourceImagePathArg : string ) = > {
const smartNetwork = new SmartNetwork ( ) ;
const port = Number ( await smartNetwork . findFreePort ( 41000 , 42000 , { randomize : true } ) ) ;
assert ( port , 'Could not find a free source image server port' ) ;
const server = createServer ( ( req , res ) = > {
if ( req . url !== '/source.img' ) {
res . statusCode = 404 ;
res . end ( 'not found' ) ;
return ;
}
res . setHeader ( 'content-type' , 'application/octet-stream' ) ;
createReadStream ( sourceImagePathArg ) . pipe ( res ) ;
} ) ;
await new Promise < void > ( ( resolveArg ) = > server . listen ( { port , host : '127.0.0.1' } , resolveArg ) ) ;
return {
server ,
url : ` http://127.0.0.1: ${ port } /source.img ` ,
} ;
} ;
const stopServer = async ( serverArg : Server ) = > {
await new Promise < void > ( ( resolveArg , rejectArg ) = > {
serverArg . close ( ( errorArg ) = > errorArg ? rejectArg ( errorArg ) : resolveArg ( ) ) ;
} ) ;
} ;
const readGuestFile = async ( imagePathArg : string , filePathArg : string ) = > {
const { stdout } = await execFileAsync ( 'guestfish' , [
'--ro' ,
'--format=raw' ,
'-a' ,
imagePathArg ,
'-m' ,
'/dev/sda1' ,
'cat' ,
filePathArg ,
] , {
maxBuffer : 1024 * 1024 * 5 ,
} ) ;
return stdout ;
} ;
const main = async ( ) = > {
await ensureTools ( ) ;
mkdirSync ( buildDir , { recursive : true } ) ;
const sourceImagePath = join ( buildDir , 'source.img' ) ;
const artifactPath = join ( buildDir , 'artifact.img.xz' ) ;
const decompressedArtifactPath = join ( buildDir , 'artifact.img' ) ;
try {
await createSourceImage ( sourceImagePath ) ;
} catch ( error ) {
if ( isLibguestfsUnavailable ( error ) ) {
console . log ( '[baseos-image-pipeline] Skipping: libguestfs appliance is unavailable on this host' ) ;
if ( ! process . env . SERVEZONE_KEEP_TEST_ARTIFACTS ) {
rmSync ( buildDir , { recursive : true , force : true } ) ;
}
return ;
}
throw error ;
}
const sourceServer = await startSourceServer ( sourceImagePath ) ;
const smarts3 = await tapNodeTools . createSmarts3 ( ) ;
await smarts3 . createBucket ( bucketName ) ;
const smartNetwork = new SmartNetwork ( ) ;
const corebuildPort = Number ( await smartNetwork . findFreePort ( 42001 , 43000 , { randomize : true } ) ) ;
assert ( corebuildPort , 'Could not find a free CoreBuild port' ) ;
const corebuildServer = new CoreBuildServer ( {
port : corebuildPort ,
token : apiToken ,
workdir : join ( buildDir , 'corebuild-workdir' ) ,
isoCreatorCommand : ` deno run --allow-all ${ join ( repoRoot , 'isocreator' , 'mod.ts' ) } ` ,
workerId : 'baseos-image-pipeline-test' ,
} ) ;
try {
await corebuildServer . start ( ) ;
const capabilitiesResponse = await fetch ( ` http://127.0.0.1: ${ corebuildPort } /corebuild/v1/capabilities ` ) ;
const capabilities = await capabilitiesResponse . json ( ) as {
supportedArchitectures : string [ ] ;
supportedImageKinds : string [ ] ;
2026-05-07 20:33:14 +00:00
supportedSourcePresets : string [ ] ;
2026-05-07 19:49:57 +00:00
} ;
assert ( capabilities . supportedArchitectures . includes ( 'rpi' ) , 'CoreBuild did not advertise rpi support' ) ;
assert ( capabilities . supportedImageKinds . includes ( 'balena-raw' ) , 'CoreBuild did not advertise balena-raw support' ) ;
2026-05-07 20:33:14 +00:00
assert ( capabilities . supportedSourcePresets . includes ( 'balena-raspberrypi4-64' ) , 'CoreBuild did not advertise Raspberry Pi balenaOS source preset support' ) ;
2026-05-07 19:49:57 +00:00
const s3Descriptor = await smarts3 . getS3Descriptor ( { bucketName } ) ;
const artifactKey = ` ${ smokeId } /baseos-rpi.img.xz ` ;
const jobResponse = await fetch ( ` http://127.0.0.1: ${ corebuildPort } /corebuild/v1/jobs/baseos-image ` , {
method : 'POST' ,
headers : {
authorization : ` Bearer ${ apiToken } ` ,
'content-type' : 'application/json' ,
} ,
body : JSON.stringify ( {
job : {
id : smokeId ,
architecture : 'rpi' ,
imageKind : 'balena-raw' ,
cloudlyUrl : 'http://cloudly.test.local' ,
provisioningToken : 'join-token-for-baseos-image-test' ,
sourceImageUrl : sourceServer.url ,
hostname : 'baseos-rpi-test' ,
wifi : {
ssid : 'baseos-test-wifi' ,
password : 'baseos-test-password' ,
} ,
sshPublicKey : 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITestKey baseos@test' ,
s3Descriptor ,
artifactKey ,
} ,
} ) ,
} ) ;
const jobResult = await jobResponse . json ( ) as {
success : boolean ;
errorText? : string ;
artifact ? : { key : string ; contentType : string ; filename : string } ;
logs : string [ ] ;
} ;
assert ( jobResponse . ok && jobResult . success , jobResult . errorText || jobResult . logs . join ( '\n' ) ) ;
assert ( jobResult . artifact ? . filename === 'baseos-rpi.img.xz' , 'Unexpected BaseOS artifact filename' ) ;
assert ( jobResult . artifact ? . contentType === 'application/x-xz' , 'Unexpected BaseOS artifact content type' ) ;
const bucketClient = await new smartbucket . SmartBucket ( {
. . . s3Descriptor ,
port : Number ( s3Descriptor . port || 443 ) ,
} as any ) . getBucketByName ( bucketName ) ;
const artifactStream = await bucketClient . fastGetStream ( { path : artifactKey } , 'nodestream' ) as NodeJS . ReadableStream ;
await pipeline ( artifactStream , createWriteStream ( artifactPath ) ) ;
await execFileAsync ( 'bash' , [ '-lc' , 'xz -dc "$1" > "$2"' , 'bash' , artifactPath , decompressedArtifactPath ] ) ;
const configJson = JSON . parse ( await readGuestFile ( decompressedArtifactPath , '/config.json' ) ) as any ;
assert ( configJson . hostname === 'baseos-rpi-test' , 'BaseOS config.json hostname was not written' ) ;
assert ( configJson . os ? . sshKeys ? . length === 1 , 'BaseOS config.json SSH key was not written' ) ;
assert ( configJson . serveZone ? . baseos ? . cloudlyUrl === 'http://cloudly.test.local' , 'Cloudly URL metadata was not written' ) ;
const wifiConnection = await readGuestFile ( decompressedArtifactPath , '/system-connections/baseos-wifi.nmconnection' ) ;
assert ( wifiConnection . includes ( 'ssid=baseos-test-wifi' ) , 'WiFi system connection was not written' ) ;
const baseosEnv = await readGuestFile ( decompressedArtifactPath , '/baseos/baserunner.env' ) ;
assert ( baseosEnv . includes ( 'BASEOS_CLOUDLY_URL="http://cloudly.test.local"' ) , 'BaseOS env Cloudly URL was not written' ) ;
assert ( baseosEnv . includes ( 'BASEOS_JOIN_TOKEN="join-token-for-baseos-image-test"' ) , 'BaseOS env join token was not written' ) ;
2026-05-07 20:33:14 +00:00
assert ( baseosEnv . includes ( 'BASEOS_PRELOAD_TARGET_STATE_PATH="/data/baseos/preload-target-state.json"' ) , 'BaseOS preload target-state path was not written' ) ;
2026-05-07 19:49:57 +00:00
console . log ( '[baseos-image-pipeline] BaseOS raw-image pipeline scenario completed successfully' ) ;
} finally {
await corebuildServer . stop ( ) . catch ( ( ) = > undefined ) ;
await stopServer ( sourceServer . server ) . catch ( ( ) = > undefined ) ;
await smarts3 . stop ( ) . catch ( ( ) = > undefined ) ;
if ( ! process . env . SERVEZONE_KEEP_TEST_ARTIFACTS ) {
rmSync ( buildDir , { recursive : true , force : true } ) ;
}
}
} ;
await main ( ) ;