Merge pull request #2013 from balena-io/async-await-oclif

Convert oclif actions to async/await
This commit is contained in:
bulldozer-balena[bot] 2020-08-28 14:08:48 +00:00 committed by GitHub
commit ac0ce8f702
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 270 additions and 318 deletions

View File

@ -196,7 +196,6 @@ ${dockerignoreHelp}
buildOpts: any; // arguments to forward to docker build command buildOpts: any; // arguments to forward to docker build command
}, },
) { ) {
const Bluebird = await import('bluebird');
const _ = await import('lodash'); const _ = await import('lodash');
const doodles = await import('resin-doodles'); const doodles = await import('resin-doodles');
const sdk = getBalenaSdk(); const sdk = getBalenaSdk();
@ -206,149 +205,139 @@ ${dockerignoreHelp}
const appType = (opts.app?.application_type as ApplicationType[])?.[0]; const appType = (opts.app?.application_type as ApplicationType[])?.[0];
return loadProject(logger, composeOpts, opts.image) try {
.then(function (project) { const project = await loadProject(logger, composeOpts, opts.image);
if ( if (project.descriptors.length > 1 && !appType?.supports_multicontainer) {
project.descriptors.length > 1 && throw new ExpectedError(
!appType?.supports_multicontainer 'Target application does not support multiple containers. Aborting!',
) { );
throw new ExpectedError( }
'Target application does not support multiple containers. Aborting!',
);
}
// find which services use images that already exist locally // find which services use images that already exist locally
return ( let servicesToSkip = await Promise.all(
Bluebird.map(project.descriptors, function (d: any) { project.descriptors.map(async function (d: any) {
// unconditionally build (or pull) if explicitly requested // unconditionally build (or pull) if explicitly requested
if (opts.shouldPerformBuild) { if (opts.shouldPerformBuild) {
return d; return d;
} }
return docker try {
await docker
.getImage( .getImage(
(typeof d.image === 'string' ? d.image : d.image.tag) || '', (typeof d.image === 'string' ? d.image : d.image.tag) || '',
) )
.inspect() .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');
const msg = getChalk().yellow( return d.serviceName;
'Target application requires legacy deploy method.', } catch {
); // Ignore
logger.logWarn(msg); }
}),
);
servicesToSkip = servicesToSkip.filter((d) => !!d);
return Promise.all([ // multibuild takes in a composition and always attempts to
sdk.auth.getToken(), // build or pull all services. we workaround that here by
sdk.auth.whoami(), // passing a modified composition.
sdk.settings.get('balenaUrl'), const compositionToBuild = _.cloneDeep(project.composition);
{ compositionToBuild.services = _.omit(
// opts.appName may be prefixed by 'owner/', unlike opts.app.app_name compositionToBuild.services,
appName: opts.appName, servicesToSkip,
imageName: images[0].name, );
buildLogs: images[0].logs, if (_.size(compositionToBuild.services) === 0) {
shouldUploadLogs: opts.shouldUploadLogs, logger.logInfo(
}, 'Everything is up to date (use --build to force a rebuild)',
])
.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,
),
);
})
); );
}) return {};
.then(function (release: any) { }
logger.outputDeferredMessages(); const builtImages = await compose.buildProject(
logger.logSuccess('Deploy succeeded!'); docker,
logger.logSuccess(`Release: ${release.commit}`); logger,
console.log(); project.path,
console.log(doodles.getDoodle()); // Show charlie project.name,
console.log(); compositionToBuild,
}) opts.app.arch,
.catch((err) => { (opts.app?.is_for__device_type as DeviceType[])?.[0].slug,
logger.logError('Deploy failed'); opts.buildEmulated,
throw err; 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;
}
} }
} }

View File

@ -76,26 +76,23 @@ export default class DevicesSupportedCmd extends Command {
public async run() { public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(DevicesSupportedCmd); const { flags: options } = this.parse<FlagsDef, {}>(DevicesSupportedCmd);
let deviceTypes: Array<Partial< const dts = await getBalenaSdk().models.config.getDeviceTypes();
SDK.DeviceTypeJson.DeviceType let deviceTypes: Array<Partial<SDK.DeviceTypeJson.DeviceType>> = dts.map(
>> = await getBalenaSdk() (d) => {
.models.config.getDeviceTypes() if (d.aliases && d.aliases.length) {
.then((dts) => // remove aliases that are equal to the slug
dts.map((d) => { d.aliases = d.aliases.filter((alias: string) => alias !== d.slug);
if (d.aliases && d.aliases.length) { if (!options.json) {
// remove aliases that are equal to the slug // stringify the aliases array with commas and spaces
d.aliases = d.aliases.filter((alias: string) => alias !== d.slug); d.aliases = [d.aliases.join(', ')];
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 = [];
} }
return d; } else {
}), // ensure it is always an array (for the benefit of JSON output)
); d.aliases = [];
}
return d;
},
);
if (!options.discontinued) { if (!options.discontinued) {
deviceTypes = deviceTypes.filter((dt) => dt.state !== 'DISCONTINUED'); deviceTypes = deviceTypes.filter((dt) => dt.state !== 'DISCONTINUED');
} }

View File

@ -89,20 +89,17 @@ export default class LocalConfigureCmd extends Command {
const dmHandler = (cb: () => void) => const dmHandler = (cb: () => void) =>
reconfix reconfix
.readConfiguration(configurationSchema, params.target) .readConfiguration(configurationSchema, params.target)
.tap((config: any) => { .then(async (config: any) => {
logger.logDebug('Current config:'); logger.logDebug('Current config:');
logger.logDebug(JSON.stringify(config)); logger.logDebug(JSON.stringify(config));
}) const answers = await this.getConfiguration(config);
.then((config: any) => this.getConfiguration(config))
.tap((config: any) => {
logger.logDebug('New config:'); logger.logDebug('New config:');
logger.logDebug(JSON.stringify(config)); logger.logDebug(JSON.stringify(answers));
})
.then(async (answers: any) => {
if (!answers.hostname) { if (!answers.hostname) {
await this.removeHostname(configurationSchema); await this.removeHostname(configurationSchema);
} }
return reconfix.writeConfiguration( return await reconfix.writeConfiguration(
configurationSchema, configurationSchema,
answers, answers,
params.target, params.target,
@ -220,9 +217,8 @@ export default class LocalConfigureCmd extends Command {
persistentLogging: data.persistentLogging || false, persistentLogging: data.persistentLogging || false,
}); });
return inquirer const answers = await inquirer.prompt(this.inquirerOptions(data));
.prompt(this.inquirerOptions(data)) return _.merge(data, answers);
.then((answers: any) => _.merge(data, answers));
}; };
// Taken from https://goo.gl/kr1kCt // Taken from https://goo.gl/kr1kCt
@ -259,62 +255,50 @@ export default class LocalConfigureCmd extends Command {
const _ = await import('lodash'); const _ = await import('lodash');
const imagefs = await import('resin-image-fs'); const imagefs = await import('resin-image-fs');
return imagefs const files = await imagefs.listDirectory({
.listDirectory({ image: target,
image: target, partition: this.BOOT_PARTITION,
partition: this.BOOT_PARTITION, path: this.CONNECTIONS_FOLDER,
path: this.CONNECTIONS_FOLDER, });
})
.then((files: string[]) => {
// The required file already exists
if (_.includes(files, 'resin-wifi')) {
return null;
}
// Fresh image, new mode, accoding to https://github.com/balena-os/meta-balena/pull/770/files let connectionFileName;
if (_.includes(files, 'resin-sample.ignore')) { if (_.includes(files, 'resin-wifi')) {
return imagefs // The required file already exists, nothing to do
.copy( } else if (_.includes(files, 'resin-sample.ignore')) {
{ // Fresh image, new mode, accoding to https://github.com/balena-os/meta-balena/pull/770/files
image: target, await imagefs.copy(
partition: this.BOOT_PARTITION, {
path: `${this.CONNECTIONS_FOLDER}/resin-sample.ignore`, 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`, 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),
); );
} 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) { async removeHostname(schema: any) {

View File

@ -302,86 +302,72 @@ Can be repeated to add multiple certificates.\
allDeviceTypes: DeviceTypeJson.DeviceType[]; allDeviceTypes: DeviceTypeJson.DeviceType[];
async getDeviceTypes() { async getDeviceTypes() {
if (this.allDeviceTypes !== undefined) { if (this.allDeviceTypes === undefined) {
return this.allDeviceTypes; const balena = getBalenaSdk();
const deviceTypes = await balena.models.config.getDeviceTypes();
this.allDeviceTypes = _.sortBy(deviceTypes, 'name');
} }
const balena = getBalenaSdk(); return this.allDeviceTypes;
return balena.models.config
.getDeviceTypes()
.then((deviceTypes) => _.sortBy(deviceTypes, 'name'))
.then((deviceTypes) => {
this.allDeviceTypes = deviceTypes;
return deviceTypes;
});
} }
isCurrentCommit(commit: string) { isCurrentCommit(commit: string) {
return commit === 'latest' || commit === 'current'; return commit === 'latest' || commit === 'current';
} }
getDeviceTypesWithSameArch(deviceTypeSlug: string) { async getDeviceTypesWithSameArch(deviceTypeSlug: string) {
return this.getDeviceTypes().then((deviceTypes) => { const deviceTypes = await this.getDeviceTypes();
const deviceType = _.find(deviceTypes, { slug: deviceTypeSlug }); const deviceType = _.find(deviceTypes, { slug: deviceTypeSlug });
if (!deviceType) { if (!deviceType) {
throw new Error( throw new Error(`Device type "${deviceTypeSlug}" not found in API query`);
`Device type "${deviceTypeSlug}" not found in API query`, }
); return _(deviceTypes).filter({ arch: deviceType.arch }).map('slug').value();
}
return _(deviceTypes)
.filter({ arch: deviceType.arch })
.map('slug')
.value();
});
} }
getApplicationsWithSuccessfulBuilds(deviceTypeSlug: string) { async getApplicationsWithSuccessfulBuilds(deviceTypeSlug: string) {
const balena = getBalenaSdk(); const balena = getBalenaSdk();
return this.getDeviceTypesWithSameArch(deviceTypeSlug).then( const deviceTypes = await this.getDeviceTypesWithSameArch(deviceTypeSlug);
(deviceTypes) => { // TODO: remove the explicit types once https://github.com/balena-io/balena-sdk/pull/889 gets merged
// TODO: remove the explicit types once https://github.com/balena-io/balena-sdk/pull/889 gets merged return balena.pine.get<
return balena.pine.get< Application,
Application, Array<
Array< ApplicationWithDeviceType & {
ApplicationWithDeviceType & { should_be_running__release: [Release?];
should_be_running__release: [Release?]; }
} >
> >({
>({ resource: 'my_application',
resource: 'my_application', options: {
options: { $filter: {
$filter: { is_for__device_type: {
is_for__device_type: { $any: {
$any: { $alias: 'dt',
$alias: 'dt', $expr: {
$expr: { dt: {
dt: { slug: { $in: deviceTypes },
slug: { $in: deviceTypes },
},
},
},
},
owns__release: {
$any: {
$alias: 'r',
$expr: {
r: {
status: 'success',
},
},
}, },
}, },
}, },
$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 visuals = getVisuals();
const applicationInfoSpinner = new visuals.Spinner( const applicationInfoSpinner = new visuals.Spinner(
@ -389,24 +375,23 @@ Can be repeated to add multiple certificates.\
); );
applicationInfoSpinner.start(); applicationInfoSpinner.start();
return this.getApplicationsWithSuccessfulBuilds(deviceTypeSlug).then( const applications = await this.getApplicationsWithSuccessfulBuilds(
(applications) => { 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,
})),
});
},
); );
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[]) { selectApplicationCommit(releases: Release[]) {
@ -462,23 +447,20 @@ preloaded device to the selected release.
Would you like to disable automatic updates for this application now?\ Would you like to disable automatic updates for this application now?\
`; `;
return getCliForm() const update = await getCliForm().ask({
.ask({ message,
message, type: 'confirm',
type: 'confirm', });
}) if (!update) {
.then(function (update) { return;
if (!update) { }
return; return await balena.pine.patch({
} resource: 'application',
return balena.pine.patch({ id: application.id,
resource: 'application', body: {
id: application.id, should_track_latest_release: false,
body: { },
should_track_latest_release: false, });
},
});
});
} }
getAppWithReleases(balenaSdk: BalenaSDK, appId: string | number) { getAppWithReleases(balenaSdk: BalenaSDK, appId: string | number) {