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(aws-codebuild): build script from asset #677

Merged
merged 6 commits into from
Sep 7, 2018
Merged
Show file tree
Hide file tree
Changes from 3 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
174 changes: 152 additions & 22 deletions packages/@aws-cdk/aws-codebuild/lib/project.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import assets = require('@aws-cdk/assets');
import cloudwatch = require('@aws-cdk/aws-cloudwatch');
import codepipeline = require('@aws-cdk/aws-codepipeline-api');
import events = require('@aws-cdk/aws-events');
Expand All @@ -8,9 +9,11 @@ import cdk = require('@aws-cdk/cdk');
import { BuildArtifacts, CodePipelineBuildArtifacts, NoBuildArtifacts } from './artifacts';
import { cloudformation, ProjectArn, ProjectName } from './codebuild.generated';
import { CommonPipelineBuildActionProps, PipelineBuildAction } from './pipeline-actions';
import { BuildSource } from './source';
import { BuildSource, NoSource } from './source';

const CODEPIPELINE_TYPE = 'CODEPIPELINE';
const S3_BUCKET_ENV = 'SCRIPT_S3_BUCKET';
const S3_KEY_ENV = 'SCRIPT_S3_KEY';

/**
* Properties of a reference to a CodeBuild Project.
Expand Down Expand Up @@ -322,6 +325,26 @@ export interface CommonProjectProps {
*/
buildSpec?: any;

/**
* Run a script from an asset as build script
*
* If supplied together with buildSpec, the asset script will be run
* _after_ the existing commands in buildspec.
*
* This feature can also be used without a source, to simply run an
* arbitrary script in a serverless way.
*
* @default No asset build script
*/
buildScriptAsset?: assets.Asset;
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess we should have abstract FileAsset and DirectoryAsset, and this should be a DirectoryAsset, correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that would be a good way to ensure that.


/**
* The script in the asset to run.
*
* @default build.sh
*/
buildScriptAssetEntrypoint?: string;

/**
* Service Role to assume while running the build.
* If not specified, a role will be created.
Expand Down Expand Up @@ -378,8 +401,10 @@ export interface CommonProjectProps {
export interface ProjectProps extends CommonProjectProps {
/**
* The source of the build.
*
* @default NoSource
*/
source: BuildSource;
source?: BuildSource;

/**
* Defines where build artifacts will be stored.
Expand Down Expand Up @@ -409,15 +434,20 @@ export class Project extends ProjectRef {
*/
public readonly projectName: ProjectName;

private readonly source: BuildSource;
private readonly buildImage: IBuildImage;

constructor(parent: cdk.Construct, name: string, props: ProjectProps) {
super(parent, name);

if (props.buildScriptAssetEntrypoint && !props.buildScriptAsset) {
throw new Error('To use buildScriptAssetEntrypoint, supply buildScriptAsset as well.');
}

this.role = props.role || new iam.Role(this, 'Role', {
assumedBy: new cdk.ServicePrincipal('codebuild.amazonaws.com')
});

const environment = this.renderEnvironment(props.environment, props.environmentVariables);

let cache: cloudformation.ProjectResource.ProjectCacheProperty | undefined;
if (props.cacheBucket) {
const cacheDir = props.cacheDir != null ? props.cacheDir : '';
Expand All @@ -429,29 +459,45 @@ export class Project extends ProjectRef {
props.cacheBucket.grantReadWrite(this.role);
}

this.buildImage = (props.environment && props.environment.buildImage) || LinuxBuildImage.UBUNTU_14_04_BASE;

// let source "bind" to the project. this usually involves granting permissions
// for the code build role to interact with the source.
const source = props.source;
source.bind(this);
this.source = props.source || new NoSource();
this.source.bind(this);

const artifacts = this.parseArtifacts(props);
artifacts.bind(this);

const sourceJson = source.toSourceJSON();
if (typeof props.buildSpec === 'string') {
sourceJson.buildSpec = props.buildSpec;
} else {
sourceJson.buildSpec = JSON.stringify(props.buildSpec);
// Inject download commands for asset if requested
const environmentVariables = props.environmentVariables || {};
const buildSpec = props.buildSpec || {};

if (props.buildScriptAsset) {
environmentVariables[S3_BUCKET_ENV] = { value: props.buildScriptAsset.s3BucketName };
environmentVariables[S3_KEY_ENV] = { value: props.buildScriptAsset.s3ObjectKey };
extendBuildSpec(buildSpec, this.buildImage.runScriptBuildspec(props.buildScriptAssetEntrypoint || 'build.sh'));
props.buildScriptAsset.grantRead(this.role);
}

this.validateCodePipelineSettings(source, artifacts);
// Render the source and add in the buildspec
const sourceJson = this.source.toSourceJSON();
if (typeof buildSpec === 'string') {
sourceJson.buildSpec = buildSpec; // Filename to buildspec file
} else if (Object.keys(buildSpec).length > 0) {
// We have to pretty-print the buildspec, otherwise
// CodeBuild will not recognize it as an inline buildspec.
sourceJson.buildSpec = JSON.stringify(buildSpec, undefined, 2); // Literal buildspec
}

this.validateCodePipelineSettings(artifacts);

const resource = new cloudformation.ProjectResource(this, 'Resource', {
description: props.description,
source: sourceJson,
artifacts: artifacts.toArtifactsJSON(),
serviceRole: this.role.roleArn,
environment,
environment: this.renderEnvironment(props.environment, environmentVariables),
encryptionKey: props.encryptionKey && props.encryptionKey.keyArn,
badgeEnabled: props.badge,
cache,
Expand Down Expand Up @@ -513,17 +559,16 @@ export class Project extends ProjectRef {

const hasEnvironmentVars = Object.keys(vars).length > 0;

const buildImage = env.buildImage || LinuxBuildImage.UBUNTU_14_04_BASE;
const errors = buildImage.validate(env);
const errors = this.buildImage.validate(env);
if (errors.length > 0) {
throw new Error("Invalid CodeBuild environment: " + errors.join('\n'));
}

return {
type: buildImage.type,
image: buildImage.imageId,
type: this.buildImage.type,
image: this.buildImage.imageId,
privilegedMode: env.priviledged || false,
computeType: env.computeType || buildImage.defaultComputeType,
computeType: env.computeType || this.buildImage.defaultComputeType,
environmentVariables: !hasEnvironmentVars ? undefined : Object.keys(vars).map(name => ({
name,
type: vars[name].type || BuildEnvironmentVariableType.PlainText,
Expand All @@ -536,15 +581,15 @@ export class Project extends ProjectRef {
if (props.artifacts) {
return props.artifacts;
}
if (props.source.toSourceJSON().type === CODEPIPELINE_TYPE) {
if (this.source.toSourceJSON().type === CODEPIPELINE_TYPE) {
return new CodePipelineBuildArtifacts();
} else {
return new NoBuildArtifacts();
}
}

private validateCodePipelineSettings(source: BuildSource, artifacts: BuildArtifacts) {
const sourceType = source.toSourceJSON().type;
private validateCodePipelineSettings(artifacts: BuildArtifacts) {
const sourceType = this.source.toSourceJSON().type;
const artifactsType = artifacts.toArtifactsJSON().type;

if ((sourceType === CODEPIPELINE_TYPE || artifactsType === CODEPIPELINE_TYPE) &&
Expand Down Expand Up @@ -627,6 +672,11 @@ export interface IBuildImage {
* @param buildEnvironment the current build environment
*/
validate(buildEnvironment: BuildEnvironment): string[];

/**
* Make a buildspec to run the indicated script
*/
runScriptBuildspec(entrypoint: string): any;
}

/**
Expand Down Expand Up @@ -671,6 +721,34 @@ export class LinuxBuildImage implements IBuildImage {
public validate(_: BuildEnvironment): string[] {
return [];
}

public runScriptBuildspec(entrypoint: string): any {
return {
version: '0.2',
phases: {
pre_build: {
commands: [
// Better echo the location here; if this fails, the error message only contains
// the unexpanded variables by default. It might fail if you're running an old
// definition of the CodeBuild project--the permissions will have been changed
// to only allow downloading the very latest version.
`echo "Downloading scripts from s3://\${${S3_BUCKET_ENV}}/\${${S3_KEY_ENV}}"`,
`aws s3 cp s3://\${${S3_BUCKET_ENV}}/\${${S3_KEY_ENV}} /tmp`,
`mkdir -p /tmp/scriptdir`,
`unzip /tmp/$(basename \$${S3_KEY_ENV}) -d /tmp/scriptdir`,
]
},
build: {
commands: [
'export SCRIPT_DIR=/tmp/scriptdir',
`echo "Running ${entrypoint}"`,
`chmod +x /tmp/scriptdir/${entrypoint}`,
`/tmp/scriptdir/${entrypoint}`,
]
}
}
};
}
}

/**
Expand All @@ -697,6 +775,31 @@ export class WindowsBuildImage implements IBuildImage {
}
return ret;
}

public runScriptBuildspec(entrypoint: string): any {
return {
version: '0.2',
phases: {
pre_build: {
// Would love to do downloading here and executing in the next step,
// but I don't know how to propagate the value of $TEMPDIR.
//
// Punting for someone who knows PowerShell well enough.
commands: []
},
build: {
commands: [
`Set-Variable -Name TEMPDIR -Value (New-TemporaryFile).DirectoryName`,
`aws s3 cp s3://$env:${S3_BUCKET_ENV}/$env:${S3_KEY_ENV} $TEMPDIR\\scripts.zip`,
'New-Item -ItemType Directory -Path $TEMPDIR\\scriptdir',
'Expand-Archive -Path $TEMPDIR/scripts.zip -DestinationPath $TEMPDIR\\scriptdir',
'$env:SCRIPT_DIR = "$TEMPDIR\\scriptdir"',
`& $TEMPDIR\\scriptdir\\${entrypoint}`
]
}
}
};
}
}

export interface BuildEnvironmentVariable {
Expand All @@ -723,4 +826,31 @@ export enum BuildEnvironmentVariableType {
* An environment variable stored in Systems Manager Parameter Store.
*/
ParameterStore = 'PARAMETER_STORE'
}
}

/**
* Extend buildSpec phases with the contents of another one
*/
function extendBuildSpec(buildSpec: any, extend: any) {
if (typeof buildSpec === 'string') {
throw new Error('Cannot extend buildspec that is given as a string. Pass the buildspec as a structure instead.');
}
if (buildSpec.version === '0.1') {
throw new Error('Cannot extend buildspec at version "0.1". Set the version to "0.2" or higher instead.');
}
if (buildSpec.version === undefined) {
buildSpec.version = extend.version;
}

if (!buildSpec.phases) {
buildSpec.phases = {};
}

for (const phaseName of Object.keys(extend.phases)) {
if (!(phaseName in buildSpec.phases)) { buildSpec.phases[phaseName] = {}; }
const phase = buildSpec.phases[phaseName];

if (!(phase.commands)) { phase.commands = []; }
phase.commands.push(...extend.phases[phaseName].commands);
}
}
15 changes: 14 additions & 1 deletion packages/@aws-cdk/aws-codebuild/lib/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ export abstract class BuildSource {
public abstract toSourceJSON(): cloudformation.ProjectResource.SourceProperty;
}

export class NoSource extends BuildSource {
constructor() {
super();
}

public toSourceJSON(): cloudformation.ProjectResource.SourceProperty {
return {
type: SourceType.None,
};
}
}

/**
* CodeCommit Source definition for a CodeBuild project
*/
Expand Down Expand Up @@ -135,10 +147,11 @@ export class S3BucketSource extends BuildSource {
* Source types for CodeBuild Project
*/
export enum SourceType {
None = 'NO_SOURCE',
CodeCommit = 'CODECOMMIT',
CodePipeline = 'CODEPIPELINE',
GitHub = 'GITHUB',
GitHubEnterPrise = 'GITHUB_ENTERPRISE',
BitBucket = 'BITBUCKET',
S3 = 'S3'
S3 = 'S3',
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-codebuild/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"pkglint": "^0.8.2"
},
"dependencies": {
"@aws-cdk/assets": "^0.8.2",
"@aws-cdk/aws-cloudwatch": "^0.8.2",
"@aws-cdk/aws-codecommit": "^0.8.2",
"@aws-cdk/aws-codepipeline-api": "^0.8.2",
Expand Down
Loading