UNPKG

balena-cli

Version:

The official balena Command Line Interface

287 lines • 9.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RemoteBuildFailedError = void 0; exports.startRemoteBuild = startRemoteBuild; const JSONStream = require("JSONStream"); const readline = require("readline"); const request = require("request"); const streamToPromise = require("stream-to-promise"); const errors_1 = require("../errors"); const compose_ts_1 = require("./compose_ts"); const lazy_1 = require("./lazy"); const Logger = require("./logger"); const globalLogger = Logger.getLogger(); const DEBUG_MODE = !!process.env.DEBUG; const CURSOR_METADATA_REGEX = /([a-z]+)([0-9]+)?/; const TRIM_REGEX = /\n+$/; class RemoteBuildFailedError extends errors_1.ExpectedError { constructor(message = 'Remote build failed') { super(message); } } exports.RemoteBuildFailedError = RemoteBuildFailedError; async function getBuilderEndpoint(baseUrl, appSlug, opts) { const querystring = await Promise.resolve().then(() => require('querystring')); const args = querystring.stringify({ slug: appSlug, dockerfilePath: opts.dockerfilePath, emulated: opts.emulated, nocache: opts.nocache, headless: opts.headless, isdraft: opts.isDraft, }); let builderUrl = process.env.BALENARC_BUILDER_URL || `https://builder.${baseUrl}`; if (builderUrl.endsWith('/')) { builderUrl = builderUrl.slice(0, -1); } return `${builderUrl}/v3/build?${args}`; } async function startRemoteBuild(build) { const [buildRequest, stream] = await getRemoteBuildStream(build); let cancellationPromise = Promise.resolve(); const sigintHandler = () => { process.exitCode = 130; console.error('\nReceived SIGINT, cleaning up. Please wait.'); try { cancellationPromise = cancelBuildIfNecessary(build); } catch (err) { console.error(err.message); } finally { buildRequest.abort(); const sigintErr = new errors_1.SIGINTError('Build aborted on SIGINT signal'); sigintErr.code = 'SIGINT'; stream.emit('error', sigintErr); } }; const { addSIGINTHandler } = await Promise.resolve().then(() => require('./helpers')); addSIGINTHandler(sigintHandler); try { if (build.opts.headless) { await handleHeadlessBuildStream(build, stream); } else { await handleRemoteBuildStream(build, stream); } } finally { process.removeListener('SIGINT', sigintHandler); globalLogger.outputDeferredMessages(); await cancellationPromise; } return build.releaseId; } async function handleRemoteBuildStream(build, stream) { await new Promise((resolve, reject) => { const msgHandler = getBuilderMessageHandler(build); stream.on('data', msgHandler); stream.once('end', resolve); stream.once('error', reject); }); if (build.hadError) { throw new RemoteBuildFailedError(); } } async function handleHeadlessBuildStream(build, stream) { let message; try { const response = await streamToPromise(stream); message = JSON.parse(response.toString()); } catch (e) { if (e.code === 'SIGINT') { throw e; } throw new Error(`There was an error reading the response from the remote builder: ${e}`); } if (!process.stdout.isTTY) { process.stdout.write(JSON.stringify(message)); return; } if (message.started) { console.log('Build successfully started'); console.log(` Release ID: ${message.releaseId}`); build.releaseId = message.releaseId; } else { console.log('Failed to start remote build'); console.log(` Error: ${message.error}`); console.log(` Message: ${message.message}`); } } function handleBuilderMetadata(obj, build) { switch (obj.resource) { case 'cursor': { if (obj.value == null) { return; } const match = obj.value.match(CURSOR_METADATA_REGEX); if (!match) { console.log((0, lazy_1.stripIndent) ` Warning: ignoring unknown builder command. You may experience odd build output. Maybe you need to update balena-cli?`); return; } const value = match[1]; const amount = Number(match[2]) || 1; switch (value) { case 'erase': readline.clearLine(process.stdout, 0); process.stdout.write('\r'); break; case 'up': readline.moveCursor(process.stdout, 0, -amount); break; case 'down': readline.moveCursor(process.stdout, 0, amount); break; } break; } case 'buildLogId': build.releaseId = parseInt(obj.value, 10); break; } } function getBuilderMessageHandler(build) { return (obj) => { if (DEBUG_MODE) { console.error(`[debug] handling message: ${JSON.stringify(obj)}`); } if (obj.type != null && obj.type === 'metadata') { return handleBuilderMetadata(obj, build); } if (obj.message) { readline.clearLine(process.stdout, 0); const message = obj.message.replace(TRIM_REGEX, ''); if (obj.replace) { process.stdout.write(`\r${message}`); } else { process.stdout.write(`\r${message}\n`); } } if (obj.isError) { build.hadError = true; } }; } async function cancelBuildIfNecessary(build) { if (build.releaseId != null) { console.error(`Setting 'cancelled' release status for release ID ${build.releaseId} ...`); await build.sdk.pine.patch({ resource: 'release', id: build.releaseId, options: { $filter: { status: { $ne: 'success' }, }, }, body: { status: 'cancelled', end_timestamp: Date.now(), }, }); } } async function getTarStream(build) { let tarSpinner = { start: () => { }, stop: () => { }, }; if (process.stdout.isTTY) { const visuals = (0, lazy_1.getVisuals)(); tarSpinner = new visuals.Spinner('Packaging the project source...'); } const path = await Promise.resolve().then(() => require('path')); const preFinalizeCallback = (pack) => { pack.entry({ name: '.balena/registry-secrets.json' }, JSON.stringify(build.opts.registrySecrets)); }; try { tarSpinner.start(); const preFinalizeCb = Object.keys(build.opts.registrySecrets).length > 0 ? preFinalizeCallback : undefined; globalLogger.logDebug('Tarring all non-ignored files...'); const tarStartTime = Date.now(); const tarStream = await (0, compose_ts_1.tarDirectory)(path.resolve(build.source), { preFinalizeCallback: preFinalizeCb, convertEol: build.opts.convertEol, multiDockerignore: build.opts.multiDockerignore, }); globalLogger.logDebug(`Tarring complete in ${Date.now() - tarStartTime} ms`); return tarStream; } finally { tarSpinner.stop(); } } function createRemoteBuildRequest(build, tarStream, builderUrl, onError) { const zlib = require('zlib'); if (DEBUG_MODE) { console.error(`[debug] Connecting to builder at ${builderUrl}`); } return request .post({ url: builderUrl, auth: { bearer: build.auth }, headers: { 'Content-Encoding': 'gzip' }, body: tarStream.pipe(zlib.createGzip({ level: 6 })), }) .once('error', onError) .once('response', (response) => { if (response.statusCode >= 100 && response.statusCode < 400) { if (DEBUG_MODE) { console.error(`[debug] received HTTP ${response.statusCode} ${response.statusMessage}`); } } else { const msgArr = [ 'Remote builder responded with HTTP error:', `${response.statusCode} ${response.statusMessage}`, ]; if (response.body) { msgArr.push(response.body); } onError(new errors_1.ExpectedError(msgArr.join('\n'))); } }); } async function getRemoteBuildStream(build) { const builderUrl = await getBuilderEndpoint(build.baseUrl, build.appSlug, build.opts); let stream; let uploadSpinner = { stop: () => { }, }; const onError = (error) => { uploadSpinner.stop(); if (stream) { stream.emit('error', error); } }; if (process.stdout.isTTY) { const visuals = (0, lazy_1.getVisuals)(); uploadSpinner = new visuals.Spinner(`Uploading source package to ${new URL(builderUrl).origin}`); uploadSpinner.start(); } const tarStream = await getTarStream(build); const buildRequest = createRemoteBuildRequest(build, tarStream, builderUrl, onError); if (build.opts.headless) { stream = buildRequest; } else { stream = buildRequest.pipe(JSONStream.parse('*')); } stream = stream .once('error', () => uploadSpinner.stop()) .once('close', () => uploadSpinner.stop()) .once('data', () => uploadSpinner.stop()) .once('end', () => uploadSpinner.stop()) .once('finish', () => uploadSpinner.stop()); return [buildRequest, stream]; } //# sourceMappingURL=remote-build.js.map