Skip to content

Commit ae95d95

Browse files
authored
feat(cx-api): throw CloudAssemblyError instead of untyped Errors (#33390)
### Issue Relates to #32569 ### Description of changes `ValidationErrors` everywhere ### Describe any new or updated permissions being added n/a ### Description of how you validated changes Existing tests. Exemptions granted as this is a refactor of existing code. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent d3f3309 commit ae95d95

11 files changed

+76
-18
lines changed

packages/aws-cdk-lib/.eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const enableNoThrowDefaultErrorIn = [
6060
'aws-ssmincidents',
6161
'aws-ssmquicksetup',
6262
'aws-synthetics',
63+
'cx-api',
6364
'aws-s3',
6465
'aws-s3-assets',
6566
'aws-s3-deployment',

packages/aws-cdk-lib/core/lib/errors.ts

+11
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { IConstruct } from 'constructs';
22
import { constructInfoFromConstruct } from './helpers-internal';
3+
import type { CloudAssemblyError } from '../../cx-api/lib/private/error';
34

45
const CONSTRUCT_ERROR_SYMBOL = Symbol.for('@aws-cdk/core.SynthesisError');
56
const VALIDATION_ERROR_SYMBOL = Symbol.for('@aws-cdk/core.ValidationError');
7+
const ASSEMBLY_ERROR_SYMBOL = Symbol.for('@aws-cdk/cx-api.CloudAssemblyError');
68

79
/**
810
* Helper to check if an error is of a certain type.
@@ -27,6 +29,15 @@ export class Errors {
2729
public static isValidationError(x: any): x is ValidationError {
2830
return Errors.isConstructError(x) && VALIDATION_ERROR_SYMBOL in x;
2931
}
32+
33+
/**
34+
* Test whether the given error is a CloudAssemblyError.
35+
*
36+
* A CloudAssemblyError is thrown for unexpected problems with the synthesized assembly.
37+
*/
38+
public static isCloudAssemblyError(x: any): x is CloudAssemblyError {
39+
return x !== null && typeof(x) === 'object' && ASSEMBLY_ERROR_SYMBOL in x;
40+
}
3041
}
3142

3243
interface ConstructInfo {

packages/aws-cdk-lib/cx-api/lib/artifacts/asset-manifest-artifact.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as path from 'path';
33
import * as cxschema from '../../../cloud-assembly-schema';
44
import { CloudArtifact } from '../cloud-artifact';
55
import { CloudAssembly } from '../cloud-assembly';
6+
import { CloudAssemblyError } from '../private/error';
67

78
const ASSET_MANIFEST_ARTIFACT_SYM = Symbol.for('@aws-cdk/cx-api.AssetManifestArtifact');
89

@@ -55,7 +56,7 @@ export class AssetManifestArtifact extends CloudArtifact {
5556

5657
const properties = (this.manifest.properties || {}) as cxschema.AssetManifestProperties;
5758
if (!properties.file) {
58-
throw new Error('Invalid AssetManifestArtifact. Missing "file" property');
59+
throw new CloudAssemblyError('Invalid AssetManifestArtifact. Missing "file" property');
5960
}
6061
this.file = path.resolve(this.assembly.directory, properties.file);
6162
this.requiresBootstrapStackVersion = properties.requiresBootstrapStackVersion;

packages/aws-cdk-lib/cx-api/lib/artifacts/cloudformation-artifact.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as cxschema from '../../../cloud-assembly-schema';
44
import { CloudArtifact } from '../cloud-artifact';
55
import type { CloudAssembly } from '../cloud-assembly';
66
import { Environment, EnvironmentUtils } from '../environment';
7+
import { CloudAssemblyError } from '../private/error';
78
const CLOUDFORMATION_STACK_ARTIFACT_SYM = Symbol.for('@aws-cdk/cx-api.CloudFormationStackArtifact');
89

910
export class CloudFormationStackArtifact extends CloudArtifact {
@@ -162,10 +163,10 @@ export class CloudFormationStackArtifact extends CloudArtifact {
162163

163164
const properties = (this.manifest.properties || {}) as cxschema.AwsCloudFormationStackProperties;
164165
if (!properties.templateFile) {
165-
throw new Error('Invalid CloudFormation stack artifact. Missing "templateFile" property in cloud assembly manifest');
166+
throw new CloudAssemblyError('Invalid CloudFormation stack artifact. Missing "templateFile" property in cloud assembly manifest');
166167
}
167168
if (!artifact.environment) {
168-
throw new Error('Invalid CloudFormation stack artifact. Missing environment');
169+
throw new CloudAssemblyError('Invalid CloudFormation stack artifact. Missing environment');
169170
}
170171
this.environment = EnvironmentUtils.parse(artifact.environment);
171172
this.templateFile = properties.templateFile;

packages/aws-cdk-lib/cx-api/lib/artifacts/tree-cloud-artifact.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as cxschema from '../../../cloud-assembly-schema';
22
import { CloudArtifact } from '../cloud-artifact';
33
import { CloudAssembly } from '../cloud-assembly';
4+
import { CloudAssemblyError } from '../private/error';
45

56
const TREE_CLOUD_ARTIFACT_SYM = Symbol.for('@aws-cdk/cx-api.TreeCloudArtifact');
67

@@ -33,7 +34,7 @@ export class TreeCloudArtifact extends CloudArtifact {
3334

3435
const properties = (this.manifest.properties || {}) as cxschema.TreeArtifactProperties;
3536
if (!properties.file) {
36-
throw new Error('Invalid TreeCloudArtifact. Missing "file" property');
37+
throw new CloudAssemblyError('Invalid TreeCloudArtifact. Missing "file" property');
3738
}
3839
this.file = properties.file;
3940
}

packages/aws-cdk-lib/cx-api/lib/cloud-artifact.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { CloudAssembly } from './cloud-assembly';
22
import { MetadataEntryResult, SynthesisMessage, SynthesisMessageLevel } from './metadata';
33
import * as cxschema from '../../cloud-assembly-schema';
4+
import { CloudAssemblyError } from './private/error';
45

56
/**
67
* Artifact properties for CloudFormation stacks.
@@ -45,7 +46,7 @@ export class CloudArtifact {
4546
public static fromManifest(assembly: CloudAssembly, id: string, artifact: cxschema.ArtifactManifest): CloudArtifact | undefined {
4647
// Implementation is defined in a separate file to break cyclic dependencies
4748
void(assembly), void(id), void(artifact);
48-
throw new Error('Implementation not overridden yet');
49+
throw new CloudAssemblyError('Implementation not overridden yet');
4950
}
5051

5152
/**
@@ -84,7 +85,7 @@ export class CloudArtifact {
8485
this._deps = this._dependencyIDs.map(id => {
8586
const dep = this.assembly.tryGetArtifact(id);
8687
if (!dep) {
87-
throw new Error(`Artifact ${this.id} depends on non-existing artifact ${id}`);
88+
throw new CloudAssemblyError(`Artifact ${this.id} depends on non-existing artifact ${id}`);
8889
}
8990
return dep;
9091
});

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

+10-9
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { TreeCloudArtifact } from './artifacts/tree-cloud-artifact';
1111
import { CloudArtifact } from './cloud-artifact';
1212
import { topologicalSort } from './toposort';
1313
import * as cxschema from '../../cloud-assembly-schema';
14+
import { CloudAssemblyError } from './private/error';
1415

1516
const CLOUD_ASSEMBLY_SYMBOL = Symbol.for('@aws-cdk/cx-api.CloudAssembly');
1617

@@ -98,12 +99,12 @@ export class CloudAssembly implements ICloudAssembly {
9899
public getStackByName(stackName: string): CloudFormationStackArtifact {
99100
const artifacts = this.artifacts.filter(a => a instanceof CloudFormationStackArtifact && a.stackName === stackName);
100101
if (!artifacts || artifacts.length === 0) {
101-
throw new Error(`Unable to find stack with stack name "${stackName}"`);
102+
throw new CloudAssemblyError(`Unable to find stack with stack name "${stackName}"`);
102103
}
103104

104105
if (artifacts.length > 1) {
105106
// eslint-disable-next-line max-len
106-
throw new Error(`There are multiple stacks with the stack name "${stackName}" (${artifacts.map(a => a.id).join(',')}). Use "getStackArtifact(id)" instead`);
107+
throw new CloudAssemblyError(`There are multiple stacks with the stack name "${stackName}" (${artifacts.map(a => a.id).join(',')}). Use "getStackArtifact(id)" instead`);
107108
}
108109

109110
return artifacts[0] as CloudFormationStackArtifact;
@@ -128,11 +129,11 @@ export class CloudAssembly implements ICloudAssembly {
128129
const artifact = this.tryGetArtifactRecursively(artifactId);
129130

130131
if (!artifact) {
131-
throw new Error(`Unable to find artifact with id "${artifactId}"`);
132+
throw new CloudAssemblyError(`Unable to find artifact with id "${artifactId}"`);
132133
}
133134

134135
if (!(artifact instanceof CloudFormationStackArtifact)) {
135-
throw new Error(`Artifact ${artifactId} is not a CloudFormation stack`);
136+
throw new CloudAssemblyError(`Artifact ${artifactId} is not a CloudFormation stack`);
136137
}
137138

138139
return artifact;
@@ -167,11 +168,11 @@ export class CloudAssembly implements ICloudAssembly {
167168
public getNestedAssemblyArtifact(artifactId: string): NestedCloudAssemblyArtifact {
168169
const artifact = this.tryGetArtifact(artifactId);
169170
if (!artifact) {
170-
throw new Error(`Unable to find artifact with id "${artifactId}"`);
171+
throw new CloudAssemblyError(`Unable to find artifact with id "${artifactId}"`);
171172
}
172173

173174
if (!(artifact instanceof NestedCloudAssemblyArtifact)) {
174-
throw new Error(`Found artifact '${artifactId}' but it's not a nested cloud assembly`);
175+
throw new CloudAssemblyError(`Found artifact '${artifactId}' but it's not a nested cloud assembly`);
175176
}
176177

177178
return artifact;
@@ -196,12 +197,12 @@ export class CloudAssembly implements ICloudAssembly {
196197
if (trees.length === 0) {
197198
return undefined;
198199
} else if (trees.length > 1) {
199-
throw new Error(`Multiple artifacts of type ${cxschema.ArtifactType.CDK_TREE} found in manifest`);
200+
throw new CloudAssemblyError(`Multiple artifacts of type ${cxschema.ArtifactType.CDK_TREE} found in manifest`);
200201
}
201202
const tree = trees[0];
202203

203204
if (!(tree instanceof TreeCloudArtifact)) {
204-
throw new Error('"Tree" artifact is not of expected type');
205+
throw new CloudAssemblyError('"Tree" artifact is not of expected type');
205206
}
206207

207208
return tree;
@@ -481,7 +482,7 @@ function determineOutputDirectory(outdir?: string) {
481482
function ensureDirSync(dir: string) {
482483
if (fs.existsSync(dir)) {
483484
if (!fs.statSync(dir).isDirectory()) {
484-
throw new Error(`${dir} must be a directory`);
485+
throw new CloudAssemblyError(`${dir} must be a directory`);
485486
}
486487
} else {
487488
fs.mkdirSync(dir, { recursive: true });

packages/aws-cdk-lib/cx-api/lib/environment.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { CloudAssemblyError } from './private/error';
2+
13
/**
24
* Parser for the artifact environment field.
35
*
@@ -26,14 +28,14 @@ export class EnvironmentUtils {
2628
public static parse(environment: string): Environment {
2729
const env = AWS_ENV_REGEX.exec(environment);
2830
if (!env) {
29-
throw new Error(
31+
throw new CloudAssemblyError(
3032
`Unable to parse environment specification "${environment}". ` +
3133
'Expected format: aws://account/region');
3234
}
3335

3436
const [, account, region] = env;
3537
if (!account || !region) {
36-
throw new Error(`Invalid environment specification: ${environment}`);
38+
throw new CloudAssemblyError(`Invalid environment specification: ${environment}`);
3739
}
3840

3941
return { account, region, name: environment };

packages/aws-cdk-lib/cx-api/lib/features.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1432,6 +1432,8 @@ export const CURRENT_VERSION_FLAG_DEFAULTS = Object.fromEntries(Object.entries(F
14321432
export function futureFlagDefault(flag: string): boolean {
14331433
const value = CURRENT_VERSION_FLAG_DEFAULTS[flag] ?? false;
14341434
if (typeof value !== 'boolean') {
1435+
// This should never happen, if this error is thrown it's a bug
1436+
// eslint-disable-next-line @cdklabs/no-throw-default-error
14351437
throw new Error(`futureFlagDefault: default type of flag '${flag}' should be boolean, got '${typeof value}'`);
14361438
}
14371439
return value;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const ASSEMBLY_ERROR_SYMBOL = Symbol.for('@aws-cdk/cx-api.CloudAssemblyError');
2+
3+
/**
4+
* A CloudAssemblyError is thrown for issues with the synthesized CloudAssembly.
5+
*
6+
* These are typically exceptions that are unexpected for end-users,
7+
* and should only occur during abnormal operation, e.g. when the synthesis
8+
* didn't fully complete.
9+
*
10+
* @internal
11+
*/
12+
export class CloudAssemblyError extends Error {
13+
#time: string;
14+
15+
/**
16+
* The time the error was thrown.
17+
*/
18+
public get time(): string {
19+
return this.#time;
20+
}
21+
22+
public get type(): 'assembly' {
23+
return 'assembly';
24+
}
25+
26+
constructor(msg: string) {
27+
super(msg);
28+
29+
Object.setPrototypeOf(this, CloudAssemblyError.prototype);
30+
Object.defineProperty(this, ASSEMBLY_ERROR_SYMBOL, { value: true });
31+
32+
this.name = new.target.name;
33+
this.#time = new Date().toISOString();
34+
}
35+
}

packages/aws-cdk-lib/cx-api/lib/toposort.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { CloudAssemblyError } from './private/error';
2+
13
export type KeyFunc<T> = (x: T) => string;
24
export type DepFunc<T> = (x: T) => string[];
35

@@ -30,7 +32,7 @@ export function topologicalSort<T>(xs: Iterable<T>, keyFn: KeyFunc<T>, depFn: De
3032

3133
// If we didn't make any progress, we got stuck
3234
if (selectable.length === 0) {
33-
throw new Error(`Could not determine ordering between: ${Array.from(remaining.keys()).join(', ')}`);
35+
throw new CloudAssemblyError(`Could not determine ordering between: ${Array.from(remaining.keys()).join(', ')}`);
3436
}
3537
}
3638

0 commit comments

Comments
 (0)