Skip to content

Commit

Permalink
refactor(codebuild): introduce BuildSpec object (#2820)
Browse files Browse the repository at this point in the history
Minimal representation of BuildSpec, formalizing some logic
that was already in `Project` but not really well-placed there.

It has very minimal support for merging buildspecs (only commands,
not artifacts or anything else).

Internal representation is hidden though, and can be improved and
extended later.

BREAKING CHANGE:

* **codebuild**: buildSpec argument is now a `BuildSpec` object.
  • Loading branch information
rix0rrr authored Jun 12, 2019
1 parent b088c8c commit 86a2192
Show file tree
Hide file tree
Showing 14 changed files with 188 additions and 82 deletions.
110 changes: 110 additions & 0 deletions packages/@aws-cdk/aws-codebuild/lib/build-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { IResolveContext, Lazy, Stack } from '@aws-cdk/cdk';

/**
* BuildSpec for CodeBuild projects
*/
export abstract class BuildSpec {
public static fromObject(value: {[key: string]: any}): BuildSpec {
return new ObjectBuildSpec(value);
}

/**
* Use a file from the source as buildspec
*
* Use this if you want to use a file different from 'buildspec.yml'`
*/
public static fromSourceFilename(filename: string): BuildSpec {
return new FilenameBuildSpec(filename);
}

/**
* Whether the buildspec is directly available or deferred until build-time
*/
public abstract readonly isImmediate: boolean;

protected constructor() {
}

/**
* Render the represented BuildSpec
*/
public abstract toBuildSpec(): string;
}

/**
* BuildSpec that just returns the input unchanged
*/
class FilenameBuildSpec extends BuildSpec {
public readonly isImmediate: boolean = false;

constructor(private readonly filename: string) {
super();
}

public toBuildSpec(): string {
return this.filename;
}

public toString() {
return `<buildspec file: ${this.filename}>`;
}
}

/**
* BuildSpec that understands about structure
*/
class ObjectBuildSpec extends BuildSpec {
public readonly isImmediate: boolean = true;

constructor(public readonly spec: {[key: string]: any}) {
super();
}

public toBuildSpec(): string {
// We have to pretty-print the buildspec, otherwise
// CodeBuild will not recognize it as an inline buildspec.
return Lazy.stringValue({ produce: (ctx: IResolveContext) =>
Stack.of(ctx.scope).toJsonString(this.spec, 2)
});
}
}

/**
* Merge two buildspecs into a new BuildSpec
*
* NOTE: will currently only merge commands, not artifact
* declarations, environment variables, secrets, or any
* other configuration elements.
*
* Internal for now because it's not complete/good enough
* to expose on the objects directly, but we need to it to
* keep feature-parity for Project.
*
* @internal
*/
export function mergeBuildSpecs(lhs: BuildSpec, rhs: BuildSpec): BuildSpec {
if (!(lhs instanceof ObjectBuildSpec) || !(rhs instanceof ObjectBuildSpec)) {
throw new Error('Can only merge buildspecs created using BuildSpec.fromObject()');
}

return new ObjectBuildSpec(copyCommands(lhs.spec, rhs.spec));
}

/**
* Extend buildSpec phases with the contents of another one
*/
function copyCommands(buildSpec: any, extend: any): any {
if (buildSpec.version === '0.1') {
throw new Error('Cannot extend buildspec at version "0.1". Set the version to "0.2" or higher instead.');
}

const ret = Object.assign({}, buildSpec); // Return a copy
ret.phases = Object.assign({}, ret.phases);

for (const phaseName of Object.keys(extend.phases)) {
const phase = ret.phases[phaseName] = Object.assign({}, ret.phases[phaseName]);
phase.commands = [...phase.commands || [], ...extend.phases[phaseName].commands];
}

return ret;
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-codebuild/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './project';
export * from './source';
export * from './artifacts';
export * from './cache';
export * from './build-spec';

// AWS::CodeBuild CloudFormation Resources:
export * from './codebuild.generated';
75 changes: 21 additions & 54 deletions packages/@aws-cdk/aws-codebuild/lib/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import iam = require('@aws-cdk/aws-iam');
import kms = require('@aws-cdk/aws-kms');
import { Aws, Construct, IResource, Lazy, PhysicalName, Resource, ResourceIdentifiers, Stack } from '@aws-cdk/cdk';
import { BuildArtifacts, CodePipelineBuildArtifacts, NoBuildArtifacts } from './artifacts';
import { BuildSpec, mergeBuildSpecs } from './build-spec';
import { Cache } from './cache';
import { CfnProject } from './codebuild.generated';
import { BuildSource, NoSource, SourceType } from './source';
Expand Down Expand Up @@ -371,7 +372,7 @@ export interface CommonProjectProps {
*
* @default - Empty buildspec.
*/
readonly buildSpec?: any;
readonly buildSpec?: BuildSpec;

/**
* Run a script from an asset as build script
Expand Down Expand Up @@ -647,12 +648,14 @@ export class Project extends ProjectBase {

// Inject download commands for asset if requested
const environmentVariables = props.environmentVariables || {};
const buildSpec = props.buildSpec || {};
let 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'));

const runScript = this.buildImage.runScriptBuildspec(props.buildScriptAssetEntrypoint || 'build.sh');
buildSpec = buildSpec ? mergeBuildSpecs(buildSpec, runScript) : runScript;
props.buildScriptAsset.grantRead(this.role);
}

Expand All @@ -662,24 +665,15 @@ export class Project extends ProjectBase {
throw new Error(`Badge is not supported for source type ${this.source.type}`);
}

const sourceJson = this.source._toSourceJSON();
if (typeof buildSpec === 'string') {
return {
...sourceJson,
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.
return {
...sourceJson,
buildSpec: JSON.stringify(buildSpec, undefined, 2)
};
} else if (this.source.type === SourceType.None) {
throw new Error("If the Project's source is NoSource, you need to provide a buildSpec");
} else {
return sourceJson;
if (this.source.type === SourceType.None && (buildSpec === undefined || !buildSpec.isImmediate)) {
throw new Error("If the Project's source is NoSource, you need to provide a concrete buildSpec");
}

const sourceJson = this.source._toSourceJSON();
return {
...sourceJson,
buildSpec: buildSpec && buildSpec.toBuildSpec()
};
};

this._secondarySources = [];
Expand Down Expand Up @@ -1025,7 +1019,7 @@ export interface IBuildImage {
/**
* Make a buildspec to run the indicated script
*/
runScriptBuildspec(entrypoint: string): any;
runScriptBuildspec(entrypoint: string): BuildSpec;
}

/**
Expand Down Expand Up @@ -1124,8 +1118,8 @@ export class LinuxBuildImage implements IBuildImage {
return [];
}

public runScriptBuildspec(entrypoint: string): any {
return {
public runScriptBuildspec(entrypoint: string): BuildSpec {
return BuildSpec.fromObject({
version: '0.2',
phases: {
pre_build: {
Expand All @@ -1149,7 +1143,7 @@ export class LinuxBuildImage implements IBuildImage {
]
}
}
};
});
}
}

Expand Down Expand Up @@ -1220,8 +1214,8 @@ export class WindowsBuildImage implements IBuildImage {
return ret;
}

public runScriptBuildspec(entrypoint: string): any {
return {
public runScriptBuildspec(entrypoint: string): BuildSpec {
return BuildSpec.fromObject({
version: '0.2',
phases: {
pre_build: {
Expand All @@ -1242,7 +1236,7 @@ export class WindowsBuildImage implements IBuildImage {
]
}
}
};
});
}
}

Expand Down Expand Up @@ -1272,33 +1266,6 @@ export enum BuildEnvironmentVariableType {
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);
}
}

function ecrAccessForCodeBuildService(): iam.PolicyStatement {
return new iam.PolicyStatement()
.describe('CodeBuild')
Expand Down
4 changes: 2 additions & 2 deletions packages/@aws-cdk/aws-codebuild/test/integ.caching.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ const bucket = new s3.Bucket(stack, 'CacheBucket', {

new codebuild.Project(stack, 'MyProject', {
cache: Cache.bucket(bucket),
buildSpec: {
buildSpec: codebuild.BuildSpec.fromObject({
build: {
commands: ['echo Hello']
},
cache: {
paths: ['/root/.cache/pip/**/*']
}
}
})
});

app.synth();
4 changes: 2 additions & 2 deletions packages/@aws-cdk/aws-codebuild/test/integ.defaults.lit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class TestStack extends cdk.Stack {

/// !show
new codebuild.Project(this, 'MyProject', {
buildSpec: {
buildSpec: codebuild.BuildSpec.fromObject({
version: '0.2',
phases: {
build: {
Expand All @@ -16,7 +16,7 @@ class TestStack extends cdk.Stack {
]
}
}
}
})
});
/// !hide
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ class TestStack extends cdk.Stack {
super(scope, id);

new codebuild.Project(this, 'MyProject', {
buildSpec: {
buildSpec: codebuild.BuildSpec.fromObject({
version: "0.2",
phases: {
build: {
commands: [ 'ls' ]
}
}
},
}),
/// !show
environment: {
buildImage: codebuild.LinuxBuildImage.fromAsset(this, 'MyImage', {
Expand Down
4 changes: 2 additions & 2 deletions packages/@aws-cdk/aws-codebuild/test/integ.ecr.lit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ class TestStack extends cdk.Stack {
const ecrRepository = new ecr.Repository(this, 'MyRepo');

new codebuild.Project(this, 'MyProject', {
buildSpec: {
buildSpec: codebuild.BuildSpec.fromObject({
version: "0.2",
phases: {
build: {
commands: [ 'ls' ]
}
}
},
}),
/// !show
environment: {
buildImage: codebuild.LinuxBuildImage.fromEcrRepository(ecrRepository, "v1.0")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ const bucket = new s3.Bucket(stack, 'MyBucket', {
});

new codebuild.Project(stack, 'MyProject', {
buildSpec: {
buildSpec: codebuild.BuildSpec.fromObject({
version: '0.2',
},
}),
secondarySources: [
new codebuild.S3BucketSource({
bucket,
Expand Down
12 changes: 6 additions & 6 deletions packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -762,9 +762,9 @@ export = {
const stack = new cdk.Stack();
const bucket = new s3.Bucket(stack, 'Bucket');
new codebuild.Project(stack, 'Project', {
buildSpec: {
buildSpec: codebuild.BuildSpec.fromObject({
version: '0.2',
},
}),
artifacts: new codebuild.S3BucketBuildArtifacts({
path: 'some/path',
name: 'some_name',
Expand All @@ -791,9 +791,9 @@ export = {

test.throws(() => {
new codebuild.Project(stack, 'MyProject', {
buildSpec: {
buildSpec: codebuild.BuildSpec.fromObject({
version: '0.2',
},
}),
secondarySources: [
new codebuild.CodePipelineSource(),
],
Expand Down Expand Up @@ -857,9 +857,9 @@ export = {

test.throws(() => {
new codebuild.Project(stack, 'MyProject', {
buildSpec: {
buildSpec: codebuild.BuildSpec.fromObject({
version: '0.2',
},
}),
secondaryArtifacts: [
new codebuild.S3BucketBuildArtifacts({
bucket: new s3.Bucket(stack, 'MyBucket'),
Expand Down
Loading

0 comments on commit 86a2192

Please sign in to comment.