From d48d77aa1adbddf27c94e279ee756bdc2c2d18cf Mon Sep 17 00:00:00 2001 From: Sumu Pitchayan <35242245+sumupitchayan@users.noreply.github.com> Date: Tue, 24 Dec 2024 15:18:06 -0500 Subject: [PATCH] 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 Co-authored-by: Momo Kornher --- .../lib/api/aws-auth/awscli-compatible.ts | 3 +- .../lib/api/aws-auth/credential-plugins.ts | 5 +- .../aws-cdk/lib/api/aws-auth/sdk-provider.ts | 9 ++-- packages/aws-cdk/lib/api/aws-auth/sdk.ts | 5 +- .../api/bootstrap/bootstrap-environment.ts | 19 ++++---- .../aws-cdk/lib/api/cxapp/cloud-assembly.ts | 15 +++--- .../aws-cdk/lib/api/cxapp/cloud-executable.ts | 5 +- .../aws-cdk/lib/api/cxapp/environments.ts | 5 +- packages/aws-cdk/lib/api/cxapp/exec.ts | 13 ++--- .../garbage-collection/garbage-collector.ts | 13 ++--- .../api/garbage-collection/stack-refresh.ts | 5 +- .../api/hotswap/appsync-mapping-templates.ts | 3 +- packages/aws-cdk/lib/api/hotswap/common.ts | 5 +- .../lib/api/hotswap/lambda-functions.ts | 3 +- packages/aws-cdk/lib/api/plugin/plugin.ts | 9 ++-- packages/aws-cdk/lib/assets.ts | 9 ++-- packages/aws-cdk/lib/build.ts | 3 +- packages/aws-cdk/lib/cdk-toolkit.ts | 43 +++++++++-------- packages/aws-cdk/lib/cli.ts | 17 +++---- packages/aws-cdk/lib/diff.ts | 3 +- packages/aws-cdk/lib/import.ts | 3 +- packages/aws-cdk/lib/init-hooks.ts | 3 +- packages/aws-cdk/lib/init.ts | 11 +++-- packages/aws-cdk/lib/notices.ts | 15 +++--- packages/aws-cdk/lib/os.ts | 3 +- packages/aws-cdk/lib/settings.ts | 21 ++++---- packages/aws-cdk/lib/toolkit/error.ts | 48 +++++++++++++++++++ packages/aws-cdk/lib/version.ts | 3 +- packages/aws-cdk/test/toolkit-error.test.ts | 17 +++++++ 29 files changed, 204 insertions(+), 112 deletions(-) create mode 100644 packages/aws-cdk/lib/toolkit/error.ts create mode 100644 packages/aws-cdk/test/toolkit-error.test.ts diff --git a/packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts b/packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts index 31ef2640b5682..fd6f255d0523d 100644 --- a/packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts +++ b/packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts @@ -9,6 +9,7 @@ import { makeCachingProvider } from './provider-caching'; import type { SdkHttpOptions } from './sdk-provider'; import { readIfPossible } from './util'; import { debug } from '../../logging'; +import { AuthenticationError } from '../../toolkit/error'; const DEFAULT_CONNECTION_TIMEOUT = 10000; const DEFAULT_TIMEOUT = 300000; @@ -291,7 +292,7 @@ async function tokenCodeFn(serialArn: string): Promise { return token; } catch (err: any) { debug('Failed to get MFA token', err); - const e = new Error(`Error fetching MFA token: ${err.message ?? err}`); + const e = new AuthenticationError(`Error fetching MFA token: ${err.message ?? err}`); e.name = 'SharedIniFileCredentialsProviderFailure'; throw e; } diff --git a/packages/aws-cdk/lib/api/aws-auth/credential-plugins.ts b/packages/aws-cdk/lib/api/aws-auth/credential-plugins.ts index b2047bd3fbbfb..1c9e214eca067 100644 --- a/packages/aws-cdk/lib/api/aws-auth/credential-plugins.ts +++ b/packages/aws-cdk/lib/api/aws-auth/credential-plugins.ts @@ -3,6 +3,7 @@ import type { AwsCredentialIdentity, AwsCredentialIdentityProvider } from '@smit import { debug, warning } from '../../logging'; import { CredentialProviderSource, PluginProviderResult, Mode, PluginHost, SDKv2CompatibleCredentials, SDKv3CompatibleCredentialProvider, SDKv3CompatibleCredentials } from '../plugin'; import { credentialsAboutToExpire, makeCachingProvider } from './provider-caching'; +import { AuthenticationError } from '../../toolkit/error'; /** * Cache for credential providers. @@ -124,7 +125,7 @@ async function v3ProviderFromPlugin(producer: () => Promise>; @@ -158,14 +159,14 @@ export class SdkProvider { // At this point, we need at least SOME credentials if (baseCreds.source === 'none') { - throw new Error(fmtObtainCredentialsError(env.account, baseCreds)); + throw new AuthenticationError(fmtObtainCredentialsError(env.account, baseCreds)); } // Simple case is if we don't need to "assumeRole" here. If so, we must now have credentials for the right // account. if (options?.assumeRoleArn === undefined) { if (baseCreds.source === 'incorrectDefault') { - throw new Error(fmtObtainCredentialsError(env.account, baseCreds)); + throw new AuthenticationError(fmtObtainCredentialsError(env.account, baseCreds)); } // Our current credentials must be valid and not expired. Confirm that before we get into doing @@ -240,7 +241,7 @@ export class SdkProvider { const account = env.account !== UNKNOWN_ACCOUNT ? env.account : (await this.defaultAccount())?.accountId; if (!account) { - throw new Error( + throw new AuthenticationError( 'Unable to resolve AWS account to use. It must be either configured when you define your CDK Stack, or through the environment', ); } @@ -377,7 +378,7 @@ export class SdkProvider { } debug(`Assuming role failed: ${err.message}`); - throw new Error( + throw new AuthenticationError( [ 'Could not assume role in target account', ...(sourceDescription ? [`using ${sourceDescription}`] : []), diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk.ts b/packages/aws-cdk/lib/api/aws-auth/sdk.ts index 1b1c771987b7f..722f946623fad 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk.ts @@ -319,6 +319,7 @@ import { cachedAsync } from './cached'; import { Account } from './sdk-provider'; import { defaultCliUserAgent } from './user-agent'; import { debug } from '../../logging'; +import { AuthenticationError } from '../../toolkit/error'; import { traceMethods } from '../../util/tracing'; export interface S3ClientOptions { @@ -902,7 +903,7 @@ export class SDK { return upload.done(); } catch (e: any) { - throw new Error(`Upload failed: ${e.message}`); + throw new AuthenticationError(`Upload failed: ${e.message}`); } }, }; @@ -957,7 +958,7 @@ export class SDK { const accountId = result.Account; const partition = result.Arn!.split(':')[1]; if (!accountId) { - throw new Error("STS didn't return an account ID"); + throw new AuthenticationError("STS didn't return an account ID"); } debug('Default account ID:', accountId); diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts b/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts index 6c28af3c019b7..bb221c6f52869 100644 --- a/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts @@ -6,6 +6,7 @@ import { BootstrapStack, bootstrapVersionFromTemplate } from './deploy-bootstrap import { legacyBootstrapTemplate } from './legacy-template'; import { warning } from '../../logging'; import { loadStructuredFile, serializeStructure } from '../../serialize'; +import { ToolkitError } from '../../toolkit/error'; import { rootDir } from '../../util/directories'; import type { SDK, SdkProvider } from '../aws-auth'; import type { SuccessfulDeployStackResult } from '../deploy-stack'; @@ -48,16 +49,16 @@ export class Bootstrapper { const params = options.parameters ?? {}; if (params.trustedAccounts?.length) { - throw new Error('--trust can only be passed for the modern bootstrap experience.'); + throw new ToolkitError('--trust can only be passed for the modern bootstrap experience.'); } if (params.cloudFormationExecutionPolicies?.length) { - throw new Error('--cloudformation-execution-policies can only be passed for the modern bootstrap experience.'); + throw new ToolkitError('--cloudformation-execution-policies can only be passed for the modern bootstrap experience.'); } if (params.createCustomerMasterKey !== undefined) { - throw new Error('--bootstrap-customer-key can only be passed for the modern bootstrap experience.'); + throw new ToolkitError('--bootstrap-customer-key can only be passed for the modern bootstrap experience.'); } if (params.qualifier) { - throw new Error('--qualifier can only be passed for the modern bootstrap experience.'); + throw new ToolkitError('--qualifier can only be passed for the modern bootstrap experience.'); } const current = await BootstrapStack.lookup(sdkProvider, environment, options.toolkitStackName); @@ -88,7 +89,7 @@ export class Bootstrapper { const partition = await current.partition(); if (params.createCustomerMasterKey !== undefined && params.kmsKeyId) { - throw new Error( + throw new ToolkitError( "You cannot pass '--bootstrap-kms-key-id' and '--bootstrap-customer-key' together. Specify one or the other", ); } @@ -131,7 +132,7 @@ export class Bootstrapper { `Using default execution policy of '${implicitPolicy}'. Pass '--cloudformation-execution-policies' to customize.`, ); } else if (cloudFormationExecutionPolicies.length === 0) { - throw new Error( + throw new ToolkitError( `Please pass \'--cloudformation-execution-policies\' when using \'--trust\' to specify deployment permissions. Try a managed policy of the form \'arn:${partition}:iam::aws:policy/\'.`, ); } else { @@ -226,7 +227,7 @@ export class Bootstrapper { ); const policyName = arn.split('/').pop(); if (!policyName) { - throw new Error('Could not retrieve the example permission boundary!'); + throw new ToolkitError('Could not retrieve the example permission boundary!'); } return Promise.resolve(policyName); } @@ -308,7 +309,7 @@ export class Bootstrapper { if (createPolicyResponse.Policy?.Arn) { return createPolicyResponse.Policy.Arn; } else { - throw new Error(`Could not retrieve the example permission boundary ${arn}!`); + throw new ToolkitError(`Could not retrieve the example permission boundary ${arn}!`); } } @@ -319,7 +320,7 @@ export class Bootstrapper { const regexp: RegExp = /[\w+\/=,.@-]+/; const matches = regexp.exec(permissionsBoundary); if (!(matches && matches.length === 1 && matches[0] === permissionsBoundary)) { - throw new Error(`The permissions boundary name ${permissionsBoundary} does not match the IAM conventions.`); + throw new ToolkitError(`The permissions boundary name ${permissionsBoundary} does not match the IAM conventions.`); } } diff --git a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts index 96843b706b005..322508881321d 100644 --- a/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts +++ b/packages/aws-cdk/lib/api/cxapp/cloud-assembly.ts @@ -3,6 +3,7 @@ import * as chalk from 'chalk'; import { minimatch } from 'minimatch'; import * as semver from 'semver'; import { error, print, warning } from '../../logging'; +import { ToolkitError } from '../../toolkit/error'; import { flatten } from '../../util'; export enum DefaultSelection { @@ -109,7 +110,7 @@ export class CloudAssembly { if (options.ignoreNoStacks) { return new StackCollection(this, []); } - throw new Error('This app contains no stacks'); + throw new ToolkitError('This app contains no stacks'); } if (allTopLevel) { @@ -129,7 +130,7 @@ export class CloudAssembly { if (topLevelStacks.length > 0) { return this.extendStacks(topLevelStacks, stacks, extend); } else { - throw new Error('No stack found in the main cloud assembly. Use "list" to print manifest'); + throw new ToolkitError('No stack found in the main cloud assembly. Use "list" to print manifest'); } } @@ -161,11 +162,11 @@ export class CloudAssembly { if (topLevelStacks.length === 1) { return new StackCollection(this, topLevelStacks); } else { - throw new Error('Since this app includes more than a single stack, specify which stacks to use (wildcards are supported) or specify `--all`\n' + + throw new ToolkitError('Since this app includes more than a single stack, specify which stacks to use (wildcards are supported) or specify `--all`\n' + `Stacks: ${stacks.map(x => x.hierarchicalId).join(' · ')}`); } default: - throw new Error(`invalid default behavior: ${defaultSelection}`); + throw new ToolkitError(`invalid default behavior: ${defaultSelection}`); } } @@ -221,7 +222,7 @@ export class StackCollection { public get firstStack() { if (this.stackCount < 1) { - throw new Error('StackCollection contains no stack artifacts (trying to access the first one)'); + throw new ToolkitError('StackCollection contains no stack artifacts (trying to access the first one)'); } return this.stackArtifacts[0]; } @@ -270,11 +271,11 @@ export class StackCollection { } if (errors && !options.ignoreErrors) { - throw new Error('Found errors'); + throw new ToolkitError('Found errors'); } if (options.strict && warnings) { - throw new Error('Found warnings (--strict mode)'); + throw new ToolkitError('Found warnings (--strict mode)'); } function printMessage(logFn: (s: string) => void, prefix: string, id: string, entry: cxapi.MetadataEntry) { diff --git a/packages/aws-cdk/lib/api/cxapp/cloud-executable.ts b/packages/aws-cdk/lib/api/cxapp/cloud-executable.ts index fd97007c1e22d..6fc24758d3dce 100644 --- a/packages/aws-cdk/lib/api/cxapp/cloud-executable.ts +++ b/packages/aws-cdk/lib/api/cxapp/cloud-executable.ts @@ -6,6 +6,7 @@ import { CloudAssembly } from './cloud-assembly'; import * as contextproviders from '../../context-providers'; import { debug, warning } from '../../logging'; import { Configuration } from '../../settings'; +import { ToolkitError } from '../../toolkit/error'; import { SdkProvider } from '../aws-auth'; /** @@ -82,7 +83,7 @@ export class CloudExecutable { const missingKeys = missingContextKeys(assembly.manifest.missing); if (!this.canLookup) { - throw new Error( + throw new ToolkitError( 'Context lookups have been disabled. ' + '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. ' + `Missing context keys: '${Array.from(missingKeys).join(', ')}'`); @@ -214,7 +215,7 @@ function _makeCdkMetadataAvailableCondition() { */ function _fnOr(operands: any[]): any { if (operands.length === 0) { - throw new Error('Cannot build `Fn::Or` with zero operands!'); + throw new ToolkitError('Cannot build `Fn::Or` with zero operands!'); } if (operands.length === 1) { return operands[0]; diff --git a/packages/aws-cdk/lib/api/cxapp/environments.ts b/packages/aws-cdk/lib/api/cxapp/environments.ts index c46281759a881..a1b8c66e6f442 100644 --- a/packages/aws-cdk/lib/api/cxapp/environments.ts +++ b/packages/aws-cdk/lib/api/cxapp/environments.ts @@ -1,6 +1,7 @@ import * as cxapi from '@aws-cdk/cx-api'; import { minimatch } from 'minimatch'; import { StackCollection } from './cloud-assembly'; +import { ToolkitError } from '../../toolkit/error'; import { SdkProvider } from '../aws-auth'; export function looksLikeGlob(environment: string) { @@ -21,7 +22,7 @@ export async function globEnvironmentsFromStacks(stacks: StackCollection, enviro if (environments.length === 0) { const globs = JSON.stringify(environmentGlobs); const envList = availableEnvironments.length > 0 ? availableEnvironments.map(env => env!.name).join(', ') : ''; - throw new Error(`No environments were found when selecting across ${globs} (available: ${envList})`); + throw new ToolkitError(`No environments were found when selecting across ${globs} (available: ${envList})`); } return environments; @@ -36,7 +37,7 @@ export function environmentsFromDescriptors(envSpecs: string[]): cxapi.Environme for (const spec of envSpecs) { const parts = spec.replace(/^aws:\/\//, '').split('/'); if (parts.length !== 2) { - throw new Error(`Expected environment name in format 'aws:///', got: ${spec}`); + throw new ToolkitError(`Expected environment name in format 'aws:///', got: ${spec}`); } ret.push({ diff --git a/packages/aws-cdk/lib/api/cxapp/exec.ts b/packages/aws-cdk/lib/api/cxapp/exec.ts index 6b62d7ae2527f..b02ad38445a07 100644 --- a/packages/aws-cdk/lib/api/cxapp/exec.ts +++ b/packages/aws-cdk/lib/api/cxapp/exec.ts @@ -7,6 +7,7 @@ import * as fs from 'fs-extra'; import * as semver from 'semver'; import { debug, warning } from '../../logging'; import { Configuration, PROJECT_CONFIG, USER_DEFAULTS } from '../../settings'; +import { ToolkitError } from '../../toolkit/error'; import { loadTree, some } from '../../tree'; import { splitBySize } from '../../util/objects'; import { versionNumber } from '../../version'; @@ -30,7 +31,7 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom const app = config.settings.get(['app']); if (!app) { - throw new Error(`--app is required either in command-line, in ${PROJECT_CONFIG} or in ${USER_DEFAULTS}`); + throw new ToolkitError(`--app is required either in command-line, in ${PROJECT_CONFIG} or in ${USER_DEFAULTS}`); } // bypass "synth" if app points to a cloud assembly @@ -47,15 +48,15 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom const outdir = config.settings.get(['output']); if (!outdir) { - throw new Error('unexpected: --output is required'); + throw new ToolkitError('unexpected: --output is required'); } if (typeof outdir !== 'string') { - throw new Error(`--output takes a string, got ${JSON.stringify(outdir)}`); + throw new ToolkitError(`--output takes a string, got ${JSON.stringify(outdir)}`); } try { await fs.mkdirp(outdir); } catch (error: any) { - throw new Error(`Could not create output directory ${outdir} (${error.message})`); + throw new ToolkitError(`Could not create output directory ${outdir} (${error.message})`); } debug('outdir:', outdir); @@ -127,7 +128,7 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom return ok(); } else { debug('failed command:', commandAndArgs); - return fail(new Error(`Subprocess exited with error ${code}`)); + return fail(new ToolkitError(`Subprocess exited with error ${code}`)); } }); }); @@ -147,7 +148,7 @@ export function createAssembly(appDir: string) { if (error.message.includes(cxschema.VERSION_MISMATCH)) { // this means the CLI version is too old. // we instruct the user to upgrade. - 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})`); + 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})`); } throw error; } diff --git a/packages/aws-cdk/lib/api/garbage-collection/garbage-collector.ts b/packages/aws-cdk/lib/api/garbage-collection/garbage-collector.ts index a73e44e25c16a..19441be95f788 100644 --- a/packages/aws-cdk/lib/api/garbage-collection/garbage-collector.ts +++ b/packages/aws-cdk/lib/api/garbage-collection/garbage-collector.ts @@ -8,6 +8,7 @@ import { IECRClient, IS3Client, SDK, SdkProvider } from '../aws-auth'; import { DEFAULT_TOOLKIT_STACK_NAME, ToolkitInfo } from '../toolkit-info'; import { ProgressPrinter } from './progress-printer'; import { ActiveAssetCache, BackgroundStackRefresh, refreshStacks } from './stack-refresh'; +import { ToolkitError } from '../../toolkit/error'; import { Mode } from '../plugin'; // Must use a require() otherwise esbuild complains @@ -91,14 +92,14 @@ export class ObjectAsset { private getTag(tag: string) { if (!this.cached_tags) { - throw new Error('Cannot call getTag before allTags'); + throw new ToolkitError('Cannot call getTag before allTags'); } return this.cached_tags.find((t: any) => t.Key === tag)?.Value; } private hasTag(tag: string) { if (!this.cached_tags) { - throw new Error('Cannot call hasTag before allTags'); + throw new ToolkitError('Cannot call hasTag before allTags'); } return this.cached_tags.some((t: any) => t.Key === tag); } @@ -225,7 +226,7 @@ export class GarbageCollector { await this.garbageCollectEcr(sdk, activeAssets, backgroundStackRefresh); } } catch (err: any) { - throw new Error(err); + throw new ToolkitError(err); } finally { backgroundStackRefresh.stop(); } @@ -298,7 +299,7 @@ export class GarbageCollector { printer.reportScannedAsset(batch.length); } } catch (err: any) { - throw new Error(err); + throw new ToolkitError(err); } finally { printer.stop(); } @@ -374,7 +375,7 @@ export class GarbageCollector { printer.reportScannedAsset(batch.length); } } catch (err: any) { - throw new Error(err); + throw new ToolkitError(err); } finally { printer.stop(); } @@ -727,7 +728,7 @@ export class GarbageCollector { // Anything other than yes/y/delete-all is treated as no if (!response || !['yes', 'y', 'delete-all'].includes(response.toLowerCase())) { - throw new Error('Deletion aborted by user'); + throw new ToolkitError('Deletion aborted by user'); } else if (response.toLowerCase() == 'delete-all') { this.confirm = false; } diff --git a/packages/aws-cdk/lib/api/garbage-collection/stack-refresh.ts b/packages/aws-cdk/lib/api/garbage-collection/stack-refresh.ts index 8fdfb4e3a2562..2a795e23757d5 100644 --- a/packages/aws-cdk/lib/api/garbage-collection/stack-refresh.ts +++ b/packages/aws-cdk/lib/api/garbage-collection/stack-refresh.ts @@ -1,5 +1,6 @@ import { ParameterDeclaration } from '@aws-sdk/client-cloudformation'; import { debug } from '../../logging'; +import { ToolkitError } from '../../toolkit/error'; import { ICloudFormationClient } from '../aws-auth'; export class ActiveAssetCache { @@ -103,7 +104,7 @@ export async function refreshStacks(cfn: ICloudFormationClient, activeAssets: Ac activeAssets.rememberStack(stack); } } catch (err) { - throw new Error(`Error refreshing stacks: ${err}`); + throw new ToolkitError(`Error refreshing stacks: ${err}`); } } @@ -180,7 +181,7 @@ export class BackgroundStackRefresh { // We will wait for the latest refresh to land or reject if it takes too long return Promise.race([ new Promise(resolve => this.queuedPromises.push(resolve)), - new Promise((_, reject) => setTimeout(() => reject(new Error('refreshStacks took too long; the background thread likely threw an error')), ms)), + new Promise((_, reject) => setTimeout(() => reject(new ToolkitError('refreshStacks took too long; the background thread likely threw an error')), ms)), ]); } diff --git a/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts b/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts index d5b520d6a13e5..3218bc5fc3157 100644 --- a/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts +++ b/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts @@ -9,6 +9,7 @@ import { lowerCaseFirstCharacter, transformObjectKeys, } from './common'; +import { ToolkitError } from '../../toolkit/error'; import type { SDK } from '../aws-auth'; import type { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; @@ -143,7 +144,7 @@ export async function isHotswappableAppSyncChange( schemaCreationResponse = await sdk.appsync().getSchemaCreationStatus(getSchemaCreationStatusRequest); } if (schemaCreationResponse.status === 'FAILED') { - throw new Error(schemaCreationResponse.details); + throw new ToolkitError(schemaCreationResponse.details ?? 'Schema creation has failed.'); } } else { //isApiKey diff --git a/packages/aws-cdk/lib/api/hotswap/common.ts b/packages/aws-cdk/lib/api/hotswap/common.ts index ed176791f28dd..4012c119d5130 100644 --- a/packages/aws-cdk/lib/api/hotswap/common.ts +++ b/packages/aws-cdk/lib/api/hotswap/common.ts @@ -1,4 +1,5 @@ import type { PropertyDifference, Resource } from '@aws-cdk/cloudformation-diff'; +import { ToolkitError } from '../../toolkit/error'; import type { SDK } from '../aws-auth'; export const ICON = '✨'; @@ -121,10 +122,10 @@ export class EcsHotswapProperties { public constructor (minimumHealthyPercent?: number, maximumHealthyPercent?: number) { if (minimumHealthyPercent !== undefined && minimumHealthyPercent < 0 ) { - throw new Error('hotswap-ecs-minimum-healthy-percent can\'t be a negative number'); + throw new ToolkitError('hotswap-ecs-minimum-healthy-percent can\'t be a negative number'); } if (maximumHealthyPercent !== undefined && maximumHealthyPercent < 0 ) { - throw new Error('hotswap-ecs-maximum-healthy-percent can\'t be a negative number'); + throw new ToolkitError('hotswap-ecs-maximum-healthy-percent can\'t be a negative number'); } // In order to preserve the current behaviour, when minimumHealthyPercent is not defined, it will be set to the currently default value of 0 if (minimumHealthyPercent == undefined) { diff --git a/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts b/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts index e927ad03cac8f..64ec5a53465ae 100644 --- a/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts +++ b/packages/aws-cdk/lib/api/hotswap/lambda-functions.ts @@ -1,6 +1,7 @@ import { Writable } from 'stream'; import { type FunctionConfiguration, type UpdateFunctionConfigurationCommandInput } from '@aws-sdk/client-lambda'; import { type ChangeHotswapResult, classifyChanges, type HotswappableChangeCandidate, PropDiffs } from './common'; +import { ToolkitError } from '../../toolkit/error'; import { flatMap } from '../../util'; import type { ILambdaClient, SDK } from '../aws-auth'; import { CfnEvaluationException, type EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template'; @@ -245,7 +246,7 @@ async function evaluateLambdaFunctionProps( break; default: // we will never get here, but just in case we do throw an error - throw new Error( + throw new ToolkitError( 'while apply()ing, found a property that cannot be hotswapped. Please report this at github.com/aws/aws-cdk/issues/new/choose', ); } diff --git a/packages/aws-cdk/lib/api/plugin/plugin.ts b/packages/aws-cdk/lib/api/plugin/plugin.ts index 16df596e4cccb..270c8343be404 100644 --- a/packages/aws-cdk/lib/api/plugin/plugin.ts +++ b/packages/aws-cdk/lib/api/plugin/plugin.ts @@ -4,6 +4,7 @@ import * as chalk from 'chalk'; import { type ContextProviderPlugin, isContextProviderPlugin } from './context-provider-plugin'; import type { CredentialProviderSource } from './credential-provider-source'; import { error } from '../../logging'; +import { ToolkitError } from '../../toolkit/error'; export let TESTING = false; @@ -59,7 +60,7 @@ export class PluginHost { constructor() { if (!TESTING && PluginHost.instance && PluginHost.instance !== this) { - throw new Error('New instances of PluginHost must not be built. Use PluginHost.instance instead!'); + throw new ToolkitError('New instances of PluginHost must not be built. Use PluginHost.instance instead!'); } } @@ -75,14 +76,14 @@ export class PluginHost { /* eslint-enable */ if (!isPlugin(plugin)) { error(`Module ${chalk.green(moduleSpec)} is not a valid plug-in, or has an unsupported version.`); - throw new Error(`Module ${moduleSpec} does not define a valid plug-in.`); + throw new ToolkitError(`Module ${moduleSpec} does not define a valid plug-in.`); } if (plugin.init) { plugin.init(this); } } catch (e: any) { error(`Unable to load ${chalk.green(moduleSpec)}: ${e.stack}`); - throw new Error(`Unable to load plug-in: ${moduleSpec}: ${e}`); + throw new ToolkitError(`Unable to load plug-in: ${moduleSpec}: ${e}`); } function isPlugin(x: any): x is Plugin { @@ -134,7 +135,7 @@ export class PluginHost { */ public registerContextProviderAlpha(pluginProviderName: string, provider: ContextProviderPlugin) { if (!isContextProviderPlugin(provider)) { - throw new Error(`Object you gave me does not look like a ContextProviderPlugin: ${inspect(provider)}`); + throw new ToolkitError(`Object you gave me does not look like a ContextProviderPlugin: ${inspect(provider)}`); } this.contextProviderPlugins[pluginProviderName] = provider; } diff --git a/packages/aws-cdk/lib/assets.ts b/packages/aws-cdk/lib/assets.ts index 317e8c3f34272..e1bdd5ce4fb3c 100644 --- a/packages/aws-cdk/lib/assets.ts +++ b/packages/aws-cdk/lib/assets.ts @@ -6,6 +6,7 @@ import * as chalk from 'chalk'; import { EnvironmentResources } from './api/environment-resources'; import { ToolkitInfo } from './api/toolkit-info'; import { debug } from './logging'; +import { ToolkitError } from './toolkit/error'; import { AssetManifestBuilder } from './util/asset-manifest-builder'; /** @@ -26,7 +27,7 @@ export async function addMetadataAssetsToManifest(stack: cxapi.CloudFormationSta const toolkitInfo = await envResources.lookupToolkit(); if (!toolkitInfo.found) { // eslint-disable-next-line max-len - throw new Error(`This stack uses assets, so the toolkit stack must be deployed to the environment (Run "${chalk.blue('cdk bootstrap ' + stack.environment!.name)}")`); + throw new ToolkitError(`This stack uses assets, so the toolkit stack must be deployed to the environment (Run "${chalk.blue('cdk bootstrap ' + stack.environment!.name)}")`); } const params: Record = {}; @@ -43,7 +44,7 @@ export async function addMetadataAssetsToManifest(stack: cxapi.CloudFormationSta debug(`Preparing asset ${asset.id}: ${JSON.stringify(asset)}`); if (!stack.assembly) { - throw new Error('Unexpected: stack assembly is required in order to find assets in assembly directory'); + throw new ToolkitError('Unexpected: stack assembly is required in order to find assets in assembly directory'); } Object.assign(params, await prepareAsset(asset, assetManifest, envResources, toolkitInfo)); @@ -66,7 +67,7 @@ async function prepareAsset(asset: cxschema.AssetMetadataEntry, assetManifest: A return prepareDockerImageAsset(asset, assetManifest, envResources); default: // eslint-disable-next-line max-len - throw new Error(`Unsupported packaging type: ${(asset as any).packaging}. You might need to upgrade your aws-cdk toolkit to support this asset type.`); + throw new ToolkitError(`Unsupported packaging type: ${(asset as any).packaging}. You might need to upgrade your aws-cdk toolkit to support this asset type.`); } } @@ -110,7 +111,7 @@ async function prepareDockerImageAsset( // Post-1.21.0, repositoryName will always be specified and it will be a shared repository between // all assets, and asset will have imageTag specified as well. Validate the combination. if (!asset.imageNameParameter && (!asset.repositoryName || !asset.imageTag)) { - throw new Error('Invalid Docker image asset configuration: "repositoryName" and "imageTag" are required when "imageNameParameter" is left out'); + throw new ToolkitError('Invalid Docker image asset configuration: "repositoryName" and "imageTag" are required when "imageNameParameter" is left out'); } const repositoryName = asset.repositoryName ?? 'cdk/' + asset.id.replace(/[:/]/g, '-').toLowerCase(); diff --git a/packages/aws-cdk/lib/build.ts b/packages/aws-cdk/lib/build.ts index 93440f0861bd1..974db182ccdfe 100644 --- a/packages/aws-cdk/lib/build.ts +++ b/packages/aws-cdk/lib/build.ts @@ -1,4 +1,5 @@ import * as cxapi from '@aws-cdk/cx-api'; +import { ToolkitError } from './toolkit/error'; type Options = { buildStackAssets: (stack: cxapi.CloudFormationStackArtifact) => Promise; @@ -18,6 +19,6 @@ export async function buildAllStackAssets(stacks: cxapi.CloudFormationStackArtif } if (buildingErrors.length) { - throw Error(`Building Assets Failed: ${buildingErrors.join(', ')}`); + throw new ToolkitError(`Building Assets Failed: ${buildingErrors.join(', ')}`); } } diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 34c766efdd519..32f6616e5cdb6 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -48,6 +48,7 @@ import { listStacks } from './list-stacks'; import { data, debug, error, highlight, print, success, warning, withCorkedLogging } from './logging'; import { deserializeStructure, serializeStructure } from './serialize'; import { Configuration, PROJECT_CONFIG } from './settings'; +import { ToolkitError } from './toolkit/error'; import { numberFromBool, partition } from './util'; import { validateSnsTopicArn } from './util/validate-notification-arn'; import { Concurrency, WorkGraph } from './util/work-graph'; @@ -161,13 +162,13 @@ export class CdkToolkit { if (options.templatePath !== undefined) { // Compare single stack against fixed template if (stacks.stackCount !== 1) { - throw new Error( + throw new ToolkitError( 'Can only select one stack when comparing to fixed template. Use --exclusively to avoid selecting multiple stacks.', ); } if (!(await fs.pathExists(options.templatePath))) { - throw new Error(`There is no file at ${options.templatePath}`); + throw new ToolkitError(`There is no file at ${options.templatePath}`); } const template = deserializeStructure(await fs.readFile(options.templatePath, { encoding: 'UTF-8' })); @@ -336,7 +337,7 @@ export class CdkToolkit { if (!stack.environment) { // eslint-disable-next-line max-len - throw new Error( + throw new ToolkitError( `Stack ${stack.displayName} does not define an environment, and AWS credentials could not be obtained from standard locations or no region was configured.`, ); } @@ -381,7 +382,7 @@ export class CdkToolkit { for (const notificationArn of notificationArns ?? []) { if (!validateSnsTopicArn(notificationArn)) { - throw new Error(`Notification arn ${notificationArn} is not a valid arn for an SNS topic`); + throw new ToolkitError(`Notification arn ${notificationArn} is not a valid arn for an SNS topic`); } } @@ -402,7 +403,7 @@ export class CdkToolkit { let iteration = 0; while (!deployResult) { if (++iteration > 2) { - throw new Error('This loop should have stabilized in 2 iterations, but didn\'t. If you are seeing this error, please report it at https://github.com/aws/aws-cdk/issues/new/choose'); + throw new ToolkitError('This loop should have stabilized in 2 iterations, but didn\'t. If you are seeing this error, please report it at https://github.com/aws/aws-cdk/issues/new/choose'); } const r = await this.props.deployments.deployStack({ @@ -480,7 +481,7 @@ export class CdkToolkit { } default: - throw new Error(`Unexpected result type from deployStack: ${JSON.stringify(r)}. If you are seeing this error, please report it at https://github.com/aws/aws-cdk/issues/new/choose`); + throw new ToolkitError(`Unexpected result type from deployStack: ${JSON.stringify(r)}. If you are seeing this error, please report it at https://github.com/aws/aws-cdk/issues/new/choose`); } } @@ -509,7 +510,7 @@ export class CdkToolkit { } catch (e: any) { // It has to be exactly this string because an integration test tests for // "bold(stackname) failed: ResourceNotReady: " - throw new Error( + throw new ToolkitError( [`❌ ${chalk.bold(stack.stackName)} failed:`, ...(e.name ? [`${e.name}:`] : []), e.message].join(' '), ); } finally { @@ -603,11 +604,11 @@ export class CdkToolkit { print('\n✨ Rollback time: %ss\n', formatTime(elapsedRollbackTime)); } catch (e: any) { error('\n ❌ %s failed: %s', chalk.bold(stack.displayName), e.message); - throw new Error('Rollback failed (use --force to orphan failing resources)'); + throw new ToolkitError('Rollback failed (use --force to orphan failing resources)'); } } if (!anyRollbackable) { - throw new Error('No stacks were in a state that could be rolled back'); + throw new ToolkitError('No stacks were in a state that could be rolled back'); } } @@ -618,7 +619,7 @@ export class CdkToolkit { const watchSettings: { include?: string | string[]; exclude: string | string[] } | undefined = this.props.configuration.settings.get(['watch']); if (!watchSettings) { - throw new Error( + throw new ToolkitError( "Cannot use the 'watch' command without specifying at least one directory to monitor. " + 'Make sure to add a "watch" key to your cdk.json', ); @@ -716,13 +717,13 @@ export class CdkToolkit { const stacks = await this.selectStacksForDeploy(options.selector, true, true, false); if (stacks.stackCount > 1) { - throw new Error( + throw new ToolkitError( `Stack selection is ambiguous, please choose a specific stack for import [${stacks.stackArtifacts.map((x) => x.id).join(', ')}]`, ); } if (!process.stdout.isTTY && !options.resourceMappingFile) { - throw new Error('--resource-mapping is required when input is not a terminal'); + throw new ToolkitError('--resource-mapping is required when input is not a terminal'); } const stack = stacks.stackArtifacts[0]; @@ -980,12 +981,12 @@ export class CdkToolkit { if (globSpecs.length > 0 && !this.props.cloudExecutable.hasApp) { if (userEnvironmentSpecs.length > 0) { // User did request this glob - throw new Error( + throw new ToolkitError( `'${globSpecs}' is not an environment name. Specify an environment name like 'aws://123456789012/us-east-1', or run in a directory with 'cdk.json' to use wildcards.`, ); } else { // User did not request anything - throw new Error( + throw new ToolkitError( "Specify an environment name like 'aws://123456789012/us-east-1', or run in a directory with 'cdk.json'.", ); } @@ -1053,7 +1054,7 @@ export class CdkToolkit { } else if (scanType == TemplateSourceOptions.STACK) { const template = await readFromStack(options.stackName, this.props.sdkProvider, environment); if (!template) { - throw new Error(`No template found for stack-name: ${options.stackName}`); + throw new ToolkitError(`No template found for stack-name: ${options.stackName}`); } generateTemplateOutput = { migrateJson: { @@ -1063,7 +1064,7 @@ export class CdkToolkit { }; } else { // We shouldn't ever get here, but just in case. - throw new Error(`Invalid source option provided: ${scanType}`); + throw new ToolkitError(`Invalid source option provided: ${scanType}`); } const stack = generateStack(generateTemplateOutput.migrateJson.templateBody, options.stackName, language); success(' ⏳ Generating CDK app for %s...', chalk.blue(options.stackName)); @@ -1177,7 +1178,7 @@ export class CdkToolkit { */ private validateStacksSelected(stacks: StackCollection, stackNames: string[]) { if (stackNames.length != 0 && stacks.stackCount == 0) { - throw new Error(`No stacks match the name(s) ${stackNames}`); + throw new ToolkitError(`No stacks match the name(s) ${stackNames}`); } } @@ -1197,7 +1198,7 @@ export class CdkToolkit { // Could have been a glob so check that we evaluated to exactly one if (stacks.stackCount > 1) { - throw new Error(`This command requires exactly one stack and we matched more than one: ${stacks.stackIds}`); + throw new ToolkitError(`This command requires exactly one stack and we matched more than one: ${stacks.stackIds}`); } return assembly.stackById(stacks.firstStack.id); @@ -1939,15 +1940,15 @@ async function askUserConfirmation( await withCorkedLogging(async () => { // only talk to user if STDIN is a terminal (otherwise, fail) if (!TESTING && !process.stdin.isTTY) { - throw new Error(`${motivation}, but terminal (TTY) is not attached so we are unable to get a confirmation from the user`); + throw new ToolkitError(`${motivation}, but terminal (TTY) is not attached so we are unable to get a confirmation from the user`); } // only talk to user if concurrency is 1 (otherwise, fail) if (concurrency > 1) { - throw new Error(`${motivation}, but concurrency is greater than 1 so we are unable to get a confirmation from the user`); + throw new ToolkitError(`${motivation}, but concurrency is greater than 1 so we are unable to get a confirmation from the user`); } const confirmed = await promptly.confirm(`${chalk.cyan(question)} (y/n)?`); - if (!confirmed) { throw new Error('Aborted by user'); } + if (!confirmed) { throw new ToolkitError('Aborted by user'); } }); } diff --git a/packages/aws-cdk/lib/cli.ts b/packages/aws-cdk/lib/cli.ts index 2af03e05eb728..72344548aff9b 100644 --- a/packages/aws-cdk/lib/cli.ts +++ b/packages/aws-cdk/lib/cli.ts @@ -27,6 +27,7 @@ import { Notices } from '../lib/notices'; import { Command, Configuration, Settings } from '../lib/settings'; import * as version from '../lib/version'; import { SdkToCliLogger } from './api/aws-auth/sdk-logger'; +import { ToolkitError } from './toolkit/error'; /* eslint-disable max-len */ /* eslint-disable @typescript-eslint/no-shadow */ // yargs @@ -142,7 +143,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise Promise; @@ -85,6 +86,6 @@ async function dotnetAddProject(targetDirectory: string, context: HookContext, e try { await shell(['dotnet', 'sln', slnPath, 'add', csprojPath]); } catch (e: any) { - throw new Error(`Could not add project ${pname}.${ext} to solution ${pname}.sln. ${e.message}`); + throw new ToolkitError(`Could not add project ${pname}.${ext} to solution ${pname}.sln. ${e.message}`); } }; diff --git a/packages/aws-cdk/lib/init.ts b/packages/aws-cdk/lib/init.ts index c6801b0229554..d1e61b8173c42 100644 --- a/packages/aws-cdk/lib/init.ts +++ b/packages/aws-cdk/lib/init.ts @@ -4,6 +4,7 @@ import * as chalk from 'chalk'; import * as fs from 'fs-extra'; import { invokeBuiltinHooks } from './init-hooks'; import { error, print, warning } from './logging'; +import { ToolkitError } from './toolkit/error'; import { cdkHomeDir, rootDir } from './util/directories'; import { rangeFromSemver } from './util/version-range'; @@ -40,7 +41,7 @@ export async function cliInit(options: CliInitOptions) { const template = (await availableInitTemplates()).find((t) => t.hasName(type!)); if (!template) { await printAvailableTemplates(options.language); - throw new Error(`Unknown init template: ${type}`); + throw new ToolkitError(`Unknown init template: ${type}`); } if (!options.language && template.languages.length === 1) { const language = template.languages[0]; @@ -50,7 +51,7 @@ export async function cliInit(options: CliInitOptions) { } if (!options.language) { print(`Available languages for ${chalk.green(type)}: ${template.languages.map((l) => chalk.blue(l)).join(', ')}`); - throw new Error('No language was selected'); + throw new ToolkitError('No language was selected'); } await initializeProject( @@ -119,7 +120,7 @@ export class InitTemplate { `The ${chalk.blue(language)} language is not supported for ${chalk.green(this.name)} ` + `(it supports: ${this.languages.map((l) => chalk.blue(l)).join(', ')})`, ); - throw new Error(`Unsupported language: ${language}`); + throw new ToolkitError(`Unsupported language: ${language}`); } const projectInfo: ProjectInfo = { @@ -340,7 +341,7 @@ async function initializeProject( async function assertIsEmptyDirectory(workDir: string) { const files = await fs.readdir(workDir); if (files.filter((f) => !f.startsWith('.')).length !== 0) { - throw new Error('`cdk init` cannot be run in a non-empty directory!'); + throw new ToolkitError('`cdk init` cannot be run in a non-empty directory!'); } } @@ -466,7 +467,7 @@ async function execute(cmd: string, args: string[], { cwd }: { cwd: string }) { return ok(stdout); } else { process.stderr.write(stdout); - return fail(new Error(`${cmd} exited with status ${status}`)); + return fail(new ToolkitError(`${cmd} exited with status ${status}`)); } }); }); diff --git a/packages/aws-cdk/lib/notices.ts b/packages/aws-cdk/lib/notices.ts index 8970485f321af..ca0f1487a97fc 100644 --- a/packages/aws-cdk/lib/notices.ts +++ b/packages/aws-cdk/lib/notices.ts @@ -7,8 +7,9 @@ import * as fs from 'fs-extra'; import * as semver from 'semver'; import { SdkHttpOptions } from './api'; import { AwsCliCompatible } from './api/aws-auth/awscli-compatible'; -import { debug, error, print, warning } from './logging'; +import { debug, print, warning, error } from './logging'; import { Context } from './settings'; +import { ToolkitError } from './toolkit/error'; import { loadTreeFromDir, some } from './tree'; import { flatMap } from './util'; import { cdkCacheDir } from './util/directories'; @@ -399,7 +400,7 @@ export class WebsiteNoticeDataSource implements NoticeDataSource { let timer = setTimeout(() => { if (req) { - req.destroy(new Error('Request timed out')); + req.destroy(new ToolkitError('Request timed out')); } }, timeout); @@ -423,24 +424,24 @@ export class WebsiteNoticeDataSource implements NoticeDataSource { try { const data = JSON.parse(rawData).notices as Notice[]; if (!data) { - throw new Error("'notices' key is missing"); + throw new ToolkitError("'notices' key is missing"); } debug('Notices refreshed'); resolve(data ?? []); } catch (e: any) { - reject(new Error(`Failed to parse notices: ${e.message}`)); + reject(new ToolkitError(`Failed to parse notices: ${e.message}`)); } }); res.on('error', e => { - reject(new Error(`Failed to fetch notices: ${e.message}`)); + reject(new ToolkitError(`Failed to fetch notices: ${e.message}`)); }); } else { - reject(new Error(`Failed to fetch notices. Status code: ${res.statusCode}`)); + reject(new ToolkitError(`Failed to fetch notices. Status code: ${res.statusCode}`)); } }); req.on('error', reject); } catch (e: any) { - reject(new Error(`HTTPS 'get' call threw an error: ${e.message}`)); + reject(new ToolkitError(`HTTPS 'get' call threw an error: ${e.message}`)); } }); } diff --git a/packages/aws-cdk/lib/os.ts b/packages/aws-cdk/lib/os.ts index 6483081eb9182..660a83a6bfbd6 100644 --- a/packages/aws-cdk/lib/os.ts +++ b/packages/aws-cdk/lib/os.ts @@ -1,6 +1,7 @@ import * as child_process from 'child_process'; import * as chalk from 'chalk'; import { debug } from './logging'; +import { ToolkitError } from './toolkit/error'; /** * OS helpers @@ -32,7 +33,7 @@ export async function shell(command: string[]): Promise { if (code === 0) { resolve(Buffer.from(stdout).toString('utf-8')); } else { - reject(new Error(`${commandLine} exited with error code ${code}`)); + reject(new ToolkitError(`${commandLine} exited with error code ${code}`)); } }); }); diff --git a/packages/aws-cdk/lib/settings.ts b/packages/aws-cdk/lib/settings.ts index 7e558a391d5ac..58755af786643 100644 --- a/packages/aws-cdk/lib/settings.ts +++ b/packages/aws-cdk/lib/settings.ts @@ -3,6 +3,7 @@ import * as fs_path from 'path'; import * as fs from 'fs-extra'; import { Tag } from './cdk-toolkit'; import { debug, warning } from './logging'; +import { ToolkitError } from './toolkit/error'; import * as util from './util'; export type SettingsMap = {[key: string]: any}; @@ -94,14 +95,14 @@ export class Configuration { private get projectConfig() { if (!this._projectConfig) { - throw new Error('#load has not been called yet!'); + throw new ToolkitError('#load has not been called yet!'); } return this._projectConfig; } public get projectContext() { if (!this._projectContext) { - throw new Error('#load has not been called yet!'); + throw new ToolkitError('#load has not been called yet!'); } return this._projectContext; } @@ -117,7 +118,7 @@ export class Configuration { const readUserContext = this.props.readUserContext ?? true; if (userConfig.get(['build'])) { - throw new Error('The `build` key cannot be specified in the user config (~/.cdk.json), specify it in the project config (cdk.json) instead'); + throw new ToolkitError('The `build` key cannot be specified in the user config (~/.cdk.json), specify it in the project config (cdk.json) instead'); } const contextSources = [ @@ -355,7 +356,7 @@ export class Settings { if (parts.length === 2) { debug('CLI argument context: %s=%s', parts[0], parts[1]); if (parts[0].match(/^aws:.+/)) { - throw new Error(`User-provided context cannot use keys prefixed with 'aws:', but ${parts[0]} was provided.`); + throw new ToolkitError(`User-provided context cannot use keys prefixed with 'aws:', but ${parts[0]} was provided.`); } context[parts[0]] = parts[1]; } else { @@ -398,7 +399,7 @@ export class Settings { public async load(fileName: string): Promise { if (this.readOnly) { - throw new Error(`Can't load ${fileName}: settings object is readonly`); + throw new ToolkitError(`Can't load ${fileName}: settings object is readonly`); } this.settings = {}; @@ -439,7 +440,7 @@ export class Settings { public clear() { if (this.readOnly) { - throw new Error('Cannot clear(): settings are readonly'); + throw new ToolkitError('Cannot clear(): settings are readonly'); } this.settings = {}; } @@ -454,7 +455,7 @@ export class Settings { public set(path: string[], value: any): Settings { if (this.readOnly) { - throw new Error(`Can't set ${path}: settings object is readonly`); + throw new ToolkitError(`Can't set ${path}: settings object is readonly`); } if (path.length === 0) { // deepSet can't handle this case @@ -473,7 +474,7 @@ export class Settings { if (!this.settings.context) { return; } if (key in this.settings.context) { // eslint-disable-next-line max-len - throw new Error(`The 'context.${key}' key was found in ${fs_path.resolve(fileName)}, but it is no longer supported. Please remove it.`); + throw new ToolkitError(`The 'context.${key}' key was found in ${fs_path.resolve(fileName)}, but it is no longer supported. Please remove it.`); } } @@ -520,11 +521,11 @@ function isTransientValue(value: any) { function expectStringList(x: unknown): string[] | undefined { if (x === undefined) { return undefined; } if (!Array.isArray(x)) { - throw new Error(`Expected array, got '${x}'`); + throw new ToolkitError(`Expected array, got '${x}'`); } const nonStrings = x.filter(e => typeof e !== 'string'); if (nonStrings.length > 0) { - throw new Error(`Expected list of strings, found ${nonStrings}`); + throw new ToolkitError(`Expected list of strings, found ${nonStrings}`); } return x; } diff --git a/packages/aws-cdk/lib/toolkit/error.ts b/packages/aws-cdk/lib/toolkit/error.ts new file mode 100644 index 0000000000000..cc22b935b5648 --- /dev/null +++ b/packages/aws-cdk/lib/toolkit/error.ts @@ -0,0 +1,48 @@ +const TOOLKIT_ERROR_SYMBOL = Symbol.for('@aws-cdk/core.TooklitError'); +const AUTHENTICATION_ERROR_SYMBOL = Symbol.for('@aws-cdk/core.AuthenticationError'); + +/** + * Represents a general toolkit error in the AWS CDK Toolkit. + */ +class ToolkitError extends Error { + /** + * Determines if a given error is an instance of ToolkitError. + */ + public static isToolkitError(x: any): x is ToolkitError { + return x !== null && typeof(x) === 'object' && TOOLKIT_ERROR_SYMBOL in x; + } + + /** + * Determines if a given error is an instance of AuthenticationError. + */ + public static isAuthenticationError(x: any): x is AuthenticationError { + return this.isToolkitError(x) && AUTHENTICATION_ERROR_SYMBOL in x; + } + + /** + * The type of the error, defaults to "toolkit". + */ + public readonly type: string; + + 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; + } +} + +/** + * Represents an authentication-specific error in the AWS CDK Toolkit. + */ +class AuthenticationError extends ToolkitError { + constructor(message: string) { + super(message, 'authentication'); + Object.setPrototypeOf(this, AuthenticationError.prototype); + Object.defineProperty(this, AUTHENTICATION_ERROR_SYMBOL, { value: true }); + } +} + +// Export classes for internal usage only +export { ToolkitError, AuthenticationError }; diff --git a/packages/aws-cdk/lib/version.ts b/packages/aws-cdk/lib/version.ts index bdeee8d51b482..a3b7f8383a28b 100644 --- a/packages/aws-cdk/lib/version.ts +++ b/packages/aws-cdk/lib/version.ts @@ -5,6 +5,7 @@ import * as semver from 'semver'; import { cdkCacheDir, rootDir } from './util/directories'; import { getLatestVersionFromNpm } from './util/npm'; import { debug, print } from '../lib/logging'; +import { ToolkitError } from './toolkit/error'; import { formatAsBanner } from '../lib/util/console-formatters'; const ONE_DAY_IN_SECONDS = 1 * 24 * 60 * 60; @@ -46,7 +47,7 @@ export class VersionCheckTTL { fs.mkdirsSync(path.dirname(this.file)); fs.accessSync(path.dirname(this.file), fs.constants.W_OK); } catch { - throw new Error(`Directory (${path.dirname(this.file)}) is not writable.`); + throw new ToolkitError(`Directory (${path.dirname(this.file)}) is not writable.`); } this.ttlSecs = ttlSecs || ONE_DAY_IN_SECONDS; } diff --git a/packages/aws-cdk/test/toolkit-error.test.ts b/packages/aws-cdk/test/toolkit-error.test.ts new file mode 100644 index 0000000000000..1aef772e186a5 --- /dev/null +++ b/packages/aws-cdk/test/toolkit-error.test.ts @@ -0,0 +1,17 @@ +import { AuthenticationError, ToolkitError } from '../lib/toolkit/error'; + +describe('toolkit error', () => { + let toolkitError = new ToolkitError('Test toolkit error'); + let authError = new AuthenticationError('Test authentication error'); + test('types are correctly assigned', async () => { + expect(toolkitError.type).toBe('toolkit'); + expect(authError.type).toBe('authentication'); + }); + + test('isToolkitError and isAuthenticationError functions work', () => { + expect(ToolkitError.isToolkitError(toolkitError)).toBe(true); + expect(ToolkitError.isToolkitError(authError)).toBe(true); + expect(ToolkitError.isAuthenticationError(toolkitError)).toBe(false); + expect(ToolkitError.isAuthenticationError(authError)).toBe(true); + }); +});