Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(codepipeline-actions): support commands action #31667

Merged
merged 45 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
668b675
feat(codepipeline-actions): support Commands action
go-to-k Oct 5, 2024
992ca86
typo
go-to-k Oct 5, 2024
f4a3ad8
typo
go-to-k Oct 5, 2024
fc37844
path for S3 in integ tests
go-to-k Oct 5, 2024
abfa49e
docs
go-to-k Oct 5, 2024
ad3b118
output comments
go-to-k Oct 5, 2024
b12ba83
doc for outputVariables
go-to-k Oct 5, 2024
f593204
doc for variable
go-to-k Oct 5, 2024
01d3c1b
Update commands-action.ts
go-to-k Oct 6, 2024
f810819
Update commands-action.ts
go-to-k Oct 6, 2024
11e5225
Update commands-action.ts
go-to-k Oct 6, 2024
16304a6
docs
go-to-k Oct 6, 2024
a68feff
validation for variable
go-to-k Oct 6, 2024
4064f47
Merge branch 'main' of https://github.com/go-to-k/aws-cdk into cp-act…
go-to-k Oct 18, 2024
5f095f5
wip integ
go-to-k Oct 18, 2024
1c319dc
iam
go-to-k Oct 18, 2024
d5e4f19
tweak
go-to-k Oct 19, 2024
5db973b
integ
go-to-k Oct 22, 2024
98770d0
versioned in integ
go-to-k Oct 25, 2024
21652ba
zip
go-to-k Oct 25, 2024
b13bbc0
snapshots
go-to-k Oct 25, 2024
6bda2e3
Merge branch 'main' into cp-action-commands
go-to-k Oct 25, 2024
09d4b73
@example
go-to-k Oct 25, 2024
deb343a
README
go-to-k Oct 25, 2024
f010735
length
go-to-k Oct 25, 2024
b1fb571
readme
go-to-k Oct 26, 2024
650a628
readme
go-to-k Oct 26, 2024
e8e4b17
fix readme
go-to-k Oct 26, 2024
991b400
fix
go-to-k Oct 26, 2024
47d61b1
readme
go-to-k Oct 26, 2024
06a8744
readme
go-to-k Oct 26, 2024
1f29d89
unit tests for commands action
go-to-k Oct 26, 2024
5d09ba9
comments
go-to-k Oct 26, 2024
0946882
artifact test
go-to-k Oct 26, 2024
8b256a8
artifact test
go-to-k Oct 26, 2024
f0a238d
tests
go-to-k Oct 26, 2024
8995a9f
Merge branch 'main' into cp-action-commands
go-to-k Feb 7, 2025
8b8c5d7
change logs policies
go-to-k Feb 7, 2025
c2e417e
change integ
go-to-k Feb 7, 2025
b8f5a8c
fix unit tests
go-to-k Feb 7, 2025
080b073
snapshots
go-to-k Feb 7, 2025
6b82e0a
rm snapshots
go-to-k Feb 7, 2025
bb58063
snapshots
go-to-k Feb 7, 2025
73875e5
upgrade cdk and update snapshots
go-to-k Feb 7, 2025
bfaa773
Merge branch 'main' into cp-action-commands
mergify[bot] Feb 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import * as codepipeline from 'aws-cdk-lib/aws-codepipeline';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as cdk from 'aws-cdk-lib';
import { Duration } from 'aws-cdk-lib';
import { IntegTest, ExpectedResult, Match } from '@aws-cdk/integ-tests-alpha';
import * as cpactions from 'aws-cdk-lib/aws-codepipeline-actions';
import { IKey, Key } from 'aws-cdk-lib/aws-kms';

const app = new cdk.App();

const stack = new cdk.Stack(app, 'aws-cdk-codepipeline-commands');

const key: IKey = new Key(stack, 'EnvVarEncryptKey', {
description: 'sample key',
});

const bucket = new s3.Bucket(stack, 'PipelineBucket', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
versioned: true,
encryptionKey: key,
});
const sourceOutput = new codepipeline.Artifact('SourceArtifact');
const sourceAction = new cpactions.S3SourceAction({
actionName: 'Source',
output: sourceOutput,
bucket,
bucketKey: 'test.zip',
});

const commandsOutput = new codepipeline.Artifact('CommandsArtifact', ['my-dir/**/*']);
const commandsAction = new cpactions.CommandsAction({
actionName: 'Commands',
commands: [
'pwd',
'ls -la',
'mkdir -p my-dir',
'echo "HelloWorld" > my-dir/file.txt',
'export MY_OUTPUT=my-key',
'touch ignored.txt',
],
input: sourceOutput,
output: commandsOutput,
outputVariables: ['MY_OUTPUT', 'CODEBUILD_BUILD_ID', 'AWS_DEFAULT_REGION'],
});

const deployBucket = new s3.Bucket(stack, 'DeployBucket', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});

const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', {
artifactBucket: bucket,
stages: [
{
stageName: 'Source',
actions: [sourceAction],
},
{
stageName: 'Compute',
actions: [commandsAction],
},
{
stageName: 'Deploy',
actions: [
new cpactions.S3DeployAction({
actionName: 'DeployAction',
extract: true,
input: commandsOutput,
bucket: deployBucket,
objectKey: commandsAction.variable('MY_OUTPUT'),
encryptionKey: key,
}),
],
},
],
});

const integ = new IntegTest(app, 'aws-cdk-codepipeline-commands-test', {
testCases: [stack],
diffAssets: true,
});

const putObjectCall = integ.assertions.awsApiCall('S3', 'putObject', {
Bucket: bucket.bucketName,
Key: 'test.zip',
});

const getObjectCall = integ.assertions.awsApiCall('S3', 'getObject', {
Bucket: deployBucket.bucketName,
Key: 'file.txt',
});

putObjectCall.next(
integ.assertions.awsApiCall('CodePipeline', 'getPipelineState', {
name: pipeline.pipelineName,
}).expect(ExpectedResult.objectLike({
stageStates: Match.arrayWith([
Match.objectLike({
stageName: 'Deploy',
latestExecution: Match.objectLike({
status: 'Succeeded',
}),
}),
]),
})).waitForAssertions({
totalTimeout: Duration.minutes(5),
}).next(getObjectCall),
);

app.synth();
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { Construct } from 'constructs';
import * as codepipeline from '../../../aws-codepipeline';
import * as iam from '../../../aws-iam';
import * as cdk from '../../../core';
import { Action } from '../action';

/**
* Construction properties of the `CommandsAction`.
*/
export interface CommandsActionProps extends codepipeline.CommonAwsActionProps {
/**
* The source to use as input for this action.
*/
readonly input: codepipeline.Artifact;

/**
* The list of additional input Artifacts for this action.
*
* @default - no extra inputs
*/
readonly extraInputs?: codepipeline.Artifact[];

/**
* The list of output Artifacts for this action.
*
* @default - the action will not have any outputs
*/
readonly output?: codepipeline.Artifact;

/**
* The names of the variables in your environment that you want to export.
*
* @see https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-env-vars.html
* @default - No output variables are exported
*/
readonly outputVariables?: string[];

/**
* Shell commands for the Commands action to run.
*
* All formats are supported except multi-line formats.
*/
readonly commands: string[];
}

/**
* CodePipeline compute action that uses AWS Commands.
*/
export class CommandsAction extends Action {
constructor(props: CommandsActionProps) {
super({
...props,
category: codepipeline.ActionCategory.COMPUTE,
provider: 'Commands',
artifactBounds: { minInputs: 1, maxInputs: 10, minOutputs: 0, maxOutputs: 1 },
inputs: [props.input, ...props.extraInputs || []],
outputs: props.output ? [props.output] : [],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since CFN L1 resource can take a list of items as outputs, I'm curious on the design decision on output being one item instead of a list of artifacts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The number of output artifacts can only be 0 to 1 on the Commands action.

Output artifacts
Number of artifacts: 0 to 1

https://docs.aws.amazon.com/codepipeline/latest/userguide/action-reference-Commands.html#action-reference-Commands-output

So I made it output as an optional param that can be 0 to 1.

Various actions including the commands action use the same CFn type, so it seems the CFn needs to get an array type.

commands: props.commands,
outputVariables: props.outputVariables,
});
}

/**
* Reference a CodePipeline variable exported in the Commands action.
*
* @param variableName the name of the variable to reference
* @see https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-env-vars.html
*/
public variable(variableName: string): string {
return this.variableExpression(variableName);
}

protected bound(scope: Construct, _stage: codepipeline.IStage, options: codepipeline.ActionBindOptions):
codepipeline.ActionConfig {
const logGroupArn = cdk.Stack.of(scope).formatArn({
service: 'logs',
resource: 'log-group',
resourceName: `/aws/codepipeline/${_stage.pipeline.pipelineName}:*`,
arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME,
});

// grant the Pipeline role the required permissions to the log group
options.role.addToPrincipalPolicy(new iam.PolicyStatement({
resources: [logGroupArn],
actions: [
// To put the logs in the log group
// see: https://docs.aws.amazon.com/codepipeline/latest/userguide/action-reference-Commands.html#action-reference-Commands-policy
'logs:CreateLogGroup',
'logs:CreateLogStream',
'logs:PutLogEvents',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// To view the logs in the Commands action on the CodePipeline console
// see: https://docs.aws.amazon.com/codepipeline/latest/userguide/security-iam-permissions-console-logs.html
'logs:GetLogEvents',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

],
}));

// `CommandsAction` does not need the `configuration` in the `ActionConfig`.
return {};
}
}
1 change: 1 addition & 0 deletions packages/aws-cdk-lib/aws-codepipeline-actions/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from './codebuild/build-action';
export * from './codecommit/source-action';
export * from './codedeploy/ecs-deploy-action';
export * from './codedeploy/server-deploy-action';
export * from './commands/commands-action';
export * from './ecr/source-action';
export * from './ecs/deploy-action';
export * from './elastic-beanstalk/deploy-action';
Expand Down
15 changes: 15 additions & 0 deletions packages/aws-cdk-lib/aws-codepipeline/lib/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export enum ActionCategory {
APPROVAL = 'Approval',
DEPLOY = 'Deploy',
INVOKE = 'Invoke',
COMPUTE = 'Compute',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

/**
Expand Down Expand Up @@ -104,6 +105,20 @@ export interface ActionProperties {
* @default - a name will be generated, based on the stage and action names
*/
readonly variablesNamespace?: string;

/**
* Shell commands for the Commands action to run.
*
* @default - no commands
*/
readonly commands?: string[];

/**
* The names of the variables in your environment that you want to export.
*
* @default - no output variables
*/
readonly outputVariables?: string[];
Comment on lines +108 to +121
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

export interface ActionBindOptions {
Expand Down
13 changes: 10 additions & 3 deletions packages/aws-cdk-lib/aws-codepipeline/lib/artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,31 @@ export class Artifact {
* Mainly meant to be used from `decdk`.
*
* @param name the (required) name of the Artifact
* @param files file paths that you want to export as output artifacts for the action. (can only be used in artifacts for `CommandAction`)
*/
public static artifact(name: string): Artifact {
return new Artifact(name);
public static artifact(name: string, files?: string[]): Artifact {
return new Artifact(name, files);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

private _artifactName?: string;
private _artifactFiles?: string[];
private readonly metadata: { [key: string]: any } = {};

constructor(artifactName?: string) {
constructor(artifactName?: string, artifactFiles?: string[]) {
validation.validateArtifactName(artifactName);

this._artifactName = artifactName;
this._artifactFiles = artifactFiles;
}

public get artifactName(): string | undefined {
return this._artifactName;
}

public get artifactFiles(): string[] | undefined {
return this._artifactFiles;
}

/**
* Returns an ArtifactPath for a file within this artifact.
* CfnOutput is in the form "<artifact-name>::<file-name>"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export class FullActionDescriptor {
public readonly region?: string;
public readonly role?: iam.IRole;
public readonly configuration: any;
public readonly commands?: string[];
public readonly outputVariables?: string[];

constructor(props: FullActionDescriptorProps) {
this.action = props.action;
Expand All @@ -45,6 +47,8 @@ export class FullActionDescriptor {
this.role = actionProperties.role ?? props.actionRole;

this.configuration = props.actionConfig.configuration;
this.commands = actionProperties.commands;
this.outputVariables = actionProperties.outputVariables;
}
}

Expand Down
6 changes: 4 additions & 2 deletions packages/aws-cdk-lib/aws-codepipeline/lib/private/stage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,17 +173,19 @@ export class Stage implements IStage {
provider: action.provider,
},
configuration: action.configuration,
commands: action.commands,
outputVariables: action.outputVariables,
runOrder: action.runOrder,
roleArn: action.role ? action.role.roleArn : undefined,
region: action.region,
namespace: action.namespace,
};
}

private renderArtifacts(artifacts: Artifact[]): CfnPipeline.InputArtifactProperty[] {
private renderArtifacts(artifacts: Artifact[]): CfnPipeline.OutputArtifactProperty[] {
return artifacts
.filter(a => a.artifactName)
.map(a => ({ name: a.artifactName! }));
.map(a => ({ name: a.artifactName!, files: a.artifactFiles }));
}
}

Expand Down
Loading