UNPKG

balena-cli

Version:

The official balena Command Line Interface

425 lines • 15.4 kB
"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