Skip to content

Commit

Permalink
chore: reclassify file permissions errors as AmplifyError (#12336)
Browse files Browse the repository at this point in the history
* chore: reclassify file permissions errors as AmplifyError

* fix: run extract-api
  • Loading branch information
Amplifiyer authored Mar 30, 2023
1 parent d4874b0 commit bc09983
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 61 deletions.
2 changes: 1 addition & 1 deletion packages/amplify-cli-core/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export class AmplifyError extends AmplifyException {
}

// @public (undocumented)
export type AmplifyErrorType = 'AmplifyStudioError' | 'AmplifyStudioLoginError' | 'AmplifyStudioNotEnabledError' | 'ApiCategorySchemaNotFoundError' | 'AuthImportError' | 'BackendConfigValidationError' | 'BucketAlreadyExistsError' | 'BucketNotFoundError' | 'CategoryNotEnabledError' | 'CloudFormationTemplateError' | 'CommandNotSupportedError' | 'ConfigurationError' | 'CustomPoliciesFormatError' | 'DebugConfigValueNotSetError' | 'DeploymentError' | 'DeploymentInProgressError' | 'DestructiveMigrationError' | 'DiagnoseReportUploadError' | 'DirectoryAlreadyExistsError' | 'DirectoryError' | 'DuplicateLogicalIdError' | 'EnvironmentConfigurationError' | 'EnvironmentNameError' | 'EnvironmentNotInitializedError' | 'ExportError' | 'FeatureFlagsValidationError' | 'FrameworkNotSupportedError' | 'FunctionTooLargeError' | 'GraphQLError' | 'InputValidationError' | 'InvalidAmplifyAppIdError' | 'InvalidCustomResourceError' | 'InvalidDirectiveError' | 'InvalidGSIMigrationError' | 'InvalidMigrationError' | 'InvalidOverrideError' | 'InvalidStackError' | 'InvalidTransformerError' | 'IterativeRollbackError' | 'LambdaFunctionInvokeError' | 'LambdaLayerDeleteError' | 'MigrationError' | 'MissingAmplifyMetaFileError' | 'MissingExpectedParameterError' | 'MissingOverridesInstallationRequirementsError' | 'MockProcessError' | 'ModelgenError' | 'NestedProjectInitError' | 'NotImplementedError' | 'NoUpdateBackendError' | 'OpenSslCertificateError' | 'PackagingLambdaFunctionError' | 'ParameterNotFoundError' | 'PermissionsError' | 'PluginMethodNotFoundError' | 'PluginNotFoundError' | 'PluginPolicyAddError' | 'ProfileConfigurationError' | 'ProjectAppIdResolveError' | 'ProjectInitError' | 'ProjectNotFoundError' | 'ProjectNotInitializedError' | 'PushResourcesError' | 'RegionNotAvailableError' | 'RemoveNotificationAppError' | 'ResourceAlreadyExistsError' | 'ResourceCountLimitExceedError' | 'ResourceDoesNotExistError' | 'ResourceInUseError' | 'ResourceNotReadyError' | 'SchemaNotFoundError' | 'SchemaValidationError' | 'SearchableMockProcessError' | 'SearchableMockUnavailablePortError' | 'SearchableMockUnsupportedPlatformError' | 'StackNotFoundError' | 'StackStateError' | 'StorageImportError' | 'TransformerContractError' | 'UnknownDirectiveError' | 'UnsupportedLockFileTypeError' | 'UserInputError';
export type AmplifyErrorType = 'AmplifyStudioError' | 'AmplifyStudioLoginError' | 'AmplifyStudioNotEnabledError' | 'ApiCategorySchemaNotFoundError' | 'AuthImportError' | 'BackendConfigValidationError' | 'BucketAlreadyExistsError' | 'BucketNotFoundError' | 'CategoryNotEnabledError' | 'CloudFormationTemplateError' | 'CommandNotSupportedError' | 'ConfigurationError' | 'CustomPoliciesFormatError' | 'DebugConfigValueNotSetError' | 'DeploymentError' | 'DeploymentInProgressError' | 'DestructiveMigrationError' | 'DiagnoseReportUploadError' | 'DirectoryAlreadyExistsError' | 'DirectoryError' | 'DuplicateLogicalIdError' | 'EnvironmentConfigurationError' | 'EnvironmentNameError' | 'EnvironmentNotInitializedError' | 'ExportError' | 'FeatureFlagsValidationError' | 'FileSystemPermissionsError' | 'FrameworkNotSupportedError' | 'FunctionTooLargeError' | 'GraphQLError' | 'InputValidationError' | 'InvalidAmplifyAppIdError' | 'InvalidCustomResourceError' | 'InvalidDirectiveError' | 'InvalidGSIMigrationError' | 'InvalidMigrationError' | 'InvalidOverrideError' | 'InvalidStackError' | 'InvalidTransformerError' | 'IterativeRollbackError' | 'LambdaFunctionInvokeError' | 'LambdaLayerDeleteError' | 'MigrationError' | 'MissingAmplifyMetaFileError' | 'MissingExpectedParameterError' | 'MissingOverridesInstallationRequirementsError' | 'MockProcessError' | 'ModelgenError' | 'NestedProjectInitError' | 'NotImplementedError' | 'NoUpdateBackendError' | 'OpenSslCertificateError' | 'PackagingLambdaFunctionError' | 'ParameterNotFoundError' | 'PermissionsError' | 'PluginMethodNotFoundError' | 'PluginNotFoundError' | 'PluginPolicyAddError' | 'ProfileConfigurationError' | 'ProjectAppIdResolveError' | 'ProjectInitError' | 'ProjectNotFoundError' | 'ProjectNotInitializedError' | 'PushResourcesError' | 'RegionNotAvailableError' | 'RemoveNotificationAppError' | 'ResourceAlreadyExistsError' | 'ResourceCountLimitExceedError' | 'ResourceDoesNotExistError' | 'ResourceInUseError' | 'ResourceNotReadyError' | 'SchemaNotFoundError' | 'SchemaValidationError' | 'SearchableMockProcessError' | 'SearchableMockUnavailablePortError' | 'SearchableMockUnsupportedPlatformError' | 'StackNotFoundError' | 'StackStateError' | 'StorageImportError' | 'TransformerContractError' | 'UnknownDirectiveError' | 'UnsupportedLockFileTypeError' | 'UserInputError';

// @public (undocumented)
export enum AmplifyEvent {
Expand Down
1 change: 1 addition & 0 deletions packages/amplify-cli-core/src/errors/amplify-exception.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export type AmplifyErrorType =
| 'EnvironmentNotInitializedError'
| 'ExportError'
| 'FeatureFlagsValidationError'
| 'FileSystemPermissionsError'
| 'FrameworkNotSupportedError'
| 'FunctionTooLargeError'
| 'GraphQLError'
Expand Down
120 changes: 78 additions & 42 deletions packages/amplify-cli/src/__tests__/amplify-exception-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,25 @@ const processExit = jest.spyOn(process, 'exit').mockImplementation((__code?: num
jest.mock('amplify-prompts');

describe('test exception handler', () => {
const emitErrorMock = jest.fn();
const contextMock = {
amplify: {},
usageData: {
emitError: emitErrorMock,
},
input: {
options: {},
},
} as unknown as Context;
beforeEach(() => {
jest.resetAllMocks();
init(contextMock);
});
it('error handler should call usageData emitError', async () => {
const amplifyError = new AmplifyError('NotImplementedError', {
message: 'Test Not implemented',
resolution: 'Test Not implemented',
});
const contextMock = {
amplify: {},
usageData: {
emitError: jest.fn(),
},
input: {
options: {},
},
} as unknown as Context;

init(contextMock);
await handleException(amplifyError);

expect(contextMock.usageData.emitError).toHaveBeenCalledWith(amplifyError);
Expand All @@ -44,17 +47,6 @@ describe('test exception handler', () => {
message: 'Test Not implemented',
resolution: 'Test Not implemented',
});
const contextMock = {
amplify: {},
usageData: {
emitError: jest.fn(),
},
input: {
options: {},
},
} as unknown as Context;

init(contextMock);
await handleException(amplifyError);

expect(reportErrorMock).toHaveBeenCalledWith(contextMock, amplifyError);
Expand All @@ -67,17 +59,7 @@ describe('test exception handler', () => {
details: 'Test Not implemented',
resolution: 'Test Not implemented',
});
const contextMock = {
amplify: {},
usageData: {
emitError: jest.fn(),
},
input: {
options: {},
},
} as unknown as Context;

init(contextMock);
await handleException(amplifyError);

expect(printerMock.error).toHaveBeenCalledWith(amplifyError.message);
Expand All @@ -91,17 +73,7 @@ describe('test exception handler', () => {
details: 'Test Not implemented',
resolution: 'Test Not implemented',
});
const contextMock = {
amplify: {},
usageData: {
emitError: jest.fn(),
},
input: {
options: {},
},
} as unknown as Context;

init(contextMock);
reportErrorMock.mockRejectedValueOnce(new Error('MockTestError'));
await handleException(amplifyError);

Expand All @@ -110,6 +82,70 @@ describe('test exception handler', () => {
expect(printerMock.debug).toHaveBeenCalledWith(amplifyError.stack);
expect(printerMock.error).toHaveBeenCalledWith('Failed to report error: MockTestError');
});

it('error handler should handle nodejs file permission errors for log files', async () => {
const code = 'EACCES';
const path = '/user/name/.amplify/path/to/log';
const nodeJSError = new Error(`permission denied, open ${path}`) as NodeJS.ErrnoException;
nodeJSError.code = code;
nodeJSError.path = path;

await handleException(nodeJSError);
expect(emitErrorMock).toHaveBeenCalledTimes(1);
expect(emitErrorMock).toHaveBeenCalledWith(
new AmplifyError('FileSystemPermissionsError', { message: `permission denied, open ${path}` }),
);
expect(printerMock.info).toHaveBeenCalledWith(`Resolution: Try running 'sudo chown -R $(whoami):$(id -gn) ~/.amplify' to fix this`);
});

it('error handler should handle nodejs file permission errors for ~/.aws/amplify files', async () => {
const code = 'EACCES';
const path = '/user/name/.aws/amplify/someFile';
const nodeJSError = new Error(`permission denied, open ${path}`) as NodeJS.ErrnoException;
nodeJSError.code = code;
nodeJSError.path = path;

await handleException(nodeJSError);
expect(emitErrorMock).toHaveBeenCalledTimes(1);
expect(emitErrorMock).toHaveBeenCalledWith(
new AmplifyError('FileSystemPermissionsError', { message: `permission denied, open ${path}` }),
);
expect(printerMock.info).toHaveBeenCalledWith(`Resolution: Try running 'sudo chown -R $(whoami):$(id -gn) ~/.aws/amplify' to fix this`);
});

it('error handler should handle nodejs file permission errors for amplify project', async () => {
const code = 'EACCES';
const path = '/user/name/workspace/amplify/path/to/manifest';
const nodeJSError = new Error(`permission denied, open ${path}`) as NodeJS.ErrnoException;
nodeJSError.code = code;
nodeJSError.path = path;

await handleException(nodeJSError);
expect(emitErrorMock).toHaveBeenCalledTimes(1);
expect(emitErrorMock).toHaveBeenCalledWith(
new AmplifyError('FileSystemPermissionsError', { message: `permission denied, open ${path}` }),
);
// different resolution based on the file path compared to last test
expect(printerMock.info).toHaveBeenCalledWith(
`Resolution: Try running 'sudo chown -R $(whoami):$(id -gn) <your amplify app directory>' to fix this`,
);
});

it('error handler should handle nodejs file permission errors for other files', async () => {
const code = 'EACCES';
const path = '/usr/name/.aws/config';
const nodeJSError = new Error(`permission denied, open ${path}`) as NodeJS.ErrnoException;
nodeJSError.code = code;
nodeJSError.path = path;

await handleException(nodeJSError);
expect(emitErrorMock).toHaveBeenCalledTimes(1);
expect(emitErrorMock).toHaveBeenCalledWith(
new AmplifyError('FileSystemPermissionsError', { message: `permission denied, open ${path}` }),
);
// different resolution based on the file path compared to last test
expect(printerMock.info).toHaveBeenCalledWith(`Resolution: Try running 'sudo chown -R $(whoami):$(id -gn) ${path}' to fix this`);
});
});

describe('test unhandled rejection handler', () => {
Expand Down
51 changes: 33 additions & 18 deletions packages/amplify-cli/src/amplify-exception-handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { $TSAny, AmplifyException, AmplifyFaultType, AmplifyFault, executeHooks, HooksMeta, isWindowsPlatform } from 'amplify-cli-core';
import {
$TSAny,
AmplifyException,
AmplifyFaultType,
AmplifyFault,
executeHooks,
HooksMeta,
isWindowsPlatform,
AmplifyError,
} from 'amplify-cli-core';
import { getAmplifyLogger } from '@aws-amplify/amplify-cli-logger';
import { AmplifyPrinter, printer } from '@aws-amplify/amplify-prompts';
import { reportError } from './commands/diagnose';
Expand Down Expand Up @@ -153,44 +162,50 @@ const printHeadlessAmplifyException = (amplifyException: AmplifyException): void
const unknownErrorToAmplifyException = (err: unknown): AmplifyException =>
new AmplifyFault(unknownErrorTypeToAmplifyExceptionType(), {
message: typeof err === 'object' && err !== null && 'message' in err ? (err as $TSAny).message : 'Unknown error',
resolution: mapUnknownErrorToResolution(),
resolution: genericFaultResolution,
});

const genericErrorToAmplifyException = (err: Error): AmplifyException =>
new AmplifyFault(
genericErrorTypeToAmplifyExceptionType(),
{
message: err.message,
resolution: mapGenericErrorToResolution(),
resolution: genericFaultResolution,
},
err,
);

const nodeErrorToAmplifyException = (err: NodeJS.ErrnoException): AmplifyException =>
new AmplifyFault(
const nodeErrorToAmplifyException = (err: NodeJS.ErrnoException): AmplifyException => {
if (!isWindowsPlatform() && err.code === 'EACCES') {
let path = err.path;
if (err.message.includes('/.amplify/')) {
path = '~/.amplify';
} else if (err.message.includes('/.aws/amplify/')) {
path = '~/.aws/amplify';
} else if (err.message.includes('/amplify/')) {
path = '<your amplify app directory>';
}
return new AmplifyError(
'FileSystemPermissionsError',
{ message: err.message, resolution: `Try running 'sudo chown -R $(whoami):$(id -gn) ${path}' to fix this` },
err,
);
}
return new AmplifyFault(
nodeErrorTypeToAmplifyExceptionType(),
{
message: err.message,
resolution: mapNodeErrorToResolution(err),
resolution: genericFaultResolution,
code: err.code,
},
err,
);

const nodeErrorTypeToAmplifyExceptionType = (): AmplifyFaultType => 'UnknownNodeJSFault';
const mapNodeErrorToResolution = (err: NodeJS.ErrnoException): string => {
if (!isWindowsPlatform() && err.code === 'EACCES' && err.message.includes('/.amplify/')) {
return `Try running 'sudo chown -R $(whoami):$(id -gn) ~/.amplify' to fix this`;
}
return `Please report this issue at https://github.com/aws-amplify/amplify-cli/issues and include the project identifier from: 'amplify diagnose --send-report'`;
};

const nodeErrorTypeToAmplifyExceptionType = (): AmplifyFaultType => 'UnknownNodeJSFault';
const genericErrorTypeToAmplifyExceptionType = (): AmplifyFaultType => 'UnknownFault';
const mapGenericErrorToResolution = (): string =>
`Please report this issue at https://github.com/aws-amplify/amplify-cli/issues and include the project identifier from: 'amplify diagnose --send-report'`;

const unknownErrorTypeToAmplifyExceptionType = (): AmplifyFaultType => 'UnknownFault';
const mapUnknownErrorToResolution = (): string =>
`Please report this issue at https://github.com/aws-amplify/amplify-cli/issues and include the project identifier from: 'amplify diagnose --send-report'`;

const genericFaultResolution = `Please report this issue at https://github.com/aws-amplify/amplify-cli/issues and include the project identifier from: 'amplify diagnose --send-report'`;

const isNodeJsError = (err: Error): err is NodeJS.ErrnoException => (err as $TSAny).code !== undefined;

0 comments on commit bc09983

Please sign in to comment.