diff --git a/packages/@aws-cdk/aws-codebuild/README.md b/packages/@aws-cdk/aws-codebuild/README.md index e6d3a43ce3b61..d3c5e533221c5 100644 --- a/packages/@aws-cdk/aws-codebuild/README.md +++ b/packages/@aws-cdk/aws-codebuild/README.md @@ -371,11 +371,41 @@ For example: ```ts const vpc = new ec2.Vpc(this, 'MyVPC'); const project = new codebuild.Project(this, 'MyProject', { - vpc: vpc, - buildSpec: codebuild.BuildSpec.fromObject({ - // ... - }), + vpc: vpc, + buildSpec: codebuild.BuildSpec.fromObject({ + // ... + }), }); project.connections.allowTo(loadBalancer, ec2.Port.tcp(443)); ``` + +## Project File System Location EFS + +Add support for CodeBuild to build on AWS EFS file system mounts using +the new ProjectFileSystemLocation. +The `fileSystemLocations` property which accepts a list `ProjectFileSystemLocation` +as represented by the interface `IFileSystemLocations`. +The only supported file system type is `EFS`. + +For example: + +```ts +new codebuild.Project(stack, 'MyProject', { + buildSpec: codebuild.BuildSpec.fromObject({ + version: '0.2', + }), + fileSystemLocations: [ + codebuild.FileSystemLocation.efs({ + identifier: "myidentifier2", + location: "myclodation.mydnsroot.com:/loc", + mountPoint: "/media", + mountOptions: "opts" + }) + ] +}); +``` + +Here's a CodeBuild project with a simple example that creates a project mounted on AWS EFS: + +[Minimal Example](./test/integ.project-file-system-location.ts) \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codebuild/lib/file-location.ts b/packages/@aws-cdk/aws-codebuild/lib/file-location.ts new file mode 100644 index 0000000000000..d32e5fb15168f --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/lib/file-location.ts @@ -0,0 +1,85 @@ +import { Construct } from '@aws-cdk/core'; +import { CfnProject } from './codebuild.generated'; +import { IProject } from './project'; + +/** + * The type returned from {@link IFileSystemLocation#bind}. + */ +export interface FileSystemConfig { + /** + * File system location wrapper property. + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codebuild-project-projectfilesystemlocation.html + */ + readonly location: CfnProject.ProjectFileSystemLocationProperty; +} + +/** + * The interface of a CodeBuild FileSystemLocation. + * Implemented by {@link EfsFileSystemLocation}. + */ +export interface IFileSystemLocation { + /** + * Called by the project when a file system is added so it can perform + * binding operations on this file system location. + */ + bind(scope: Construct, project: IProject): FileSystemConfig; +} + +/** + * FileSystemLocation provider definition for a CodeBuild Project. + */ +export class FileSystemLocation { + /** + * EFS file system provider. + * @param props the EFS File System location property. + */ + public static efs(props: EfsFileSystemLocationProps): IFileSystemLocation { + return new EfsFileSystemLocation(props); + } +} + +/** + * EfsFileSystemLocation definition for a CodeBuild project. + */ +class EfsFileSystemLocation implements IFileSystemLocation { + constructor(private readonly props: EfsFileSystemLocationProps) {} + + public bind(_scope: Construct, _project: IProject): FileSystemConfig { + return { + location: { + identifier: this.props.identifier, + location: this.props.location, + mountOptions: this.props.mountOptions, + mountPoint: this.props.mountPoint, + type: 'EFS', + }, + }; + } +} + +/** + * Construction properties for {@link EfsFileSystemLocation}. + */ +export interface EfsFileSystemLocationProps { + /** + * The name used to access a file system created by Amazon EFS. + */ + readonly identifier: string; + + /** + * A string that specifies the location of the file system, like Amazon EFS. + * @example 'fs-abcd1234.efs.us-west-2.amazonaws.com:/my-efs-mount-directory'. + */ + readonly location: string; + + /** + * The mount options for a file system such as Amazon EFS. + * @default 'nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2'. + */ + readonly mountOptions?: string; + + /** + * The location in the container where you mount the file system. + */ + readonly mountPoint: string; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codebuild/lib/index.ts b/packages/@aws-cdk/aws-codebuild/lib/index.ts index 7b1685ca66e41..a1d569e5656d1 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/index.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/index.ts @@ -6,6 +6,7 @@ export * from './source-credentials'; export * from './artifacts'; export * from './cache'; export * from './build-spec'; +export * from './file-location'; // AWS::CodeBuild CloudFormation Resources: export * from './codebuild.generated'; diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index ccb1629bc8566..e0efb49a6ee0c 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -13,6 +13,7 @@ import { BuildSpec } from './build-spec'; import { Cache } from './cache'; import { CfnProject } from './codebuild.generated'; import { CodePipelineArtifacts } from './codepipeline-artifacts'; +import { IFileSystemLocation } from './file-location'; import { NoArtifacts } from './no-artifacts'; import { NoSource } from './no-source'; import { ISource } from './source'; @@ -508,6 +509,16 @@ export interface CommonProjectProps { * @default true */ readonly allowAllOutbound?: boolean; + + /** + * An ProjectFileSystemLocation objects for a CodeBuild build project. + * + * A ProjectFileSystemLocation object specifies the identifier, location, mountOptions, mountPoint, + * and type of a file system created using Amazon Elastic File System. + * + * @default - no file system locations + */ + readonly fileSystemLocations?: IFileSystemLocation[]; } export interface ProjectProps extends CommonProjectProps { @@ -657,6 +668,7 @@ export class Project extends ProjectBase { private readonly _secondarySourceVersions: CfnProject.ProjectSourceVersionProperty[]; private readonly _secondaryArtifacts: CfnProject.ArtifactsProperty[]; private _encryptionKey?: kms.IKey; + private readonly _fileSystemLocations: CfnProject.ProjectFileSystemLocationProperty[]; constructor(scope: Construct, id: string, props: ProjectProps) { super(scope, id, { @@ -700,6 +712,7 @@ export class Project extends ProjectBase { this._secondarySources = []; this._secondarySourceVersions = []; + this._fileSystemLocations = []; for (const secondarySource of props.secondarySources || []) { this.addSecondarySource(secondarySource); } @@ -711,6 +724,10 @@ export class Project extends ProjectBase { this.validateCodePipelineSettings(artifacts); + for (const fileSystemLocation of props.fileSystemLocations || []) { + this.addFileSystemLocation(fileSystemLocation); + } + const resource = new CfnProject(this, 'Resource', { description: props.description, source: { @@ -720,6 +737,7 @@ export class Project extends ProjectBase { artifacts: artifactsConfig.artifactsProperty, serviceRole: this.role.roleArn, environment: this.renderEnvironment(props.environment, environmentVariables), + fileSystemLocations: this.renderFileSystemLocations(), // lazy, because we have a setter for it in setEncryptionKey encryptionKey: Lazy.stringValue({ produce: () => this._encryptionKey && this._encryptionKey.keyArn }), badgeEnabled: props.badge, @@ -770,6 +788,16 @@ export class Project extends ProjectBase { } } + /** + * Adds a fileSystemLocation to the Project. + * + * @param fileSystemLocation the fileSystemLocation to add + */ + public addFileSystemLocation(fileSystemLocation: IFileSystemLocation): void { + const fileSystemConfig = fileSystemLocation.bind(this, this); + this._fileSystemLocations.push(fileSystemConfig.location); + } + /** * Adds a secondary artifact to the Project. * @@ -898,6 +926,12 @@ export class Project extends ProjectBase { }; } + private renderFileSystemLocations(): CfnProject.ProjectFileSystemLocationProperty[] | undefined { + return this._fileSystemLocations.length === 0 + ? undefined + : this._fileSystemLocations; + } + private renderSecondarySources(): CfnProject.SourceProperty[] | undefined { return this._secondarySources.length === 0 ? undefined diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.project-file-system-location.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.project-file-system-location.expected.json new file mode 100644 index 0000000000000..5e52b1ef3dedd --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/test/integ.project-file-system-location.expected.json @@ -0,0 +1,436 @@ +{ + "Resources": { + "MyVPCAFB07A31": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-codebuild-file-system-locations/MyVPC" + } + ] + } + }, + "MyVPCPublicSubnet1Subnet0C75866A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/17", + "VpcId": { + "Ref": "MyVPCAFB07A31" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-codebuild-file-system-locations/MyVPC/PublicSubnet1" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "MyVPCPublicSubnet1RouteTable538A9511": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "MyVPCAFB07A31" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-codebuild-file-system-locations/MyVPC/PublicSubnet1" + } + ] + } + }, + "MyVPCPublicSubnet1RouteTableAssociation8A950D8E": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "MyVPCPublicSubnet1RouteTable538A9511" + }, + "SubnetId": { + "Ref": "MyVPCPublicSubnet1Subnet0C75866A" + } + } + }, + "MyVPCPublicSubnet1DefaultRouteAF81AA9B": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "MyVPCPublicSubnet1RouteTable538A9511" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "MyVPCIGW30AB6DD6" + } + }, + "DependsOn": [ + "MyVPCVPCGWE6F260E1" + ] + }, + "MyVPCPublicSubnet1EIP5EB6147D": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-codebuild-file-system-locations/MyVPC/PublicSubnet1" + } + ] + } + }, + "MyVPCPublicSubnet1NATGateway838228A5": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "MyVPCPublicSubnet1EIP5EB6147D", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "MyVPCPublicSubnet1Subnet0C75866A" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-codebuild-file-system-locations/MyVPC/PublicSubnet1" + } + ] + } + }, + "MyVPCPrivateSubnet1Subnet641543F4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/17", + "VpcId": { + "Ref": "MyVPCAFB07A31" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-codebuild-file-system-locations/MyVPC/PrivateSubnet1" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + } + ] + } + }, + "MyVPCPrivateSubnet1RouteTable133BD901": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "MyVPCAFB07A31" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-codebuild-file-system-locations/MyVPC/PrivateSubnet1" + } + ] + } + }, + "MyVPCPrivateSubnet1RouteTableAssociation85DFBFBB": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "MyVPCPrivateSubnet1RouteTable133BD901" + }, + "SubnetId": { + "Ref": "MyVPCPrivateSubnet1Subnet641543F4" + } + } + }, + "MyVPCPrivateSubnet1DefaultRouteA8EE6636": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "MyVPCPrivateSubnet1RouteTable133BD901" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "MyVPCPublicSubnet1NATGateway838228A5" + } + } + }, + "MyVPCIGW30AB6DD6": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-codebuild-file-system-locations/MyVPC" + } + ] + } + }, + "MyVPCVPCGWE6F260E1": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "MyVPCAFB07A31" + }, + "InternetGatewayId": { + "Ref": "MyVPCIGW30AB6DD6" + } + } + }, + "SecurityGroup1F554B36F": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Example", + "GroupName": "Jane", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "MyVPCAFB07A31" + } + } + }, + "MyProjectRole9BBE5233": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codebuild.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyProjectRoleDefaultPolicyB19B7C29": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "ec2:CreateNetworkInterfacePermission", + "Condition": { + "StringEquals": { + "ec2:Subnet": [ + { + "Fn::Join": [ + "", + [ + "arn:aws:ec2:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":subnet/", + { + "Ref": "MyVPCPrivateSubnet1Subnet641543F4" + } + ] + ] + } + ], + "ec2:AuthorizedService": "codebuild.amazonaws.com" + } + }, + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:ec2:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":network-interface/*" + ] + ] + } + }, + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "MyProject39F7B0AE" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/codebuild/", + { + "Ref": "MyProject39F7B0AE" + }, + ":*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyProjectRoleDefaultPolicyB19B7C29", + "Roles": [ + { + "Ref": "MyProjectRole9BBE5233" + } + ] + } + }, + "MyProject39F7B0AE": { + "Type": "AWS::CodeBuild::Project", + "Properties": { + "Artifacts": { + "Type": "NO_ARTIFACTS" + }, + "Environment": { + "ComputeType": "BUILD_GENERAL1_SMALL", + "Image": "aws/codebuild/standard:1.0", + "PrivilegedMode": true, + "Type": "LINUX_CONTAINER" + }, + "ServiceRole": { + "Fn::GetAtt": [ + "MyProjectRole9BBE5233", + "Arn" + ] + }, + "Source": { + "BuildSpec": "{\n \"version\": \"0.2\"\n}", + "Type": "NO_SOURCE" + }, + "FileSystemLocations": [ + { + "Identifier": "myidentifier", + "Location": "fs-c8d04839.efs.eu-west-2.amazonaws.com:/mnt", + "MountOptions": "nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2", + "MountPoint": "/media", + "Type": "EFS" + } + ], + "VpcConfig": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "SecurityGroup1F554B36F", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "MyVPCPrivateSubnet1Subnet641543F4" + } + ], + "VpcId": { + "Ref": "MyVPCAFB07A31" + } + } + }, + "DependsOn": [ + "MyProjectPolicyDocument646EE0F2" + ] + }, + "MyProjectPolicyDocument646EE0F2": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ec2:CreateNetworkInterface", + "ec2:DescribeNetworkInterfaces", + "ec2:DeleteNetworkInterface", + "ec2:DescribeSubnets", + "ec2:DescribeSecurityGroups", + "ec2:DescribeDhcpOptions", + "ec2:DescribeVpcs" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyProjectPolicyDocument646EE0F2", + "Roles": [ + { + "Ref": "MyProjectRole9BBE5233" + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.project-file-system-location.ts b/packages/@aws-cdk/aws-codebuild/test/integ.project-file-system-location.ts new file mode 100644 index 0000000000000..d07e823475d8d --- /dev/null +++ b/packages/@aws-cdk/aws-codebuild/test/integ.project-file-system-location.ts @@ -0,0 +1,36 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import * as codebuild from '../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-codebuild-file-system-locations'); +const vpc = new ec2.Vpc(stack, 'MyVPC', { + maxAzs: 1, + natGateways: 1, +}); +const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup1', { + allowAllOutbound: true, + description: 'Example', + securityGroupName: 'Jane', + vpc, +}); + +new codebuild.Project(stack, 'MyProject', { + buildSpec: codebuild.BuildSpec.fromObject({ + version: '0.2', + }), + environment: { + privileged: true + }, + vpc, + securityGroups: [securityGroup], + fileSystemLocations: [codebuild.FileSystemLocation.efs({ + identifier: "myidentifier", + location: "fs-c8d04839.efs.eu-west-2.amazonaws.com:/mnt", + mountOptions: "nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2", + mountPoint: "/media" + })] +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts b/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts index 11ae1fea6ac69..9127e324d83ed 100644 --- a/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts +++ b/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts @@ -930,6 +930,78 @@ export = { }, }, + 'fileSystemLocations': { + 'create fileSystemLocation and validate attributes'(test: Test) { + const stack = new cdk.Stack(); + new codebuild.Project(stack, 'MyProject', { + buildSpec: codebuild.BuildSpec.fromObject({ + version: '0.2', + }), + fileSystemLocations: [codebuild.FileSystemLocation.efs({ + identifier: "myidentifier2", + location: "myclodation.mydnsroot.com:/loc", + mountPoint: "/media", + mountOptions: "opts" + })] + }); + + expect(stack).to(haveResourceLike('AWS::CodeBuild::Project', { + "FileSystemLocations": [ + { + "Identifier": "myidentifier2", + "MountPoint": "/media", + "MountOptions": "opts", + "Location": "myclodation.mydnsroot.com:/loc", + "Type": "EFS" + }, + ], + })); + + test.done(); + }, + 'Multiple fileSystemLocation created'(test: Test) { + const stack = new cdk.Stack(); + const project = new codebuild.Project(stack, 'MyProject', { + buildSpec: codebuild.BuildSpec.fromObject({ + version: '0.2', + }), + fileSystemLocations: [codebuild.FileSystemLocation.efs({ + identifier: "myidentifier2", + location: "myclodation.mydnsroot.com:/loc", + mountPoint: "/media", + mountOptions: "opts" + })] + }); + project.addFileSystemLocation(codebuild.FileSystemLocation.efs({ + identifier: "myidentifier3", + location: "myclodation.mydnsroot.com:/loc", + mountPoint: "/media", + mountOptions: "opts" + })); + + expect(stack).to(haveResourceLike('AWS::CodeBuild::Project', { + "FileSystemLocations": [ + { + "Identifier": "myidentifier2", + "MountPoint": "/media", + "MountOptions": "opts", + "Location": "myclodation.mydnsroot.com:/loc", + "Type": "EFS" + }, + { + "Identifier": "myidentifier3", + "MountPoint": "/media", + "MountOptions": "opts", + "Location": "myclodation.mydnsroot.com:/loc", + "Type": "EFS" + } + ], + })); + + test.done(); + } + }, + 'secondary artifacts': { 'require providing an identifier when creating a Project'(test: Test) { const stack = new cdk.Stack();