Skip to content

Commit e3b0856

Browse files
authored
feat(toolkit-lib): wrap errors from assembly builder into AssemblyError (#200)
Fixes errors thrown from a CloudAssembly produced by a builder function are unstructured. Wrapping errors will allow better handling by implementors. Also introduced the `source` property on `ToolkitError`s as per the RFC, to denote the source of an error (user or toolkit). --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license
1 parent 6e0b7f2 commit e3b0856

File tree

5 files changed

+126
-18
lines changed

5 files changed

+126
-18
lines changed

packages/@aws-cdk/tmp-toolkit-helpers/src/api/toolkit-error.ts

+60-6
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
const TOOLKIT_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit.ToolkitError');
2-
const AUTHENTICATION_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit.AuthenticationError');
3-
const ASSEMBLY_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit.AssemblyError');
4-
const CONTEXT_PROVIDER_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit.ContextProviderError');
1+
import type * as cxapi from '@aws-cdk/cx-api';
2+
3+
const TOOLKIT_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit-lib.ToolkitError');
4+
const AUTHENTICATION_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit-lib.AuthenticationError');
5+
const ASSEMBLY_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit-lib.AssemblyError');
6+
const CONTEXT_PROVIDER_ERROR_SYMBOL = Symbol.for('@aws-cdk/toolkit-lib.ContextProviderError');
57

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

45+
/**
46+
* Denotes the source of the error as the toolkit.
47+
*/
48+
public readonly source: 'toolkit' | 'user';
49+
4350
constructor(message: string, type: string = 'toolkit') {
4451
super(message);
4552
Object.setPrototypeOf(this, ToolkitError.prototype);
4653
Object.defineProperty(this, TOOLKIT_ERROR_SYMBOL, { value: true });
4754
this.name = new.target.name;
4855
this.type = type;
56+
this.source = 'toolkit';
4957
}
5058
}
5159

5260
/**
5361
* Represents an authentication-specific error in the AWS CDK Toolkit.
5462
*/
5563
export class AuthenticationError extends ToolkitError {
64+
/**
65+
* Denotes the source of the error as user.
66+
*/
67+
public readonly source = 'user';
68+
5669
constructor(message: string) {
5770
super(message, 'authentication');
5871
Object.setPrototypeOf(this, AuthenticationError.prototype);
@@ -61,20 +74,61 @@ export class AuthenticationError extends ToolkitError {
6174
}
6275

6376
/**
64-
* Represents an authentication-specific error in the AWS CDK Toolkit.
77+
* Represents an error causes by cloud assembly synthesis
78+
*
79+
* This includes errors thrown during app execution, as well as failing annotations.
6580
*/
6681
export class AssemblyError extends ToolkitError {
67-
constructor(message: string) {
82+
/**
83+
* An AssemblyError with an original error as cause
84+
*/
85+
public static withCause(message: string, error: unknown): AssemblyError {
86+
return new AssemblyError(message, undefined, error);
87+
}
88+
89+
/**
90+
* An AssemblyError with a list of stacks as cause
91+
*/
92+
public static withStacks(message: string, stacks?: cxapi.CloudFormationStackArtifact[]): AssemblyError {
93+
return new AssemblyError(message, stacks);
94+
}
95+
96+
/**
97+
* Denotes the source of the error as user.
98+
*/
99+
public readonly source = 'user';
100+
101+
/**
102+
* The stacks that caused the error, if available
103+
*
104+
* The `messages` property of each `cxapi.CloudFormationStackArtifact` will contain the respective errors.
105+
* Absence indicates synthesis didn't fully complete.
106+
*/
107+
public readonly stacks?: cxapi.CloudFormationStackArtifact[];
108+
109+
/**
110+
* The specific original cause of the error, if available
111+
*/
112+
public readonly cause?: unknown;
113+
114+
private constructor(message: string, stacks?: cxapi.CloudFormationStackArtifact[], cause?: unknown) {
68115
super(message, 'assembly');
69116
Object.setPrototypeOf(this, AssemblyError.prototype);
70117
Object.defineProperty(this, ASSEMBLY_ERROR_SYMBOL, { value: true });
118+
this.stacks = stacks;
119+
this.cause = cause;
71120
}
72121
}
73122

74123
/**
75124
* Represents an error originating from a Context Provider
76125
*/
77126
export class ContextProviderError extends ToolkitError {
127+
/**
128+
* Denotes the source of the error as user.
129+
*/
130+
public readonly source = 'user';
131+
78132
constructor(message: string) {
79133
super(message, 'context-provider');
80134
Object.setPrototypeOf(this, ContextProviderError.prototype);

packages/@aws-cdk/tmp-toolkit-helpers/test/api/toolkit-error.test.ts

+28-5
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,58 @@ import { AssemblyError, AuthenticationError, ContextProviderError, ToolkitError
33
describe('toolkit error', () => {
44
let toolkitError = new ToolkitError('Test toolkit error');
55
let authError = new AuthenticationError('Test authentication error');
6-
let assemblyError = new AssemblyError('Test authentication error');
76
let contextProviderError = new ContextProviderError('Test context provider error');
7+
let assemblyError = AssemblyError.withStacks('Test authentication error', []);
8+
let assemblyCauseError = AssemblyError.withCause('Test authentication error', new Error('other error'));
89

910
test('types are correctly assigned', async () => {
1011
expect(toolkitError.type).toBe('toolkit');
1112
expect(authError.type).toBe('authentication');
1213
expect(assemblyError.type).toBe('assembly');
14+
expect(assemblyCauseError.type).toBe('assembly');
1315
expect(contextProviderError.type).toBe('context-provider');
1416
});
1517

1618
test('isToolkitError works', () => {
19+
expect(toolkitError.source).toBe('toolkit');
20+
1721
expect(ToolkitError.isToolkitError(toolkitError)).toBe(true);
1822
expect(ToolkitError.isToolkitError(authError)).toBe(true);
1923
expect(ToolkitError.isToolkitError(assemblyError)).toBe(true);
24+
expect(ToolkitError.isToolkitError(assemblyCauseError)).toBe(true);
2025
expect(ToolkitError.isToolkitError(contextProviderError)).toBe(true);
2126
});
2227

2328
test('isAuthenticationError works', () => {
29+
expect(authError.source).toBe('user');
30+
2431
expect(ToolkitError.isAuthenticationError(toolkitError)).toBe(false);
2532
expect(ToolkitError.isAuthenticationError(authError)).toBe(true);
2633
});
2734

28-
test('isAssemblyError works', () => {
29-
expect(ToolkitError.isAssemblyError(assemblyError)).toBe(true);
30-
expect(ToolkitError.isAssemblyError(toolkitError)).toBe(false);
31-
expect(ToolkitError.isAssemblyError(authError)).toBe(false);
35+
describe('isAssemblyError works', () => {
36+
test('AssemblyError.fromStacks', () => {
37+
expect(assemblyError.source).toBe('user');
38+
expect(assemblyError.stacks).toStrictEqual([]);
39+
40+
expect(ToolkitError.isAssemblyError(assemblyError)).toBe(true);
41+
expect(ToolkitError.isAssemblyError(toolkitError)).toBe(false);
42+
expect(ToolkitError.isAssemblyError(authError)).toBe(false);
43+
});
44+
45+
test('AssemblyError.fromCause', () => {
46+
expect(assemblyCauseError.source).toBe('user');
47+
expect((assemblyCauseError.cause as any)?.message).toBe('other error');
48+
49+
expect(ToolkitError.isAssemblyError(assemblyCauseError)).toBe(true);
50+
expect(ToolkitError.isAssemblyError(toolkitError)).toBe(false);
51+
expect(ToolkitError.isAssemblyError(authError)).toBe(false);
52+
});
3253
});
3354

3455
test('isContextProviderError works', () => {
56+
expect(contextProviderError.source).toBe('user');
57+
3558
expect(ToolkitError.isContextProviderError(contextProviderError)).toBe(true);
3659
expect(ToolkitError.isContextProviderError(toolkitError)).toBe(false);
3760
expect(ToolkitError.isContextProviderError(authError)).toBe(false);

packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/source-builder.ts

+16-5
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { assemblyFromDirectory, changeDir, determineOutputDirectory, guessExecut
77
import { ToolkitServices } from '../../../toolkit/private';
88
import { Context, ILock, RWLock, Settings } from '../../aws-cdk';
99
import { CODES } from '../../io/private';
10-
import { ToolkitError } from '../../shared-public';
10+
import { ToolkitError, AssemblyError } from '../../shared-public';
1111
import { AssemblyBuilder } from '../source-builder';
1212

1313
export abstract class CloudAssemblySourceBuilder {
@@ -42,10 +42,21 @@ export abstract class CloudAssemblySourceBuilder {
4242
const env = await prepareDefaultEnvironment(services, { outdir });
4343
const assembly = await changeDir(async () =>
4444
withContext(context.all, env, props.synthOptions ?? {}, async (envWithContext, ctx) =>
45-
withEnv(envWithContext, () => builder({
46-
outdir,
47-
context: ctx,
48-
})),
45+
withEnv(envWithContext, () => {
46+
try {
47+
return builder({
48+
outdir,
49+
context: ctx,
50+
});
51+
} catch (error: unknown) {
52+
// re-throw toolkit errors unchanged
53+
if (ToolkitError.isToolkitError(error)) {
54+
throw error;
55+
}
56+
// otherwise, wrap into an assembly error
57+
throw AssemblyError.withCause('Assembly builder failed', error);
58+
}
59+
}),
4960
), props.workingDirectory);
5061

5162
if (cxapi.CloudAssembly.isCloudAssembly(assembly)) {

packages/@aws-cdk/toolkit-lib/test/api/cloud-assembly/source-builder.test.ts

+20
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import { ToolkitError } from '../../../lib';
12
import { Toolkit } from '../../../lib/toolkit';
23
import { appFixture, builderFixture, cdkOutFixture, TestIoHost } from '../../_helpers';
34

5+
// these tests often run a bit longer than the default
6+
jest.setTimeout(10_000);
7+
48
const ioHost = new TestIoHost();
59
const toolkit = new Toolkit({ ioHost });
610

@@ -30,6 +34,22 @@ describe('fromAssemblyBuilder', () => {
3034
// THEN
3135
expect(JSON.stringify(stack)).toContain('amzn-s3-demo-bucket');
3236
});
37+
38+
test('errors are wrapped as AssemblyError', async () => {
39+
// GIVEN
40+
const cx = await toolkit.fromAssemblyBuilder(() => {
41+
throw new Error('a wild error appeared');
42+
});
43+
44+
// WHEN
45+
try {
46+
await cx.produce();
47+
} catch (err: any) {
48+
// THEN
49+
expect(ToolkitError.isAssemblyError(err)).toBe(true);
50+
expect(err.cause?.message).toContain('a wild error appeared');
51+
}
52+
});
3353
});
3454

3555
describe('fromCdkApp', () => {

packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -336,11 +336,11 @@ export class StackCollection {
336336
}
337337

338338
if (errors && failAt != 'none') {
339-
throw new AssemblyError('Found errors');
339+
throw AssemblyError.withStacks('Found errors', this.stackArtifacts);
340340
}
341341

342342
if (warnings && failAt === 'warn') {
343-
throw new AssemblyError('Found warnings (--strict mode)');
343+
throw AssemblyError.withStacks('Found warnings (--strict mode)', this.stackArtifacts);
344344
}
345345
}
346346
}

0 commit comments

Comments
 (0)