Improve NotFoundError handling

Use isNotFoundError which converts an error of the default type
`unknown` into NotFoundError if the error is an instance of NotFoundError.
Thrown errors are of type `unknown` by default so we should use methods
with type guards for better type narrowing.

Signed-off-by: Christina Ying Wang <christina@balena.io>
This commit is contained in:
Christina Ying Wang 2022-11-08 15:41:52 -08:00
parent 80e213ab7e
commit 75bf2aa3b4
11 changed files with 48 additions and 49 deletions

View File

@ -9,7 +9,7 @@ services:
dockerfile: Dockerfile.template dockerfile: Dockerfile.template
args: args:
ARCH: ${ARCH:-amd64} ARCH: ${ARCH:-amd64}
command: ['/wait-for-it.sh', '--', '/usr/src/app/entry.sh'] command: [ '/wait-for-it.sh', '--', '/usr/src/app/entry.sh' ]
# Use bridge networking for the tests # Use bridge networking for the tests
network_mode: 'bridge' network_mode: 'bridge'
networks: networks:
@ -30,7 +30,7 @@ services:
- ./test/data/root:/mnt/root - ./test/data/root:/mnt/root
- ./test/lib/wait-for-it.sh:/wait-for-it.sh - ./test/lib/wait-for-it.sh:/wait-for-it.sh
tmpfs: tmpfs:
- /data - /data # sqlite3 database
dbus: dbus:
image: balenablocks/dbus image: balenablocks/dbus
@ -74,7 +74,7 @@ services:
'--', '--',
'npm', 'npm',
'run', 'run',
'test:integration', 'test:integration'
] ]
depends_on: depends_on:
- balena-supervisor - balena-supervisor

View File

@ -20,7 +20,7 @@
"test:env": "ARCH=$(./build-utils/detect-arch.sh) docker-compose -f docker-compose.test.yml -f docker-compose.dev.yml up --build; npm run compose:down", "test:env": "ARCH=$(./build-utils/detect-arch.sh) docker-compose -f docker-compose.test.yml -f docker-compose.dev.yml up --build; npm run compose:down",
"test:compose": "ARCH=$(./build-utils/detect-arch.sh) docker-compose -f docker-compose.yml -f docker-compose.test.yml up --build --remove-orphans --exit-code-from=sut ; npm run compose:down", "test:compose": "ARCH=$(./build-utils/detect-arch.sh) docker-compose -f docker-compose.yml -f docker-compose.test.yml up --build --remove-orphans --exit-code-from=sut ; npm run compose:down",
"test": "npm run lint && npm run test:build && npm run test:unit && npm run test:legacy", "test": "npm run lint && npm run test:build && npm run test:unit && npm run test:legacy",
"compose:down": "docker-compose -f docker-compose.test.yml down", "compose:down": "docker-compose -f docker-compose.test.yml down && docker volume rm $(docker volume ls -f name=balena-supervisor -q)",
"prettify": "balena-lint -e ts -e js --fix src/ test/ typings/ build-utils/ webpack.config.js", "prettify": "balena-lint -e ts -e js --fix src/ test/ typings/ build-utils/ webpack.config.js",
"release": "tsc --project tsconfig.release.json && mv build/src/* build", "release": "tsc --project tsconfig.release.json && mv build/src/* build",
"sync": "ts-node --files sync/sync.ts", "sync": "ts-node --files sync/sync.ts",

View File

@ -18,7 +18,7 @@ import constants = require('../lib/constants');
import { getStepsFromStrategy } from './update-strategies'; import { getStepsFromStrategy } from './update-strategies';
import { InternalInconsistencyError, NotFoundError } from '../lib/errors'; import { InternalInconsistencyError, isNotFoundError } from '../lib/errors';
import * as config from '../config'; import * as config from '../config';
import { checkTruthy, checkString } from '../lib/validation'; import { checkTruthy, checkString } from '../lib/validation';
import { ServiceComposeConfig, DeviceMetadata } from './types/service'; import { ServiceComposeConfig, DeviceMetadata } from './types/service';
@ -804,8 +804,8 @@ export class App {
let imageInfo: ImageInspectInfo | undefined; let imageInfo: ImageInspectInfo | undefined;
try { try {
imageInfo = await imageManager.inspectByName(svc.image); imageInfo = await imageManager.inspectByName(svc.image);
} catch (e: any) { } catch (e: unknown) {
if (!NotFoundError(e)) { if (!isNotFoundError(e)) {
throw e; throw e;
} }
} }

View File

@ -11,7 +11,7 @@ import { DeltaFetchOptions, FetchOptions, docker } from '../lib/docker-utils';
import * as dockerUtils from '../lib/docker-utils'; import * as dockerUtils from '../lib/docker-utils';
import { import {
DeltaStillProcessingError, DeltaStillProcessingError,
NotFoundError, isNotFoundError,
StatusError, StatusError,
} from '../lib/errors'; } from '../lib/errors';
import * as LogTypes from '../lib/log-types'; import * as LogTypes from '../lib/log-types';
@ -236,8 +236,8 @@ export async function triggerFetch(
await markAsSupervised({ ...image, dockerImageId: img.Id }); await markAsSupervised({ ...image, dockerImageId: img.Id });
success = true; success = true;
} catch (e: any) { } catch (e: unknown) {
if (!NotFoundError(e)) { if (!isNotFoundError(e)) {
if (!(e instanceof ImageDownloadBackoffError)) { if (!(e instanceof ImageDownloadBackoffError)) {
addImageFailure(image.name); addImageFailure(image.name);
} }
@ -729,8 +729,8 @@ async function removeImageIfNotNeeded(image: Image): Promise<void> {
// Mark the image as removed // Mark the image as removed
removed = true; removed = true;
} catch (e: any) { } catch (e: unknown) {
if (NotFoundError(e)) { if (isNotFoundError(e)) {
removed = false; removed = false;
} else { } else {
throw e; throw e;

View File

@ -3,7 +3,7 @@ import * as _ from 'lodash';
import * as constants from '../lib/constants'; import * as constants from '../lib/constants';
import { docker } from '../lib/docker-utils'; import { docker } from '../lib/docker-utils';
import { NotFoundError } from '../lib/errors'; import { isNotFoundError } from '../lib/errors';
import logTypes = require('../lib/log-types'); import logTypes = require('../lib/log-types');
import log from '../lib/supervisor-console'; import log from '../lib/supervisor-console';
import { exists } from '../lib/fs-utils'; import { exists } from '../lib/fs-utils';
@ -45,8 +45,8 @@ export async function create(network: Network) {
// We have a network with the same config and name // We have a network with the same config and name
// already created, we can skip this // already created, we can skip this
} catch (e: any) { } catch (e: unknown) {
if (!NotFoundError(e)) { if (!isNotFoundError(e)) {
logger.logSystemEvent(logTypes.createNetworkError, { logger.logSystemEvent(logTypes.createNetworkError, {
network: { name: network.name, appUuid: network.appUuid }, network: { name: network.name, appUuid: network.appUuid },
error: e, error: e,
@ -120,7 +120,7 @@ export function ensureSupervisorNetwork(): Bluebird<void> {
}); });
} }
}) })
.catch(NotFoundError, () => { .catch(isNotFoundError, () => {
log.debug(`Creating ${constants.supervisorNetworkInterface} network`); log.debug(`Creating ${constants.supervisorNetworkInterface} network`);
return Bluebird.resolve( return Bluebird.resolve(
docker.createNetwork({ docker.createNetwork({

View File

@ -15,7 +15,7 @@ import { PermissiveNumber } from '../config/types';
import constants = require('../lib/constants'); import constants = require('../lib/constants');
import { import {
InternalInconsistencyError, InternalInconsistencyError,
NotFoundError, isNotFoundError,
StatusCodeError, StatusCodeError,
isStatusError, isStatusError,
} from '../lib/errors'; } from '../lib/errors';
@ -72,8 +72,8 @@ export const getAll = async (
service.status = vState.status; service.status = vState.status;
} }
return service; return service;
} catch (e: any) { } catch (e: unknown) {
if (NotFoundError(e)) { if (isNotFoundError(e)) {
return null; return null;
} }
throw e; throw e;
@ -206,8 +206,8 @@ export async function remove(service: Service) {
try { try {
await docker.getContainer(existingService.containerId).remove({ v: true }); await docker.getContainer(existingService.containerId).remove({ v: true });
} catch (e: any) { } catch (e: unknown) {
if (!NotFoundError(e)) { if (!isNotFoundError(e)) {
logger.logSystemEvent(LogTypes.removeDeadServiceError, { logger.logSystemEvent(LogTypes.removeDeadServiceError, {
service, service,
error: e, error: e,
@ -227,8 +227,8 @@ async function create(service: Service) {
); );
} }
return docker.getContainer(existing.containerId); return docker.getContainer(existing.containerId);
} catch (e: any) { } catch (e: unknown) {
if (!NotFoundError(e)) { if (!isNotFoundError(e)) {
logger.logSystemEvent(LogTypes.installServiceError, { logger.logSystemEvent(LogTypes.installServiceError, {
service, service,
error: e, error: e,
@ -383,8 +383,8 @@ export function listenToEvents() {
let service: Service | null = null; let service: Service | null = null;
try { try {
service = await getByDockerContainerId(data.id); service = await getByDockerContainerId(data.id);
} catch (e: any) { } catch (e: unknown) {
if (!NotFoundError(e)) { if (!isNotFoundError(e)) {
throw e; throw e;
} }
} }

View File

@ -3,7 +3,7 @@ import * as Path from 'path';
import { VolumeInspectInfo } from 'dockerode'; import { VolumeInspectInfo } from 'dockerode';
import constants = require('../lib/constants'); import constants = require('../lib/constants');
import { NotFoundError, InternalInconsistencyError } from '../lib/errors'; import { isNotFoundError, InternalInconsistencyError } from '../lib/errors';
import { safeRename } from '../lib/fs-utils'; import { safeRename } from '../lib/fs-utils';
import { docker } from '../lib/docker-utils'; import { docker } from '../lib/docker-utils';
import * as LogTypes from '../lib/log-types'; import * as LogTypes from '../lib/log-types';
@ -58,8 +58,8 @@ export async function create(volume: Volume): Promise<void> {
if (!volume.isEqualConfig(existing)) { if (!volume.isEqualConfig(existing)) {
throw new ResourceRecreationAttemptError('volume', volume.name); throw new ResourceRecreationAttemptError('volume', volume.name);
} }
} catch (e: any) { } catch (e: unknown) {
if (!NotFoundError(e)) { if (!isNotFoundError(e)) {
logger.logSystemEvent(LogTypes.createVolumeError, { logger.logSystemEvent(LogTypes.createVolumeError, {
volume: { name: volume.name }, volume: { name: volume.name },
error: e, error: e,

View File

@ -22,16 +22,23 @@ export class StatusError extends Error {
export const isStatusError = (x: unknown): x is StatusError => export const isStatusError = (x: unknown): x is StatusError =>
x != null && x instanceof Error && !isNaN((x as any).statusCode); x != null && x instanceof Error && !isNaN((x as any).statusCode);
export class NotFoundError extends Error {
public statusCode: number;
constructor() {
super();
this.statusCode = 404;
}
}
export const isNotFoundError = (e: unknown): e is NotFoundError =>
isStatusError(e) && e.statusCode === 404;
interface CodedSysError extends Error { interface CodedSysError extends Error {
code?: string; code?: string;
} }
export class DeviceNotFoundError extends TypedError {} export class DeviceNotFoundError extends TypedError {}
export function NotFoundError(err: StatusCodeError): boolean {
return checkInt(err.statusCode) === 404;
}
export function ENOENT(err: CodedSysError): boolean { export function ENOENT(err: CodedSysError): boolean {
return err.code === 'ENOENT'; return err.code === 'ENOENT';
} }

View File

@ -10,7 +10,7 @@ import * as applicationManager from '../compose/application-manager';
import { import {
StatusError, StatusError,
DatabaseParseError, DatabaseParseError,
NotFoundError, isNotFoundError,
InternalInconsistencyError, InternalInconsistencyError,
} from '../lib/errors'; } from '../lib/errors';
import * as constants from '../lib/constants'; import * as constants from '../lib/constants';
@ -145,12 +145,12 @@ export async function normaliseLegacyDatabase() {
const imageFromDocker = await docker const imageFromDocker = await docker
.getImage(service.image) .getImage(service.image)
.inspect() .inspect()
.catch((error) => { .catch((e: unknown) => {
if (error instanceof NotFoundError) { if (isNotFoundError(e)) {
return; return;
} }
throw error; throw e;
}); });
const imagesFromDatabase = await db const imagesFromDatabase = await db
.models('image') .models('image')

View File

@ -9,7 +9,7 @@ const rimrafAsync = Bluebird.promisify(rimraf);
import * as volumeManager from '../compose/volume-manager'; import * as volumeManager from '../compose/volume-manager';
import * as deviceState from '../device-state'; import * as deviceState from '../device-state';
import * as constants from '../lib/constants'; import * as constants from '../lib/constants';
import { BackupError, NotFoundError } from '../lib/errors'; import { BackupError, isNotFoundError } from '../lib/errors';
import { exec, pathExistsOnHost, mkdirp } from '../lib/fs-utils'; import { exec, pathExistsOnHost, mkdirp } from '../lib/fs-utils';
import { log } from '../lib/supervisor-console'; import { log } from '../lib/supervisor-console';
@ -67,11 +67,11 @@ export async function loadBackupFromMigration(
.then((volume) => { .then((volume) => {
return volume.remove(); return volume.remove();
}) })
.catch((error) => { .catch((e: unknown) => {
if (error instanceof NotFoundError) { if (isNotFoundError(e)) {
return; return;
} }
throw error; throw e;
}); });
await volumeManager.createFromPath( await volumeManager.createFromPath(

View File

@ -3,15 +3,7 @@ process.env.DOCKER_HOST = 'unix:///your/dockerode/mocks/are/not/working';
import * as dockerode from 'dockerode'; import * as dockerode from 'dockerode';
import { Stream } from 'stream'; import { Stream } from 'stream';
import _ = require('lodash'); import _ = require('lodash');
import { TypedError } from 'typed-error'; import { NotFoundError } from '~/lib/errors';
export class NotFoundError extends TypedError {
public statusCode: number;
constructor() {
super();
this.statusCode = 404;
}
}
const overrides: Dictionary<(...args: any[]) => Resolvable<any>> = {}; const overrides: Dictionary<(...args: any[]) => Resolvable<any>> = {};