Skip to content

Commit 44b2bf7

Browse files
committed
feat(codepipeline-actions): support environment variables for action
1 parent 452a5e1 commit 44b2bf7

File tree

6 files changed

+287
-0
lines changed

6 files changed

+287
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import * as codepipeline from 'aws-cdk-lib/aws-codepipeline';
2+
import * as s3 from 'aws-cdk-lib/aws-s3';
3+
import * as cdk from 'aws-cdk-lib';
4+
import { Duration } from 'aws-cdk-lib';
5+
import { IntegTest, ExpectedResult, Match } from '@aws-cdk/integ-tests-alpha';
6+
import * as cpactions from 'aws-cdk-lib/aws-codepipeline-actions';
7+
import * as path from 'path';
8+
import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment';
9+
import { Secret } from 'aws-cdk-lib/aws-secretsmanager';
10+
11+
const app = new cdk.App({
12+
postCliContext: {
13+
'@aws-cdk/aws-lambda:useCdkManagedLogGroup': false,
14+
'@aws-cdk/pipelines:reduceStageRoleTrustScope': false,
15+
},
16+
});
17+
18+
const stack = new cdk.Stack(app, 'aws-cdk-codepipeline-environment-variables');
19+
20+
const bucket = new s3.Bucket(stack, 'SourceBucket', {
21+
removalPolicy: cdk.RemovalPolicy.DESTROY,
22+
autoDeleteObjects: true,
23+
versioned: true,
24+
});
25+
26+
// To start the pipeline
27+
const bucketDeployment = new BucketDeployment(stack, 'BucketDeployment', {
28+
sources: [Source.asset(path.join(__dirname, 'assets', 'nodejs.zip'))],
29+
destinationBucket: bucket,
30+
extract: false,
31+
});
32+
const zipKey = cdk.Fn.select(0, bucketDeployment.objectKeys);
33+
34+
const sourceOutput = new codepipeline.Artifact('SourceArtifact');
35+
const sourceAction = new cpactions.S3SourceAction({
36+
actionName: 'Source',
37+
output: sourceOutput,
38+
bucket,
39+
bucketKey: zipKey,
40+
});
41+
42+
const commandsOutput = new codepipeline.Artifact('CommandsArtifact', ['my-dir/**/*']);
43+
44+
const secret = new Secret(stack, 'Secret', {
45+
secretStringValue: cdk.SecretValue.unsafePlainText('This is the content stored in secrets manager'),
46+
});
47+
const secretEnvVar = codepipeline.EnvironmentVariable.fromSecretsManager({
48+
name: 'MY_SECRET',
49+
secret: secret,
50+
});
51+
52+
const plaintextValue = 'my-plaintext';
53+
const plaintextEnvVar = codepipeline.EnvironmentVariable.fromPlaintext({
54+
name: 'MY_PLAINTEXT',
55+
value: plaintextValue,
56+
});
57+
58+
const commandsAction = new cpactions.CommandsAction({
59+
actionName: 'Commands',
60+
commands: [
61+
`echo "MY_SECRET:$${secretEnvVar.name}"`,
62+
`echo "MY_PLAINTEXT:$${plaintextEnvVar.name}"`,
63+
'mkdir -p my-dir',
64+
'echo "HelloWorld" > my-dir/file.txt',
65+
],
66+
input: sourceOutput,
67+
output: commandsOutput,
68+
outputVariables: [plaintextEnvVar.name],
69+
actionEnvironmentVariables: [
70+
secretEnvVar,
71+
plaintextEnvVar,
72+
],
73+
});
74+
75+
const deployBucket = new s3.Bucket(stack, 'DeployBucket', {
76+
removalPolicy: cdk.RemovalPolicy.DESTROY,
77+
autoDeleteObjects: true,
78+
});
79+
const deployAction = new cpactions.S3DeployAction({
80+
actionName: 'DeployAction',
81+
extract: true,
82+
input: commandsOutput,
83+
bucket: deployBucket,
84+
objectKey: commandsAction.variable(plaintextEnvVar.name),
85+
});
86+
87+
const pipelineBucket = new s3.Bucket(stack, 'PipelineBucket', {
88+
removalPolicy: cdk.RemovalPolicy.DESTROY,
89+
autoDeleteObjects: true,
90+
});
91+
const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', {
92+
artifactBucket: pipelineBucket,
93+
stages: [
94+
{
95+
stageName: 'Source',
96+
actions: [sourceAction],
97+
},
98+
{
99+
stageName: 'Compute',
100+
actions: [commandsAction],
101+
},
102+
{
103+
stageName: 'Deploy',
104+
actions: [deployAction],
105+
},
106+
],
107+
});
108+
109+
const integ = new IntegTest(app, 'aws-cdk-codepipeline-environment-variables-test', {
110+
testCases: [stack],
111+
diffAssets: true,
112+
});
113+
114+
const startPipelineCall = integ.assertions.awsApiCall('CodePipeline', 'startPipelineExecution', {
115+
name: pipeline.pipelineName,
116+
});
117+
118+
const getObjectCall = integ.assertions.awsApiCall('S3', 'getObject', {
119+
Bucket: deployBucket.bucketName,
120+
Key: `${plaintextValue}/my-dir/file.txt`,
121+
});
122+
123+
startPipelineCall.next(
124+
integ.assertions.awsApiCall('CodePipeline', 'getPipelineState', {
125+
name: pipeline.pipelineName,
126+
}).expect(ExpectedResult.objectLike({
127+
stageStates: Match.arrayWith([
128+
Match.objectLike({
129+
stageName: 'Deploy',
130+
latestExecution: Match.objectLike({
131+
status: 'Succeeded',
132+
}),
133+
}),
134+
]),
135+
})).waitForAssertions({
136+
totalTimeout: Duration.minutes(5),
137+
}).next(getObjectCall),
138+
);

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as events from '../../aws-events';
55
import * as iam from '../../aws-iam';
66
import * as s3 from '../../aws-s3';
77
import { Duration, IResource, Lazy, UnscopedValidationError } from '../../core';
8+
import { EnvironmentVariable, SecretsManagerEnvironmentVariable } from './environment-variable';
89

910
export enum ActionCategory {
1011
SOURCE = 'Source',
@@ -132,6 +133,13 @@ export interface ActionProperties {
132133
* @see https://docs.aws.amazon.com/codepipeline/latest/userguide/limits.html
133134
*/
134135
readonly timeout?: Duration;
136+
137+
/**
138+
* The environment variables for the action.
139+
*
140+
* @default - no environment variables
141+
*/
142+
readonly actionEnvironmentVariables?: EnvironmentVariable[];
135143
}
136144

137145
export interface ActionBindOptions {
@@ -351,6 +359,13 @@ export interface CommonActionProps {
351359
* no namespace will be set
352360
*/
353361
readonly variablesNamespace?: string;
362+
363+
/**
364+
* The environment variables for the action.
365+
*
366+
* @default - no environment variables
367+
*/
368+
readonly actionEnvironmentVariables?: EnvironmentVariable[];
354369
}
355370

356371
/**
@@ -434,6 +449,20 @@ export abstract class Action implements IAction {
434449
? `${stage.stageName}_${this.actionProperties.actionName}_NS`
435450
: this._customerProvidedNamespace;
436451

452+
const envVars = this.actionProperties.actionEnvironmentVariables;
453+
envVars?.forEach(envVar => {
454+
if (envVar instanceof SecretsManagerEnvironmentVariable) {
455+
if (this.actionProperties.actionName !== 'Commands') {
456+
throw new UnscopedValidationError(`Secrets Manager environment variables are only supported for the Commands action, got: ${this.actionProperties.actionName}`);
457+
}
458+
envVar.secret.grantRead(options.role);
459+
}
460+
});
461+
462+
if (envVars && envVars.length > 10) {
463+
throw new UnscopedValidationError(`The length of \`environmentVariables\` in action '${this.actionProperties.actionName}' must be less than or equal to 10, got: ${envVars.length}`);
464+
}
465+
437466
return this.bound(scope, stage, options);
438467
}
439468

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { CfnPipeline } from './codepipeline.generated';
2+
import { ISecret } from '../../aws-secretsmanager';
3+
4+
/**
5+
* Properties for an environment variable.
6+
*/
7+
interface CommonEnvironmentVariableProps {
8+
/**
9+
* The environment variable name in the key-value pair.
10+
*/
11+
readonly name: string;
12+
}
13+
14+
/**
15+
* Properties for a plaintext environment variable.
16+
*/
17+
export interface PlaintextEnvironmentVariableProps extends CommonEnvironmentVariableProps {
18+
/**
19+
* The environment variable value in the key-value pair.
20+
*/
21+
readonly value: string;
22+
}
23+
24+
/**
25+
* Properties for a Secrets Manager environment variable.
26+
*/
27+
export interface SecretsManagerEnvironmentVariableProps extends CommonEnvironmentVariableProps {
28+
/**
29+
* The Secrets Manager secret reference.
30+
*/
31+
readonly secret: ISecret;
32+
}
33+
34+
/**
35+
* An environment variable.
36+
*/
37+
export abstract class EnvironmentVariable {
38+
/**
39+
* Create a new plaintext environment variable.
40+
*/
41+
public static fromPlaintext(props: PlaintextEnvironmentVariableProps): PlaintextEnvironmentVariable {
42+
return new PlaintextEnvironmentVariable(props);
43+
}
44+
45+
/**
46+
* Create a new Secrets Manager environment variable.
47+
*/
48+
public static fromSecretsManager(props: SecretsManagerEnvironmentVariableProps): SecretsManagerEnvironmentVariable {
49+
return new SecretsManagerEnvironmentVariable(props);
50+
}
51+
52+
public readonly name: string;
53+
54+
/**
55+
* Create a new environment variable.
56+
*/
57+
protected constructor(props: CommonEnvironmentVariableProps) {
58+
this.name = props.name;
59+
}
60+
61+
/**
62+
* Render the environment variable to a CloudFormation resource.
63+
* @internal
64+
*/
65+
public _render(): CfnPipeline.EnvironmentVariableProperty {
66+
return {
67+
name: this.name,
68+
value: this.value,
69+
type: this.type,
70+
};
71+
}
72+
73+
protected abstract get value(): string;
74+
protected abstract get type(): string;
75+
}
76+
77+
/**
78+
* A plaintext environment variable.
79+
*/
80+
export class PlaintextEnvironmentVariable extends EnvironmentVariable {
81+
private readonly _value: string;
82+
83+
constructor(props: PlaintextEnvironmentVariableProps) {
84+
super(props);
85+
this._value = props.value;
86+
}
87+
88+
protected get value(): string {
89+
return this._value;
90+
}
91+
92+
protected get type(): string {
93+
return 'PLAINTEXT';
94+
}
95+
}
96+
97+
/**
98+
* A Secrets Manager environment variable.
99+
*/
100+
export class SecretsManagerEnvironmentVariable extends EnvironmentVariable {
101+
public readonly secret: ISecret;
102+
103+
constructor(props: SecretsManagerEnvironmentVariableProps) {
104+
super(props);
105+
this.secret = props.secret;
106+
}
107+
108+
protected get value(): string {
109+
return this.secret.secretName;
110+
}
111+
112+
protected get type(): string {
113+
return 'SECRETS_MANAGER';
114+
}
115+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './action';
22
export * from './artifact';
3+
export * from './environment-variable';
34
export * from './pipeline';
45
export * from './trigger';
56
export * from './variable';

packages/aws-cdk-lib/aws-codepipeline/lib/private/full-action-descriptor.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as iam from '../../../aws-iam';
22
import { Duration } from '../../../core';
33
import { ActionArtifactBounds, ActionCategory, ActionConfig, IAction } from '../action';
4+
import { EnvironmentVariable } from '../environment-variable';
45
import { Artifact } from '../artifact';
56

67
export interface FullActionDescriptorProps {
@@ -31,6 +32,7 @@ export class FullActionDescriptor {
3132
public readonly commands?: string[];
3233
public readonly outputVariables?: string[];
3334
public readonly timeout?: Duration;
35+
public readonly environmentVariables?: EnvironmentVariable[];
3436

3537
constructor(props: FullActionDescriptorProps) {
3638
this.action = props.action;
@@ -52,6 +54,7 @@ export class FullActionDescriptor {
5254
this.commands = actionProperties.commands;
5355
this.outputVariables = actionProperties.outputVariables;
5456
this.timeout = actionProperties.timeout;
57+
this.environmentVariables = actionProperties.actionEnvironmentVariables;
5558
}
5659
}
5760

packages/aws-cdk-lib/aws-codepipeline/lib/private/stage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ export class Stage implements IStage {
194194
roleArn: action.role ? action.role.roleArn : undefined,
195195
region: action.region,
196196
namespace: action.namespace,
197+
environmentVariables: action.environmentVariables?.length ? action.environmentVariables.map(envVar => envVar._render()) : undefined,
197198
};
198199
}
199200

0 commit comments

Comments
 (0)