balena-cli
Version:
The official balena Command Line Interface
287 lines • 9.9 kB
JavaScript
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
;