balena-cli
Version:
The official balena Command Line Interface
425 lines • 15.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BuildProgressInline = exports.BuildProgressUI = exports.pushProgressRenderer = exports.authorizePush = exports.getPreviousRepos = exports.tagServiceImages = exports.createRelease = void 0;
exports.generateOpts = generateOpts;
exports.createProject = createProject;
const path = require("path");
const lazy_1 = require("./lazy");
function generateOpts(options) {
const { promises: fs } = require('fs');
return fs.realpath(options.source || '.').then((projectPath) => ({
projectName: options.projectName,
projectPath,
inlineLogs: !options.nologs,
convertEol: !options['noconvert-eol'],
dockerfilePath: options.dockerfile,
multiDockerignore: !!options['multi-dockerignore'],
noParentCheck: options['noparent-check'],
}));
}
function createProject(composePath, composeStr, projectName = '', imageTag = '') {
const yml = require('js-yaml');
const compose = require('@balena/compose/dist/parse');
const rawComposition = yml.load(composeStr);
const composition = compose.normalize(rawComposition);
projectName || (projectName = path.basename(composePath));
const descriptors = compose.parse(composition).map(function (descr) {
if (typeof descr.image !== 'string' &&
descr.image.context != null &&
descr.image.tag == null) {
const { makeImageName } = require('./compose_ts');
descr.image.tag = makeImageName(projectName, descr.serviceName, imageTag);
}
return descr;
});
return {
path: composePath,
name: projectName,
composition,
descriptors,
};
}
const getRequestRetryParameters = () => {
if (process.env.BALENA_CLI_TEST_TYPE != null &&
process.env.BALENA_CLI_TEST_TYPE !== '') {
const { intVar } = require('@balena/env-parsing');
return {
minDelayMs: intVar('BALENARCTEST_API_RETRY_MIN_DELAY_MS'),
maxDelayMs: intVar('BALENARCTEST_API_RETRY_MAX_DELAY_MS'),
maxAttempts: intVar('BALENARCTEST_API_RETRY_MAX_ATTEMPTS'),
};
}
return {
minDelayMs: 1000,
maxDelayMs: 60000,
maxAttempts: 7,
};
};
const createRelease = async function (sdk, logger, appId, composition, draft, semver, contract) {
const _ = require('lodash');
const crypto = require('crypto');
const releaseMod = require('@balena/compose/dist/release');
const pinejsClient = sdk.pine.clone({
retry: {
...getRequestRetryParameters(),
onRetry: (err, delayMs, attempt, maxAttempts) => {
var _a;
const code = (_a = err === null || err === void 0 ? void 0 : err.statusCode) !== null && _a !== void 0 ? _a : 0;
logger.logDebug(`API call failed with code ${code}. Attempting retry ${attempt} of ${maxAttempts} in ${delayMs / 1000} seconds`);
},
},
}, {
apiVersion: 'v7',
});
const { id: userId } = await sdk.auth.getUserInfo();
const { release, serviceImages } = await releaseMod.create({
client: pinejsClient,
user: userId,
application: appId,
composition,
source: 'local',
commit: crypto.pseudoRandomBytes(16).toString('hex').toLowerCase(),
semver,
is_final: !draft,
contract,
});
return {
client: pinejsClient,
release: _.pick(release, [
'id',
'status',
'commit',
'composition',
'source',
'is_final',
'contract',
'semver',
'start_timestamp',
'end_timestamp',
]),
serviceImages: _.mapValues(serviceImages, (serviceImage) => _.omit(serviceImage, ['created_at', 'is_a_build_of__service'])),
};
};
exports.createRelease = createRelease;
const tagServiceImages = (docker, images, serviceImages) => Promise.all(images.map(function (d) {
const serviceImage = serviceImages[d.serviceName];
const imageName = serviceImage.is_stored_at__image_location;
const match = /(.*?)\/(.*?)(?::([^/]*))?$/.exec(imageName);
if (match == null) {
throw new Error(`Could not parse imageName: '${imageName}'`);
}
const [, registry, repo, tag = 'latest'] = match;
const name = `${registry}/${repo}`;
return docker
.getImage(d.name)
.tag({ repo: name, tag, force: true })
.then(() => docker.getImage(`${name}:${tag}`))
.then((localImage) => ({
serviceName: d.serviceName,
serviceImage,
localImage,
registry,
repo,
logs: d.logs,
props: d.props,
}));
}));
exports.tagServiceImages = tagServiceImages;
const getPreviousRepos = (sdk, logger, appID) => sdk.pine
.get({
resource: 'release',
options: {
$select: 'id',
$filter: {
belongs_to__application: appID,
status: 'success',
},
$expand: {
contains__image: {
$select: 'image',
$expand: { image: { $select: 'is_stored_at__image_location' } },
},
},
$orderby: 'id desc',
$top: 1,
},
})
.then(function (release) {
if (release.length > 0) {
const images = release[0].contains__image;
const { getRegistryAndName } = require('@balena/compose/dist/multibuild');
return Promise.all(images.map(function (d) {
const imageName = d.image[0].is_stored_at__image_location || '';
const registry = getRegistryAndName(imageName);
logger.logDebug(`Requesting access to previously pushed image repo (${registry.imageName})`);
return registry.imageName;
}));
}
else {
return [];
}
})
.catch((e) => {
logger.logDebug(`Failed to access previously pushed image repo: ${e}`);
return [];
});
exports.getPreviousRepos = getPreviousRepos;
const authorizePush = function (sdk, tokenAuthEndpoint, registry, images, previousRepos) {
if (!Array.isArray(images)) {
images = [images];
}
images.push(...previousRepos);
return sdk.request
.send({
baseUrl: tokenAuthEndpoint,
url: '/auth/v1/token',
qs: {
service: registry,
scope: images.map((repo) => `repository:${repo}:pull,push`),
},
})
.then(({ body }) => body.token)
.catch(() => '');
};
exports.authorizePush = authorizePush;
const formatDuration = (seconds) => {
const SECONDS_PER_MINUTE = 60;
const SECONDS_PER_HOUR = 3600;
const hours = Math.floor(seconds / SECONDS_PER_HOUR);
seconds %= SECONDS_PER_HOUR;
const minutes = Math.floor(seconds / SECONDS_PER_MINUTE);
seconds = Math.floor(seconds % SECONDS_PER_MINUTE);
return hours > 0
? `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
: `${minutes}:${seconds.toString().padStart(2, '0')}`;
};
const renderProgressBar = function (percentage, stepCount) {
const _ = require('lodash');
percentage = _.clamp(percentage, 0, 100);
const barCount = Math.floor((stepCount * percentage) / 100);
const spaceCount = stepCount - barCount;
const bar = `[${_.repeat('=', barCount)}>${_.repeat(' ', spaceCount)}]`;
return `${bar} ${_.padStart(`${percentage}`, 3)}%`;
};
const pushProgressRenderer = function (tty, prefix) {
const fn = function (e) {
const { error, percentage } = e;
if (error != null) {
throw new Error(error);
}
const bar = renderProgressBar(percentage, 40);
return tty.replaceLine(`${prefix}${bar}\r`);
};
fn.end = () => {
tty.clearLine();
};
return fn;
};
exports.pushProgressRenderer = pushProgressRenderer;
class BuildProgressUI {
constructor(tty, descriptors) {
this._serviceToDataMap = {};
this._lineWidths = [];
this._handleEvent = this._handleEvent.bind(this);
this.start = this.start.bind(this);
this.end = this.end.bind(this);
this._display = this._display.bind(this);
const _ = require('lodash');
const through = require('through2');
const eventHandler = this._handleEvent;
const services = _.map(descriptors, 'serviceName');
const streams = _(services)
.map(function (service) {
const stream = through.obj(function (event, _enc, cb) {
eventHandler(service, event);
return cb();
});
stream.pipe(tty.stream, { end: false });
return [service, stream];
})
.fromPairs()
.value();
this._tty = tty;
this._services = services;
const prefix = (0, lazy_1.getChalk)().blue('[Build]') + ' ';
const offset = 10;
this._prefixWidth =
offset + prefix.length + _.max(_.map(services, (s) => s.length));
this._prefix = prefix;
this._ended = false;
this._cancelled = false;
this._spinner = require('./compose_ts').createSpinner();
this.streams = streams;
}
_handleEvent(service, event) {
this._serviceToDataMap[service] = event;
}
start() {
this._tty.hideCursor();
this._services.forEach((service) => {
this.streams[service].write({ status: 'Preparing...' });
});
this._runloop = require('./compose_ts').createRunLoop(this._display);
this._startTime = Date.now();
}
end(summary) {
var _a;
if (this._ended) {
return;
}
this._ended = true;
(_a = this._runloop) === null || _a === void 0 ? void 0 : _a.end();
this._runloop = undefined;
this._clear();
this._renderStatus(true);
this._renderSummary(summary !== null && summary !== void 0 ? summary : this._getServiceSummary());
this._tty.showCursor();
}
_display() {
this._clear();
this._renderStatus();
this._renderSummary(this._getServiceSummary());
this._tty.cursorUp(this._services.length + 1);
}
_clear() {
this._tty.deleteToEnd();
this._maxLineWidth = this._tty.currentWindowSize().width;
}
_getServiceSummary() {
const _ = require('lodash');
const services = this._services;
const serviceToDataMap = this._serviceToDataMap;
return _(services)
.map(function (service) {
var _a;
const { status, progress, error } = (_a = serviceToDataMap[service]) !== null && _a !== void 0 ? _a : {};
if (error) {
return `${error}`;
}
else if (progress) {
const bar = renderProgressBar(progress, 20);
if (status) {
return `${bar} ${status}`;
}
return `${bar}`;
}
else if (status) {
return `${status}`;
}
else {
return 'Waiting...';
}
})
.map((data, index) => [services[index], data])
.fromPairs()
.value();
}
_renderStatus(end = false) {
this._tty.clearLine();
this._tty.write(this._prefix);
if (end && this._cancelled) {
this._tty.writeLine('Build cancelled');
}
else if (end) {
const serviceCount = this._services.length;
const serviceStr = serviceCount === 1 ? '1 service' : `${serviceCount} services`;
const durationStr = this._startTime == null
? 'unknown time'
: formatDuration((Date.now() - this._startTime) / 1000);
this._tty.writeLine(`Built ${serviceStr} in ${durationStr}`);
}
else {
this._tty.writeLine(`Building services... ${this._spinner()}`);
}
}
_renderSummary(serviceToStrMap) {
const _ = require('lodash');
const chalk = (0, lazy_1.getChalk)();
const truncate = require('cli-truncate');
const strlen = require('string-width');
this._services.forEach((service, index) => {
let str = _.padEnd(this._prefix + chalk.bold(service), this._prefixWidth);
str += serviceToStrMap[service];
if (this._maxLineWidth != null) {
str = truncate(str, this._maxLineWidth);
}
this._lineWidths[index] = strlen(str);
this._tty.clearLine();
this._tty.writeLine(str);
});
}
}
exports.BuildProgressUI = BuildProgressUI;
class BuildProgressInline {
constructor(outStream, descriptors) {
this.start = this.start.bind(this);
this.end = this.end.bind(this);
this._renderEvent = this._renderEvent.bind(this);
const _ = require('lodash');
const through = require('through2');
const services = _.map(descriptors, 'serviceName');
const eventHandler = this._renderEvent;
const streams = _(services)
.map(function (service) {
const stream = through.obj(function (event, _enc, cb) {
eventHandler(service, event);
return cb();
});
stream.pipe(outStream, { end: false });
return [service, stream];
})
.fromPairs()
.value();
const offset = 10;
this._prefixWidth = offset + _.max(_.map(services, (s) => s.length));
this._outStream = outStream;
this._services = services;
this._ended = false;
this.streams = streams;
}
start() {
this._outStream.write('Building services...\n');
this._services.forEach((service) => {
this.streams[service].write({ status: 'Preparing...' });
});
this._startTime = Date.now();
}
end(summary) {
if (this._ended) {
return;
}
this._ended = true;
if (summary != null) {
this._services.forEach((service) => {
this._renderEvent(service, { status: summary[service] });
});
}
const serviceCount = this._services.length;
const serviceStr = serviceCount === 1 ? '1 service' : `${serviceCount} services`;
const durationStr = this._startTime == null
? 'unknown time'
: formatDuration((Date.now() - this._startTime) / 1000);
this._outStream.write(`Built ${serviceStr} in ${durationStr}\n`);
}
_renderEvent(service, event) {
const _ = require('lodash');
const str = (function () {
const { status, error } = event;
if (error) {
return `${error}`;
}
else if (status) {
return `${status}`;
}
else {
return 'Waiting...';
}
})();
const prefix = _.padEnd((0, lazy_1.getChalk)().bold(service), this._prefixWidth);
this._outStream.write(prefix);
this._outStream.write(str);
this._outStream.write('\n');
}
}
exports.BuildProgressInline = BuildProgressInline;
//# sourceMappingURL=compose.js.map