Skip to content

Commit d48d77a

Browse files
chore(cli): use typed errors ToolkitError and AuthenticationError in CLI (#32548)
Closes #32347 This PR creates two new error types, `ToolkitError` and `AuthenticationError` and uses them in `aws-cdk`. ### 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* --------- Signed-off-by: Sumu <[email protected]> Co-authored-by: Momo Kornher <[email protected]>
1 parent 5735e9e commit d48d77a

29 files changed

+204
-112
lines changed

packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { makeCachingProvider } from './provider-caching';
99
import type { SdkHttpOptions } from './sdk-provider';
1010
import { readIfPossible } from './util';
1111
import { debug } from '../../logging';
12+
import { AuthenticationError } from '../../toolkit/error';
1213

1314
const DEFAULT_CONNECTION_TIMEOUT = 10000;
1415
const DEFAULT_TIMEOUT = 300000;
@@ -291,7 +292,7 @@ async function tokenCodeFn(serialArn: string): Promise<string> {
291292
return token;
292293
} catch (err: any) {
293294
debug('Failed to get MFA token', err);
294-
const e = new Error(`Error fetching MFA token: ${err.message ?? err}`);
295+
const e = new AuthenticationError(`Error fetching MFA token: ${err.message ?? err}`);
295296
e.name = 'SharedIniFileCredentialsProviderFailure';
296297
throw e;
297298
}

packages/aws-cdk/lib/api/aws-auth/credential-plugins.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { AwsCredentialIdentity, AwsCredentialIdentityProvider } from '@smit
33
import { debug, warning } from '../../logging';
44
import { CredentialProviderSource, PluginProviderResult, Mode, PluginHost, SDKv2CompatibleCredentials, SDKv3CompatibleCredentialProvider, SDKv3CompatibleCredentials } from '../plugin';
55
import { credentialsAboutToExpire, makeCachingProvider } from './provider-caching';
6+
import { AuthenticationError } from '../../toolkit/error';
67

78
/**
89
* Cache for credential providers.
@@ -124,7 +125,7 @@ async function v3ProviderFromPlugin(producer: () => Promise<PluginProviderResult
124125
// V2 credentials that refresh and cache themselves
125126
return v3ProviderFromV2Credentials(initial);
126127
} else {
127-
throw new Error(`Plugin returned a value that doesn't resemble AWS credentials: ${inspect(initial)}`);
128+
throw new AuthenticationError(`Plugin returned a value that doesn't resemble AWS credentials: ${inspect(initial)}`);
128129
}
129130
}
130131

@@ -152,7 +153,7 @@ function refreshFromPluginProvider(current: AwsCredentialIdentity, producer: ()
152153
if (credentialsAboutToExpire(current)) {
153154
const newCreds = await producer();
154155
if (!isV3Credentials(newCreds)) {
155-
throw new Error(`Plugin initially returned static V3 credentials but now returned something else: ${inspect(newCreds)}`);
156+
throw new AuthenticationError(`Plugin initially returned static V3 credentials but now returned something else: ${inspect(newCreds)}`);
156157
}
157158
current = newCreds;
158159
}

packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { debug, warning } from '../../logging';
1313
import { traceMethods } from '../../util/tracing';
1414
import { Mode } from '../plugin';
1515
import { makeCachingProvider } from './provider-caching';
16+
import { AuthenticationError } from '../../toolkit/error';
1617

1718
export type AssumeRoleAdditionalOptions = Partial<Omit<AssumeRoleCommandInput, 'ExternalId' | 'RoleArn'>>;
1819

@@ -158,14 +159,14 @@ export class SdkProvider {
158159

159160
// At this point, we need at least SOME credentials
160161
if (baseCreds.source === 'none') {
161-
throw new Error(fmtObtainCredentialsError(env.account, baseCreds));
162+
throw new AuthenticationError(fmtObtainCredentialsError(env.account, baseCreds));
162163
}
163164

164165
// Simple case is if we don't need to "assumeRole" here. If so, we must now have credentials for the right
165166
// account.
166167
if (options?.assumeRoleArn === undefined) {
167168
if (baseCreds.source === 'incorrectDefault') {
168-
throw new Error(fmtObtainCredentialsError(env.account, baseCreds));
169+
throw new AuthenticationError(fmtObtainCredentialsError(env.account, baseCreds));
169170
}
170171

171172
// Our current credentials must be valid and not expired. Confirm that before we get into doing
@@ -240,7 +241,7 @@ export class SdkProvider {
240241
const account = env.account !== UNKNOWN_ACCOUNT ? env.account : (await this.defaultAccount())?.accountId;
241242

242243
if (!account) {
243-
throw new Error(
244+
throw new AuthenticationError(
244245
'Unable to resolve AWS account to use. It must be either configured when you define your CDK Stack, or through the environment',
245246
);
246247
}
@@ -377,7 +378,7 @@ export class SdkProvider {
377378
}
378379

379380
debug(`Assuming role failed: ${err.message}`);
380-
throw new Error(
381+
throw new AuthenticationError(
381382
[
382383
'Could not assume role in target account',
383384
...(sourceDescription ? [`using ${sourceDescription}`] : []),

packages/aws-cdk/lib/api/aws-auth/sdk.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ import { cachedAsync } from './cached';
319319
import { Account } from './sdk-provider';
320320
import { defaultCliUserAgent } from './user-agent';
321321
import { debug } from '../../logging';
322+
import { AuthenticationError } from '../../toolkit/error';
322323
import { traceMethods } from '../../util/tracing';
323324

324325
export interface S3ClientOptions {
@@ -902,7 +903,7 @@ export class SDK {
902903

903904
return upload.done();
904905
} catch (e: any) {
905-
throw new Error(`Upload failed: ${e.message}`);
906+
throw new AuthenticationError(`Upload failed: ${e.message}`);
906907
}
907908
},
908909
};
@@ -957,7 +958,7 @@ export class SDK {
957958
const accountId = result.Account;
958959
const partition = result.Arn!.split(':')[1];
959960
if (!accountId) {
960-
throw new Error("STS didn't return an account ID");
961+
throw new AuthenticationError("STS didn't return an account ID");
961962
}
962963
debug('Default account ID:', accountId);
963964

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

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { BootstrapStack, bootstrapVersionFromTemplate } from './deploy-bootstrap
66
import { legacyBootstrapTemplate } from './legacy-template';
77
import { warning } from '../../logging';
88
import { loadStructuredFile, serializeStructure } from '../../serialize';
9+
import { ToolkitError } from '../../toolkit/error';
910
import { rootDir } from '../../util/directories';
1011
import type { SDK, SdkProvider } from '../aws-auth';
1112
import type { SuccessfulDeployStackResult } from '../deploy-stack';
@@ -48,16 +49,16 @@ export class Bootstrapper {
4849
const params = options.parameters ?? {};
4950

5051
if (params.trustedAccounts?.length) {
51-
throw new Error('--trust can only be passed for the modern bootstrap experience.');
52+
throw new ToolkitError('--trust can only be passed for the modern bootstrap experience.');
5253
}
5354
if (params.cloudFormationExecutionPolicies?.length) {
54-
throw new Error('--cloudformation-execution-policies can only be passed for the modern bootstrap experience.');
55+
throw new ToolkitError('--cloudformation-execution-policies can only be passed for the modern bootstrap experience.');
5556
}
5657
if (params.createCustomerMasterKey !== undefined) {
57-
throw new Error('--bootstrap-customer-key can only be passed for the modern bootstrap experience.');
58+
throw new ToolkitError('--bootstrap-customer-key can only be passed for the modern bootstrap experience.');
5859
}
5960
if (params.qualifier) {
60-
throw new Error('--qualifier can only be passed for the modern bootstrap experience.');
61+
throw new ToolkitError('--qualifier can only be passed for the modern bootstrap experience.');
6162
}
6263

6364
const current = await BootstrapStack.lookup(sdkProvider, environment, options.toolkitStackName);
@@ -88,7 +89,7 @@ export class Bootstrapper {
8889
const partition = await current.partition();
8990

9091
if (params.createCustomerMasterKey !== undefined && params.kmsKeyId) {
91-
throw new Error(
92+
throw new ToolkitError(
9293
"You cannot pass '--bootstrap-kms-key-id' and '--bootstrap-customer-key' together. Specify one or the other",
9394
);
9495
}
@@ -131,7 +132,7 @@ export class Bootstrapper {
131132
`Using default execution policy of '${implicitPolicy}'. Pass '--cloudformation-execution-policies' to customize.`,
132133
);
133134
} else if (cloudFormationExecutionPolicies.length === 0) {
134-
throw new Error(
135+
throw new ToolkitError(
135136
`Please pass \'--cloudformation-execution-policies\' when using \'--trust\' to specify deployment permissions. Try a managed policy of the form \'arn:${partition}:iam::aws:policy/<PolicyName>\'.`,
136137
);
137138
} else {
@@ -226,7 +227,7 @@ export class Bootstrapper {
226227
);
227228
const policyName = arn.split('/').pop();
228229
if (!policyName) {
229-
throw new Error('Could not retrieve the example permission boundary!');
230+
throw new ToolkitError('Could not retrieve the example permission boundary!');
230231
}
231232
return Promise.resolve(policyName);
232233
}
@@ -308,7 +309,7 @@ export class Bootstrapper {
308309
if (createPolicyResponse.Policy?.Arn) {
309310
return createPolicyResponse.Policy.Arn;
310311
} else {
311-
throw new Error(`Could not retrieve the example permission boundary ${arn}!`);
312+
throw new ToolkitError(`Could not retrieve the example permission boundary ${arn}!`);
312313
}
313314
}
314315

@@ -319,7 +320,7 @@ export class Bootstrapper {
319320
const regexp: RegExp = /[\w+\/=,.@-]+/;
320321
const matches = regexp.exec(permissionsBoundary);
321322
if (!(matches && matches.length === 1 && matches[0] === permissionsBoundary)) {
322-
throw new Error(`The permissions boundary name ${permissionsBoundary} does not match the IAM conventions.`);
323+
throw new ToolkitError(`The permissions boundary name ${permissionsBoundary} does not match the IAM conventions.`);
323324
}
324325
}
325326

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as chalk from 'chalk';
33
import { minimatch } from 'minimatch';
44
import * as semver from 'semver';
55
import { error, print, warning } from '../../logging';
6+
import { ToolkitError } from '../../toolkit/error';
67
import { flatten } from '../../util';
78

89
export enum DefaultSelection {
@@ -109,7 +110,7 @@ export class CloudAssembly {
109110
if (options.ignoreNoStacks) {
110111
return new StackCollection(this, []);
111112
}
112-
throw new Error('This app contains no stacks');
113+
throw new ToolkitError('This app contains no stacks');
113114
}
114115

115116
if (allTopLevel) {
@@ -129,7 +130,7 @@ export class CloudAssembly {
129130
if (topLevelStacks.length > 0) {
130131
return this.extendStacks(topLevelStacks, stacks, extend);
131132
} else {
132-
throw new Error('No stack found in the main cloud assembly. Use "list" to print manifest');
133+
throw new ToolkitError('No stack found in the main cloud assembly. Use "list" to print manifest');
133134
}
134135
}
135136

@@ -161,11 +162,11 @@ export class CloudAssembly {
161162
if (topLevelStacks.length === 1) {
162163
return new StackCollection(this, topLevelStacks);
163164
} else {
164-
throw new Error('Since this app includes more than a single stack, specify which stacks to use (wildcards are supported) or specify `--all`\n' +
165+
throw new ToolkitError('Since this app includes more than a single stack, specify which stacks to use (wildcards are supported) or specify `--all`\n' +
165166
`Stacks: ${stacks.map(x => x.hierarchicalId).join(' · ')}`);
166167
}
167168
default:
168-
throw new Error(`invalid default behavior: ${defaultSelection}`);
169+
throw new ToolkitError(`invalid default behavior: ${defaultSelection}`);
169170
}
170171
}
171172

@@ -221,7 +222,7 @@ export class StackCollection {
221222

222223
public get firstStack() {
223224
if (this.stackCount < 1) {
224-
throw new Error('StackCollection contains no stack artifacts (trying to access the first one)');
225+
throw new ToolkitError('StackCollection contains no stack artifacts (trying to access the first one)');
225226
}
226227
return this.stackArtifacts[0];
227228
}
@@ -270,11 +271,11 @@ export class StackCollection {
270271
}
271272

272273
if (errors && !options.ignoreErrors) {
273-
throw new Error('Found errors');
274+
throw new ToolkitError('Found errors');
274275
}
275276

276277
if (options.strict && warnings) {
277-
throw new Error('Found warnings (--strict mode)');
278+
throw new ToolkitError('Found warnings (--strict mode)');
278279
}
279280

280281
function printMessage(logFn: (s: string) => void, prefix: string, id: string, entry: cxapi.MetadataEntry) {

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { CloudAssembly } from './cloud-assembly';
66
import * as contextproviders from '../../context-providers';
77
import { debug, warning } from '../../logging';
88
import { Configuration } from '../../settings';
9+
import { ToolkitError } from '../../toolkit/error';
910
import { SdkProvider } from '../aws-auth';
1011

1112
/**
@@ -82,7 +83,7 @@ export class CloudExecutable {
8283
const missingKeys = missingContextKeys(assembly.manifest.missing);
8384

8485
if (!this.canLookup) {
85-
throw new Error(
86+
throw new ToolkitError(
8687
'Context lookups have been disabled. '
8788
+ 'Make sure all necessary context is already in \'cdk.context.json\' by running \'cdk synth\' on a machine with sufficient AWS credentials and committing the result. '
8889
+ `Missing context keys: '${Array.from(missingKeys).join(', ')}'`);
@@ -214,7 +215,7 @@ function _makeCdkMetadataAvailableCondition() {
214215
*/
215216
function _fnOr(operands: any[]): any {
216217
if (operands.length === 0) {
217-
throw new Error('Cannot build `Fn::Or` with zero operands!');
218+
throw new ToolkitError('Cannot build `Fn::Or` with zero operands!');
218219
}
219220
if (operands.length === 1) {
220221
return operands[0];

packages/aws-cdk/lib/api/cxapp/environments.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as cxapi from '@aws-cdk/cx-api';
22
import { minimatch } from 'minimatch';
33
import { StackCollection } from './cloud-assembly';
4+
import { ToolkitError } from '../../toolkit/error';
45
import { SdkProvider } from '../aws-auth';
56

67
export function looksLikeGlob(environment: string) {
@@ -21,7 +22,7 @@ export async function globEnvironmentsFromStacks(stacks: StackCollection, enviro
2122
if (environments.length === 0) {
2223
const globs = JSON.stringify(environmentGlobs);
2324
const envList = availableEnvironments.length > 0 ? availableEnvironments.map(env => env!.name).join(', ') : '<none>';
24-
throw new Error(`No environments were found when selecting across ${globs} (available: ${envList})`);
25+
throw new ToolkitError(`No environments were found when selecting across ${globs} (available: ${envList})`);
2526
}
2627

2728
return environments;
@@ -36,7 +37,7 @@ export function environmentsFromDescriptors(envSpecs: string[]): cxapi.Environme
3637
for (const spec of envSpecs) {
3738
const parts = spec.replace(/^aws:\/\//, '').split('/');
3839
if (parts.length !== 2) {
39-
throw new Error(`Expected environment name in format 'aws://<account>/<region>', got: ${spec}`);
40+
throw new ToolkitError(`Expected environment name in format 'aws://<account>/<region>', got: ${spec}`);
4041
}
4142

4243
ret.push({

packages/aws-cdk/lib/api/cxapp/exec.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as fs from 'fs-extra';
77
import * as semver from 'semver';
88
import { debug, warning } from '../../logging';
99
import { Configuration, PROJECT_CONFIG, USER_DEFAULTS } from '../../settings';
10+
import { ToolkitError } from '../../toolkit/error';
1011
import { loadTree, some } from '../../tree';
1112
import { splitBySize } from '../../util/objects';
1213
import { versionNumber } from '../../version';
@@ -30,7 +31,7 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom
3031

3132
const app = config.settings.get(['app']);
3233
if (!app) {
33-
throw new Error(`--app is required either in command-line, in ${PROJECT_CONFIG} or in ${USER_DEFAULTS}`);
34+
throw new ToolkitError(`--app is required either in command-line, in ${PROJECT_CONFIG} or in ${USER_DEFAULTS}`);
3435
}
3536

3637
// bypass "synth" if app points to a cloud assembly
@@ -47,15 +48,15 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom
4748

4849
const outdir = config.settings.get(['output']);
4950
if (!outdir) {
50-
throw new Error('unexpected: --output is required');
51+
throw new ToolkitError('unexpected: --output is required');
5152
}
5253
if (typeof outdir !== 'string') {
53-
throw new Error(`--output takes a string, got ${JSON.stringify(outdir)}`);
54+
throw new ToolkitError(`--output takes a string, got ${JSON.stringify(outdir)}`);
5455
}
5556
try {
5657
await fs.mkdirp(outdir);
5758
} catch (error: any) {
58-
throw new Error(`Could not create output directory ${outdir} (${error.message})`);
59+
throw new ToolkitError(`Could not create output directory ${outdir} (${error.message})`);
5960
}
6061

6162
debug('outdir:', outdir);
@@ -127,7 +128,7 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom
127128
return ok();
128129
} else {
129130
debug('failed command:', commandAndArgs);
130-
return fail(new Error(`Subprocess exited with error ${code}`));
131+
return fail(new ToolkitError(`Subprocess exited with error ${code}`));
131132
}
132133
});
133134
});
@@ -147,7 +148,7 @@ export function createAssembly(appDir: string) {
147148
if (error.message.includes(cxschema.VERSION_MISMATCH)) {
148149
// this means the CLI version is too old.
149150
// we instruct the user to upgrade.
150-
throw new Error(`This CDK CLI is not compatible with the CDK library used by your application. Please upgrade the CLI to the latest version.\n(${error.message})`);
151+
throw new ToolkitError(`This CDK CLI is not compatible with the CDK library used by your application. Please upgrade the CLI to the latest version.\n(${error.message})`);
151152
}
152153
throw error;
153154
}

0 commit comments

Comments
 (0)