UNPKG

balena-cli

Version:

The official balena Command Line Interface

408 lines (401 loc) • 17.6 kB
"use strict"; var _a; Object.defineProperty(exports, "__esModule", { value: true }); const errors_1 = require("../../errors"); const cf = require("../../utils/common-flags"); const lazy_1 = require("../../utils/lazy"); const messages_1 = require("../../utils/messages"); const docker_1 = require("../../utils/docker"); const validation_1 = require("../../utils/validation"); const core_1 = require("@oclif/core"); const _ = require("lodash"); class PreloadCmd extends core_1.Command { constructor() { super(...arguments); this.applicationExpandOptions = { owns__release: { $select: ['id', 'commit', 'end_timestamp', 'composition'], $expand: { contains__image: { $select: ['image'], $expand: { image: { $select: ['image_size', 'is_stored_at__image_location'], }, }, }, }, $filter: { status: 'success', }, $orderby: [{ end_timestamp: 'desc' }, { id: 'desc' }], }, should_be_running__release: { $select: 'commit', }, }; } async run() { const { args: params, flags: options } = await this.parse(_a); const balena = (0, lazy_1.getBalenaSdk)(); const balenaPreload = await Promise.resolve().then(() => require('balena-preload')); const visuals = (0, lazy_1.getVisuals)(); const nodeCleanup = await Promise.resolve().then(() => require('node-cleanup')); const { instanceOf } = await Promise.resolve().then(() => require('../../errors')); try { const fs = await Promise.resolve().then(() => require('fs')); await fs.promises.access(params.image); const path = await Promise.resolve().then(() => require('path')); if (path.extname(params.image) === '.zip') { console.warn((0, lazy_1.stripIndent) ` ------------------------------------------------------------------------------ Warning: A zip file is only accepted for the Intel Edison device type. ------------------------------------------------------------------------------ `); } } catch (error) { throw new errors_1.ExpectedError(`The provided image path does not exist: ${params.image}`); } const fleetSlug = options.fleet ? await (await Promise.resolve().then(() => require('../../utils/sdk'))).getFleetSlug(balena, options.fleet) : undefined; const progressBars = {}; const progressHandler = function (event) { var _b; var _c; const progressBar = ((_b = progressBars[_c = event.name]) !== null && _b !== void 0 ? _b : (progressBars[_c] = new visuals.Progress(event.name))); return progressBar.update({ percentage: event.percentage }); }; const spinners = {}; const spinnerHandler = function (event) { var _b; var _c; const spinner = ((_b = spinners[_c = event.name]) !== null && _b !== void 0 ? _b : (spinners[_c] = new visuals.Spinner(event.name))); if (event.action === 'start') { return spinner.start(); } else { console.log(); return spinner.stop(); } }; const commit = this.isCurrentCommit(options.commit || '') ? 'latest' : options.commit; const image = params.image; const splashImage = options['splash-image']; const additionalSpace = options['additional-space']; const dontCheckArch = options['dont-check-arch'] || false; const pinDevice = options['pin-device-to-release']; if (dontCheckArch && !fleetSlug) { throw new errors_1.ExpectedError('You need to specify a fleet if you disable the architecture check.'); } const certificates = options['add-certificate'] || []; for (const certificate of certificates) { if (!certificate.endsWith('.crt')) { throw new errors_1.ExpectedError('Certificate file name must end with ".crt"'); } } const dockerUtils = await Promise.resolve().then(() => require('../../utils/docker')); const docker = await dockerUtils.getDocker(options); const preloader = new balenaPreload.Preloader(undefined, docker, fleetSlug, commit, image, splashImage, undefined, dontCheckArch, pinDevice !== null && pinDevice !== void 0 ? pinDevice : false, certificates, additionalSpace); let gotSignal = false; nodeCleanup(function (_exitCode, signal) { if (signal) { gotSignal = true; nodeCleanup.uninstall(); preloader .cleanup() .then(() => { process.kill(process.pid, signal); }) .catch((e) => { if (process.env.DEBUG) { console.error(e); } }); return false; } }); if (process.env.DEBUG) { preloader.stderr.pipe(process.stderr); } preloader.on('progress', progressHandler); preloader.on('spinner', spinnerHandler); try { await new Promise((resolve, reject) => { preloader.on('error', reject); resolve(this.prepareAndPreload(preloader, balena, { slug: fleetSlug, commit, pinDevice, })); }); } catch (err) { if (instanceOf(err, balena.errors.BalenaError)) { const code = err.code ? `(${err.code})` : ''; throw new errors_1.ExpectedError(`${err.message} ${code}`); } else { throw err; } } finally { if (!gotSignal) { await preloader.cleanup(); } } } isCurrentCommit(commit) { return commit === 'latest' || commit === 'current'; } async getApplicationsWithSuccessfulBuilds(deviceTypeSlug) { const balena = (0, lazy_1.getBalenaSdk)(); try { await balena.models.deviceType.get(deviceTypeSlug); } catch (_b) { throw new Error(`Device type "${deviceTypeSlug}" not found in API query`); } const options = { $select: ['id', 'slug', 'should_track_latest_release'], $expand: this.applicationExpandOptions, $filter: { is_for__device_type: { $any: { $alias: 'dt', $expr: { dt: { is_of__cpu_architecture: { $any: { $alias: 'ioca', $expr: { ioca: { is_supported_by__device_type: { $any: { $alias: 'isbdt', $expr: { isbdt: { slug: deviceTypeSlug, }, }, }, }, }, }, }, }, }, }, }, }, owns__release: { $any: { $alias: 'r', $expr: { r: { status: 'success', }, }, }, }, }, $orderby: 'slug asc', }; return (await balena.models.application.getAllDirectlyAccessible(options)); } async selectApplication(deviceTypeSlug) { const visuals = (0, lazy_1.getVisuals)(); const applicationInfoSpinner = new visuals.Spinner('Downloading list of applications and releases.'); applicationInfoSpinner.start(); const applications = await this.getApplicationsWithSuccessfulBuilds(deviceTypeSlug); applicationInfoSpinner.stop(); if (applications.length === 0) { throw new errors_1.ExpectedError(`No fleets found with successful releases for device type '${deviceTypeSlug}'`); } return (0, lazy_1.getCliForm)().ask({ message: 'Select a fleet', type: 'list', choices: applications.map((app) => ({ name: app.slug, value: app, })), }); } selectApplicationCommit(releases) { if (releases.length === 0) { throw new errors_1.ExpectedError('This fleet has no successful releases.'); } const DEFAULT_CHOICE = { name: 'current', value: 'current' }; const choices = [DEFAULT_CHOICE].concat(releases.map((release) => ({ name: `${release.end_timestamp} - ${release.commit}`, value: release.commit, }))); return (0, lazy_1.getCliForm)().ask({ message: 'Select a release', type: 'list', default: 'current', choices, }); } async offerToDisableAutomaticUpdates(application, commit, pinDevice) { const balena = (0, lazy_1.getBalenaSdk)(); if (this.isCurrentCommit(commit) || !application.should_track_latest_release || pinDevice != null) { return; } const message = `\ This fleet is set to track the latest release, and non-pinned devices are automatically updated when a new release is available. This may lead to unexpected behavior: The preloaded device will download and install the latest release once it is online. This prompt gives you the opportunity to disable automatic updates for this fleet now. Note that this would result in the fleet being pinned to the current latest release, rather than some other release that may have been selected for preloading. The pinned released may be further managed through the web dashboard or programatically through the balena API / SDK. Documentation about release policies and pinning can be found at: https://www.balena.io/docs/learn/deploy/release-strategy/release-policy/ Alternatively, the --pin-device-to-release or --no-pin-device-to-release flags may be used to avoid this interactive confirmation and pin only the preloaded device to the selected release or keep it unpinned respectively. Would you like to disable automatic updates for this fleet now?\ `; const update = await (0, lazy_1.getCliForm)().ask({ message, type: 'confirm', }); if (!update) { return; } return await balena.pine.patch({ resource: 'application', id: application.id, body: { should_track_latest_release: false, }, }); } async getAppWithReleases(balenaSdk, slug) { const { getApplication } = await Promise.resolve().then(() => require('../../utils/sdk')); return await getApplication(balenaSdk, slug, { $expand: this.applicationExpandOptions, }); } async prepareAndPreload(preloader, balenaSdk, options) { var _b; await preloader.prepare(); const application = options.slug ? await this.getAppWithReleases(balenaSdk, options.slug) : await this.selectApplication(preloader.config.deviceType); let commit; const appCommit = (_b = application.should_be_running__release[0]) === null || _b === void 0 ? void 0 : _b.commit; if (options.commit) { if (this.isCurrentCommit(options.commit)) { if (!appCommit) { throw new Error(`Unexpected empty commit hash for fleet slug "${application.slug}"`); } commit = 'latest'; } else { const release = _.find(application.owns__release, (r) => r.commit.startsWith(options.commit)); if (!release) { throw new errors_1.ExpectedError(`There is no release matching commit "${options.commit}"`); } commit = release.commit; } } else { commit = await this.selectApplicationCommit(application.owns__release); } await preloader.setAppIdAndCommit(application.id, this.isCurrentCommit(commit) ? appCommit : commit); await this.offerToDisableAutomaticUpdates(application, commit, options.pinDevice); await preloader.preload(); } } _a = PreloadCmd; PreloadCmd.description = (0, lazy_1.stripIndent) ` Preload a release on a disk image (or Edison zip archive). Preload a release (service images/containers) from a balena fleet, and optionally a balenaOS splash screen, in a previously downloaded '.img' balenaOS image file in the local disk (a zip file is only accepted for the Intel Edison device type). After preloading, the balenaOS image file can be flashed to a device's SD card. When the device boots, it will not need to download the release, as it was preloaded. This is usually combined with release pinning (https://www.balena.io/docs/learn/deploy/release-strategy/release-policy/) to avoid the device downloading a newer release straight away, if available. Check also the Preloading and Preregistering section of the balena CLI's advanced masterclass document: https://www.balena.io/docs/learn/more/masterclasses/advanced-cli/#5-preloading-and-preregistering ${messages_1.applicationIdInfo.split('\n').join('\n\t\t')} Note that the this command requires Docker to be installed, as further detailed in the balena CLI's installation instructions: https://github.com/balena-io/balena-cli/blob/master/INSTALL.md The \`--dockerHost\` and \`--dockerPort\` flags allow a remote Docker engine to be used, however the image file must be accessible to the remote Docker engine on the same path given on the command line. This is because Docker's bind mount feature is used to "share" the image with a container that performs the preload. `; PreloadCmd.examples = [ '$ balena preload balena.img --fleet MyFleet --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0', '$ balena preload balena.img --fleet myorg/myfleet --splash-image image.png', '$ balena preload balena.img', ]; PreloadCmd.args = { image: core_1.Args.string({ description: 'the image file path', required: true, }), }; PreloadCmd.flags = { fleet: cf.fleet, commit: core_1.Flags.string({ description: `\ The commit hash of the release to preload. Use "current" to specify the current release (ignored if no appId is given). The current release is usually also the latest, but can be pinned to a specific release. See: https://www.balena.io/docs/learn/deploy/release-strategy/release-policy/ https://www.balena.io/docs/learn/more/masterclasses/fleet-management/#63-pin-using-the-api https://github.com/balena-io-examples/staged-releases\ `, char: 'c', }), 'splash-image': core_1.Flags.string({ description: 'path to a png image to replace the splash screen', char: 's', }), 'dont-check-arch': core_1.Flags.boolean({ default: false, description: 'disable architecture compatibility check between image and fleet', }), 'pin-device-to-release': core_1.Flags.boolean({ allowNo: true, description: 'pin the preloaded device to the preloaded release on provision', char: 'p', }), 'additional-space': core_1.Flags.integer({ description: 'expand the image by this amount of bytes instead of automatically estimating the required amount', parse: async (x) => (0, validation_1.parseAsInteger)(x, 'additional-space'), }), 'add-certificate': core_1.Flags.string({ description: `\ Add the given certificate (in PEM format) to /etc/ssl/certs in the preloading container. The file name must end with '.crt' and must not be already contained in the preloader's /etc/ssl/certs folder. Can be repeated to add multiple certificates.\ `, multiple: true, }), ...docker_1.dockerConnectionCliFlags, dockerPort: core_1.Flags.integer({ description: 'Docker daemon TCP port number (hint: 2375 for balena devices)', parse: async (p) => (0, validation_1.parseAsInteger)(p, 'dockerPort'), }), }; PreloadCmd.authenticated = true; PreloadCmd.primary = true; exports.default = PreloadCmd; //# sourceMappingURL=index.js.map