Skip to content

Commit

Permalink
feat: added support for CodeArtifact in buildspec
Browse files Browse the repository at this point in the history
refactored into "features"
  • Loading branch information
WolfAn committed Jun 6, 2021
1 parent 138bd35 commit bee0c02
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 86 deletions.
49 changes: 31 additions & 18 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

Name|Description
----|-----------
[BuildProjectFeature](#tts-cdk-build-pipelines-buildprojectfeature)|*No description*
[BuildSpecPipeline](#tts-cdk-build-pipelines-buildspecpipeline)|*No description*
[CustomExtensionPipeline](#tts-cdk-build-pipelines-customextensionpipeline)|*No description*

Expand All @@ -17,6 +18,33 @@ Name|Description



## class BuildProjectFeature <a id="tts-cdk-build-pipelines-buildprojectfeature"></a>




### Initializer




```ts
new BuildProjectFeature()
```




### Properties


Name | Type | Description
-----|------|-------------
**policyStatements** | <code>Array<[PolicyStatement](#aws-cdk-aws-iam-policystatement)></code> | <span></span>
**preBuildCommands** | <code>Array<string></code> | <span></span>



## class BuildSpecPipeline <a id="tts-cdk-build-pipelines-buildspecpipeline"></a>


Expand All @@ -40,7 +68,6 @@ new BuildSpecPipeline(scope: Construct, name: string, props?: BuildSpecPipelineP
* **buildEnvironment** (<code>[BuildEnvironment](#aws-cdk-aws-codebuild-buildenvironment)</code>) *No description* __*Optional*__
* **buildSpec** (<code>Map<string, any></code>) *No description* __*Optional*__
* **buildSpecFile** (<code>string</code>) *No description* __*Optional*__
* **codeArtifactDomain** (<code>string</code>) *No description* __*Optional*__
* **existingRepositoryObj** (<code>[Repository](#aws-cdk-aws-codecommit-repository)</code>) *No description* __*Optional*__
* **projectDescription** (<code>string</code>) *No description* __*Optional*__
* **projectName** (<code>string</code>) *No description* __*Optional*__
Expand All @@ -54,26 +81,13 @@ new BuildSpecPipeline(scope: Construct, name: string, props?: BuildSpecPipelineP

Name | Type | Description
-----|------|-------------
**buildSpec** | <code>Map<string, any></code> | <span></span>
**codebuildProject** | <code>[PipelineProject](#aws-cdk-aws-codebuild-pipelineproject)</code> | <span></span>
**features** | <code>Array<[BuildProjectFeature](#tts-cdk-build-pipelines-buildprojectfeature)></code> | <span></span>
**pipeline** | <code>[Pipeline](#aws-cdk-aws-codepipeline-pipeline)</code> | <span></span>
**props** | <code>[BuildSpecPipelineProps](#tts-cdk-build-pipelines-buildspecpipelineprops)</code> | <span></span>
**repository** | <code>[Repository](#aws-cdk-aws-codecommit-repository)</code> | <span></span>

### Methods


#### protected extendBuildSpec(buildSpec) <a id="tts-cdk-build-pipelines-buildspecpipeline-extendbuildspec"></a>



```ts
protected extendBuildSpec(buildSpec: any): void
```

* **buildSpec** (<code>any</code>) *No description*






## class CustomExtensionPipeline <a id="tts-cdk-build-pipelines-customextensionpipeline"></a>
Expand Down Expand Up @@ -143,7 +157,6 @@ Name | Type | Description
**buildEnvironment**? | <code>[BuildEnvironment](#aws-cdk-aws-codebuild-buildenvironment)</code> | __*Optional*__
**buildSpec**? | <code>Map<string, any></code> | __*Optional*__
**buildSpecFile**? | <code>string</code> | __*Optional*__
**codeArtifactDomain**? | <code>string</code> | __*Optional*__
**existingRepositoryObj**? | <code>[Repository](#aws-cdk-aws-codecommit-repository)</code> | __*Optional*__
**projectDescription**? | <code>string</code> | __*Optional*__
**projectName**? | <code>string</code> | __*Optional*__
Expand Down
8 changes: 7 additions & 1 deletion buildspec.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
---
version: 0.2
env:
code-artifact:
domain: tts
repos:
pip: tts-pypi
phases:
install:
runtime-versions:
python: '3.8'
commands:
- npm install -g projen cdk jsii-release
- npm install -g yarn projen jsii-release
pre_build:
commands:
- yarn install
- projen
- projen bump
build:
Expand Down
198 changes: 132 additions & 66 deletions src/BuildSpecPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { Effect, PolicyStatement } from '@aws-cdk/aws-iam';
import { Construct, Duration, RemovalPolicy, Stack } from '@aws-cdk/core';
import * as YAML from 'yaml';

type dict = { [key: string]: any };
// type dict = { [key: string]: any };
type dict = Record<string, any>;

/**
* @summary Properties used for BuildSpecPipeline construct
Expand All @@ -18,7 +19,6 @@ export interface BuildSpecPipelineProps {
readonly projectName?: string;
readonly projectDescription?: string;


readonly existingRepositoryObj?: Repository;
readonly repositoryProps?: RepositoryProps;
readonly retainRepository?: boolean;
Expand All @@ -30,14 +30,80 @@ export interface BuildSpecPipelineProps {
readonly buildSpec?: dict;
readonly buildSpecFile?: string;

readonly codeArtifactDomain?: string;
// readonly codeArtifactDomain?: string;
}

const buildSpecPipelinePropsDefaults: BuildSpecPipelineProps = {
retainRepository: true,
branch: 'master',
};

export class BuildProjectFeature {
readonly policyStatements: Array<PolicyStatement> = [];
readonly preBuildCommands: Array<string> = [];
}

interface CodeArtifactFeatureProps {
readonly domain: string;
readonly repos: Record<string, string>;
}

class CodeArtifactFeature extends BuildProjectFeature {

constructor(pipeline: BuildSpecPipeline) {

super();

const params: CodeArtifactFeatureProps = pipeline.buildSpec.env?.['code-artifact'];

if (params?.domain && params?.repos) {
const region = Stack.of(pipeline).region;
const account = Stack.of(pipeline).account;

this.policyStatements.push(new PolicyStatement({
actions: ['codeartifact:*'],
resources: [
`arn:aws:codeartifact:${region}:${account}:package/${params.domain}/*`,
`arn:aws:codeartifact:${region}:${account}:repository/${params.domain}/*`,
`arn:aws:codeartifact:${region}:${account}:domain/${params.domain}`,
],
effect: Effect.ALLOW,
}));

this.policyStatements.push(new PolicyStatement({
actions: ['sts:GetServiceBearerToken'],
resources: ['*'],
effect: Effect.ALLOW,
}));

Object.keys(params.repos).forEach(key => {
this.preBuildCommands.push(`aws codeartifact login --tool ${key} --repository ${params.repos[key]} --domain ${params.domain} --domain-owner ${account}`);
});
}
}
}

class SSMParametersFeature extends BuildProjectFeature {

constructor(pipeline: BuildSpecPipeline) {

super();

const region = Stack.of(pipeline).region;
const account = Stack.of(pipeline).account;

const parameters: Array<string> = Object.values(pipeline.buildSpec.env?.['parameter-store'] ?? []);

if (parameters.length > 0) {
this.policyStatements.push(new PolicyStatement({
effect: Effect.ALLOW,
actions: ['ssm:GetParameters', 'ssm:GetParameter'],
resources: parameters.map((param: string) => `arn:aws:ssm:${region}:${account}:parameter${param}`),
}));
}
}
}

/**
* @summary Constructs a CodePipeline and reads build specs from '.buildspec' file.
* @param {Construct} scope - scope for all resources created by this construct
Expand All @@ -51,83 +117,65 @@ export class BuildSpecPipeline extends Construct {
public readonly pipeline: Pipeline;
public readonly codebuildProject: PipelineProject;

public readonly props: BuildSpecPipelineProps;
public readonly buildSpec: dict;

public readonly features: Array<BuildProjectFeature> = [];

constructor(scope: Construct, name: string, props?: BuildSpecPipelineProps) {
super(scope, name);

const p: BuildSpecPipelineProps = { ...buildSpecPipelinePropsDefaults, ...props };
this.repository = this.createOrUseRepository(p);
this.codebuildProject = this.createCodebuildProject(p);
this.pipeline = this.createPipeline(p);
}
this.props = { ...buildSpecPipelinePropsDefaults, ...props };
this.buildSpec = this.readBuildSpec();

// @ts-ignore
protected extendBuildSpec(buildSpec: any) {
// INTENTIONALLY EMPTY
this.features.push(
new CodeArtifactFeature(this),
new SSMParametersFeature(this),
);

this.repository = this.createOrUseRepository();
this.codebuildProject = this.createCodebuildProject();
this.pipeline = this.createPipeline();
}

private createOrUseRepository(props: BuildSpecPipelineProps): Repository {
private createOrUseRepository(): Repository {

if (props?.existingRepositoryObj && props?.repositoryProps) {
if (this.props?.existingRepositoryObj && this.props?.repositoryProps) {
throw new Error('Cannot specify both repository properties and an existing repository');
}

let repository: Repository;

if (props?.existingRepositoryObj) {
repository = props.existingRepositoryObj;
if (this.props?.existingRepositoryObj) {
repository = this.props.existingRepositoryObj;

} else if (props?.repositoryProps) {
repository = new Repository(this, 'Repository', props.repositoryProps);
repository.applyRemovalPolicy(props?.retainRepository ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY);
} else if (this.props?.repositoryProps) {
repository = new Repository(this, 'Repository', this.props.repositoryProps);
repository.applyRemovalPolicy(this.props?.retainRepository ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY);

} else {

repository = new Repository(this, 'Repository', {
repositoryName: this.getProjectName(props),
repositoryName: this.getProjectName(this.props),
});
repository.applyRemovalPolicy(props?.retainRepository ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY);
repository.applyRemovalPolicy(this.props?.retainRepository ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY);
}

return repository;
}

private createPipeline(p: BuildSpecPipelineProps) {
private createPipeline() {

const sourceOutput = new Artifact('source');
const sourceAction = new CodeCommitSourceAction({
actionName: 'SourceAction',
trigger: CodeCommitTrigger.EVENTS,
repository: this.repository,
branch: p.branch,
branch: this.props.branch,
output: sourceOutput,
codeBuildCloneOutput: true,
});

if (p.codeArtifactDomain) {
const codeArtifactPolicy = new PolicyStatement({
actions: ['codeartifact:*'],
resources: [
`arn:aws:codeartifact:${Stack.of(this).region}:${Stack.of(this).account}:package/${p.codeArtifactDomain}/*`,
`arn:aws:codeartifact:${Stack.of(this).region}:${Stack.of(this).account}:repository/${p.codeArtifactDomain}/*`,
`arn:aws:codeartifact:${Stack.of(this).region}:${Stack.of(this).account}:domain/${p.codeArtifactDomain}`,
],
effect: Effect.ALLOW,
});

const codeArtifactTokenPolicyStatement = new PolicyStatement({
actions: ['sts:GetServiceBearerToken'],
resources: ['*'],
effect: Effect.ALLOW,
});
this.codebuildProject.addToRolePolicy(codeArtifactPolicy);
this.codebuildProject.addToRolePolicy(codeArtifactTokenPolicyStatement);
}

this.codebuildProject.addToRolePolicy(new PolicyStatement({
actions: ['ssm:GetParameter', 'ssm:GetParameters'],
resources: ['*'],
effect: Effect.ALLOW,
}));

const codeBuildAction = new CodeBuildAction({
actionName: 'BuildAction',
input: sourceOutput,
Expand All @@ -145,25 +193,21 @@ export class BuildSpecPipeline extends Construct {
});
}

private getBuildSpec(props: BuildSpecPipelineProps) {
private readBuildSpec(): dict {

let buildSpec: dict;
let buildSpec: dict = {};

if (props?.buildSpec) {
buildSpec = props.buildSpec;
if (this.props?.buildSpec) {
buildSpec = this.props.buildSpec;

} else if (props?.buildSpecFile) {
buildSpec = this.readBuildSpecFromFile(props.buildSpecFile);
} else if (this.props?.buildSpecFile ) { // TODO file does not exist
buildSpec = this.readBuildSpecFromFile(this.props.buildSpecFile);

} else {
} else { // TODO file does not exist
buildSpec = this.readBuildSpecFromFile('buildspec.yml');
}

console.log(buildSpec);

this.extendBuildSpec(buildSpec);

return BuildSpec.fromObject(buildSpec);
return buildSpec;
}

private readBuildSpecFromFile(file: string) {
Expand All @@ -175,16 +219,38 @@ export class BuildSpecPipeline extends Construct {
return props.projectName ?? path.basename(process.cwd());
}

private createCodebuildProject(p: BuildSpecPipelineProps) {
return new PipelineProject(this, 'PipelineProject', {
projectName: `${this.getProjectName(p)}-Build`,
buildSpec: this.getBuildSpec(p),
environment: p.buildEnvironment ?? {
private createCodebuildProject() {

if (!this.buildSpec.phases.pre_build) {
Object.defineProperty(this.buildSpec.phases, 'pre_build', { value: {} });
}

if (!this.buildSpec.phases.pre_build.commands) {
Object.defineProperty(this.buildSpec.phases.pre_build, 'commands', { value: [] });
}

this.features.forEach(feature => {
this.buildSpec.phases.pre_build.commands = [
...feature.preBuildCommands,
...this.buildSpec.phases.pre_build.commands,
];
});

const buildProject = new PipelineProject(this, 'PipelineProject', {
projectName: `${this.getProjectName(this.props)}-Build`,
buildSpec: BuildSpec.fromObject(this.buildSpec),
environment: this.props.buildEnvironment ?? {
buildImage: LinuxBuildImage.STANDARD_5_0,
privileged: false,
},
description: `CodePipeline for ${p.projectName}`,
description: `CodePipeline for ${this.props.projectName}`,
timeout: Duration.hours(1),
});

this.features.forEach(feature => {
feature.policyStatements.forEach(statement => buildProject.addToRolePolicy(statement));
});

return buildProject;
}
}
Loading

0 comments on commit bee0c02

Please sign in to comment.