Skip to content

Commit 14b1098

Browse files
authored
feat(pipelines): throw ValidationError instead of untyped Errors (#33385)
### 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 19cf902 commit 14b1098

23 files changed

+103
-89
lines changed

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

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const enableNoThrowDefaultErrorIn = [
6363
'aws-s3objectlambda',
6464
'aws-s3outposts',
6565
'aws-s3tables',
66+
'pipelines',
6667
];
6768
baseConfig.overrides.push({
6869
files: enableNoThrowDefaultErrorIn.map(m => `./${m}/lib/**`),

packages/aws-cdk-lib/pipelines/lib/blueprint/file-set.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Step } from './step';
2+
import { UnscopedValidationError } from '../../../core';
23

34
/**
45
* A set of files traveling through the deployment pipeline
@@ -26,7 +27,7 @@ export class FileSet implements IFileSetProducer {
2627
*/
2728
public get producer() {
2829
if (!this._producer) {
29-
throw new Error(`FileSet '${this.id}' doesn\'t have a producer; call 'fileSet.producedBy()'`);
30+
throw new UnscopedValidationError(`FileSet '${this.id}' doesn\'t have a producer; call 'fileSet.producedBy()'`);
3031
}
3132
return this._producer;
3233
}
@@ -38,7 +39,7 @@ export class FileSet implements IFileSetProducer {
3839
*/
3940
public producedBy(producer?: Step) {
4041
if (this._producer) {
41-
throw new Error(`FileSet '${this.id}' already has a producer (${this._producer}) while setting producer: ${producer}`);
42+
throw new UnscopedValidationError(`FileSet '${this.id}' already has a producer (${this._producer}) while setting producer: ${producer}`);
4243
}
4344
this._producer = producer;
4445
}

packages/aws-cdk-lib/pipelines/lib/blueprint/shell-step.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { FileSet, IFileSetProducer } from './file-set';
22
import { StackDeployment } from './stack-deployment';
33
import { Step } from './step';
4-
import { CfnOutput, Stack } from '../../../core';
4+
import { CfnOutput, Stack, UnscopedValidationError } from '../../../core';
55
import { mapValues } from '../private/javascript';
66

77
/**
@@ -160,20 +160,20 @@ export class ShellStep extends Step {
160160
if (props.input) {
161161
const fileSet = props.input.primaryOutput;
162162
if (!fileSet) {
163-
throw new Error(`'${id}': primary input should be a step that has produced a file set, got ${props.input}`);
163+
throw new UnscopedValidationError(`'${id}': primary input should be a step that has produced a file set, got ${props.input}`);
164164
}
165165
this.addDependencyFileSet(fileSet);
166166
this.inputs.push({ directory: '.', fileSet });
167167
}
168168

169169
for (const [directory, step] of Object.entries(props.additionalInputs ?? {})) {
170170
if (directory === '.') {
171-
throw new Error(`'${id}': input for directory '.' should be passed via 'input' property`);
171+
throw new UnscopedValidationError(`'${id}': input for directory '.' should be passed via 'input' property`);
172172
}
173173

174174
const fileSet = step.primaryOutput;
175175
if (!fileSet) {
176-
throw new Error(`'${id}': additionalInput for directory '${directory}' should be a step that has produced a file set, got ${step}`);
176+
throw new UnscopedValidationError(`'${id}': additionalInput for directory '${directory}' should be a step that has produced a file set, got ${step}`);
177177
}
178178
this.addDependencyFileSet(fileSet);
179179
this.inputs.push({ directory, fileSet });
@@ -200,7 +200,7 @@ export class ShellStep extends Step {
200200
public primaryOutputDirectory(directory: string): FileSet {
201201
if (this._primaryOutputDirectory !== undefined) {
202202
if (this._primaryOutputDirectory !== directory) {
203-
throw new Error(`${this}: primaryOutputDirectory is '${this._primaryOutputDirectory}', cannot be changed to '${directory}'`);
203+
throw new UnscopedValidationError(`${this}: primaryOutputDirectory is '${this._primaryOutputDirectory}', cannot be changed to '${directory}'`);
204204
}
205205

206206
return this.primaryOutput!;

packages/aws-cdk-lib/pipelines/lib/blueprint/stack-deployment.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as path from 'path';
22
import { AssetType } from './asset-type';
33
import { Step } from './step';
4+
import { UnscopedValidationError } from '../../../core';
45
import * as cxapi from '../../../cx-api';
56
import { AssetManifestReader, DockerImageManifestEntry, FileManifestEntry } from '../private/asset-manifest';
67
import { isAssetManifest } from '../private/cloud-assembly-internals';
@@ -308,7 +309,7 @@ function extractStackAssets(stackArtifact: cxapi.CloudFormationStackArtifact): S
308309
isTemplate = entry.source.packaging === 'file' && entry.source.path === stackArtifact.templateFile;
309310
assetType = AssetType.FILE;
310311
} else {
311-
throw new Error(`Unrecognized asset type: ${entry.type}`);
312+
throw new UnscopedValidationError(`Unrecognized asset type: ${entry.type}`);
312313
}
313314

314315
ret.push({

packages/aws-cdk-lib/pipelines/lib/blueprint/stage-deployment.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { StackDeployment } from './stack-deployment';
22
import { StackSteps, Step } from './step';
33
import * as cdk from '../../../core';
4+
import { ValidationError } from '../../../core';
45
import { CloudFormationStackArtifact } from '../../../cx-api';
56
import { isStackArtifact } from '../private/cloud-assembly-internals';
67
import { pipelineSynth } from '../private/construct-internals';
@@ -56,13 +57,13 @@ export class StageDeployment {
5657
if (assembly.stacks.length === 0) {
5758
// If we don't check here, a more puzzling "stage contains no actions"
5859
// error will be thrown come deployment time.
59-
throw new Error(`The given Stage construct ('${stage.node.path}') should contain at least one Stack`);
60+
throw new ValidationError(`The given Stage construct ('${stage.node.path}') should contain at least one Stack`, stage);
6061
}
6162

6263
const stepFromArtifact = new Map<CloudFormationStackArtifact, StackDeployment>();
6364
for (const artifact of assembly.stacks) {
6465
if (artifact.assumeRoleAdditionalOptions?.Tags && artifact.assumeRoleArn) {
65-
throw new Error(`Deployment of stack ${artifact.stackName} requires assuming the role ${artifact.assumeRoleArn} with session tags, but assuming roles with session tags is not supported by CodePipeline.`);
66+
throw new ValidationError(`Deployment of stack ${artifact.stackName} requires assuming the role ${artifact.assumeRoleArn} with session tags, but assuming roles with session tags is not supported by CodePipeline.`, stage);
6667
}
6768
const step = StackDeployment.fromArtifact(artifact);
6869
stepFromArtifact.set(artifact, step);
@@ -72,7 +73,7 @@ export class StageDeployment {
7273
const stackArtifact = assembly.getStackArtifact(stackstep.stack.artifactId);
7374
const thisStep = stepFromArtifact.get(stackArtifact);
7475
if (!thisStep) {
75-
throw new Error('Logic error: we just added a step for this artifact but it disappeared.');
76+
throw new ValidationError('Logic error: we just added a step for this artifact but it disappeared.', stage);
7677
}
7778
thisStep.addStackSteps(stackstep.pre ?? [], stackstep.changeSet ?? [], stackstep.post ?? []);
7879
}
@@ -81,14 +82,14 @@ export class StageDeployment {
8182
for (const artifact of assembly.stacks) {
8283
const thisStep = stepFromArtifact.get(artifact);
8384
if (!thisStep) {
84-
throw new Error('Logic error: we just added a step for this artifact but it disappeared.');
85+
throw new ValidationError('Logic error: we just added a step for this artifact but it disappeared.', stage);
8586
}
8687

8788
const stackDependencies = artifact.dependencies.filter(isStackArtifact);
8889
for (const dep of stackDependencies) {
8990
const depStep = stepFromArtifact.get(dep);
9091
if (!depStep) {
91-
throw new Error(`Stack '${artifact.id}' depends on stack not found in same Stage: '${dep.id}'`);
92+
throw new ValidationError(`Stack '${artifact.id}' depends on stack not found in same Stage: '${dep.id}'`, stage);
9293
}
9394
thisStep.addStackDependency(depStep);
9495
}

packages/aws-cdk-lib/pipelines/lib/blueprint/step.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { FileSet, IFileSetProducer } from './file-set';
22
import { StackOutputReference } from './shell-step';
3-
import { Stack, Token } from '../../../core';
3+
import { Stack, Token, UnscopedValidationError } from '../../../core';
44
import { StepOutput } from '../helpers-internal/step-output';
55

66
/**
@@ -47,7 +47,7 @@ export abstract class Step implements IFileSetProducer {
4747
/** Identifier for this step */
4848
public readonly id: string) {
4949
if (Token.isUnresolved(id)) {
50-
throw new Error(`Step id cannot be unresolved, got '${id}'`);
50+
throw new UnscopedValidationError(`Step id cannot be unresolved, got '${id}'`);
5151
}
5252
}
5353

packages/aws-cdk-lib/pipelines/lib/codepipeline/codebuild-step.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { makeCodePipelineOutput } from './private/outputs';
33
import * as codebuild from '../../../aws-codebuild';
44
import * as ec2 from '../../../aws-ec2';
55
import * as iam from '../../../aws-iam';
6-
import { Duration } from '../../../core';
6+
import { Duration, UnscopedValidationError } from '../../../core';
77
import { ShellStep, ShellStepProps } from '../blueprint';
88

99
/**
@@ -261,7 +261,7 @@ export class CodeBuildStep extends ShellStep {
261261
*/
262262
public get project(): codebuild.IProject {
263263
if (!this._project) {
264-
throw new Error('Call pipeline.buildPipeline() before reading this property');
264+
throw new UnscopedValidationError('Call pipeline.buildPipeline() before reading this property');
265265
}
266266
return this._project;
267267
}
@@ -317,7 +317,7 @@ export class CodeBuildStep extends ShellStep {
317317
*/
318318
public exportedVariable(variableName: string): string {
319319
if (this.exportedVarsRendered && !this.exportedVariables.has(variableName)) {
320-
throw new Error('exportVariable(): Pipeline has already been produced, cannot call this function anymore');
320+
throw new UnscopedValidationError('exportVariable(): Pipeline has already been produced, cannot call this function anymore');
321321
}
322322

323323
this.exportedVariables.add(variableName);

packages/aws-cdk-lib/pipelines/lib/codepipeline/codepipeline-source.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Action, CodeCommitTrigger, GitHubTrigger, S3Trigger } from '../../../aw
99
import { IRepository } from '../../../aws-ecr';
1010
import * as iam from '../../../aws-iam';
1111
import { IBucket } from '../../../aws-s3';
12-
import { Fn, SecretValue, Token } from '../../../core';
12+
import { Fn, SecretValue, Token, UnscopedValidationError } from '../../../core';
1313
import { FileSet, Step } from '../blueprint';
1414

1515
/**
@@ -257,7 +257,7 @@ class GitHubSource extends CodePipelineSource {
257257

258258
const parts = repoString.split('/');
259259
if (Token.isUnresolved(repoString) || parts.length !== 2) {
260-
throw new Error(`GitHub repository name should be a resolved string like '<owner>/<repo>', got '${repoString}'`);
260+
throw new UnscopedValidationError(`GitHub repository name should be a resolved string like '<owner>/<repo>', got '${repoString}'`);
261261
}
262262
this.owner = parts[0];
263263
this.repo = parts[1];
@@ -425,7 +425,7 @@ class CodeStarConnectionSource extends CodePipelineSource {
425425
super(repoString);
426426

427427
if (!this.isValidRepoString(repoString)) {
428-
throw new Error(`CodeStar repository name should be a resolved string like '<owner>/<repo>' or '<owner>/<group1>/<group2>/.../<repo>', got '${repoString}'`);
428+
throw new UnscopedValidationError(`CodeStar repository name should be a resolved string like '<owner>/<repo>' or '<owner>/<group1>/<group2>/.../<repo>', got '${repoString}'`);
429429
}
430430

431431
const parts = repoString.split('/');

packages/aws-cdk-lib/pipelines/lib/codepipeline/codepipeline.ts

+19-19
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import * as cpa from '../../../aws-codepipeline-actions';
1313
import * as ec2 from '../../../aws-ec2';
1414
import * as iam from '../../../aws-iam';
1515
import * as s3 from '../../../aws-s3';
16-
import { Aws, CfnCapabilities, Duration, PhysicalName, Stack, Names, FeatureFlags } from '../../../core';
16+
import { Aws, CfnCapabilities, Duration, PhysicalName, Stack, Names, FeatureFlags, UnscopedValidationError, ValidationError } from '../../../core';
1717
import * as cxapi from '../../../cx-api';
1818
import { AssetType, FileSet, IFileSetProducer, ManualApprovalStep, ShellStep, StackAsset, StackDeployment, Step } from '../blueprint';
1919
import { DockerCredential, dockerCredentialsInstallCommands, DockerCredentialUsage } from '../docker-credentials';
@@ -400,7 +400,7 @@ export class CodePipeline extends PipelineBase {
400400
*/
401401
public get synthProject(): cb.IProject {
402402
if (!this._synthProject) {
403-
throw new Error('Call pipeline.buildPipeline() before reading this property');
403+
throw new UnscopedValidationError('Call pipeline.buildPipeline() before reading this property');
404404
}
405405
return this._synthProject;
406406
}
@@ -413,10 +413,10 @@ export class CodePipeline extends PipelineBase {
413413
*/
414414
public get selfMutationProject(): cb.IProject {
415415
if (!this._pipeline) {
416-
throw new Error('Call pipeline.buildPipeline() before reading this property');
416+
throw new UnscopedValidationError('Call pipeline.buildPipeline() before reading this property');
417417
}
418418
if (!this._selfMutationProject) {
419-
throw new Error('No selfMutationProject since the selfMutation property was set to false');
419+
throw new UnscopedValidationError('No selfMutationProject since the selfMutation property was set to false');
420420
}
421421
return this._selfMutationProject;
422422
}
@@ -428,39 +428,39 @@ export class CodePipeline extends PipelineBase {
428428
*/
429429
public get pipeline(): cp.Pipeline {
430430
if (!this._pipeline) {
431-
throw new Error('Pipeline not created yet');
431+
throw new UnscopedValidationError('Pipeline not created yet');
432432
}
433433
return this._pipeline;
434434
}
435435

436436
protected doBuildPipeline(): void {
437437
if (this._pipeline) {
438-
throw new Error('Pipeline already created');
438+
throw new ValidationError('Pipeline already created', this);
439439
}
440440

441441
this._myCxAsmRoot = path.resolve(assemblyBuilderOf(appOf(this)).outdir);
442442

443443
if (this.props.codePipeline) {
444444
if (this.props.pipelineName) {
445-
throw new Error('Cannot set \'pipelineName\' if an existing CodePipeline is given using \'codePipeline\'');
445+
throw new ValidationError('Cannot set \'pipelineName\' if an existing CodePipeline is given using \'codePipeline\'', this);
446446
}
447447
if (this.props.crossAccountKeys !== undefined) {
448-
throw new Error('Cannot set \'crossAccountKeys\' if an existing CodePipeline is given using \'codePipeline\'');
448+
throw new ValidationError('Cannot set \'crossAccountKeys\' if an existing CodePipeline is given using \'codePipeline\'', this);
449449
}
450450
if (this.props.enableKeyRotation !== undefined) {
451-
throw new Error('Cannot set \'enableKeyRotation\' if an existing CodePipeline is given using \'codePipeline\'');
451+
throw new ValidationError('Cannot set \'enableKeyRotation\' if an existing CodePipeline is given using \'codePipeline\'', this);
452452
}
453453
if (this.props.crossRegionReplicationBuckets !== undefined) {
454-
throw new Error('Cannot set \'crossRegionReplicationBuckets\' if an existing CodePipeline is given using \'codePipeline\'');
454+
throw new ValidationError('Cannot set \'crossRegionReplicationBuckets\' if an existing CodePipeline is given using \'codePipeline\'', this);
455455
}
456456
if (this.props.reuseCrossRegionSupportStacks !== undefined) {
457-
throw new Error('Cannot set \'reuseCrossRegionSupportStacks\' if an existing CodePipeline is given using \'codePipeline\'');
457+
throw new ValidationError('Cannot set \'reuseCrossRegionSupportStacks\' if an existing CodePipeline is given using \'codePipeline\'', this);
458458
}
459459
if (this.props.role !== undefined) {
460-
throw new Error('Cannot set \'role\' if an existing CodePipeline is given using \'codePipeline\'');
460+
throw new ValidationError('Cannot set \'role\' if an existing CodePipeline is given using \'codePipeline\'', this);
461461
}
462462
if (this.props.artifactBucket !== undefined) {
463-
throw new Error('Cannot set \'artifactBucket\' if an existing CodePipeline is given using \'codePipeline\'');
463+
throw new ValidationError('Cannot set \'artifactBucket\' if an existing CodePipeline is given using \'codePipeline\'', this);
464464
}
465465

466466
this._pipeline = this.props.codePipeline;
@@ -496,7 +496,7 @@ export class CodePipeline extends PipelineBase {
496496

497497
private get myCxAsmRoot(): string {
498498
if (!this._myCxAsmRoot) {
499-
throw new Error('Can\'t read \'myCxAsmRoot\' if build deployment not called yet');
499+
throw new ValidationError('Can\'t read \'myCxAsmRoot\' if build deployment not called yet', this);
500500
}
501501
return this._myCxAsmRoot;
502502
}
@@ -515,7 +515,7 @@ export class CodePipeline extends PipelineBase {
515515
let beforeSelfMutation = this.selfMutationEnabled;
516516
for (const stageNode of flatten(structure.graph.sortedChildren())) {
517517
if (!isGraph(stageNode)) {
518-
throw new Error(`Top-level children must be graphs, got '${stageNode}'`);
518+
throw new ValidationError(`Top-level children must be graphs, got '${stageNode}'`, this);
519519
}
520520

521521
// Group our ordered tranches into blocks of 50.
@@ -610,7 +610,7 @@ export class CodePipeline extends PipelineBase {
610610
case 'group':
611611
case 'stack-group':
612612
case undefined:
613-
throw new Error(`actionFromNode: did not expect to get group nodes: ${node.data?.type}`);
613+
throw new ValidationError(`actionFromNode: did not expect to get group nodes: ${node.data?.type}`, this);
614614

615615
case 'self-update':
616616
return this.selfMutateAction();
@@ -630,7 +630,7 @@ export class CodePipeline extends PipelineBase {
630630
return this.actionFromStep(node, node.data.step);
631631

632632
default:
633-
throw new Error(`CodePipeline does not support graph nodes of type '${node.data?.type}'. You are probably using a feature this CDK Pipelines implementation does not support.`);
633+
throw new ValidationError(`CodePipeline does not support graph nodes of type '${node.data?.type}'. You are probably using a feature this CDK Pipelines implementation does not support.`, this);
634634
}
635635
}
636636

@@ -679,7 +679,7 @@ export class CodePipeline extends PipelineBase {
679679
};
680680
}
681681

682-
throw new Error(`Deployment step '${step}' is not supported for CodePipeline-backed pipelines`);
682+
throw new ValidationError(`Deployment step '${step}' is not supported for CodePipeline-backed pipelines`, this);
683683
}
684684

685685
private createChangeSetAction(stack: StackDeployment): ICodePipelineActionFactory {
@@ -824,7 +824,7 @@ export class CodePipeline extends PipelineBase {
824824

825825
const assetType = assets[0].assetType;
826826
if (assets.some(a => a.assetType !== assetType)) {
827-
throw new Error('All assets in a single publishing step must be of the same type');
827+
throw new ValidationError('All assets in a single publishing step must be of the same type', this);
828828
}
829829

830830
const role = this.obtainAssetCodeBuildRole(assets[0].assetType);

packages/aws-cdk-lib/pipelines/lib/codepipeline/confirm-permissions-broadening.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { CodePipelineActionFactoryResult, ICodePipelineActionFactory, ProduceAct
44
import { IStage } from '../../../aws-codepipeline';
55
import * as cpa from '../../../aws-codepipeline-actions';
66
import * as sns from '../../../aws-sns';
7-
import { Stage } from '../../../core';
7+
import { Stage, ValidationError } from '../../../core';
88
import { Step } from '../blueprint';
99
import { ApplicationSecurityCheck } from '../private/application-security-check';
1010

@@ -76,7 +76,7 @@ export class ConfirmPermissionsBroadening extends Step implements ICodePipelineA
7676
const existing = Node.of(pipeline).tryFindChild(id);
7777
if (existing) {
7878
if (!(existing instanceof ApplicationSecurityCheck)) {
79-
throw new Error(`Expected '${Node.of(existing).path}' to be 'ApplicationSecurityCheck' but was '${existing}'`);
79+
throw new ValidationError(`Expected '${Node.of(existing).path}' to be 'ApplicationSecurityCheck' but was '${existing}'`, pipeline);
8080
}
8181
return existing;
8282
}

0 commit comments

Comments
 (0)