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

Merge #34

Merged
merged 14 commits into from
Dec 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-codebuild/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ const gitHubSource = codebuild.Source.gitHub({
owner: 'awslabs',
repo: 'aws-cdk',
webhook: true, // optional, default: true if `webhookFilters` were provided, false otherwise
webhookTriggersBatchBuild: true, // optional, default is false
webhookFilters: [
codebuild.FilterGroup
.inEventOf(codebuild.EventAction.PUSH)
Expand Down
61 changes: 50 additions & 11 deletions packages/@aws-cdk/aws-codebuild/lib/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as iam from '@aws-cdk/aws-iam';
import * as kms from '@aws-cdk/aws-kms';
import * as s3 from '@aws-cdk/aws-s3';
import * as secretsmanager from '@aws-cdk/aws-secretsmanager';
import { Aws, Duration, IResource, Lazy, Names, PhysicalName, Resource, Stack } from '@aws-cdk/core';
import { Aws, Duration, IResource, Lazy, Names, PhysicalName, Resource, SecretValue, Stack, Tokenization } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { IArtifacts } from './artifacts';
import { BuildSpec } from './build-spec';
Expand Down Expand Up @@ -465,6 +465,17 @@ export interface CommonProjectProps {
*/
readonly environmentVariables?: { [name: string]: BuildEnvironmentVariable };

/**
* Whether to check for the presence of any secrets in the environment variables of the default type, BuildEnvironmentVariableType.PLAINTEXT.
* Since using a secret for the value of that kind of variable would result in it being displayed in plain text in the AWS Console,
* the construct will throw an exception if it detects a secret was passed there.
* Pass this property as false if you want to skip this validation,
* and keep using a secret in a plain text environment variable.
*
* @default true
*/
readonly checkSecretsInPlainTextEnvVariables?: boolean;

/**
* The physical, human-readable name of the CodeBuild Project.
*
Expand Down Expand Up @@ -659,15 +670,39 @@ export class Project extends ProjectBase {
* which is the representation of environment variables in CloudFormation.
*
* @param environmentVariables the map of string to environment variables
* @param validateNoPlainTextSecrets whether to throw an exception
* if any of the plain text environment variables contain secrets, defaults to 'false'
* @returns an array of {@link CfnProject.EnvironmentVariableProperty} instances
*/
public static serializeEnvVariables(environmentVariables: { [name: string]: BuildEnvironmentVariable }):
CfnProject.EnvironmentVariableProperty[] {
return Object.keys(environmentVariables).map(name => ({
name,
type: environmentVariables[name].type || BuildEnvironmentVariableType.PLAINTEXT,
value: environmentVariables[name].value,
}));
public static serializeEnvVariables(environmentVariables: { [name: string]: BuildEnvironmentVariable },
validateNoPlainTextSecrets: boolean = false): CfnProject.EnvironmentVariableProperty[] {

const ret = new Array<CfnProject.EnvironmentVariableProperty>();

for (const [name, envVariable] of Object.entries(environmentVariables)) {
const cfnEnvVariable: CfnProject.EnvironmentVariableProperty = {
name,
type: envVariable.type || BuildEnvironmentVariableType.PLAINTEXT,
value: envVariable.value?.toString(),
};
ret.push(cfnEnvVariable);

// validate that the plain-text environment variables don't contain any secrets in them
if (validateNoPlainTextSecrets && cfnEnvVariable.type === BuildEnvironmentVariableType.PLAINTEXT) {
const fragments = Tokenization.reverseString(cfnEnvVariable.value);
for (const token of fragments.tokens) {
if (token instanceof SecretValue) {
throw new Error(`Plaintext environment variable '${name}' contains a secret value! ` +
'This means the value of this variable will be visible in plain text in the AWS Console. ' +
"Please consider using CodeBuild's SecretsManager environment variables feature instead. " +
"If you'd like to continue with having this secret in the plaintext environment variables, " +
'please set the checkSecretsInPlainTextEnvVariables property to false');
}
}
}
}

return ret;
}

public readonly grantPrincipal: iam.IPrincipal;
Expand Down Expand Up @@ -761,7 +796,7 @@ export class Project extends ProjectBase {
},
artifacts: artifactsConfig.artifactsProperty,
serviceRole: this.role.roleArn,
environment: this.renderEnvironment(props.environment, environmentVariables),
environment: this.renderEnvironment(props, environmentVariables),
fileSystemLocations: Lazy.any({ produce: () => this.renderFileSystemLocations() }),
// lazy, because we have a setter for it in setEncryptionKey
// The 'alias/aws/s3' default is necessary because leaving the `encryptionKey` field
Expand Down Expand Up @@ -974,8 +1009,10 @@ export class Project extends ProjectBase {
}

private renderEnvironment(
env: BuildEnvironment = {},
props: ProjectProps,
projectVars: { [name: string]: BuildEnvironmentVariable } = {}): CfnProject.EnvironmentProperty {

const env = props.environment ?? {};
const vars: { [name: string]: BuildEnvironmentVariable } = {};
const containerVars = env.environmentVariables || {};

Expand Down Expand Up @@ -1030,7 +1067,9 @@ export class Project extends ProjectBase {
: undefined,
privilegedMode: env.privileged || false,
computeType: env.computeType || this.buildImage.defaultComputeType,
environmentVariables: hasEnvironmentVars ? Project.serializeEnvVariables(vars) : undefined,
environmentVariables: hasEnvironmentVars
? Project.serializeEnvVariables(vars, props.checkSecretsInPlainTextEnvVariables ?? true)
: undefined,
};
}

Expand Down
18 changes: 18 additions & 0 deletions packages/@aws-cdk/aws-codebuild/lib/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,13 @@ interface ThirdPartyGitSourceProps extends GitSourceProps {
*/
readonly webhook?: boolean;

/**
* Trigger a batch build from a webhook instead of a standard one.
*
* @default false
*/
readonly webhookTriggersBatchBuild?: boolean;

/**
* A list of webhook filters that can constraint what events in the repository will trigger a build.
* A build is triggered if any of the provided filter groups match.
Expand All @@ -500,19 +507,29 @@ abstract class ThirdPartyGitSource extends GitSource {
protected readonly webhookFilters: FilterGroup[];
private readonly reportBuildStatus: boolean;
private readonly webhook?: boolean;
private readonly webhookTriggersBatchBuild?: boolean;

protected constructor(props: ThirdPartyGitSourceProps) {
super(props);

this.webhook = props.webhook;
this.reportBuildStatus = props.reportBuildStatus === undefined ? true : props.reportBuildStatus;
this.webhookFilters = props.webhookFilters || [];
this.webhookTriggersBatchBuild = props.webhookTriggersBatchBuild;
}

public bind(_scope: CoreConstruct, _project: IProject): SourceConfig {
const anyFilterGroupsProvided = this.webhookFilters.length > 0;
const webhook = this.webhook === undefined ? (anyFilterGroupsProvided ? true : undefined) : this.webhook;

if (!webhook && anyFilterGroupsProvided) {
throw new Error('`webhookFilters` cannot be used when `webhook` is `false`');
}

if (!webhook && this.webhookTriggersBatchBuild) {
throw new Error('`webhookTriggersBatchBuild` cannot be used when `webhook` is `false`');
}

const superConfig = super.bind(_scope, _project);
return {
sourceProperty: {
Expand All @@ -522,6 +539,7 @@ abstract class ThirdPartyGitSource extends GitSource {
sourceVersion: superConfig.sourceVersion,
buildTriggers: webhook === undefined ? undefined : {
webhook,
buildType: this.webhookTriggersBatchBuild ? 'BUILD_BATCH' : undefined,
filterGroups: anyFilterGroupsProvided ? this.webhookFilters.map(fg => fg._toJson()) : undefined,
},
};
Expand Down
61 changes: 61 additions & 0 deletions packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,48 @@ export = {

test.done();
},

'with webhookTriggersBatchBuild option'(test: Test) {
const stack = new cdk.Stack();

new codebuild.Project(stack, 'Project', {
source: codebuild.Source.gitHub({
owner: 'testowner',
repo: 'testrepo',
webhook: true,
webhookTriggersBatchBuild: true,
}),
});

expect(stack).to(haveResourceLike('AWS::CodeBuild::Project', {
Triggers: {
Webhook: true,
BuildType: 'BUILD_BATCH',
},
}));

test.done();
},

'fail creating a Project when webhook false and webhookTriggersBatchBuild option'(test: Test) {
[false, undefined].forEach((webhook) => {
const stack = new cdk.Stack();

test.throws(() => {
new codebuild.Project(stack, 'Project', {
source: codebuild.Source.gitHub({
owner: 'testowner',
repo: 'testrepo',
webhook,
webhookTriggersBatchBuild: true,
}),
});
}, /`webhookTriggersBatchBuild` cannot be used when `webhook` is `false`/);
});

test.done();
},

'fail creating a Project when no build spec is given'(test: Test) {
const stack = new cdk.Stack();

Expand Down Expand Up @@ -1670,6 +1712,25 @@ export = {
test.done();
},

'cannot be used when webhook is false'(test: Test) {
const stack = new cdk.Stack();

test.throws(() => {
new codebuild.Project(stack, 'Project', {
source: codebuild.Source.bitBucket({
owner: 'owner',
repo: 'repo',
webhook: false,
webhookFilters: [
codebuild.FilterGroup.inEventOf(codebuild.EventAction.PUSH),
],
}),
});
}, /`webhookFilters` cannot be used when `webhook` is `false`/);

test.done();
},

'can have FILE_PATH filters if the Group contains PUSH and PR_CREATED events'(test: Test) {
codebuild.FilterGroup.inEventOf(
codebuild.EventAction.PULL_REQUEST_CREATED,
Expand Down
35 changes: 35 additions & 0 deletions packages/@aws-cdk/aws-codebuild/test/test.project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -924,5 +924,40 @@ export = {

test.done();
},

'should fail creating when using a secret value in a plaintext variable'(test: Test) {
// GIVEN
const stack = new cdk.Stack();

// THEN
test.throws(() => {
new codebuild.PipelineProject(stack, 'Project', {
environmentVariables: {
'a': {
value: `a_${cdk.SecretValue.secretsManager('my-secret')}_b`,
},
},
});
}, /Plaintext environment variable 'a' contains a secret value!/);

test.done();
},

"should allow opting out of the 'secret value in a plaintext variable' validation"(test: Test) {
// GIVEN
const stack = new cdk.Stack();

// THEN
new codebuild.PipelineProject(stack, 'Project', {
environmentVariables: {
'b': {
value: cdk.SecretValue.secretsManager('my-secret'),
},
},
checkSecretsInPlainTextEnvVariables: false,
});

test.done();
},
},
};
64 changes: 64 additions & 0 deletions packages/@aws-cdk/aws-codepipeline-actions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,70 @@ const deployStage = pipeline.addStage({
});
```

#### Invalidating the CloudFront cache when deploying to S3

There is currently no native support in CodePipeline for invalidating a CloudFront cache after deployment.
One workaround is to add another build step after the deploy step,
and use the AWS CLI to invalidate the cache:

```ts
// Create a Cloudfront Web Distribution
const distribution = new cloudfront.Distribution(this, `Distribution`, {
// ...
});

// Create the build project that will invalidate the cache
const invalidateBuildProject = new codebuild.PipelineProject(this, `InvalidateProject`, {
buildSpec: codebuild.BuildSpec.fromObject({
version: '0.2',
phases: {
build: {
commands:[
'aws cloudfront create-invalidation --distribution-id ${CLOUDFRONT_ID} --paths "/*"',
// Choose whatever files or paths you'd like, or all files as specified here
],
},
},
}),
environmentVariables: {
CLOUDFRONT_ID: { value: distribution.distributionId },
},
});

// Add Cloudfront invalidation permissions to the project
const distributionArn = `arn:aws:cloudfront::${this.account}:distribution/${distribution.distributionId}`;
invalidateBuildProject.addToRolePolicy(new iam.PolicyStatement({
resources: [distributionArn],
actions: [
'cloudfront:CreateInvalidation',
],
}));

// Create the pipeline (here only the S3 deploy and Invalidate cache build)
new codepipeline.Pipeline(this, 'Pipeline', {
stages: [
// ...
{
stageName: 'Deploy',
actions: [
new codepipelineActions.S3DeployAction({
actionName: 'S3Deploy',
bucket: deployBucket,
input: deployInput,
runOrder: 1,
}),
new codepipelineActions.CodeBuildAction({
actionName: 'InvalidateCache',
project: invalidateBuildProject,
input: deployInput,
runOrder: 2,
}),
],
},
],
});
```

### Alexa Skill

You can deploy to Alexa using CodePipeline with the following Action:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ export interface CodeBuildActionProps extends codepipeline.CommonAwsActionProps
*/
readonly environmentVariables?: { [name: string]: codebuild.BuildEnvironmentVariable };

/**
* Whether to check for the presence of any secrets in the environment variables of the default type, BuildEnvironmentVariableType.PLAINTEXT.
* Since using a secret for the value of that kind of variable would result in it being displayed in plain text in the AWS Console,
* the construct will throw an exception if it detects a secret was passed there.
* Pass this property as false if you want to skip this validation,
* and keep using a secret in a plain text environment variable.
*
* @default true
*/
readonly checkSecretsInPlainTextEnvVariables?: boolean;

/**
* Trigger a batch build.
*
Expand Down Expand Up @@ -139,8 +150,8 @@ export class CodeBuildAction extends Action {
resources: [this.props.project.projectArn],
actions: [
'codebuild:BatchGetBuilds',
'codebuild:StartBuild',
'codebuild:StopBuild',
`codebuild:${this.props.executeBatchBuild ? 'StartBuildBatch' : 'StartBuild'}`,
`codebuild:${this.props.executeBatchBuild ? 'StopBuildBatch' : 'StopBuild'}`,
],
}));

Expand Down Expand Up @@ -177,7 +188,8 @@ export class CodeBuildAction extends Action {
const configuration: any = {
ProjectName: this.props.project.projectName,
EnvironmentVariables: this.props.environmentVariables &&
cdk.Stack.of(scope).toJsonString(codebuild.Project.serializeEnvVariables(this.props.environmentVariables)),
cdk.Stack.of(scope).toJsonString(codebuild.Project.serializeEnvVariables(this.props.environmentVariables,
this.props.checkSecretsInPlainTextEnvVariables ?? true)),
};
if ((this.actionProperties.inputs || []).length > 1) {
// lazy, because the Artifact name might be generated lazily
Expand Down
Loading