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 42 commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
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 { Key } from 'aws-cdk-lib/aws-kms';
import * as path from 'path';
import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment';

const app = new cdk.App();

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

const sourceBucketKey = new Key(stack, 'SourceBucketKey', {
description: 'SourceBucketKey',
});
const bucket = new s3.Bucket(stack, 'SourceBucket', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
versioned: true,
encryptionKey: sourceBucketKey,
});

// To start the pipeline
const bucketDeployment = new BucketDeployment(stack, 'BucketDeployment', {
sources: [Source.asset(path.join(__dirname, 'assets', 'nodejs.zip'))],
destinationBucket: bucket,
extract: false,
});
const zipKey = cdk.Fn.select(0, bucketDeployment.objectKeys);

const sourceOutput = new codepipeline.Artifact('SourceArtifact');
const sourceAction = new cpactions.S3SourceAction({
actionName: 'Source',
output: sourceOutput,
bucket,
bucketKey: zipKey,
});

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 deployAction = new cpactions.S3DeployAction({
actionName: 'DeployAction',
extract: true,
input: commandsOutput,
bucket: deployBucket,
objectKey: commandsAction.variable('MY_OUTPUT'),
});

const pipelineBucketKey = new Key(stack, 'PipelineBucketKey', {
description: 'PipelineBucketKey',
});
const pipelineBucket = new s3.Bucket(stack, 'PipelineBucket', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
encryptionKey: pipelineBucketKey,
});
const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', {
artifactBucket: pipelineBucket,
stages: [
{
stageName: 'Source',
actions: [sourceAction],
},
{
stageName: 'Compute',
actions: [commandsAction],
},
{
stageName: 'Deploy',
actions: [deployAction],
},
],
});

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

const startPipelineCall = integ.assertions.awsApiCall('CodePipeline', 'startPipelineExecution', {
name: pipeline.pipelineName,
});

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

startPipelineCall.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),
);
101 changes: 101 additions & 0 deletions packages/aws-cdk-lib/aws-codepipeline-actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1257,3 +1257,104 @@ pipeline.addStage({

See [the AWS documentation](https://docs.aws.amazon.com/codepipeline/latest/userguide/action-reference-StepFunctions.html)
for information on Action structure reference.

## Compute

### Commands

The Commands action allows you to run shell commands in a virtual compute instance. When you run the action, commands
specified in the action configuration are run in a separate container. All artifacts that are specified as input
artifacts to a CodeBuild action are available inside of the container running the commands. This action allows you
to specify commands without first creating a CodeBuild project.

```ts
// Source action
const bucket = new s3.Bucket(this, 'SourceBucket', {
versioned: true,
});
const sourceArtifact = new codepipeline.Artifact('SourceArtifact');
const sourceAction = new codepipeline_actions.S3SourceAction({
actionName: 'Source',
output: sourceArtifact,
bucket,
bucketKey: 'my.zip',
});

// Commands action
const outputArtifact = new codepipeline.Artifact('OutputArtifact');
const commandsAction = new codepipeline_actions.CommandsAction({
actionName: 'Commands',
commands: [
'echo "some commands"',
],
input: sourceArtifact,
output: outputArtifact,
});

const pipeline = new codepipeline.Pipeline(this, 'Pipeline', {
stages: [
{
stageName: 'Source',
actions: [sourceAction],
},
{
stageName: 'Commands',
actions: [commandsAction],
},
],
});
```

If you want to filter the files to be included in the output artifact, you can specify their paths as the second
argument to `Artifact`.

```ts
declare const sourceArtifact: codepipeline.Artifact;

// filter files to be included in the output artifact
const outputArtifact = new codepipeline.Artifact('OutputArtifact', ['my-dir/**/*']);
const commandsAction = new codepipeline_actions.CommandsAction({
actionName: 'Commands',
commands: [
'mkdir -p my-dir',
'echo "HelloWorld" > my-dir/file.txt',
],
input: sourceArtifact,
output: outputArtifact,
});
```

You can also specify the `outputVariables` property in the `CommandsAction` to emit environment variables that can be used
in subsequent actions. The variables are those defined in your shell commands or exported as defaults by the CodeBuild service.
For a reference of CodeBuild environment variables, see
[Environment variables in build environments](https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-env-vars.html)
in the CodeBuild User Guide.

To use the output variables in a subsequent action, you can use the `variable` method on the action:

```ts
declare const sourceArtifact: codepipeline.Artifact;
declare const outputArtifact: codepipeline.Artifact;

const commandsAction = new codepipeline_actions.CommandsAction({
actionName: 'Commands',
commands: [
'export MY_OUTPUT=my-key',
],
input: sourceArtifact,
output: outputArtifact,
outputVariables: ['MY_OUTPUT', 'CODEBUILD_BUILD_ID'], // CODEBUILD_BUILD_ID is a variable defined by CodeBuild
});

// Deploy action
const deployAction = new codepipeline_actions.S3DeployAction({
actionName: 'DeployAction',
extract: true,
input: outputArtifact,
bucket: new s3.Bucket(this, 'DeployBucket'),
objectKey: commandsAction.variable('MY_OUTPUT'), // the variable emitted by the Commands action
});
```

See [the AWS documentation](https://docs.aws.amazon.com/codepipeline/latest/userguide/action-reference-Commands.html)
for more details about using Commands action in CodePipeline.
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
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 output artifact for this action.
*
* You can filter files that you want to export as the output artifact for the action.
*
* @example
* new codepipeline.Artifact('CommandsArtifact', ['my-dir/**']);
*
* @default - no output artifact
*/
readonly output?: codepipeline.Artifact;

/**
* The names of the variables in your environment that you want to export.
*
* These variables can be referenced in other actions by using the `variable` method
* of this class.
*
* @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.
*
* The length of the commands array must be between 1 and 50.
*/
readonly commands: string[];
}

/**
* CodePipeline compute action that uses AWS Commands.
*/
export class CommandsAction extends Action {
private readonly outputVariables: string[];
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,
});

if (props.commands.length < 1 || props.commands.length > 50) {
throw new Error(`The length of the commands array must be between 1 and 50, got: ${props.commands.length}`);
}

if (props.outputVariables !== undefined && (props.outputVariables.length < 1 || props.outputVariables.length > 15)) {
throw new Error(`The length of the outputVariables array must be between 1 and 15, got: ${props.outputVariables.length}`);
}

this.outputVariables = props.outputVariables || [];
}

/**
* Reference a CodePipeline variable exported in the Commands action.
*
* @param variableName the name of the variable exported by `outputVariables`
* @see https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-env-vars.html
*/
public variable(variableName: string): string {
if (!this.outputVariables.includes(variableName)) {
throw new Error(`Variable '${variableName}' is not exported by \`outputVariables\`, exported variables: ${this.outputVariables.join(', ')}`);
}
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,
});
const logGroupArnWithWildcard = `${logGroupArn}:*`;

// grant the Pipeline role the required permissions 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
options.role.addToPrincipalPolicy(new iam.PolicyStatement({
resources: [logGroupArn, logGroupArnWithWildcard],
actions: [
'logs:CreateLogGroup',
'logs:CreateLogStream',
'logs:PutLogEvents',
],
}));

// grant the Pipeline role the required permissions 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
options.role.addToPrincipalPolicy(new iam.PolicyStatement({
resources: [logGroupArnWithWildcard],
actions: [
'logs:GetLogEvents',
],
}));

// allow the Role access to the Bucket, if there are any inputs/outputs
if ((this.actionProperties.inputs ?? []).length > 0) {
options.bucket.grantRead(options.role);
}
if ((this.actionProperties.outputs ?? []).length > 0) {
options.bucket.grantWrite(options.role);
}

// `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
Loading