Skip to content

Commit 14f6811

Browse files
authored
feat(pipelines): Expose stack output namespaces in custom pipelines.Steps (#23110)
Implements #23000 as per request from #23000 (comment). In order for custom steps to include `CfnOutput` in their action configurations, there needed to be a way to access and/or generate the output variable namespaces. `ShellStep` already had this capability. This change generalizes the `StackOutputReference` usages by letting implementors of `Step` define the `StackOutputReference`s their step consumes, instead of hardwiring it to `ShellStep`. To actually consume the references, the `ICodePipelineActionFactory` provides a `StackOutputsMap` that exposes a method to render `StackOutputReference`s into their assigned CodePipeline variable names.
1 parent dea4216 commit 14f6811

26 files changed

+3615
-18
lines changed

allowed-breaking-changes.txt

+5-1
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,8 @@ incompatible-argument:@aws-cdk/aws-route53-targets.InterfaceVpcEndpointTarget.<i
149149
changed-type:@aws-cdk/cx-api.AssetManifestArtifact.requiresBootstrapStackVersion
150150

151151
# removed mistyped ec2 instance class
152-
removed:aws-cdk-lib.aws_ec2.InstanceClass.COMPUTE6_GRAVITON2_HIGH_NETWORK_BANDWITH
152+
removed:aws-cdk-lib.aws_ec2.InstanceClass.COMPUTE6_GRAVITON2_HIGH_NETWORK_BANDWITH
153+
154+
# added new required property StackOutputsMap
155+
strengthened:@aws-cdk/pipelines.ProduceActionOptions
156+
strengthened:aws-cdk-lib.pipelines.ProduceActionOptions

packages/@aws-cdk/pipelines/README.md

+38-1
Original file line numberDiff line numberDiff line change
@@ -830,7 +830,7 @@ class MyJenkinsStep extends pipelines.Step implements pipelines.ICodePipelineAct
830830
) {
831831
super('MyJenkinsStep');
832832

833-
// This is necessary if your step accepts parametres, like environment variables,
833+
// This is necessary if your step accepts parameters, like environment variables,
834834
// that may contain outputs from other steps. It doesn't matter what the
835835
// structure is, as long as it contains the values that may contain outputs.
836836
this.discoverReferencedOutputs({
@@ -861,6 +861,43 @@ class MyJenkinsStep extends pipelines.Step implements pipelines.ICodePipelineAct
861861
}
862862
```
863863

864+
Another example, adding a lambda step referencing outputs from a stack:
865+
866+
```ts
867+
class MyLambdaStep extends pipelines.Step implements pipelines.ICodePipelineActionFactory {
868+
private stackOutputReference: pipelines.StackOutputReference
869+
870+
constructor(
871+
private readonly function: lambda.Function,
872+
stackOutput: CfnOutput,
873+
) {
874+
super('MyLambdaStep');
875+
this.stackOutputReference = pipelines.StackOutputReference.fromCfnOutput(stackOutput);
876+
}
877+
878+
public produceAction(stage: codepipeline.IStage, options: pipelines.ProduceActionOptions): pipelines.CodePipelineActionFactoryResult {
879+
880+
stage.addAction(new cpactions.LambdaInvokeAction({
881+
actionName: options.actionName,
882+
runOrder: options.runOrder,
883+
// Map the reference to the variable name the CDK has generated for you.
884+
userParameters: {stackOutput: options.stackOutputsMap.toCodePipeline(this.stackOutputReference)},
885+
lambda: this.function,
886+
}));
887+
888+
return { runOrdersConsumed: 1 };
889+
}
890+
891+
/**
892+
* Expose stack output references, letting the CDK know
893+
* we want these variables accessible for this step.
894+
*/
895+
public get consumedStackOutputs(): pipelines.StackOutputReference[] {
896+
return [this.stackOutputReference];
897+
}
898+
}
899+
```
900+
864901
### Using an existing AWS Codepipeline
865902

866903
If you wish to use an existing `CodePipeline.Pipeline` while using the modern API's

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

+4
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,10 @@ export class ShellStep extends Step {
233233
}
234234
return fileSet;
235235
}
236+
237+
public get consumedStackOutputs(): StackOutputReference[] {
238+
return Object.values(this.envFromCfnOutputs);
239+
}
236240
}
237241

238242
/**

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

+8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Stack, Token } from '@aws-cdk/core';
22
import { StepOutput } from '../helpers-internal/step-output';
33
import { FileSet, IFileSetProducer } from './file-set';
4+
import { StackOutputReference } from './shell-step';
45

56
/**
67
* A generic Step which can be added to a Pipeline
@@ -116,6 +117,13 @@ export abstract class Step implements IFileSetProducer {
116117
StepOutput.recordProducer(output);
117118
}
118119
}
120+
121+
/**
122+
* StackOutputReferences this step consumes.
123+
*/
124+
public get consumedStackOutputs(): StackOutputReference[] {
125+
return [];
126+
}
119127
}
120128

121129
/**

packages/@aws-cdk/pipelines/lib/codepipeline/codepipeline-action-factory.ts

+13
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as cp from '@aws-cdk/aws-codepipeline';
33
import { Construct } from 'constructs';
44
import { ArtifactMap } from './artifact-map';
55
import { CodeBuildOptions, CodePipeline } from './codepipeline';
6+
import { StackOutputsMap } from './stack-outputs-map';
67

78
/**
89
* Options for the `CodePipelineActionFactory.produce()` method.
@@ -70,6 +71,18 @@ export interface ProduceActionOptions {
7071
* @default false
7172
*/
7273
readonly beforeSelfMutation?: boolean;
74+
75+
/**
76+
* Helper object to produce variables exported from stack deployments.
77+
*
78+
* If your step references outputs from a stack deployment, use
79+
* this to map the output references to Codepipeline variable names.
80+
*
81+
* Note - Codepipeline variables can only be referenced in action
82+
* configurations.
83+
*
84+
*/
85+
readonly stackOutputsMap: StackOutputsMap;
7386
}
7487

7588
/**

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

+4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { CodeBuildStep } from './codebuild-step';
2424
import { CodePipelineActionFactoryResult, ICodePipelineActionFactory } from './codepipeline-action-factory';
2525
import { CodeBuildFactory, mergeCodeBuildOptions } from './private/codebuild-factory';
2626
import { namespaceStepOutputs } from './private/outputs';
27+
import { StackOutputsMap } from './stack-outputs-map';
2728

2829

2930
/**
@@ -313,6 +314,7 @@ export class CodePipeline extends PipelineBase {
313314
private _myCxAsmRoot?: string;
314315
private readonly dockerCredentials: DockerCredential[];
315316
private readonly cachedFnSub = new CachedFnSub();
317+
private stackOutputs: StackOutputsMap;
316318

317319
/**
318320
* Asset roles shared for publishing
@@ -337,6 +339,7 @@ export class CodePipeline extends PipelineBase {
337339
this.singlePublisherPerAssetType = !(props.publishAssetsInParallel ?? true);
338340
this.cliVersion = props.cliVersion ?? preferredCliVersion();
339341
this.useChangeSets = props.useChangeSets ?? true;
342+
this.stackOutputs = new StackOutputsMap(this);
340343
}
341344

342345
/**
@@ -467,6 +470,7 @@ export class CodePipeline extends PipelineBase {
467470
codeBuildDefaults: nodeType ? this.codeBuildDefaultsFor(nodeType) : undefined,
468471
beforeSelfMutation,
469472
variablesNamespace,
473+
stackOutputsMap: this.stackOutputs,
470474
});
471475

472476
if (node.data?.type === 'self-update') {

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ export * from './codebuild-step';
33
export * from './confirm-permissions-broadening';
44
export * from './codepipeline';
55
export * from './codepipeline-action-factory';
6-
export * from './codepipeline-source';
6+
export * from './codepipeline-source';
7+
export * from './stack-outputs-map';

packages/@aws-cdk/pipelines/lib/codepipeline/private/codebuild-factory.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@ import * as iam from '@aws-cdk/aws-iam';
88
import { Stack, Token } from '@aws-cdk/core';
99
import { Construct, IDependable, Node } from 'constructs';
1010
import { FileSetLocation, ShellStep, StackOutputReference } from '../../blueprint';
11-
import { PipelineQueries } from '../../helpers-internal/pipeline-queries';
1211
import { StepOutput } from '../../helpers-internal/step-output';
1312
import { cloudAssemblyBuildSpecDir, obtainScope } from '../../private/construct-internals';
14-
import { hash, stackVariableNamespace } from '../../private/identifiers';
13+
import { hash } from '../../private/identifiers';
1514
import { mapValues, mkdict, noEmptyObject, noUndefined, partition } from '../../private/javascript';
1615
import { ArtifactMap } from '../artifact-map';
1716
import { CodeBuildStep } from '../codebuild-step';
@@ -315,10 +314,8 @@ export class CodeBuildFactory implements ICodePipelineActionFactory {
315314
});
316315
}
317316

318-
const queries = new PipelineQueries(options.pipeline);
319-
320317
const stackOutputEnv = mapValues(this.props.envFromCfnOutputs ?? {}, outputRef =>
321-
`#{${stackVariableNamespace(queries.producingStack(outputRef))}.${outputRef.outputName}}`,
318+
options.stackOutputsMap.toCodePipeline(outputRef),
322319
);
323320

324321
const configHashEnv = options.beforeSelfMutation
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { StackOutputReference } from '../blueprint';
2+
import { PipelineQueries } from '../helpers-internal/pipeline-queries';
3+
import { PipelineBase } from '../main';
4+
import { stackVariableNamespace } from '../private/identifiers';
5+
6+
/**
7+
* Translate stack outputs to Codepipline variable references
8+
*/
9+
export class StackOutputsMap {
10+
private queries: PipelineQueries
11+
12+
constructor(pipeline: PipelineBase) {
13+
this.queries = new PipelineQueries(pipeline);
14+
}
15+
16+
/**
17+
* Return the matching variable reference string for a StackOutputReference
18+
*/
19+
public toCodePipeline(x: StackOutputReference): string {
20+
return `#{${stackVariableNamespace(this.queries.producingStack(x))}.${x.outputName}}`;
21+
}
22+
}

packages/@aws-cdk/pipelines/lib/helpers-internal/pipeline-graph.ts

+4-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AssetType, FileSet, ShellStep, StackAsset, StackDeployment, StageDeployment, Step, Wave } from '../blueprint';
1+
import { AssetType, FileSet, StackAsset, StackDeployment, StageDeployment, Step, Wave } from '../blueprint';
22
import { PipelineBase } from '../main/pipeline-base';
33
import { DependencyBuilders, Graph, GraphNode, GraphNodeCollection } from './graph';
44
import { PipelineQueries } from './pipeline-queries';
@@ -274,11 +274,9 @@ export class PipelineGraph {
274274

275275
// Add stack dependencies (by use of the dependency builder this also works
276276
// if we encounter the Step before the Stack has been properly added yet)
277-
if (step instanceof ShellStep) {
278-
for (const output of Object.values(step.envFromCfnOutputs)) {
279-
const stack = this.queries.producingStack(output);
280-
this.stackOutputDependencies.get(stack).dependBy(node);
281-
}
277+
for (const output of step.consumedStackOutputs) {
278+
const stack = this.queries.producingStack(output);
279+
this.stackOutputDependencies.get(stack).dependBy(node);
282280
}
283281

284282
return node;

packages/@aws-cdk/pipelines/lib/helpers-internal/pipeline-queries.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Step, ShellStep, StackOutputReference, StackDeployment, StackAsset, StageDeployment } from '../blueprint';
1+
import { Step, StackOutputReference, StackDeployment, StackAsset, StageDeployment } from '../blueprint';
22
import { PipelineBase } from '../main/pipeline-base';
33

44
/**
@@ -25,9 +25,7 @@ export class PipelineQueries {
2525

2626
const ret = new Array<string>();
2727
for (const step of steps) {
28-
if (!(step instanceof ShellStep)) { continue; }
29-
30-
for (const outputRef of Object.values(step.envFromCfnOutputs)) {
28+
for (const outputRef of step.consumedStackOutputs) {
3129
if (outputRef.isProducedBy(stack)) {
3230
ret.push(outputRef.outputName);
3331
}

packages/@aws-cdk/pipelines/rosetta/default.ts-fixture

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import dynamodb = require('@aws-cdk/aws-dynamodb');
1010
import ecr = require('@aws-cdk/aws-ecr');
1111
import ec2 = require('@aws-cdk/aws-ec2');
1212
import iam = require('@aws-cdk/aws-iam');
13+
import lambda = require('@aws-cdk/lambda');
1314
import pipelines = require('@aws-cdk/pipelines');
1415
import secretsmanager = require('@aws-cdk/aws-secretsmanager');
1516
import sns = require('@aws-cdk/aws-sns');

packages/@aws-cdk/pipelines/test/codepipeline/codebuild-step.test.ts

+17
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as iam from '@aws-cdk/aws-iam';
44
import * as s3 from '@aws-cdk/aws-s3';
55
import { Duration, Stack } from '@aws-cdk/core';
66
import * as cdkp from '../../lib';
7+
import { StackOutputReference } from '../../lib';
78
import { PIPELINE_ENV, TestApp, ModernTestGitHubNpmPipeline, AppWithOutput } from '../testhelpers';
89

910
let app: TestApp;
@@ -296,4 +297,20 @@ test('step has caching set', () => {
296297
},
297298
},
298299
});
300+
});
301+
302+
test('step exposes consumed stack output reference', () => {
303+
// WHEN
304+
const myApp = new AppWithOutput(app, 'AppWithOutput', {
305+
stackId: 'Stack',
306+
});
307+
const step = new cdkp.ShellStep('AStep', {
308+
commands: ['/bin/true'],
309+
envFromCfnOutputs: {
310+
THE_OUTPUT: myApp.theOutput,
311+
},
312+
});
313+
314+
// THEN
315+
expect(step.consumedStackOutputs).toContainEqual(StackOutputReference.fromCfnOutput(myApp.theOutput));
299316
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"version": "22.0.0",
3+
"files": {
4+
"21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": {
5+
"source": {
6+
"path": "PipelineWithCustomStepStackOutputTestDefaultTestDeployAssert6C17E8C5.template.json",
7+
"packaging": "file"
8+
},
9+
"destinations": {
10+
"current_account-current_region": {
11+
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
12+
"objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json",
13+
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
14+
}
15+
}
16+
}
17+
},
18+
"dockerImages": {}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"Parameters": {
3+
"BootstrapVersion": {
4+
"Type": "AWS::SSM::Parameter::Value<String>",
5+
"Default": "/cdk-bootstrap/hnb659fds/version",
6+
"Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"
7+
}
8+
},
9+
"Rules": {
10+
"CheckBootstrapVersion": {
11+
"Assertions": [
12+
{
13+
"Assert": {
14+
"Fn::Not": [
15+
{
16+
"Fn::Contains": [
17+
[
18+
"1",
19+
"2",
20+
"3",
21+
"4",
22+
"5"
23+
],
24+
{
25+
"Ref": "BootstrapVersion"
26+
}
27+
]
28+
}
29+
]
30+
},
31+
"AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."
32+
}
33+
]
34+
}
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"version": "22.0.0",
3+
"files": {
4+
"1ceb4a1b5ab571218f34d0b4a62df8830a54d1ea4f95e4f115b3b4202b5fef3d": {
5+
"source": {
6+
"path": "StackOutputPipelineStack.template.json",
7+
"packaging": "file"
8+
},
9+
"destinations": {
10+
"current_account-current_region": {
11+
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
12+
"objectKey": "1ceb4a1b5ab571218f34d0b4a62df8830a54d1ea4f95e4f115b3b4202b5fef3d.json",
13+
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
14+
}
15+
}
16+
}
17+
},
18+
"dockerImages": {}
19+
}

0 commit comments

Comments
 (0)