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
},
) {
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;
}
}
}

View File

@ -76,26 +76,23 @@ export default class DevicesSupportedCmd extends Command {
public async run() {
const { flags: options } = this.parse<FlagsDef, {}>(DevicesSupportedCmd);
let deviceTypes: Array<Partial<
SDK.DeviceTypeJson.DeviceType
>> = 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<Partial<SDK.DeviceTypeJson.DeviceType>> = 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');
}

View File

@ -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) {

View File

@ -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) {