From bee0c027f070872cd2fbef59787a298cdd4e6c02 Mon Sep 17 00:00:00 2001 From: WolfAn Date: Sun, 6 Jun 2021 09:03:52 +0200 Subject: [PATCH] feat: added support for CodeArtifact in buildspec refactored into "features" --- API.md | 49 ++++++---- buildspec.yml | 8 +- src/BuildSpecPipeline.ts | 198 ++++++++++++++++++++++++++------------- src/integ.default.ts | 2 +- 4 files changed, 171 insertions(+), 86 deletions(-) diff --git a/API.md b/API.md index 3a7528f..7fac0b1 100644 --- a/API.md +++ b/API.md @@ -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* @@ -17,6 +18,33 @@ Name|Description +## class BuildProjectFeature + + + + +### Initializer + + + + +```ts +new BuildProjectFeature() +``` + + + + +### Properties + + +Name | Type | Description +-----|------|------------- +**policyStatements** | Array<[PolicyStatement](#aws-cdk-aws-iam-policystatement)> | +**preBuildCommands** | Array | + + + ## class BuildSpecPipeline @@ -40,7 +68,6 @@ new BuildSpecPipeline(scope: Construct, name: string, props?: BuildSpecPipelineP * **buildEnvironment** ([BuildEnvironment](#aws-cdk-aws-codebuild-buildenvironment)) *No description* __*Optional*__ * **buildSpec** (Map) *No description* __*Optional*__ * **buildSpecFile** (string) *No description* __*Optional*__ - * **codeArtifactDomain** (string) *No description* __*Optional*__ * **existingRepositoryObj** ([Repository](#aws-cdk-aws-codecommit-repository)) *No description* __*Optional*__ * **projectDescription** (string) *No description* __*Optional*__ * **projectName** (string) *No description* __*Optional*__ @@ -54,26 +81,13 @@ new BuildSpecPipeline(scope: Construct, name: string, props?: BuildSpecPipelineP Name | Type | Description -----|------|------------- +**buildSpec** | Map | **codebuildProject** | [PipelineProject](#aws-cdk-aws-codebuild-pipelineproject) | +**features** | Array<[BuildProjectFeature](#tts-cdk-build-pipelines-buildprojectfeature)> | **pipeline** | [Pipeline](#aws-cdk-aws-codepipeline-pipeline) | +**props** | [BuildSpecPipelineProps](#tts-cdk-build-pipelines-buildspecpipelineprops) | **repository** | [Repository](#aws-cdk-aws-codecommit-repository) | -### Methods - - -#### protected extendBuildSpec(buildSpec) - - - -```ts -protected extendBuildSpec(buildSpec: any): void -``` - -* **buildSpec** (any) *No description* - - - - ## class CustomExtensionPipeline @@ -143,7 +157,6 @@ Name | Type | Description **buildEnvironment**? | [BuildEnvironment](#aws-cdk-aws-codebuild-buildenvironment) | __*Optional*__ **buildSpec**? | Map | __*Optional*__ **buildSpecFile**? | string | __*Optional*__ -**codeArtifactDomain**? | string | __*Optional*__ **existingRepositoryObj**? | [Repository](#aws-cdk-aws-codecommit-repository) | __*Optional*__ **projectDescription**? | string | __*Optional*__ **projectName**? | string | __*Optional*__ diff --git a/buildspec.yml b/buildspec.yml index 9ed97ea..9f1d69f 100644 --- a/buildspec.yml +++ b/buildspec.yml @@ -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: diff --git a/src/BuildSpecPipeline.ts b/src/BuildSpecPipeline.ts index d9d399d..04a494c 100644 --- a/src/BuildSpecPipeline.ts +++ b/src/BuildSpecPipeline.ts @@ -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; /** * @summary Properties used for BuildSpecPipeline construct @@ -18,7 +19,6 @@ export interface BuildSpecPipelineProps { readonly projectName?: string; readonly projectDescription?: string; - readonly existingRepositoryObj?: Repository; readonly repositoryProps?: RepositoryProps; readonly retainRepository?: boolean; @@ -30,7 +30,7 @@ export interface BuildSpecPipelineProps { readonly buildSpec?: dict; readonly buildSpecFile?: string; - readonly codeArtifactDomain?: string; + // readonly codeArtifactDomain?: string; } const buildSpecPipelinePropsDefaults: BuildSpecPipelineProps = { @@ -38,6 +38,72 @@ const buildSpecPipelinePropsDefaults: BuildSpecPipelineProps = { branch: 'master', }; +export class BuildProjectFeature { + readonly policyStatements: Array = []; + readonly preBuildCommands: Array = []; +} + +interface CodeArtifactFeatureProps { + readonly domain: string; + readonly repos: Record; +} + +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 = 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 @@ -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 = []; + 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, @@ -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) { @@ -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; } } diff --git a/src/integ.default.ts b/src/integ.default.ts index ff311af..05929ea 100644 --- a/src/integ.default.ts +++ b/src/integ.default.ts @@ -6,5 +6,5 @@ const stack = new Stack(app, 'tts-cdk-pipelines-integration-test'); new BuildSpecPipeline(stack, 'BuildPipelineWithProjectName', { projectName: 'tts-cdk-pipelines', - codeArtifactDomain: 'tts', + // codeArtifactDomain: 'tts', });