Merge pull request #1791 from balena-io/errors-refactor

Errors refactor
This commit is contained in:
srlowe 2020-05-01 15:04:30 +02:00 committed by GitHub
commit 3b53b75626
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 58 additions and 74 deletions

View File

@ -87,6 +87,7 @@ Examples:
const balena = getBalenaSdk(); const balena = getBalenaSdk();
const patterns = await import('../utils/patterns'); const patterns = await import('../utils/patterns');
const messages = await import('../utils/messages'); const messages = await import('../utils/messages');
const { exitWithExpectedError } = await import('../errors');
const doLogin = async (loginOptions: Options): Promise<void> => { const doLogin = async (loginOptions: Options): Promise<void> => {
if (loginOptions.token != null) { if (loginOptions.token != null) {
@ -103,7 +104,7 @@ Examples:
} }
await balena.auth.loginWithToken(token); await balena.auth.loginWithToken(token);
if (!(await balena.auth.whoami())) { if (!(await balena.auth.whoami())) {
patterns.exitWithExpectedError('Token authentication failed'); exitWithExpectedError('Token authentication failed');
} }
return; return;
} else if (loginOptions.credentials) { } else if (loginOptions.credentials) {
@ -120,7 +121,7 @@ Examples:
const signupUrl = 'https://dashboard.balena-cloud.com/signup'; const signupUrl = 'https://dashboard.balena-cloud.com/signup';
const open = await import('open'); const open = await import('open');
open(signupUrl, { wait: false }); open(signupUrl, { wait: false });
return patterns.exitWithExpectedError(`Please sign up at ${signupUrl}`); return exitWithExpectedError(`Please sign up at ${signupUrl}`);
} }
loginOptions[loginType] = true; loginOptions[loginType] = true;

View File

@ -322,7 +322,7 @@ Examples:
generateApplicationConfig, generateApplicationConfig,
} = require('../utils/config'); } = require('../utils/config');
const helpers = require('../utils/helpers'); const helpers = require('../utils/helpers');
const { exitWithExpectedError } = require('../utils/patterns'); const { exitWithExpectedError } = require('../errors');
if (options.device == null && options.application == null) { if (options.device == null && options.application == null) {
exitWithExpectedError(`\ exitWithExpectedError(`\

View File

@ -20,7 +20,7 @@ import * as capitano from 'capitano';
import * as columnify from 'columnify'; import * as columnify from 'columnify';
import * as messages from '../utils/messages'; import * as messages from '../utils/messages';
import { getManualSortCompareFunction } from '../utils/helpers'; import { getManualSortCompareFunction } from '../utils/helpers';
import { exitWithExpectedError } from '../utils/patterns'; import { exitWithExpectedError } from '../errors';
import { getOclifHelpLinePairs } from './help_ts'; import { getOclifHelpLinePairs } from './help_ts';
const parse = object => const parse = object =>

View File

@ -1,7 +1,7 @@
import * as Promise from 'bluebird'; import * as Promise from 'bluebird';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as dockerUtils from '../../utils/docker'; import * as dockerUtils from '../../utils/docker';
import { exitWithExpectedError } from '../../utils/patterns'; import { exitWithExpectedError } from '../../errors';
import { getChalk } from '../../utils/lazy'; import { getChalk } from '../../utils/lazy';
export const dockerPort = 2375; export const dockerPort = 2375;

View File

@ -91,9 +91,8 @@ export const logs: CommandDefinition<
'../utils/device/logs' '../utils/device/logs'
); );
const { validateIPAddress } = await import('../utils/validation'); const { validateIPAddress } = await import('../utils/validation');
const { exitIfNotLoggedIn, exitWithExpectedError } = await import( const { checkLoggedIn } = await import('../utils/patterns');
'../utils/patterns' const { exitWithExpectedError } = await import('../errors');
);
const Logger = await import('../utils/logger'); const Logger = await import('../utils/logger');
const logger = Logger.getLogger(); const logger = Logger.getLogger();
@ -152,7 +151,7 @@ export const logs: CommandDefinition<
servicesToDisplay, servicesToDisplay,
); );
} else { } else {
await exitIfNotLoggedIn(); await checkLoggedIn();
if (options.tail) { if (options.tail) {
return balena.logs return balena.logs
.subscribe(params.uuidOrDevice, { count: 100 }) .subscribe(params.uuidOrDevice, { count: 100 })

View File

@ -87,7 +87,7 @@ const getApplicationsWithSuccessfulBuilds = function(deviceType) {
const selectApplication = function(deviceType) { const selectApplication = function(deviceType) {
const visuals = getVisuals(); const visuals = getVisuals();
const form = require('resin-cli-form'); const form = require('resin-cli-form');
const { exitWithExpectedError } = require('../utils/patterns'); const { exitWithExpectedError } = require('../errors');
const applicationInfoSpinner = new visuals.Spinner( const applicationInfoSpinner = new visuals.Spinner(
'Downloading list of applications and releases.', 'Downloading list of applications and releases.',
@ -116,7 +116,7 @@ const selectApplication = function(deviceType) {
const selectApplicationCommit = function(releases) { const selectApplicationCommit = function(releases) {
const form = require('resin-cli-form'); const form = require('resin-cli-form');
const { exitWithExpectedError } = require('../utils/patterns'); const { exitWithExpectedError } = require('../errors');
if (releases.length === 0) { if (releases.length === 0) {
exitWithExpectedError('This application has no successful releases.'); exitWithExpectedError('This application has no successful releases.');
@ -263,7 +263,7 @@ Examples:
const balenaPreload = require('balena-preload'); const balenaPreload = require('balena-preload');
const visuals = getVisuals(); const visuals = getVisuals();
const nodeCleanup = require('node-cleanup'); const nodeCleanup = require('node-cleanup');
const { exitWithExpectedError } = require('../utils/patterns'); const { exitWithExpectedError } = require('../errors');
const progressBars = {}; const progressBars = {};

View File

@ -60,7 +60,7 @@ export const list: CommandDefinition<
const _ = await import('lodash'); const _ = await import('lodash');
const balena = getBalenaSdk(); const balena = getBalenaSdk();
const { exitWithExpectedError } = await import('../utils/patterns'); const { exitWithExpectedError } = await import('../errors');
return Bluebird.try<ApplicationTag[] | DeviceTag[] | ReleaseTag[]>( return Bluebird.try<ApplicationTag[] | DeviceTag[] | ReleaseTag[]>(
async () => { async () => {
@ -161,7 +161,7 @@ export const set: CommandDefinition<
const _ = await import('lodash'); const _ = await import('lodash');
const balena = getBalenaSdk(); const balena = getBalenaSdk();
const { exitWithExpectedError } = await import('../utils/patterns'); const { exitWithExpectedError } = await import('../errors');
if (_.isEmpty(params.tagKey)) { if (_.isEmpty(params.tagKey)) {
return exitWithExpectedError('No tag key was provided'); return exitWithExpectedError('No tag key was provided');
@ -250,7 +250,7 @@ export const remove: CommandDefinition<
async action(params, options) { async action(params, options) {
const _ = await import('lodash'); const _ = await import('lodash');
const balena = getBalenaSdk(); const balena = getBalenaSdk();
const { exitWithExpectedError } = await import('../utils/patterns'); const { exitWithExpectedError } = await import('../errors');
if (_.isEmpty(params.tagKey)) { if (_.isEmpty(params.tagKey)) {
return exitWithExpectedError('No tag key was provided'); return exitWithExpectedError('No tag key was provided');

View File

@ -22,7 +22,7 @@ import * as events from './events';
capitano.permission('user', done => capitano.permission('user', done =>
require('./utils/patterns') require('./utils/patterns')
.exitIfNotLoggedIn() .checkLoggedIn()
.then(done, done), .then(done, done),
); );

View File

@ -1,5 +1,5 @@
/* /*
Copyright 2016-2020 Balena Copyright 2016-2020 Balena Ltd.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -18,6 +18,8 @@ import { stripIndent } from 'common-tags';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as os from 'os'; import * as os from 'os';
import { TypedError } from 'typed-error'; import { TypedError } from 'typed-error';
import { getChalk } from './utils/lazy';
import { getHelp } from './utils/messages';
export class ExpectedError extends TypedError {} export class ExpectedError extends TypedError {}
@ -130,8 +132,6 @@ const messages: {
}; };
export async function handleError(error: any) { export async function handleError(error: any) {
const { printErrorMessage } = await import('./utils/patterns');
process.exitCode = process.exitCode =
error.exitCode === 0 error.exitCode === 0
? 0 ? 0
@ -145,7 +145,7 @@ export async function handleError(error: any) {
const message = [interpret(error)]; const message = [interpret(error)];
if (process.env.DEBUG && error.stack) { if (process.env.DEBUG && error.stack) {
message.push(error.stack); message.push('\n' + error.stack);
} }
printErrorMessage(message.join('\n')); printErrorMessage(message.join('\n'));
@ -171,3 +171,29 @@ export async function handleError(error: any) {
// The exit error code was set above through `process.exitCode`. // The exit error code was set above through `process.exitCode`.
process.exit(); process.exit();
} }
export function printErrorMessage(message: string) {
const chalk = getChalk();
console.error(chalk.red(message));
console.error(chalk.red(`\n${getHelp}\n`));
}
/**
* Print a friendly error message and exit the CLI with an error code, BYPASSING
* error reporting through Sentry.io's platform (raven.Raven.captureException).
* Note that lib/errors.ts provides top-level error handling code to catch any
* otherwise uncaught errors, AND to report them through Sentry.io. But many
* "expected" errors (say, a JSON parsing error in a file provided by the user)
* don't warrant reporting through Sentry.io. For such mundane errors, catch
* them and call this function.
*
* DEPRECATED: Use `throw new ExpectedError(<message>)` instead.
*/
export function exitWithExpectedError(message: string | Error): never {
if (message instanceof Error) {
({ message } = message);
}
printErrorMessage(message);
process.exit(1);
}

View File

@ -16,7 +16,7 @@
*/ */
import { stripIndent } from 'common-tags'; import { stripIndent } from 'common-tags';
import { exitWithExpectedError } from './utils/patterns'; import { exitWithExpectedError } from './errors';
export interface AppOptions { export interface AppOptions {
// Prevent the default behavior of flushing stdout after running a command // Prevent the default behavior of flushing stdout after running a command

View File

@ -74,7 +74,7 @@ async function environmentFromInput(
serviceNames: string[], serviceNames: string[],
logger: Logger, logger: Logger,
): Promise<ParsedEnvironment> { ): Promise<ParsedEnvironment> {
const { exitWithExpectedError } = await import('../patterns'); const { exitWithExpectedError } = await import('../../errors');
// A normal environment variable regex, with an added part // A normal environment variable regex, with an added part
// to find a colon followed servicename at the start // to find a colon followed servicename at the start
const varRegex = /^(?:([^\s:]+):)?([^\s]+?)=(.*)$/; const varRegex = /^(?:([^\s:]+):)?([^\s]+?)=(.*)$/;
@ -121,7 +121,7 @@ async function environmentFromInput(
export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> { export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
const { tarDirectory } = await import('../compose'); const { tarDirectory } = await import('../compose');
const { exitWithExpectedError } = await import('../patterns'); const { exitWithExpectedError } = await import('../../errors');
const { displayDeviceLogs } = await import('./logs'); const { displayDeviceLogs } = await import('./logs');
const api = new DeviceAPI(globalLogger, opts.deviceHost); const api = new DeviceAPI(globalLogger, opts.deviceHost);
@ -574,7 +574,7 @@ export function generateTargetState(
} }
async function inspectBuildResults(images: LocalImage[]): Promise<void> { async function inspectBuildResults(images: LocalImage[]): Promise<void> {
const { exitWithExpectedError } = await import('../patterns'); const { exitWithExpectedError } = await import('../../errors');
const failures: LocalPushErrors.BuildFailure[] = []; const failures: LocalPushErrors.BuildFailure[] = [];

View File

@ -270,7 +270,7 @@ export const createClient = function(opts) {
}; };
var ensureDockerSeemsAccessible = function(docker) { var ensureDockerSeemsAccessible = function(docker) {
const { exitWithExpectedError } = require('./patterns'); const { exitWithExpectedError } = require('../errors');
return docker return docker
.ping() .ping()
.catch(() => .catch(() =>

View File

@ -20,9 +20,8 @@ import { stripIndent } from 'common-tags';
import _ = require('lodash'); import _ = require('lodash');
import _form = require('resin-cli-form'); import _form = require('resin-cli-form');
import { instanceOf, NotLoggedInError } from '../errors'; import { exitWithExpectedError, instanceOf, NotLoggedInError } from '../errors';
import { getBalenaSdk, getChalk, getVisuals } from './lazy'; import { getBalenaSdk, getVisuals } from './lazy';
import messages = require('./messages');
import validation = require('./validation'); import validation = require('./validation');
const getForm = _.once((): typeof _form => require('resin-cli-form')); const getForm = _.once((): typeof _form => require('resin-cli-form'));
@ -88,22 +87,6 @@ export async function checkLoggedIn(): Promise<void> {
} }
} }
/**
* Check if logged in, and call `exitWithExpectedError()` if not.
* DEPRECATED: Use checkLoggedIn() instead.
*/
export async function exitIfNotLoggedIn(): Promise<void> {
try {
await checkLoggedIn();
} catch (error) {
if (error instanceof NotLoggedInError) {
exitWithExpectedError(error);
} else {
throw error;
}
}
}
export function askLoginType() { export function askLoginType() {
return getForm().ask<'web' | 'credentials' | 'token' | 'register'>({ return getForm().ask<'web' | 'credentials' | 'token' | 'register'>({
message: 'How would you like to login?', message: 'How would you like to login?',
@ -435,29 +418,3 @@ export function selectFromList<T>(
})), })),
}); });
} }
export function printErrorMessage(message: string) {
const chalk = getChalk();
console.error(chalk.red(message));
console.error(chalk.red(`\n${messages.getHelp}\n`));
}
/**
* Print a friendly error message and exit the CLI with an error code, BYPASSING
* error reporting through Sentry.io's platform (raven.Raven.captureException).
* Note that lib/errors.ts provides top-level error handling code to catch any
* otherwise uncaught errors, AND to report them through Sentry.io. But many
* "expected" errors (say, a JSON parsing error in a file provided by the user)
* don't warrant reporting through Sentry.io. For such mundane errors, catch
* them and call this function.
*
* DEPRECATED: Use `throw new ExpectedError(<message>)` instead.
*/
export function exitWithExpectedError(message: string | Error): never {
if (message instanceof Error) {
({ message } = message);
}
printErrorMessage(message);
process.exit(1);
}

View File

@ -17,7 +17,7 @@
import * as BalenaSdk from 'balena-sdk'; import * as BalenaSdk from 'balena-sdk';
import { stripIndent } from 'common-tags'; import { stripIndent } from 'common-tags';
import { ExpectedError } from '../errors'; import { ExpectedError, printErrorMessage } from '../errors';
import { getVisuals } from './lazy'; import { getVisuals } from './lazy';
import Logger = require('./logger'); import Logger = require('./logger');
import { exec, execBuffered, getDeviceOsRelease } from './ssh'; import { exec, execBuffered, getDeviceOsRelease } from './ssh';
@ -325,7 +325,6 @@ async function createApplication(
): Promise<BalenaSdk.Application> { ): Promise<BalenaSdk.Application> {
const form = await import('resin-cli-form'); const form = await import('resin-cli-form');
const validation = await import('./validation'); const validation = await import('./validation');
const patterns = await import('./patterns');
let username = await sdk.auth.whoami(); let username = await sdk.auth.whoami();
if (!username) { if (!username) {
@ -352,7 +351,9 @@ async function createApplication(
], ],
}, },
}); });
patterns.printErrorMessage( // TODO: This is the only example in the codebase where `printErrorMessage()`
// is called directly. Consider refactoring.
printErrorMessage(
'You already have an application with that name; please choose another.', 'You already have an application with that name; please choose another.',
); );
continue; continue;

View File

@ -24,7 +24,7 @@ import streamToPromise = require('stream-to-promise');
import { Pack } from 'tar-stream'; import { Pack } from 'tar-stream';
import { ExpectedError } from '../errors'; import { ExpectedError } from '../errors';
import { exitWithExpectedError } from '../utils/patterns'; import { exitWithExpectedError } from '../errors';
import { tarDirectory } from './compose'; import { tarDirectory } from './compose';
import { getVisuals } from './lazy'; import { getVisuals } from './lazy';
import Logger = require('./logger'); import Logger = require('./logger');