Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 60 additions & 6 deletions packages/@aws-cdk/tmp-toolkit-helpers/src/api/toolkit-error.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
const TOOLKIT_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit.ToolkitError');
const AUTHENTICATION_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit.AuthenticationError');
const ASSEMBLY_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit.AssemblyError');
const CONTEXT_PROVIDER_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit.ContextProviderError');
import type * as cxapi from '@aws-cdk/cx-api';

const TOOLKIT_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit-lib.ToolkitError');
const AUTHENTICATION_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit-lib.AuthenticationError');
const ASSEMBLY_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit-lib.AssemblyError');
const CONTEXT_PROVIDER_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit-lib.ContextProviderError');
Comment on lines +3 to +6
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed these in the rename. They are private symbols, so it's fine.


/**
* Represents a general toolkit error in the AWS CDK Toolkit.
Expand Down Expand Up @@ -40,19 +42,30 @@ export class ToolkitError extends Error {
*/
public readonly type: string;

/**
* Denotes the source of the error as the toolkit.
*/
public readonly source: 'toolkit' | 'user';

constructor(message: string, type: string = 'toolkit') {
super(message);
Object.setPrototypeOf(this, ToolkitError.prototype);
Object.defineProperty(this, TOOLKIT_ERROR_SYMBOL, { value: true });
this.name = new.target.name;
this.type = type;
this.source = 'toolkit';
}
}

/**
* Represents an authentication-specific error in the AWS CDK Toolkit.
*/
export class AuthenticationError extends ToolkitError {
/**
* Denotes the source of the error as user.
*/
public readonly source = 'user';

constructor(message: string) {
super(message, 'authentication');
Object.setPrototypeOf(this, AuthenticationError.prototype);
Expand All @@ -61,20 +74,61 @@ export class AuthenticationError extends ToolkitError {
}

/**
* Represents an authentication-specific error in the AWS CDK Toolkit.
* Represents an error causes by cloud assembly synthesis
*
* This includes errors thrown during app execution, as well as failing annotations.
*/
export class AssemblyError extends ToolkitError {
constructor(message: string) {
/**
* An AssemblyError with an original error as cause
*/
public static withCause(message: string, error: unknown): AssemblyError {
return new AssemblyError(message, undefined, error);
}

/**
* An AssemblyError with a list of stacks as cause
*/
public static withStacks(message: string, stacks?: cxapi.CloudFormationStackArtifact[]): AssemblyError {
return new AssemblyError(message, stacks);
}

/**
* Denotes the source of the error as user.
*/
public readonly source = 'user';

/**
* The stacks that caused the error, if available
*
* The `messages` property of each `cxapi.CloudFormationStackArtifact` will contain the respective errors.
* Absence indicates synthesis didn't fully complete.
*/
public readonly stacks?: cxapi.CloudFormationStackArtifact[];

/**
* The specific original cause of the error, if available
*/
public readonly cause?: unknown;

private constructor(message: string, stacks?: cxapi.CloudFormationStackArtifact[], cause?: unknown) {
super(message, 'assembly');
Object.setPrototypeOf(this, AssemblyError.prototype);
Object.defineProperty(this, ASSEMBLY_ERROR_SYMBOL, { value: true });
this.stacks = stacks;
this.cause = cause;
}
}

/**
* Represents an error originating from a Context Provider
*/
export class ContextProviderError extends ToolkitError {
/**
* Denotes the source of the error as user.
*/
public readonly source = 'user';

constructor(message: string) {
super(message, 'context-provider');
Object.setPrototypeOf(this, ContextProviderError.prototype);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,58 @@ import { AssemblyError, AuthenticationError, ContextProviderError, ToolkitError
describe('toolkit error', () => {
let toolkitError = new ToolkitError('Test toolkit error');
let authError = new AuthenticationError('Test authentication error');
let assemblyError = new AssemblyError('Test authentication error');
let contextProviderError = new ContextProviderError('Test context provider error');
let assemblyError = AssemblyError.withStacks('Test authentication error', []);
let assemblyCauseError = AssemblyError.withCause('Test authentication error', new Error('other error'));

test('types are correctly assigned', async () => {
expect(toolkitError.type).toBe('toolkit');
expect(authError.type).toBe('authentication');
expect(assemblyError.type).toBe('assembly');
expect(assemblyCauseError.type).toBe('assembly');
expect(contextProviderError.type).toBe('context-provider');
});

test('isToolkitError works', () => {
expect(toolkitError.source).toBe('toolkit');

expect(ToolkitError.isToolkitError(toolkitError)).toBe(true);
expect(ToolkitError.isToolkitError(authError)).toBe(true);
expect(ToolkitError.isToolkitError(assemblyError)).toBe(true);
expect(ToolkitError.isToolkitError(assemblyCauseError)).toBe(true);
expect(ToolkitError.isToolkitError(contextProviderError)).toBe(true);
});

test('isAuthenticationError works', () => {
expect(authError.source).toBe('user');

expect(ToolkitError.isAuthenticationError(toolkitError)).toBe(false);
expect(ToolkitError.isAuthenticationError(authError)).toBe(true);
});

test('isAssemblyError works', () => {
expect(ToolkitError.isAssemblyError(assemblyError)).toBe(true);
expect(ToolkitError.isAssemblyError(toolkitError)).toBe(false);
expect(ToolkitError.isAssemblyError(authError)).toBe(false);
describe('isAssemblyError works', () => {
test('AssemblyError.fromStacks', () => {
expect(assemblyError.source).toBe('user');
expect(assemblyError.stacks).toStrictEqual([]);

expect(ToolkitError.isAssemblyError(assemblyError)).toBe(true);
expect(ToolkitError.isAssemblyError(toolkitError)).toBe(false);
expect(ToolkitError.isAssemblyError(authError)).toBe(false);
});

test('AssemblyError.fromCause', () => {
expect(assemblyCauseError.source).toBe('user');
expect((assemblyCauseError.cause as any)?.message).toBe('other error');

expect(ToolkitError.isAssemblyError(assemblyCauseError)).toBe(true);
expect(ToolkitError.isAssemblyError(toolkitError)).toBe(false);
expect(ToolkitError.isAssemblyError(authError)).toBe(false);
});
});

test('isContextProviderError works', () => {
expect(contextProviderError.source).toBe('user');

expect(ToolkitError.isContextProviderError(contextProviderError)).toBe(true);
expect(ToolkitError.isContextProviderError(toolkitError)).toBe(false);
expect(ToolkitError.isContextProviderError(authError)).toBe(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { assemblyFromDirectory, changeDir, determineOutputDirectory, guessExecut
import { ToolkitServices } from '../../../toolkit/private';
import { Context, ILock, RWLock, Settings } from '../../aws-cdk';
import { CODES } from '../../io/private';
import { ToolkitError } from '../../shared-public';
import { ToolkitError, AssemblyError } from '../../shared-public';
import { AssemblyBuilder } from '../source-builder';

export abstract class CloudAssemblySourceBuilder {
Expand Down Expand Up @@ -42,10 +42,21 @@ export abstract class CloudAssemblySourceBuilder {
const env = await prepareDefaultEnvironment(services, { outdir });
const assembly = await changeDir(async () =>
withContext(context.all, env, props.synthOptions ?? {}, async (envWithContext, ctx) =>
withEnv(envWithContext, () => builder({
outdir,
context: ctx,
})),
withEnv(envWithContext, () => {
try {
return builder({
outdir,
context: ctx,
});
} catch (error: unknown) {
// re-throw toolkit errors unchanged
if (ToolkitError.isToolkitError(error)) {
throw error;
}
// otherwise, wrap into an assembly error
throw AssemblyError.withCause('Assembly builder failed', error);
}
}),
), props.workingDirectory);

if (cxapi.CloudAssembly.isCloudAssembly(assembly)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { ToolkitError } from '../../../lib';
import { Toolkit } from '../../../lib/toolkit';
import { appFixture, builderFixture, cdkOutFixture, TestIoHost } from '../../_helpers';

// these tests often run a bit longer than the default
jest.setTimeout(10_000);

const ioHost = new TestIoHost();
const toolkit = new Toolkit({ ioHost });

Expand Down Expand Up @@ -30,6 +34,22 @@ describe('fromAssemblyBuilder', () => {
// THEN
expect(JSON.stringify(stack)).toContain('amzn-s3-demo-bucket');
});

test('errors are wrapped as AssemblyError', async () => {
// GIVEN
const cx = await toolkit.fromAssemblyBuilder(() => {
throw new Error('a wild error appeared');
});

// WHEN
try {
await cx.produce();
} catch (err: any) {
// THEN
expect(ToolkitError.isAssemblyError(err)).toBe(true);
expect(err.cause?.message).toContain('a wild error appeared');
}
});
});

describe('fromCdkApp', () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,11 +336,11 @@ export class StackCollection {
}

if (errors && failAt != 'none') {
throw new AssemblyError('Found errors');
throw AssemblyError.withStacks('Found errors', this.stackArtifacts);
}

if (warnings && failAt === 'warn') {
throw new AssemblyError('Found warnings (--strict mode)');
throw AssemblyError.withStacks('Found warnings (--strict mode)', this.stackArtifacts);
}
}
}
Expand Down