diff --git a/lib/actions-oclif/deploy.ts b/lib/actions-oclif/deploy.ts index c017bb63..f3dd566d 100644 --- a/lib/actions-oclif/deploy.ts +++ b/lib/actions-oclif/deploy.ts @@ -196,7 +196,6 @@ ${dockerignoreHelp} buildOpts: any; // arguments to forward to docker build command }, ) { - const Bluebird = await import('bluebird'); const _ = await import('lodash'); const doodles = await import('resin-doodles'); const sdk = getBalenaSdk(); @@ -206,149 +205,139 @@ ${dockerignoreHelp} const appType = (opts.app?.application_type as ApplicationType[])?.[0]; - return loadProject(logger, composeOpts, opts.image) - .then(function (project) { - if ( - project.descriptors.length > 1 && - !appType?.supports_multicontainer - ) { - throw new ExpectedError( - 'Target application does not support multiple containers. Aborting!', - ); - } + try { + const project = await loadProject(logger, composeOpts, opts.image); + if (project.descriptors.length > 1 && !appType?.supports_multicontainer) { + throw new ExpectedError( + 'Target application does not support multiple containers. Aborting!', + ); + } - // find which services use images that already exist locally - return ( - Bluebird.map(project.descriptors, function (d: any) { - // unconditionally build (or pull) if explicitly requested - if (opts.shouldPerformBuild) { - return d; - } - return docker + // find which services use images that already exist locally + let servicesToSkip = await Promise.all( + project.descriptors.map(async function (d: any) { + // unconditionally build (or pull) if explicitly requested + if (opts.shouldPerformBuild) { + return d; + } + try { + await docker .getImage( (typeof d.image === 'string' ? d.image : d.image.tag) || '', ) - .inspect() - .then(() => { - return d.serviceName; - }) - .catch(() => { - // Ignore - }); - }) - .filter((d) => !!d) - .then(function (servicesToSkip: any[]) { - // multibuild takes in a composition and always attempts to - // build or pull all services. we workaround that here by - // passing a modified composition. - const compositionToBuild = _.cloneDeep(project.composition); - compositionToBuild.services = _.omit( - compositionToBuild.services, - servicesToSkip, - ); - if (_.size(compositionToBuild.services) === 0) { - logger.logInfo( - 'Everything is up to date (use --build to force a rebuild)', - ); - return {}; - } - return compose - .buildProject( - docker, - logger, - project.path, - project.name, - compositionToBuild, - opts.app.arch, - (opts.app?.is_for__device_type as DeviceType[])?.[0].slug, - opts.buildEmulated, - opts.buildOpts, - composeOpts.inlineLogs, - composeOpts.convertEol, - composeOpts.dockerfilePath, - composeOpts.nogitignore, - composeOpts.multiDockerignore, - ) - .then((builtImages) => _.keyBy(builtImages, 'serviceName')); - }) - .then((builtImages: any) => - project.descriptors.map( - (d) => - builtImages[d.serviceName] ?? { - serviceName: d.serviceName, - name: typeof d.image === 'string' ? d.image : d.image.tag, - logs: 'Build skipped; image for service already exists.', - props: {}, - }, - ), - ) - // @ts-ignore slightly different return types of partial vs non-partial release - .then(function (images) { - if (appType?.is_legacy) { - const { deployLegacy } = require('../utils/deploy-legacy'); + .inspect(); - const msg = getChalk().yellow( - 'Target application requires legacy deploy method.', - ); - logger.logWarn(msg); + return d.serviceName; + } catch { + // Ignore + } + }), + ); + servicesToSkip = servicesToSkip.filter((d) => !!d); - return Promise.all([ - sdk.auth.getToken(), - sdk.auth.whoami(), - sdk.settings.get('balenaUrl'), - { - // opts.appName may be prefixed by 'owner/', unlike opts.app.app_name - appName: opts.appName, - imageName: images[0].name, - buildLogs: images[0].logs, - shouldUploadLogs: opts.shouldUploadLogs, - }, - ]) - .then(([token, username, url, options]) => { - return deployLegacy( - docker, - logger, - token, - username, - url, - options, - ); - }) - .then((releaseId) => - sdk.models.release.get(releaseId, { $select: ['commit'] }), - ); - } - return Promise.all([ - sdk.auth.getUserId(), - sdk.auth.getToken(), - sdk.settings.get('apiUrl'), - ]).then(([userId, auth, apiEndpoint]) => - $deployProject( - docker, - logger, - project.composition, - images, - opts.app.id, - userId, - `Bearer ${auth}`, - apiEndpoint, - !opts.shouldUploadLogs, - ), - ); - }) + // multibuild takes in a composition and always attempts to + // build or pull all services. we workaround that here by + // passing a modified composition. + const compositionToBuild = _.cloneDeep(project.composition); + compositionToBuild.services = _.omit( + compositionToBuild.services, + servicesToSkip, + ); + if (_.size(compositionToBuild.services) === 0) { + logger.logInfo( + 'Everything is up to date (use --build to force a rebuild)', ); - }) - .then(function (release: any) { - logger.outputDeferredMessages(); - logger.logSuccess('Deploy succeeded!'); - logger.logSuccess(`Release: ${release.commit}`); - console.log(); - console.log(doodles.getDoodle()); // Show charlie - console.log(); - }) - .catch((err) => { - logger.logError('Deploy failed'); - throw err; - }); + return {}; + } + const builtImages = await compose.buildProject( + docker, + logger, + project.path, + project.name, + compositionToBuild, + opts.app.arch, + (opts.app?.is_for__device_type as DeviceType[])?.[0].slug, + opts.buildEmulated, + opts.buildOpts, + composeOpts.inlineLogs, + composeOpts.convertEol, + composeOpts.dockerfilePath, + composeOpts.nogitignore, + composeOpts.multiDockerignore, + ); + const builtImagesByService = _.keyBy(builtImages, 'serviceName'); + + const images = project.descriptors.map( + (d) => + builtImagesByService[d.serviceName] ?? { + serviceName: d.serviceName, + name: typeof d.image === 'string' ? d.image : d.image.tag, + logs: 'Build skipped; image for service already exists.', + props: {}, + }, + ); + + let release; + if (appType?.is_legacy) { + const { deployLegacy } = require('../utils/deploy-legacy'); + + const msg = getChalk().yellow( + 'Target application requires legacy deploy method.', + ); + logger.logWarn(msg); + + const [token, username, url, options] = await Promise.all([ + sdk.auth.getToken(), + sdk.auth.whoami(), + sdk.settings.get('balenaUrl'), + { + // opts.appName may be prefixed by 'owner/', unlike opts.app.app_name + appName: opts.appName, + imageName: images[0].name, + buildLogs: images[0].logs, + shouldUploadLogs: opts.shouldUploadLogs, + }, + ]); + const releaseId = await deployLegacy( + docker, + logger, + token, + username, + url, + options, + ); + + release = await sdk.models.release.get(releaseId, { + $select: ['commit'], + }); + } else { + const [userId, auth, apiEndpoint] = await Promise.all([ + sdk.auth.getUserId(), + sdk.auth.getToken(), + sdk.settings.get('apiUrl'), + ]); + release = await $deployProject( + docker, + logger, + project.composition, + images, + opts.app.id, + userId, + `Bearer ${auth}`, + apiEndpoint, + !opts.shouldUploadLogs, + ); + } + + logger.outputDeferredMessages(); + logger.logSuccess('Deploy succeeded!'); + logger.logSuccess(`Release: ${release.commit}`); + console.log(); + console.log(doodles.getDoodle()); // Show charlie + console.log(); + } catch (err) { + logger.logError('Deploy failed'); + throw err; + } } } diff --git a/lib/actions-oclif/devices/supported.ts b/lib/actions-oclif/devices/supported.ts index c20c03d8..cae5ec8e 100644 --- a/lib/actions-oclif/devices/supported.ts +++ b/lib/actions-oclif/devices/supported.ts @@ -76,26 +76,23 @@ export default class DevicesSupportedCmd extends Command { public async run() { const { flags: options } = this.parse(DevicesSupportedCmd); - let deviceTypes: Array> = await getBalenaSdk() - .models.config.getDeviceTypes() - .then((dts) => - dts.map((d) => { - if (d.aliases && d.aliases.length) { - // remove aliases that are equal to the slug - d.aliases = d.aliases.filter((alias: string) => alias !== d.slug); - if (!options.json) { - // stringify the aliases array with commas and spaces - d.aliases = [d.aliases.join(', ')]; - } - } else { - // ensure it is always an array (for the benefit of JSON output) - d.aliases = []; + const dts = await getBalenaSdk().models.config.getDeviceTypes(); + let deviceTypes: Array> = dts.map( + (d) => { + if (d.aliases && d.aliases.length) { + // remove aliases that are equal to the slug + d.aliases = d.aliases.filter((alias: string) => alias !== d.slug); + if (!options.json) { + // stringify the aliases array with commas and spaces + d.aliases = [d.aliases.join(', ')]; } - return d; - }), - ); + } else { + // ensure it is always an array (for the benefit of JSON output) + d.aliases = []; + } + return d; + }, + ); if (!options.discontinued) { deviceTypes = deviceTypes.filter((dt) => dt.state !== 'DISCONTINUED'); } diff --git a/lib/actions-oclif/local/configure.ts b/lib/actions-oclif/local/configure.ts index 7506a796..4cbbda6a 100644 --- a/lib/actions-oclif/local/configure.ts +++ b/lib/actions-oclif/local/configure.ts @@ -89,20 +89,17 @@ export default class LocalConfigureCmd extends Command { const dmHandler = (cb: () => void) => reconfix .readConfiguration(configurationSchema, params.target) - .tap((config: any) => { + .then(async (config: any) => { logger.logDebug('Current config:'); logger.logDebug(JSON.stringify(config)); - }) - .then((config: any) => this.getConfiguration(config)) - .tap((config: any) => { + const answers = await this.getConfiguration(config); logger.logDebug('New config:'); - logger.logDebug(JSON.stringify(config)); - }) - .then(async (answers: any) => { + logger.logDebug(JSON.stringify(answers)); + if (!answers.hostname) { await this.removeHostname(configurationSchema); } - return reconfix.writeConfiguration( + return await reconfix.writeConfiguration( configurationSchema, answers, params.target, @@ -220,9 +217,8 @@ export default class LocalConfigureCmd extends Command { persistentLogging: data.persistentLogging || false, }); - return inquirer - .prompt(this.inquirerOptions(data)) - .then((answers: any) => _.merge(data, answers)); + const answers = await inquirer.prompt(this.inquirerOptions(data)); + return _.merge(data, answers); }; // Taken from https://goo.gl/kr1kCt @@ -259,62 +255,50 @@ export default class LocalConfigureCmd extends Command { const _ = await import('lodash'); const imagefs = await import('resin-image-fs'); - return imagefs - .listDirectory({ - image: target, - partition: this.BOOT_PARTITION, - path: this.CONNECTIONS_FOLDER, - }) - .then((files: string[]) => { - // The required file already exists - if (_.includes(files, 'resin-wifi')) { - return null; - } + const files = await imagefs.listDirectory({ + image: target, + partition: this.BOOT_PARTITION, + path: this.CONNECTIONS_FOLDER, + }); - // Fresh image, new mode, accoding to https://github.com/balena-os/meta-balena/pull/770/files - if (_.includes(files, 'resin-sample.ignore')) { - return imagefs - .copy( - { - image: target, - partition: this.BOOT_PARTITION, - path: `${this.CONNECTIONS_FOLDER}/resin-sample.ignore`, - }, - { - image: target, - partition: this.BOOT_PARTITION, - path: `${this.CONNECTIONS_FOLDER}/resin-wifi`, - }, - ) - .thenReturn(null); - } - - // Legacy mode, to be removed later - // We return the file name override from this branch - // When it is removed the following cleanup should be done: - // * delete all the null returns from this method - // * turn `getConfigurationSchema` back into the constant, with the connection filename always being `resin-wifi` - // * drop the final `then` from this method - // * adapt the code in the main listener to not receive the config from this method, and use that constant instead - if (_.includes(files, 'resin-sample')) { - return 'resin-sample'; - } - - // In case there's no file at all (shouldn't happen normally, but the file might have been removed) - return imagefs - .writeFile( - { - image: target, - partition: this.BOOT_PARTITION, - path: `${this.CONNECTIONS_FOLDER}/resin-wifi`, - }, - this.CONNECTION_FILE, - ) - .thenReturn(null); - }) - .then((connectionFileName) => - this.getConfigurationSchema(connectionFileName || undefined), + let connectionFileName; + if (_.includes(files, 'resin-wifi')) { + // The required file already exists, nothing to do + } else if (_.includes(files, 'resin-sample.ignore')) { + // Fresh image, new mode, accoding to https://github.com/balena-os/meta-balena/pull/770/files + await imagefs.copy( + { + image: target, + partition: this.BOOT_PARTITION, + path: `${this.CONNECTIONS_FOLDER}/resin-sample.ignore`, + }, + { + image: target, + partition: this.BOOT_PARTITION, + path: `${this.CONNECTIONS_FOLDER}/resin-wifi`, + }, ); + } else if (_.includes(files, 'resin-sample')) { + // Legacy mode, to be removed later + // We return the file name override from this branch + // When it is removed the following cleanup should be done: + // * delete all the null returns from this method + // * turn `getConfigurationSchema` back into the constant, with the connection filename always being `resin-wifi` + // * drop the final `then` from this method + // * adapt the code in the main listener to not receive the config from this method, and use that constant instead + connectionFileName = 'resin-sample'; + } else { + // In case there's no file at all (shouldn't happen normally, but the file might have been removed) + await imagefs.writeFile( + { + image: target, + partition: this.BOOT_PARTITION, + path: `${this.CONNECTIONS_FOLDER}/resin-wifi`, + }, + this.CONNECTION_FILE, + ); + } + return await this.getConfigurationSchema(connectionFileName); } async removeHostname(schema: any) { diff --git a/lib/actions-oclif/preload.ts b/lib/actions-oclif/preload.ts index 4b471dda..264e69eb 100644 --- a/lib/actions-oclif/preload.ts +++ b/lib/actions-oclif/preload.ts @@ -302,86 +302,72 @@ Can be repeated to add multiple certificates.\ allDeviceTypes: DeviceTypeJson.DeviceType[]; async getDeviceTypes() { - if (this.allDeviceTypes !== undefined) { - return this.allDeviceTypes; + if (this.allDeviceTypes === undefined) { + const balena = getBalenaSdk(); + const deviceTypes = await balena.models.config.getDeviceTypes(); + this.allDeviceTypes = _.sortBy(deviceTypes, 'name'); } - const balena = getBalenaSdk(); - return balena.models.config - .getDeviceTypes() - .then((deviceTypes) => _.sortBy(deviceTypes, 'name')) - .then((deviceTypes) => { - this.allDeviceTypes = deviceTypes; - return deviceTypes; - }); + return this.allDeviceTypes; } isCurrentCommit(commit: string) { return commit === 'latest' || commit === 'current'; } - getDeviceTypesWithSameArch(deviceTypeSlug: string) { - return this.getDeviceTypes().then((deviceTypes) => { - const deviceType = _.find(deviceTypes, { slug: deviceTypeSlug }); - if (!deviceType) { - throw new Error( - `Device type "${deviceTypeSlug}" not found in API query`, - ); - } - return _(deviceTypes) - .filter({ arch: deviceType.arch }) - .map('slug') - .value(); - }); + async getDeviceTypesWithSameArch(deviceTypeSlug: string) { + const deviceTypes = await this.getDeviceTypes(); + const deviceType = _.find(deviceTypes, { slug: deviceTypeSlug }); + if (!deviceType) { + throw new Error(`Device type "${deviceTypeSlug}" not found in API query`); + } + return _(deviceTypes).filter({ arch: deviceType.arch }).map('slug').value(); } - getApplicationsWithSuccessfulBuilds(deviceTypeSlug: string) { + async getApplicationsWithSuccessfulBuilds(deviceTypeSlug: string) { const balena = getBalenaSdk(); - return this.getDeviceTypesWithSameArch(deviceTypeSlug).then( - (deviceTypes) => { - // TODO: remove the explicit types once https://github.com/balena-io/balena-sdk/pull/889 gets merged - return balena.pine.get< - Application, - Array< - ApplicationWithDeviceType & { - should_be_running__release: [Release?]; - } - > - >({ - resource: 'my_application', - options: { - $filter: { - is_for__device_type: { - $any: { - $alias: 'dt', - $expr: { - dt: { - slug: { $in: deviceTypes }, - }, - }, - }, - }, - owns__release: { - $any: { - $alias: 'r', - $expr: { - r: { - status: 'success', - }, - }, + const deviceTypes = await this.getDeviceTypesWithSameArch(deviceTypeSlug); + // TODO: remove the explicit types once https://github.com/balena-io/balena-sdk/pull/889 gets merged + return balena.pine.get< + Application, + Array< + ApplicationWithDeviceType & { + should_be_running__release: [Release?]; + } + > + >({ + resource: 'my_application', + options: { + $filter: { + is_for__device_type: { + $any: { + $alias: 'dt', + $expr: { + dt: { + slug: { $in: deviceTypes }, }, }, }, - $expand: this.applicationExpandOptions, - $select: ['id', 'app_name', 'should_track_latest_release'], - $orderby: 'app_name asc', }, - }); + owns__release: { + $any: { + $alias: 'r', + $expr: { + r: { + status: 'success', + }, + }, + }, + }, + }, + $expand: this.applicationExpandOptions, + $select: ['id', 'app_name', 'should_track_latest_release'], + $orderby: 'app_name asc', }, - ); + }); } - selectApplication(deviceTypeSlug: string) { + async selectApplication(deviceTypeSlug: string) { const visuals = getVisuals(); const applicationInfoSpinner = new visuals.Spinner( @@ -389,24 +375,23 @@ Can be repeated to add multiple certificates.\ ); applicationInfoSpinner.start(); - return this.getApplicationsWithSuccessfulBuilds(deviceTypeSlug).then( - (applications) => { - applicationInfoSpinner.stop(); - if (applications.length === 0) { - throw new ExpectedError( - `You have no apps with successful releases for a '${deviceTypeSlug}' device type.`, - ); - } - return getCliForm().ask({ - message: 'Select an application', - type: 'list', - choices: applications.map((app) => ({ - name: app.app_name, - value: app, - })), - }); - }, + const applications = await this.getApplicationsWithSuccessfulBuilds( + deviceTypeSlug, ); + applicationInfoSpinner.stop(); + if (applications.length === 0) { + throw new ExpectedError( + `You have no apps with successful releases for a '${deviceTypeSlug}' device type.`, + ); + } + return getCliForm().ask({ + message: 'Select an application', + type: 'list', + choices: applications.map((app) => ({ + name: app.app_name, + value: app, + })), + }); } selectApplicationCommit(releases: Release[]) { @@ -462,23 +447,20 @@ preloaded device to the selected release. Would you like to disable automatic updates for this application now?\ `; - return getCliForm() - .ask({ - message, - type: 'confirm', - }) - .then(function (update) { - if (!update) { - return; - } - return balena.pine.patch({ - resource: 'application', - id: application.id, - body: { - should_track_latest_release: false, - }, - }); - }); + const update = await getCliForm().ask({ + message, + type: 'confirm', + }); + if (!update) { + return; + } + return await balena.pine.patch({ + resource: 'application', + id: application.id, + body: { + should_track_latest_release: false, + }, + }); } getAppWithReleases(balenaSdk: BalenaSDK, appId: string | number) {