├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── README.md ├── _cim.yml ├── cloudformation.yml ├── docs │ ├── bot-oauth-token.png │ ├── create-app.png │ ├── create-bot.png │ ├── github-token.png │ ├── interactive.png │ ├── logo.512.png │ ├── permissions.png │ ├── slash.png │ └── verification-token.png ├── index.js ├── lib │ ├── cloudformation.js │ ├── codebuild.js │ ├── dao.js │ ├── handle_codebuild.js │ ├── handle_codepipeline.js │ ├── handle_slack_commands.js │ ├── handle_slack_interactive_components.js │ ├── handle_sns.js │ ├── lambda.js │ ├── resources │ │ └── cloudformation.yml │ ├── s3.js │ └── slack.js ├── package.json └── test │ ├── codebuild.js │ ├── dao.js │ ├── index.js │ ├── project.js │ ├── s3.js │ └── slack.js ├── docs ├── FunctionCI-Build-Project.png ├── FunctionCI-Create-Project.png ├── FunctionCI-Deploy-Fn.png ├── banner.png ├── banner.pxm ├── build-messages.png ├── create-dialog.png ├── logo.png ├── show-fn.png ├── show-project.png └── slack-lambda.png └── kms ├── .gitignore ├── README.md ├── _cim.yml ├── cloudformation.yml └── encrypt.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Randy Findley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # Function CI 6 | FunctionCI is an open source Continuous Integration tool for AWS Lambdas. 7 | 8 | Builds are done by AWS CodeBuild and AWS CodePipeline. 9 | 10 | Receive build notifications via Slack. Deploy via Slack. View build and deployment audit trails via Slack. 11 | 12 | Fork this repo and install FunctionCI in your AWS account, then start managing your Lambdas properly. 13 | 14 | ## Features 15 | * Serverless - Function CI only costs money when you use it 16 | * Github integration 17 | * Slack bot 18 | * Versioned build artifacts stored in S3 19 | * Build and deployment audit trails with Github commit version and Slack username 20 | 21 | ## Table of content 22 | - [Usage](#usage) 23 | - [Setup](#setup) 24 | - [Commands](#commands) 25 | - [Architecture](#architecture) 26 | 27 | ## Usage 28 | Use the `/fn create project` Slack command to create a new build project. 29 | 30 |

31 | 32 |

33 | 34 | Once your build version is ready... 35 | 36 |

37 | 38 |

39 | 40 | Deploy it using the `/fn deploy fn ` Slack command. 41 | 42 | Here is an example project: https://github.com/rgfindl/functionci-demo 43 | 44 | ## Setup 45 | 1. Fork and clone this repo. 46 | 2. Install [CIM](https://github.com/thestackshack/cim) (CloudFormation Utility) 47 | 3. Install the [kms](kms/README.md) stack. 48 | 4. Install the [app](app/README.md) stack. 49 | 50 | ** If you fix or add anything please submit a Pull Request!!! 51 | 52 | ## Commands 53 | - [/fn create project](#fn-create-project) 54 | - [/fn show projeect](#fn-show-project) 55 | - [/fn show projeects](#fn-show-projects) 56 | - [/fn delete projeect](#fn-delete-project) 57 | - [/fn add fn](#fn-add-fn) 58 | - [/fn show fn](#fn-show-fn) 59 | - [/fn show fns](#fn-show-fns) 60 | - [/fn deploy fn](#fn-deploy-fn) 61 | - [/fn delete fn](#fn-delete-fn) 62 | 63 | ### /fn create project 64 | `/fn create project` 65 | 66 | Register a new Github repo that you wish FunctionCI to build. 67 | 68 | FunctionCI uses [CodeBuild](http://docs.aws.amazon.com/codebuild/latest/userguide/build-spec-ref.html), so you will need a `buildspec.yml` in the root of your repo. 69 | 70 | Here is an example `buildspec.yml` for a node.js project. 71 | 72 | ** Note you must export a single zip artifact. 73 | 74 | ``` 75 | version: 0.2 76 | 77 | # aws/codebuild/eb-nodejs-6.10.0-amazonlinux-64:4.0.0 78 | phases: 79 | install: 80 | commands: 81 | pre_build: 82 | commands: 83 | - echo Installing source NPM dependencies... 84 | - npm install 85 | build: 86 | commands: 87 | - echo Testing the code 88 | - npm test 89 | post_build: 90 | commands: 91 | - echo Removing dev dependencies 92 | - rm -Rf node_modules 93 | - npm install --production 94 | artifacts: 95 | files: 96 | - '**/*' 97 | type: zip 98 | 99 | ``` 100 | 101 | ### /fn show project 102 | `/fn show project ` 103 | 104 | Show the most recent builds in reverse chronological order. 105 | 106 | The `project_id` will be `-`. 107 | 108 | Example: 109 | * Repo: https://github.com/rgfindl/functionci-demo 110 | * Branch: master 111 | 112 | The `project_id` == `functionci-demo-master` 113 | 114 |

115 | 116 |

117 | 118 | ### /fn show projects 119 | `/fn show projects` 120 | 121 | Show all the projects under management. 122 | 123 | ### /fn delete project 124 | `/fn show project ` 125 | 126 | Delete the project and all build artifacts. 127 | 128 | ### /fn add fn 129 | `/fn add fn ` 130 | 131 | Add a Lambda function you wish to deploy build artifacts to. 132 | 133 | Ex: `/fn add fn demo functionci-demo-LambdaFunction-LFREQ3DEC3UJ` 134 | 135 | ### /fn show fn 136 | `/fn show fn ` 137 | 138 | Show a function and all its deployment history. 139 | 140 | Ex: `/fn show fn demo` 141 | 142 |

143 | 144 |

145 | 146 | ### /fn show fns 147 | `/fn show fns` 148 | 149 | Show all the functions under management 150 | 151 | Ex: `/fn show fns` 152 | 153 | ### /fn deploy fn 154 | `/fn deploy fn ` 155 | 156 | Deploy a build artifact to the Lambda function. 157 | 158 | Ex: `/fn deploy fn demo functionci-demo-master 1` 159 | 160 | ### /fn delete fn 161 | `/fn delete fn ` 162 | 163 | Delete a function and all its deployment history. 164 | 165 | Ex: `/fn delete fn demo` 166 | 167 | ## Architecture 168 | FunctionCI is also a serverless Lambda app. It includes the following AWS resources: 169 | * API Gateway (Exposes an API for our Slack bot) 170 | * Lambda (Slack bot API, SNS events, CloudWatch events, CodePipeline events) 171 | * DynamoDB (Document storage) 172 | * S3 (Build artifacts) 173 | * CloudFormation (Builds a pipeline for each project) 174 | * CodePipeline and CodeBuild (Performs builds) 175 | 176 | ### Create Project 177 | When you create a new project FunctionCI creates a new CloudFormation stack. This stack is the build pipeline for your project. 178 |

179 | 180 |

181 | 182 | ### Build Project 183 | When you commit changes to your Github repo CodePipeline is triggered. The first CodePipeline stage is CodeBuild to build, test, and package your app. The next CodePipeline stage is the FunctionCI Lambda to version and archive the build artifact. 184 |

185 | 186 |

187 | 188 | ### Deploy Lambda 189 | Now we are ready to deploy our build artifact to our Lambda. 190 |

191 | 192 |

-------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | # App Stack 2 | Creates the FunctionCI application. 3 | 4 | ## Step 1 - Create Slack App 5 | Login to your Slack team, navigate to https://api.slack.com/, and click [Start Building](https://api.slack.com/apps?new_app=1). 6 | 7 |

8 | 9 |

10 | 11 | 12 | ## Step 2 - Encrypt Slack Verification Token 13 | Encrypt your `VerificationToken` and add it to [_cim.yml](_cim.yml). 14 | 15 |

16 | 17 |

18 | 19 | 20 | ## Step 3 - Create Slack Bot 21 | Create a bot user. 22 | 23 |

24 | 25 |

26 | 27 | 28 | ## Step 4 - Install Slack to Team 29 | Install your app. Then encrypt your `Bot User OAuth Access Token` and add it to [_cim.yml](_cim.yml). 30 | 31 |

32 | 33 |

34 | 35 | ## Step 5 - Create Github Token 36 | Create a [Github personal access token](https://github.com/settings/tokens) with `repo` and `admin:repo_hook` permissions. Then encrypt the `Github Token` and add it to [_cim.yml](_cim.yml). 37 | 38 |

39 | 40 |

41 | 42 | 43 | ## Step 6 - Install the stack 44 | Install the app stack using the following [CIM](https://github.com/thestackshack/cim) command: `cim stack-up`. 45 | 46 | Deploy the Lambda using the following [CIM](https://github.com/thestackshack/cim) command: `cim deploy-lambda`. 47 | 48 | Record the stack outputs, you will need them in the next 2 steps: 49 | * SlackInteractiveComponentsUrl 50 | * SlackSlashCommandsUrl 51 | 52 | 53 | ## Step 7 - Add the `Interactive Components` Slack Feature 54 | Us the `SlackInteractiveComponentsUrl` from the stack output as the `Request Url`. 55 | 56 |

57 | 58 |

59 | 60 | 61 | ## Step 8 - Add Slack `Slash Command` 62 | Us the `SlackSlashCommandsUrl` from the stack output as the `Request Url`. 63 | 64 |

65 | 66 |

67 | 68 | 69 | ## Step 9 - Add Slack Permissions 70 | Add the following permissions to your Slack app under the Oauth & Permissions section. 71 | 72 | * channels:read 73 | * chat:write:bot 74 | * groups:read 75 | 76 |

77 | 78 |

79 | 80 | ## Step 10 - Reinstall your app 81 | After making these changes to your Slack app you will have to reinstall it. 82 | 83 | ## Step 11 - Start using your FunctionCI bot 84 | Now you're ready to start using your FunctionCI Slack bot. 85 | 86 | If you want to use your bot in private channels you'll have to invite it. `/invite functionci`. 87 | 88 | Check out the [FunctionCI Commands](../README.md#commands) to get started building and deploying your Lambda's. 89 | 90 | Here is an example project: https://github.com/rgfindl/functionci-demo -------------------------------------------------------------------------------- /app/_cim.yml: -------------------------------------------------------------------------------- 1 | version: 0.1 2 | stack: 3 | name: functionci-app 4 | template: 5 | file: cloudformation.yml 6 | bucket: functionci-cf-artifacts # Note: Update this with your bucket name. Stacks are uploaded here prior to deployment.' 7 | # 8 | # Reference parent stacks fo included shared information like stack name. 9 | # 10 | #parents: 11 | # vpc: '../vpc' 12 | 13 | # 14 | # Define stack input parameters. 15 | # 16 | # Optional - Custom domain name settings. If you specify 'TLD' you must also specify a 'Domain'. 17 | # After you 'stack-up' you will need to verify your email address so that AWS can issue the SSL certificate for you domain. 18 | # More info here: http://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate.html 19 | parameters: 20 | # # The top level domain that you created in Route53. 21 | # TLD: 'fnci.io' 22 | # # The domain you wish to use for this api. 23 | # Domain: 'api.fnci.io' 24 | # URI path 25 | Path: 'slack' 26 | 27 | # Slack params (encrypt) 28 | SlackVerificationToken: '${kms.decrypt(AQICAHhjNpxGxc7AX6NhOIEjyoXA3KHc2qhbn8lphYP7XmgoZAEi4XCUpuVY7qoTXVCvHprgAAAAdjB0BgkqhkiG9w0BBwagZzBlAgEAMGAGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMdHQkTfSpDuEbSropAgEQgDNsNl5GUltJYMkw1cCbISrk/yPC+XJarBXxBUQoOXcPgX1wY/DAuJ91nh2CqMqXoNgg8fE=)}' 29 | SlackBotOAuthToken: '${kms.decrypt(AQICAHhjNpxGxc7AX6NhOIEjyoXA3KHc2qhbn8lphYP7XmgoZAFvWjmXWOSa9+6wc0sBF1e3AAAAiTCBhgYJKoZIhvcNAQcGoHkwdwIBADByBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDOVrDECOpOp5Y3UiKAIBEIBFB/POMIG5ywcO036LCRpRZmVFgaJ2HcIlePIxAyYeOX+GfaBJOsYoSeW4p8I/SSD3Di69Qfdygt9kFj8pVzsD2gpIcCvn)}' 30 | 31 | #Github Token 32 | GithubToken: '${kms.decrypt(AQICAHhjNpxGxc7AX6NhOIEjyoXA3KHc2qhbn8lphYP7XmgoZAEdRP68TkPmVZAz5lD/5ImBAAAAhzCBhAYJKoZIhvcNAQcGoHcwdQIBADBwBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDPPkD3AuapU+/6FHfAIBEIBDki+0fm3X3FD3mp0cj0kN0+AVwchVTNjxwaH/uTjPUDdlVIT/bNaSVTH+L1tQVo1bcchD8LtxNrjfLmeMjC3aKQ8byg==)}' 33 | # 34 | # Define stack capabilities required. 35 | # 36 | capabilities: 37 | - 'CAPABILITY_IAM' 38 | 39 | # 40 | # Define global tags. 41 | # 42 | tags: 43 | app: 'functionci' 44 | 45 | lambda: 46 | functions: 47 | - 48 | function: ${stack.outputs.LambdaFunction} 49 | zip_file: index.zip 50 | deploy: 51 | phases: 52 | pre_deploy: 53 | commands: 54 | # Install all npm packages including dev packages. 55 | - npm install 56 | 57 | # Run the tests 58 | # - npm test 59 | 60 | # Remove all the npm packages. 61 | - rm -Rf node_modules 62 | 63 | # Only install the non-dev npm packages. We don't want to bloat our Lambda with dev packages. 64 | - npm install --production 65 | 66 | # Zip the Lambda for upload to S3. 67 | - zip -r index.zip . 68 | post_deploy: 69 | commands: 70 | # Remove the zip file. 71 | - rm -Rf index.zip 72 | 73 | # Reinstall the dev npm packages. 74 | - npm install 75 | -------------------------------------------------------------------------------- /app/cloudformation.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | 3 | Description: 'FunctionCI is an open source CI/CD Slack Bot for AWS Lambdas.' 4 | 5 | Parameters: 6 | KMSStack: 7 | Type: String 8 | Description: KMS Stack Name 9 | Default: 'functionci-kms' 10 | TLD: 11 | Type: String 12 | Description: TLD name needed by Route53 to perform DNS (example.com) 13 | Default: '' 14 | Domain: 15 | Type: String 16 | Description: Domain name for your api (api.example.com) 17 | Default: '' 18 | Path: 19 | Type: String 20 | Description: The path part of your api (api.example.com/path) 21 | Default: 'api' 22 | Stage: 23 | Type: String 24 | Description: The deployment stage used by API Gateway 25 | Default: 'prod' 26 | SlackVerificationToken: 27 | Type: String 28 | Description: Slack verification token 29 | NoEcho: true 30 | SlackBotOAuthToken: 31 | Type: String 32 | Description: Slack Bot OAuth Token 33 | NoEcho: true 34 | GithubToken: 35 | Type: String 36 | Description: Github Token 37 | NoEcho: true 38 | 39 | Conditions: 40 | UseCustomDomain: !And 41 | - !Not [!Equals [!Ref TLD, '']] 42 | - !Not [!Equals [!Ref Domain, '']] 43 | 44 | Resources: 45 | 46 | # 47 | # Role for CloudFormation 48 | # 49 | IamRoleCloudFormationExecution: 50 | Type: AWS::IAM::Role 51 | Properties: 52 | AssumeRolePolicyDocument: 53 | Version: '2012-10-17' 54 | Statement: 55 | - Effect: Allow 56 | Principal: 57 | Service: 58 | - cloudformation.amazonaws.com 59 | Action: 60 | - sts:AssumeRole 61 | Path: '/' 62 | 63 | # 64 | # Create a Policy and attach it to our CloudFormation Role. 65 | # 66 | IamPolicyCloudFormationExecution: 67 | Type: AWS::IAM::Policy 68 | Properties: 69 | PolicyName: IamPolicyCloudFormationExecution 70 | PolicyDocument: 71 | Version: '2012-10-17' 72 | Statement: 73 | - Effect: Allow 74 | Action: 75 | - cloudformation:CreateStack 76 | - cloudformation:DescribeStacks 77 | - cloudformation:DescribeStackEvents 78 | - cloudformation:DescribeStackResources 79 | - cloudformation:GetTemplate 80 | - cloudformation:ValidateTemplate 81 | Resource: '*' 82 | - Effect: Allow 83 | Action: 84 | - iam:Create* 85 | - iam:List* 86 | - iam:Put* 87 | - iam:Get* 88 | - iam:Attach* 89 | - iam:Detach* 90 | - iam:Delete* 91 | - iam:Pass* 92 | Resource: '*' 93 | - Effect: Allow 94 | Action: 95 | - logs:* 96 | Resource: '*' 97 | - Effect: Allow 98 | Action: 99 | - codepipeline:* 100 | Resource: '*' 101 | - Effect: Allow 102 | Action: 103 | - codebuild:* 104 | Resource: '*' 105 | - Effect: Allow 106 | Action: 107 | - sns:Publish 108 | Resource: '*' 109 | - Effect: Allow 110 | Action: 111 | - events:* 112 | Resource: '*' 113 | - Effect: Allow 114 | Action: 115 | - lambda:* 116 | Resource: '*' 117 | Roles: 118 | - Ref: IamRoleCloudFormationExecution 119 | 120 | # 121 | # DynamoDB Tables 122 | # 123 | FunctionCITable: 124 | Type: AWS::DynamoDB::Table 125 | Properties: 126 | TableName: 'FunctionCI' 127 | AttributeDefinitions: 128 | - 129 | AttributeName: 'hash_key' 130 | AttributeType: 'S' 131 | - 132 | AttributeName: 'sort_key' 133 | AttributeType: 'S' 134 | KeySchema: 135 | - 136 | AttributeName: 'hash_key' 137 | KeyType: 'HASH' 138 | - 139 | AttributeName: 'sort_key' 140 | KeyType: 'RANGE' 141 | ProvisionedThroughput: 142 | ReadCapacityUnits: '5' 143 | WriteCapacityUnits: '5' 144 | 145 | ArtifactsBucket: 146 | Type: AWS::S3::Bucket 147 | Properties: 148 | BucketName: 'functionci-artifacts' 149 | 150 | # 151 | # Role that our Lambda will assume to provide access to other AWS resources 152 | # 153 | IamRoleLambdaExecution: 154 | Type: AWS::IAM::Role 155 | Properties: 156 | AssumeRolePolicyDocument: 157 | Version: '2012-10-17' 158 | Statement: 159 | - Effect: Allow 160 | Principal: 161 | Service: 162 | - lambda.amazonaws.com 163 | Action: 164 | - sts:AssumeRole 165 | Path: '/' 166 | 167 | # 168 | # Create a Policy and attach it to our Lambda Role. 169 | # 170 | IamPolicyLambdaExecution: 171 | Type: AWS::IAM::Policy 172 | Properties: 173 | PolicyName: IamPolicyLambdaExecution 174 | PolicyDocument: 175 | Version: '2012-10-17' 176 | Statement: 177 | - Effect: Allow 178 | Action: 179 | - logs:CreateLogGroup 180 | - logs:CreateLogStream 181 | - logs:PutLogEvents 182 | Resource: '*' 183 | - Effect: Allow 184 | Action: 185 | - codebuild:ListCuratedEnvironmentImages 186 | Resource: '*' 187 | - Effect: Allow 188 | Action: 189 | - iam:GetRole 190 | - iam:PassRole 191 | Resource: !GetAtt IamRoleCloudFormationExecution.Arn 192 | - Effect: Allow 193 | Action: 194 | - lambda:UpdateFunctionCode 195 | - lambda:UpdateAlias 196 | - lambda:ListAliases 197 | - lambda:ListVersionsByFunction 198 | - lambda:DeleteFunction 199 | - lambda:PublishVersion 200 | Resource: '*' 201 | - Effect: Allow 202 | Action: 203 | - codepipeline:* 204 | Resource: '*' 205 | - Effect: Allow 206 | Action: 207 | - dynamodb:* 208 | Resource: !GetAtt FunctionCITable.Arn 209 | - Effect: Allow 210 | Action: 211 | - cloudformation:CreateStack 212 | - cloudformation:DescribeStacks 213 | - cloudformation:DescribeStackEvents 214 | - cloudformation:DescribeStackResources 215 | - cloudformation:GetTemplate 216 | - cloudformation:ValidateTemplate 217 | - cloudformation:DeleteStack 218 | Resource: '*' 219 | - Effect: Allow 220 | Action: 221 | - s3:* 222 | Resource: 223 | - Fn::Join: 224 | - '' 225 | - - 'arn:aws:s3:::' 226 | - Ref: ArtifactsBucket 227 | - Fn::Join: 228 | - '' 229 | - - 'arn:aws:s3:::' 230 | - Ref: ArtifactsBucket 231 | - '/*' 232 | Roles: 233 | - Ref: IamRoleLambdaExecution 234 | 235 | # 236 | # Our Lambda function. Basic code has been added. You will replace the code later via your Github repo. 237 | # 238 | LambdaFunction: 239 | Type: AWS::Lambda::Function 240 | DependsOn: 241 | - IamRoleCloudFormationExecution 242 | - IamPolicyCloudFormationExecution 243 | - FunctionCITable 244 | - SNSTopic 245 | - ArtifactsBucket 246 | Properties: 247 | Handler: index.handler 248 | Timeout: 5 249 | Role: 250 | Fn::GetAtt: 251 | - IamRoleLambdaExecution 252 | - Arn 253 | Code: 254 | ZipFile: !Sub | 255 | 'use strict'; 256 | 257 | exports.handler = function(event, context, callback) { 258 | const response = { 259 | statusCode: 200, 260 | body: JSON.stringify({ 261 | message: `Hello CIM`, 262 | event: event 263 | }) 264 | }; 265 | 266 | callback(null, response); 267 | }; 268 | Runtime: nodejs6.10 269 | KmsKeyArn: 270 | Fn::ImportValue: 271 | !Sub "${KMSStack}-FunctionCiKmsKeyArn" 272 | Environment: 273 | Variables: 274 | IamRoleCloudFormationExecution: !GetAtt IamRoleCloudFormationExecution.Arn 275 | FunctionCITable: !Ref FunctionCITable 276 | SNSTopic: !Ref SNSTopic 277 | SlackVerificationToken: !Ref SlackVerificationToken 278 | SlackBotOAuthToken: !Ref SlackBotOAuthToken 279 | GithubToken: !Ref GithubToken 280 | ArtifactsBucket: !Ref ArtifactsBucket 281 | 282 | # 283 | # Create the API Gateway 284 | # 285 | RestApi: 286 | Type: AWS::ApiGateway::RestApi 287 | Properties: 288 | Name: ApiGatewayRestApi 289 | 290 | ApiGatewayResource: 291 | Type: AWS::ApiGateway::Resource 292 | Properties: 293 | ParentId: !GetAtt RestApi.RootResourceId 294 | PathPart: !Ref Path #ex. example.com/api. 295 | RestApiId: !Ref RestApi 296 | 297 | ApiGatewayResourceProxy: 298 | Type: AWS::ApiGateway::Resource 299 | Properties: 300 | ParentId: !Ref ApiGatewayResource 301 | PathPart: '{proxy+}' 302 | RestApiId: !Ref RestApi 303 | 304 | ApiGatewayMethodOptions: 305 | Type: AWS::ApiGateway::Method 306 | Properties: 307 | AuthorizationType: NONE 308 | ResourceId: !Ref ApiGatewayResourceProxy 309 | RestApiId: !Ref RestApi 310 | HttpMethod: OPTIONS 311 | Integration: 312 | IntegrationResponses: 313 | - StatusCode: 200 314 | ResponseParameters: 315 | method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'" 316 | method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'" 317 | method.response.header.Access-Control-Allow-Origin: "'*'" 318 | method.response.header.Access-Control-Allow-Credentials: "'false'" 319 | ResponseTemplates: 320 | application/json: '' 321 | PassthroughBehavior: WHEN_NO_MATCH 322 | RequestTemplates: 323 | application/json: '{"statusCode": 200}' 324 | Type: MOCK 325 | MethodResponses: 326 | - StatusCode: 200 327 | ResponseModels: 328 | application/json: 'Empty' 329 | ResponseParameters: 330 | method.response.header.Access-Control-Allow-Headers: false 331 | method.response.header.Access-Control-Allow-Methods: false 332 | method.response.header.Access-Control-Allow-Origin: false 333 | method.response.header.Access-Control-Allow-Credentials: true 334 | 335 | ApiGatewayMethodAny: 336 | Type: AWS::ApiGateway::Method 337 | Properties: 338 | HttpMethod: ANY 339 | RequestParameters: {} 340 | ResourceId: !Ref ApiGatewayResource 341 | RestApiId: !Ref RestApi 342 | AuthorizationType: NONE 343 | Integration: 344 | IntegrationHttpMethod: POST 345 | Type: AWS_PROXY 346 | Uri: 347 | Fn::Join: 348 | - '' 349 | - - 'arn:aws:apigateway:' 350 | - Ref: AWS::Region 351 | - ':lambda:path/2015-03-31/functions/' 352 | - !GetAtt LambdaFunction.Arn 353 | - '/invocations' 354 | MethodResponses: [] 355 | 356 | ApiGatewayMethodProxyAny: 357 | Type: AWS::ApiGateway::Method 358 | Properties: 359 | HttpMethod: ANY 360 | RequestParameters: {} 361 | ResourceId: !Ref ApiGatewayResourceProxy 362 | RestApiId: !Ref RestApi 363 | AuthorizationType: NONE 364 | Integration: 365 | IntegrationHttpMethod: POST 366 | Type: AWS_PROXY 367 | Uri: 368 | Fn::Join: 369 | - '' 370 | - - 'arn:aws:apigateway:' 371 | - Ref: AWS::Region 372 | - ':lambda:path/2015-03-31/functions/' 373 | - !GetAtt LambdaFunction.Arn 374 | - '/invocations' 375 | MethodResponses: [] 376 | 377 | ApiGatewayDeployment: 378 | Type: AWS::ApiGateway::Deployment 379 | Properties: 380 | RestApiId: !Ref RestApi 381 | StageName: !Ref Stage # Maps to the custom domain name. BasePathMapping.Stage 382 | DependsOn: 383 | - ApiGatewayMethodOptions 384 | - ApiGatewayMethodAny 385 | - ApiGatewayMethodProxyAny 386 | 387 | # 388 | # We need to give API Gateway permission to invoke our Lambda function. 389 | # 390 | PermissionForAPIGatewayToInvokeLambda: 391 | Type: AWS::Lambda::Permission 392 | Properties: 393 | Action: lambda:invokeFunction 394 | FunctionName: !Ref LambdaFunction 395 | Principal: apigateway.amazonaws.com 396 | SourceArn: 397 | Fn::Join: 398 | - '' 399 | - - 'arn:aws:execute-api:' 400 | - Ref: AWS::Region 401 | - ':' 402 | - Ref: AWS::AccountId 403 | - ':' 404 | - Ref: RestApi 405 | - '/*/*' 406 | 407 | # 408 | # SSL Certificate needed by CloudFront. 409 | # 410 | SSL: 411 | Type: AWS::CertificateManager::Certificate 412 | Condition: UseCustomDomain 413 | Properties: 414 | DomainName: !Ref Domain 415 | DomainValidationOptions: 416 | - DomainName: !Ref Domain 417 | ValidationDomain: !Ref TLD 418 | 419 | # 420 | # Custom Domain Name 421 | # 422 | ApiDomainName: 423 | Type: AWS::ApiGateway::DomainName 424 | Condition: UseCustomDomain 425 | Properties: 426 | DomainName: !Ref Domain 427 | CertificateArn: !Ref SSL 428 | 429 | # 430 | # Wire custom domain to Api Gateway 431 | # 432 | BasePathMapping: 433 | Type: AWS::ApiGateway::BasePathMapping 434 | Condition: UseCustomDomain 435 | Properties: 436 | DomainName: !Ref ApiDomainName 437 | RestApiId: !Ref RestApi 438 | Stage: !Ref Stage 439 | 440 | # 441 | # Route53 DNS record set to map our domain to API Gateway 442 | # 443 | DomainDNS: 444 | Type: AWS::Route53::RecordSetGroup 445 | Condition: UseCustomDomain 446 | Properties: 447 | HostedZoneName: 448 | Fn::Join: 449 | - '' 450 | - - !Ref TLD 451 | - '.' 452 | RecordSets: 453 | - 454 | Name: !Ref Domain 455 | Type: 'A' 456 | AliasTarget: 457 | HostedZoneId: 'Z2FDTNDATAQYW2' # http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-aliastarget.html#cfn-route53-aliastarget-hostedzoneid 458 | DNSName: !GetAtt ApiDomainName.DistributionDomainName 459 | 460 | 461 | # 462 | # SNS Topic 463 | # 464 | SNSTopic: 465 | Type: AWS::SNS::Topic 466 | Properties: 467 | DisplayName: 468 | Fn::Join: 469 | - '' 470 | - - Ref: AWS::StackName 471 | - ' Topic' 472 | TopicName: 473 | Fn::Join: 474 | - '' 475 | - - Ref: AWS::StackName 476 | - '-topic' 477 | 478 | # 479 | # Subscribe our new Lambda function to the SNS topic. 480 | # 481 | LambdaSNSSubscription: 482 | Type: AWS::SNS::Subscription 483 | DependsOn: LambdaFunction 484 | Properties: 485 | TopicArn: !Ref SNSTopic 486 | Protocol: lambda 487 | Endpoint: !GetAtt LambdaFunction.Arn 488 | 489 | # 490 | # Give SNS permission to invoke our Lambda 491 | # 492 | PermissionForSNSToInvokeLambda: 493 | Type: AWS::Lambda::Permission 494 | Properties: 495 | FunctionName: !Ref LambdaFunction 496 | Action: 'lambda:InvokeFunction' 497 | Principal: 'sns.amazonaws.com' 498 | SourceArn: !Ref SNSTopic 499 | 500 | Outputs: 501 | LambdaFunction: 502 | Description: Lambda Function 503 | Value: !Ref LambdaFunction 504 | Export: 505 | Name: !Sub '${AWS::StackName}-LambdaFunction' 506 | LambdaFunctionArn: 507 | Description: Lambda Function Arn 508 | Value: !GetAtt LambdaFunction.Arn 509 | Export: 510 | Name: !Sub '${AWS::StackName}-LambdaFunctionArn' 511 | ArtifactsBucket: 512 | Description: Artifacts Bucket 513 | Value: !Ref ArtifactsBucket 514 | Export: 515 | Name: !Sub '${AWS::StackName}-ArtifactsBucket' 516 | ApiGatewayUrl: 517 | Description: URL of your API endpoint 518 | Value: !Join 519 | - '' 520 | - - 'https://' 521 | - !Ref RestApi 522 | - '.execute-api.' 523 | - !Ref AWS::Region 524 | - '.amazonaws.com/' 525 | - !Ref Stage 526 | - '/' 527 | - !Ref Path 528 | SlackSlashCommandsUrl: 529 | Description: Slack slash commands url 530 | Value: !Join 531 | - '' 532 | - - 'https://' 533 | - !Ref RestApi 534 | - '.execute-api.' 535 | - !Ref AWS::Region 536 | - '.amazonaws.com/' 537 | - !Ref Stage 538 | - '/' 539 | - !Ref Path 540 | - '/commands' 541 | SlackInteractiveComponentsUrl: 542 | Description: Slack interactive components url 543 | Value: !Join 544 | - '' 545 | - - 'https://' 546 | - !Ref RestApi 547 | - '.execute-api.' 548 | - !Ref AWS::Region 549 | - '.amazonaws.com/' 550 | - !Ref Stage 551 | - '/' 552 | - !Ref Path 553 | - '/interactive-components' 554 | CustomDomainUrl: 555 | Description: URL of your API endpoint 556 | Condition: UseCustomDomain 557 | Value: !Join 558 | - '' 559 | - - 'https://' 560 | - !Ref Domain 561 | - '/' 562 | - !Ref Path 563 | CustomSlackSlashCommandsUrl: 564 | Description: Slack slack commands url 565 | Condition: UseCustomDomain 566 | Value: !Join 567 | - '' 568 | - - 'https://' 569 | - !Ref Domain 570 | - '/' 571 | - !Ref Path 572 | - '/commands' 573 | CustomSlackInteractiveComponentsUrl: 574 | Description: Slack interactive components url 575 | Condition: UseCustomDomain 576 | Value: !Join 577 | - '' 578 | - - 'https://' 579 | - !Ref Domain 580 | - '/' 581 | - !Ref Path 582 | - '/interactive-components' 583 | -------------------------------------------------------------------------------- /app/docs/bot-oauth-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgfindl/functionci/bd27f52f5701037f206e8a2c8ee479e866f6c047/app/docs/bot-oauth-token.png -------------------------------------------------------------------------------- /app/docs/create-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgfindl/functionci/bd27f52f5701037f206e8a2c8ee479e866f6c047/app/docs/create-app.png -------------------------------------------------------------------------------- /app/docs/create-bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgfindl/functionci/bd27f52f5701037f206e8a2c8ee479e866f6c047/app/docs/create-bot.png -------------------------------------------------------------------------------- /app/docs/github-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgfindl/functionci/bd27f52f5701037f206e8a2c8ee479e866f6c047/app/docs/github-token.png -------------------------------------------------------------------------------- /app/docs/interactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgfindl/functionci/bd27f52f5701037f206e8a2c8ee479e866f6c047/app/docs/interactive.png -------------------------------------------------------------------------------- /app/docs/logo.512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgfindl/functionci/bd27f52f5701037f206e8a2c8ee479e866f6c047/app/docs/logo.512.png -------------------------------------------------------------------------------- /app/docs/permissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgfindl/functionci/bd27f52f5701037f206e8a2c8ee479e866f6c047/app/docs/permissions.png -------------------------------------------------------------------------------- /app/docs/slash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgfindl/functionci/bd27f52f5701037f206e8a2c8ee479e866f6c047/app/docs/slash.png -------------------------------------------------------------------------------- /app/docs/verification-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgfindl/functionci/bd27f52f5701037f206e8a2c8ee479e866f6c047/app/docs/verification-token.png -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const qs = require('querystring'); 4 | const _ = require('lodash'); 5 | 6 | const handle_slack_commands = require('./lib/handle_slack_commands'); 7 | const handle_slack_interactive_components = require('./lib/handle_slack_interactive_components'); 8 | const handle_sns = require('./lib/handle_sns'); 9 | const handle_codebuild = require('./lib/handle_codebuild'); 10 | const handle_codepipeline = require('./lib/handle_codepipeline'); 11 | 12 | // 13 | // Handles all Slack API calls 14 | // 15 | exports.handler = function(event, context, callback) { 16 | console.log(JSON.stringify(event, null, 3)); 17 | if (_.isNil(event.httpMethod)) { 18 | // 19 | // Async invocations from sns, codebuild, codepipeline 20 | // 21 | if (!_.isNil(event.source) && _.isEqual(event.source, 'aws.codebuild')) { 22 | handle_codebuild.handle(event, callback); 23 | return; 24 | } else if (!_.isNil(event['CodePipeline.job'])) { 25 | handle_codepipeline.handle(event, context); 26 | return; 27 | } else if (!_.isNil(event.Records) && _.isEqual(event.Records[0].EventSource, 'aws:sns')) { 28 | handle_sns.handle(event.Records[0], callback); 29 | return; 30 | } 31 | } else { 32 | // 33 | // Invocations for API Gateway 34 | // 35 | const body = qs.parse(event.body); 36 | console.log(JSON.stringify(body, null, 3)); 37 | 38 | if (_.isEqual(event.path, '/slack/commands')) { 39 | handle_slack_commands.handle(body, callback); 40 | return; 41 | } else if (_.isEqual(event.path, '/slack/interactive-components')) { 42 | handle_slack_interactive_components.handle(body, callback); 43 | return; 44 | } else { 45 | return callback(null, { 46 | statusCode: 200, 47 | body: 'Unknown command' 48 | }); 49 | } 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /app/lib/cloudformation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const AWS = require('aws-sdk'); 4 | const s3 = new AWS.S3(); 5 | const cloudformation = new AWS.CloudFormation(); 6 | const _ = require('lodash'); 7 | const fs = require('fs'); 8 | const async = require('async'); 9 | 10 | var functions = {}; 11 | 12 | functions.uploadCFTemplate = function(done) { 13 | console.log('uploadCFTemplate'); 14 | fs.readFile('./lib/resources/cloudformation.yml', function (err, data) { 15 | if (err) { 16 | throw err; 17 | } 18 | 19 | var base64data = new Buffer(data, 'binary'); 20 | var params = { 21 | Body: base64data, 22 | Bucket: process.env.ArtifactsBucket, 23 | Key: '_cloudformation.yml' 24 | }; 25 | s3.putObject(params, function(err, result) { 26 | if (err) { 27 | console.log(err); 28 | return done(err.message); 29 | } 30 | done(err, 'https://s3.amazonaws.com/'+process.env.ArtifactsBucket+'/_cloudformation.yml'); 31 | }); 32 | }); 33 | }; 34 | 35 | functions.createStack = function(params, done) { 36 | console.log('Creating stack'); 37 | var input = { 38 | StackName: 'functionci-'+params.project_id, 39 | Capabilities: [ 40 | 'CAPABILITY_NAMED_IAM' 41 | ], 42 | OnFailure: 'DELETE', 43 | RoleARN: process.env.IamRoleCloudFormationExecution, 44 | NotificationARNs: [ 45 | process.env.SNSTopic 46 | ], 47 | Parameters: [ 48 | { 49 | ParameterKey: 'FunctionCIStack', 50 | ParameterValue: 'functionci-app' 51 | }, 52 | { 53 | ParameterKey: 'GitHubOwner', 54 | ParameterValue: params.github_owner 55 | }, 56 | { 57 | ParameterKey: 'GitHubRepo', 58 | ParameterValue: params.github_repo 59 | }, 60 | { 61 | ParameterKey: 'GitHubBranch', 62 | ParameterValue: params.github_branch 63 | }, 64 | { 65 | ParameterKey: 'GitHubToken', 66 | ParameterValue: process.env.GithubToken 67 | }, 68 | { 69 | ParameterKey: 'CodeBuildComputeType', 70 | ParameterValue: params.codebuid_compute_type 71 | }, 72 | { 73 | ParameterKey: 'CodeBuildImage', 74 | ParameterValue: params.codebuid_image 75 | } 76 | ], 77 | Tags: [ 78 | { 79 | Key: 'App', /* required */ 80 | Value: 'functionci' /* required */ 81 | }, 82 | { 83 | Key: 'Id', /* required */ 84 | Value: params.project_id /* required */ 85 | } 86 | ], 87 | TemplateURL: params.templateS3Url 88 | }; 89 | console.log(JSON.stringify(input, null, 3)); 90 | cloudformation.createStack(input, function(err, response) { 91 | if (err) { 92 | console.log(err); 93 | return done(err.message); 94 | } 95 | done(); 96 | }); 97 | }; 98 | 99 | functions.delete_stack = function(params, done) { 100 | console.log('Delete stack'); 101 | var input = { 102 | StackName: 'functionci-' + params.project_id, 103 | RoleARN: process.env.IamRoleCloudFormationExecution 104 | }; 105 | console.log(JSON.stringify(input, null, 3)); 106 | cloudformation.deleteStack(input, function(err, response) { 107 | if (err) { 108 | console.log(err); 109 | return done(err.message); 110 | } 111 | done(); 112 | }); 113 | }; 114 | 115 | functions.build_stack_up = function(params, done) { 116 | async.waterfall([ 117 | functions.uploadCFTemplate, 118 | function(templateS3Url, next) { 119 | params.templateS3Url = templateS3Url; 120 | functions.createStack(params, next); 121 | } 122 | ], done); 123 | }; 124 | 125 | module.exports = functions; -------------------------------------------------------------------------------- /app/lib/codebuild.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const codebuild = new AWS.CodeBuild({apiVersion: '2016-10-06'}); 3 | const _ = require('lodash'); 4 | 5 | var functions = {}; 6 | 7 | functions.listCuratedEnvironmentImages = function(done) { 8 | console.log('listCuratedEnvironmentImages'); 9 | var params = { 10 | }; 11 | codebuild.listCuratedEnvironmentImages(params, function(err, data) { 12 | if (err) return done(err); 13 | var images = []; 14 | _.forEach(data.platforms, function(platform) { 15 | _.forEach(platform.languages, function(language) { 16 | _.forEach(language.images, function(image) { 17 | images.push(image.name); 18 | }); 19 | }); 20 | }); 21 | return done(null, images); 22 | }); 23 | 24 | }; 25 | 26 | module.exports = functions; -------------------------------------------------------------------------------- /app/lib/dao.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const _ = require('lodash'); 3 | const async = require('async'); 4 | var docClient = new AWS.DynamoDB.DocumentClient(); 5 | 6 | var functions = {}; 7 | 8 | functions.put_project = function(input, done) { 9 | console.log('dao.put_project'); 10 | input.hash_key = 'project'; 11 | input.sort_key = input.project_id; 12 | input.build_count = 0; 13 | var params = { 14 | TableName: process.env.FunctionCITable, 15 | Item: input, 16 | ConditionExpression: "attribute_not_exists(build_count)" 17 | }; 18 | console.log(JSON.stringify(params)); 19 | docClient.put(params, function(err, data) { 20 | if (err && _.isEqual(err.message, 'The conditional request failed')) 21 | return done('Project already exists.', null); 22 | else if (err) return done(err, null); 23 | else return done(null, data); 24 | }); 25 | }; 26 | 27 | functions.update_project_build_count = function(input, done) { 28 | console.log('dao.update_project_count'); 29 | input.hash_key = 'project'; 30 | input.sort_key = input.project_id; 31 | var params = { 32 | TableName: process.env.FunctionCITable, 33 | Key:{ 34 | "hash_key": 'project', 35 | "sort_key": input.project_id 36 | }, 37 | UpdateExpression: "set build_count = build_count + :val", 38 | ExpressionAttributeValues:{ 39 | ":val":1 40 | }, 41 | ReturnValues:"UPDATED_NEW" 42 | }; 43 | console.log(JSON.stringify(params)); 44 | docClient.update(params, done); 45 | }; 46 | 47 | functions.get_project = function(input, done) { 48 | console.log('dao.get_project'); 49 | var params = { 50 | TableName: process.env.FunctionCITable, 51 | Key:{ 52 | "hash_key": 'project', 53 | "sort_key": input.project_id 54 | } 55 | }; 56 | docClient.get(params, done); 57 | }; 58 | 59 | functions.delete_project = function(input, done) { 60 | console.log('dao.delete_project'); 61 | async.waterfall([ 62 | function(next) { 63 | var params = { 64 | TableName: process.env.FunctionCITable, 65 | Key:{ 66 | "hash_key": 'project', 67 | "sort_key": input.project_id 68 | } 69 | }; 70 | docClient.delete(params, next); 71 | }, function(results, next) { 72 | functions.delete_builds_per_project(input, next); 73 | } 74 | ], done); 75 | }; 76 | 77 | functions.get_projects = function(input, done) { 78 | console.log('dao.get_projects'); 79 | var params = { 80 | TableName: process.env.FunctionCITable, 81 | KeyConditionExpression: "#hash_key = :hash_key", 82 | ExpressionAttributeNames:{ 83 | "#hash_key": "hash_key" 84 | }, 85 | ExpressionAttributeValues: { 86 | ":hash_key":'project' 87 | } 88 | }; 89 | if (!_.isNil(input.from)) { 90 | params.ExclusiveStartKey = { 91 | sort_key: from, 92 | hash_key: 'project' 93 | }; 94 | } 95 | 96 | docClient.query(params, done); 97 | }; 98 | 99 | functions.put_fn = function(input, done) { 100 | console.log('dao.put_fn'); 101 | async.waterfall([ 102 | function(next) { 103 | input.hash_key = 'function-arn'; 104 | input.sort_key = input.arn; 105 | var params = { 106 | TableName: process.env.FunctionCITable, 107 | Item: input, 108 | ConditionExpression: "attribute_not_exists(arn)" 109 | }; 110 | console.log(JSON.stringify(params)); 111 | docClient.put(params, function(err, data) { 112 | if (err && _.isEqual(err.message, 'The conditional request failed')) 113 | return done('Function already exists.', null); 114 | else if (err) return done(err, null); 115 | else return done(null, data); 116 | }); 117 | }, 118 | function(results, next) { 119 | input.hash_key = 'function'; 120 | input.sort_key = input.short_name; 121 | var params = { 122 | TableName: process.env.FunctionCITable, 123 | Item: input, 124 | ConditionExpression: "attribute_not_exists(arn)" 125 | }; 126 | console.log(JSON.stringify(params)); 127 | docClient.put(params, function(err, data) { 128 | if (err && _.isEqual(err.message, 'The conditional request failed')) 129 | return done('Function already exists.', null); 130 | else if (err) return done(err, null); 131 | else return done(null, data); 132 | }); 133 | } 134 | ], done); 135 | }; 136 | 137 | functions.get_fn = function(input, done) { 138 | console.log('dao.get_fn'); 139 | var params = { 140 | TableName: process.env.FunctionCITable, 141 | Key:{ 142 | "hash_key": 'function', 143 | "sort_key": input.short_name 144 | } 145 | }; 146 | docClient.get(params, done); 147 | }; 148 | 149 | functions.delete_fn = function(input, done) { 150 | console.log('dao.delete_fn'); 151 | async.waterfall([ 152 | function(next) { 153 | functions.get_fn({ 154 | short_name: input.short_name 155 | }, next); 156 | }, 157 | function(data, next) { 158 | input.arn = data.Item.arn; 159 | var params = { 160 | TableName: process.env.FunctionCITable, 161 | Key:{ 162 | "hash_key": 'function-arn', 163 | "sort_key": input.arn 164 | } 165 | }; 166 | docClient.delete(params, next); 167 | }, 168 | function(results, next) { 169 | var params = { 170 | TableName: process.env.FunctionCITable, 171 | Key:{ 172 | "hash_key": 'function', 173 | "sort_key": input.short_name 174 | } 175 | }; 176 | docClient.delete(params, next); 177 | }, 178 | function(results, next) { 179 | functions.delete_deployments_per_fn(input, next); 180 | } 181 | ], done); 182 | }; 183 | 184 | functions.get_fns = function(input, done) { 185 | console.log('dao.get_projects'); 186 | var params = { 187 | TableName: process.env.FunctionCITable, 188 | KeyConditionExpression: "#hash_key = :hash_key", 189 | ExpressionAttributeNames:{ 190 | "#hash_key": "hash_key" 191 | }, 192 | ExpressionAttributeValues: { 193 | ":hash_key":'function' 194 | } 195 | }; 196 | if (!_.isNil(input.from)) { 197 | params.ExclusiveStartKey = { 198 | sort_key: from, 199 | hash_key: 'function' 200 | }; 201 | } 202 | 203 | docClient.query(params, done); 204 | }; 205 | 206 | functions.put_build = function(input, done) { 207 | console.log('dao.put_build'); 208 | input.hash_key = input.project_id; 209 | input.sort_key = input.version; 210 | var params = { 211 | TableName: process.env.FunctionCITable, 212 | Item: input 213 | }; 214 | console.log(JSON.stringify(params)); 215 | docClient.put(params, done); 216 | }; 217 | 218 | functions.get_build = function(input, done) { 219 | console.log('dao.get_build'); 220 | var params = { 221 | TableName: process.env.FunctionCITable, 222 | Key:{ 223 | "hash_key": input.project_id, 224 | "sort_key": input.version 225 | } 226 | }; 227 | docClient.get(params, done); 228 | }; 229 | 230 | functions.get_builds_per_project = function(input, done) { 231 | console.log('dao.get_builds_per_project'); 232 | var params = { 233 | TableName: process.env.FunctionCITable, 234 | ScanIndexForward: false, 235 | KeyConditionExpression: "#hash_key = :hash_key", 236 | ExpressionAttributeNames:{ 237 | "#hash_key": "hash_key" 238 | }, 239 | ExpressionAttributeValues: { 240 | ":hash_key": input.project_id 241 | }, 242 | Limit: input.limit | 3 243 | }; 244 | if (!_.isNil(input.from)) { 245 | params.ExclusiveStartKey = { 246 | sort_key: from, 247 | hash_key: input.short_name 248 | }; 249 | } 250 | 251 | docClient.query(params, done); 252 | }; 253 | 254 | functions.delete_builds = function(builds, done) { 255 | 256 | var params = { 257 | RequestItems: {} 258 | }; 259 | params.RequestItems[process.env.FunctionCITable] = []; 260 | _.forEach(builds, function(build) { 261 | params.RequestItems[process.env.FunctionCITable].push({ 262 | DeleteRequest: { 263 | Key: { 264 | hash_key: build.project_id, 265 | sort_key: build.version 266 | } 267 | } 268 | }); 269 | }); 270 | docClient.batchWrite(params, done); 271 | }; 272 | 273 | functions.delete_builds_per_project = function(input, done) { 274 | async.waterfall([ 275 | async.constant([]), 276 | function(builds, next_waterfall) { 277 | var complete = false; 278 | var params = { 279 | project_id: input.project_id, 280 | limit: 25 281 | }; 282 | async.until(function() { 283 | return complete; 284 | }, function(next) { 285 | functions.get_builds_per_project(params, function(err, data) { 286 | if (err) return next(err); 287 | builds = _.union(builds, data.Items); 288 | if (data.LastEvaluatedKey) { 289 | params.from = data.LastEvaluatedKey; 290 | } else { 291 | complete = true; 292 | } 293 | next(); 294 | }); 295 | }, function(err) { 296 | next_waterfall(err, builds); 297 | }); 298 | }, 299 | function(builds, next_waterfall) { 300 | functions.delete_builds(builds, next_waterfall); 301 | } 302 | ], done); 303 | }; 304 | 305 | functions.put_deployment = function(input, done) { 306 | console.log('dao.put_build'); 307 | input.hash_key = input.fn_short_name; 308 | input.sort_key = input.deployment_date; 309 | var params = { 310 | TableName: process.env.FunctionCITable, 311 | Item: input 312 | }; 313 | console.log(JSON.stringify(params)); 314 | docClient.put(params, done); 315 | }; 316 | 317 | functions.get_deployments_per_fn = function(input, done) { 318 | console.log('dao.get_projects'); 319 | var params = { 320 | TableName: process.env.FunctionCITable, 321 | ScanIndexForward: false, 322 | KeyConditionExpression: "#hash_key = :hash_key", 323 | ExpressionAttributeNames:{ 324 | "#hash_key": "hash_key" 325 | }, 326 | ExpressionAttributeValues: { 327 | ":hash_key": input.short_name 328 | }, 329 | Limit: input.limit | 3 330 | }; 331 | if (!_.isNil(input.from)) { 332 | params.ExclusiveStartKey = { 333 | sort_key: from, 334 | hash_key: input.short_name 335 | }; 336 | } 337 | 338 | docClient.query(params, done); 339 | }; 340 | 341 | functions.delete_deployments = function(deployments, done) { 342 | 343 | var params = { 344 | RequestItems: {} 345 | }; 346 | params.RequestItems[process.env.FunctionCITable] = []; 347 | _.forEach(deployments, function(deployment) { 348 | params.RequestItems[process.env.FunctionCITable].push({ 349 | DeleteRequest: { 350 | Key: { 351 | hash_key: deployment.fn_short_name, 352 | sort_key: deployment.deployment_date 353 | } 354 | } 355 | }); 356 | }); 357 | docClient.batchWrite(params, done); 358 | }; 359 | 360 | functions.delete_deployments_per_fn = function(input, done) { 361 | async.waterfall([ 362 | async.constant([]), 363 | function(deployments, next_waterfall) { 364 | var complete = false; 365 | var params = { 366 | short_name: input.short_name, 367 | limit: 25 368 | }; 369 | async.until(function() { 370 | return complete; 371 | }, function(next) { 372 | functions.get_deployments_per_fn(params, function(err, data) { 373 | if (err) return next(err); 374 | deployments = _.union(deployments, data.Items); 375 | if (data.LastEvaluatedKey) { 376 | params.from = data.LastEvaluatedKey; 377 | } else { 378 | complete = true; 379 | } 380 | next(); 381 | }); 382 | }, function(err) { 383 | next_waterfall(err, deployments); 384 | }); 385 | }, 386 | function(deployments, next_waterfall) { 387 | functions.delete_deployments(deployments, next_waterfall); 388 | } 389 | ], done); 390 | }; 391 | 392 | module.exports = functions; 393 | -------------------------------------------------------------------------------- /app/lib/handle_codebuild.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const slack = require('./slack'); 5 | const dao = require('./dao'); 6 | 7 | var functions = {}; 8 | 9 | functions.handle = function(event, callback) { 10 | console.log('handle_codebuild'); 11 | const project_id = _.replace(_.replace(event.detail['project-name'], 'functionci-', ''), '-code-build', ''); 12 | 13 | dao.get_project({ 14 | project_id: project_id 15 | }, function(err, data) { 16 | if (err) { 17 | console.log(err); 18 | return callback(); 19 | } 20 | const status = event.detail['build-status']; 21 | var text = 'Build '+ status + ' - ' + data.Item.project_id+''; 22 | if (_.isEqual(status, 'FAILED')) { 23 | text += '\n'; 24 | text += ''; 25 | } 26 | var d = new Date(); 27 | var seconds = d.getTime() / 1000; 28 | var message = { 29 | channel: data.Item.channel, 30 | attachments: [ 31 | { 32 | "fallback": text, 33 | "color": _.isEqual(status, 'FAILED') ? 'danger' : "#dddddd", 34 | "text": text, 35 | "ts": seconds 36 | } 37 | ] 38 | }; 39 | slack.post_message(message, callback); 40 | }); 41 | }; 42 | 43 | module.exports = functions; -------------------------------------------------------------------------------- /app/lib/handle_codepipeline.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const slack = require('./slack'); 5 | const dao = require('./dao'); 6 | const s3 = require('./s3'); 7 | const async = require('async'); 8 | 9 | const AWS = require('aws-sdk'); 10 | const codepipeline = new AWS.CodePipeline(); 11 | 12 | // Notify AWS CodePipeline of a successful job 13 | var putJobSuccess = function(jobId, message, context) { 14 | console.log('putJobSuccess'); 15 | var params = { 16 | jobId: jobId 17 | }; 18 | codepipeline.putJobSuccessResult(params, function(err, data) { 19 | if(err) { 20 | context.fail(err); 21 | } else { 22 | context.succeed(message); 23 | } 24 | }); 25 | }; 26 | 27 | // Notify AWS CodePipeline of a failed job 28 | var putJobFailure = function(jobId, message, context) { 29 | console.log('putJobFailure'); 30 | var params = { 31 | jobId: jobId, 32 | failureDetails: { 33 | message: JSON.stringify(message), 34 | type: 'JobFailed', 35 | externalExecutionId: context.invokeid 36 | } 37 | }; 38 | codepipeline.putJobFailureResult(params, function(err, data) { 39 | context.fail(message); 40 | }); 41 | }; 42 | 43 | var functions = {}; 44 | 45 | functions.handle = function(event, context) { 46 | console.log('handle_codepipeline'); 47 | 48 | // Retrieve the Job ID from the Lambda action 49 | var jobId = event["CodePipeline.job"].id; 50 | 51 | if (event["CodePipeline.job"].data.inputArtifacts[0].revision) { 52 | var SourceOutput = event["CodePipeline.job"].data.inputArtifacts[0]; 53 | var BuildOutput = event["CodePipeline.job"].data.inputArtifacts[1]; 54 | } else { 55 | var SourceOutput = event["CodePipeline.job"].data.inputArtifacts[1]; 56 | var BuildOutput = event["CodePipeline.job"].data.inputArtifacts[0]; 57 | } 58 | var bucket = BuildOutput.location.s3Location.bucketName; 59 | var key = BuildOutput.location.s3Location.objectKey; 60 | var project_id = _.replace(event["CodePipeline.job"].data.actionConfiguration.configuration.UserParameters, 'functionci-', ''); 61 | 62 | var commit = SourceOutput.revision; 63 | 64 | async.waterfall([ 65 | function(next) { 66 | // Get the project 67 | dao.get_project({ 68 | project_id: project_id 69 | }, next); 70 | }, 71 | function(project_data, next) { 72 | // Increment the build counter. 73 | dao.update_project_build_count({ 74 | project_id: project_id 75 | }, function(err, data) { 76 | if (err) return next(err); 77 | project_data.Item.build_count = data.Attributes.build_count; 78 | next(null, project_data.Item); 79 | }); 80 | }, 81 | function(project, next) { 82 | // Move the zip file. 83 | s3.copy(bucket, key, project_id+'/'+project_id+'-'+project.build_count, function(err, results) { 84 | if (err) return next(err); 85 | next(null, project); 86 | }); 87 | }, 88 | function(project, next) { 89 | // Save the build. 90 | var now = new Date(); 91 | dao.put_build({ 92 | project_id: project.project_id, 93 | build_date: now + '', 94 | version: project.build_count+'', 95 | commit: commit, 96 | bucket: bucket, 97 | key: project_id+'/'+project_id+'-'+project.build_count 98 | }, function(err, results) { 99 | if (err) return next(err); 100 | next(null, project); 101 | }); 102 | }, 103 | function(project, next) { 104 | // Notify slack 105 | var text = 'Build '+ project.build_count + ' is ready - ' + project.project_id+''; 106 | var d = new Date(); 107 | var seconds = d.getTime() / 1000; 108 | var message = { 109 | channel: project.channel, 110 | attachments: [ 111 | { 112 | "fallback": text, 113 | "color": 'good', 114 | "text": text, 115 | "ts": seconds 116 | } 117 | ] 118 | }; 119 | slack.post_message(message, next); 120 | } 121 | ], function(err, results) { 122 | if (err) { 123 | return putJobFailure(jobId, err, context); 124 | } else { 125 | return putJobSuccess(jobId, '', context); 126 | } 127 | }); 128 | }; 129 | 130 | module.exports = functions; -------------------------------------------------------------------------------- /app/lib/handle_slack_commands.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const async = require('async'); 5 | 6 | const slack = require('./slack'); 7 | const dao = require('./dao'); 8 | const cloudformation = require('./cloudformation'); 9 | const lambda = require('./lambda'); 10 | 11 | const VALID_COMMANDS = ['create project', 'delete project', 'show project', 'show projects', 'deploy fn', 'show fn', 'add fn', 'show fns', 'delete fn']; 12 | 13 | var functions = {}; 14 | 15 | functions.add_fn = function(params, callback) { 16 | if (_.isEqual(_.toLower(_.head(params.options)), 'help')) { 17 | return callback(null, { 18 | statusCode: 200, 19 | body: 'Use this command to add a new function. Usage: /fn add fn ' 20 | }); 21 | } 22 | 23 | if (_.size(params.options) != 3) { 24 | return callback(null, { 25 | statusCode: 200, 26 | body: 'Invalid format. Usage: /fn add fn ' 27 | }); 28 | } 29 | 30 | const short_name = _.toLower(_.head(params.options)); 31 | params.options = _.drop(params.options); 32 | const arn = _.head(params.options); 33 | 34 | dao.put_fn({ 35 | short_name: short_name, 36 | arn: arn 37 | }, function(err, data) { 38 | if (err && _.isString(err) && _.includes(err, 'already exists')) { 39 | 40 | return callback(null, { 41 | statusCode: 200, 42 | body: JSON.stringify({ 43 | response_type: "in_channel", 44 | text: err 45 | }) 46 | }); 47 | } else if (err) { 48 | console.log(err); 49 | return callback(null, { 50 | statusCode: 200, 51 | body: JSON.stringify({ 52 | response_type: "in_channel", 53 | text: 'Unable to complete this command. Please try again or check the logs.' 54 | }) 55 | }); 56 | } else { 57 | return callback(null, { 58 | statusCode: 200, 59 | body: JSON.stringify({ 60 | response_type: "in_channel", 61 | text: 'Function has been added.' 62 | }) 63 | }); 64 | } 65 | }); 66 | }; 67 | 68 | functions.delete_fn = function(params, callback) { 69 | if (_.isEqual(_.toLower(_.head(params.options)), 'help')) { 70 | return callback(null, { 71 | statusCode: 200, 72 | body: 'Use this command to delete a function. Usage: /fn delete fn ' 73 | }); 74 | } 75 | 76 | if (_.size(params.options) != 2) { 77 | return callback(null, { 78 | statusCode: 200, 79 | body: 'Invalid format. Usage: /fn delete fn ' 80 | }); 81 | } 82 | 83 | const short_name = _.toLower(_.head(params.options)); 84 | 85 | dao.delete_fn({ 86 | short_name: short_name 87 | }, function(err, data) { 88 | if (err) { 89 | console.log(err); 90 | return callback(null, { 91 | statusCode: 200, 92 | body: JSON.stringify({ 93 | response_type: "in_channel", 94 | text: 'Unable to complete this command. Please try again or check the logs.' 95 | }) 96 | }); 97 | } else { 98 | return callback(null, { 99 | statusCode: 200, 100 | body: JSON.stringify({ 101 | response_type: "in_channel", 102 | text: 'Function has been deleted.' 103 | }) 104 | }); 105 | } 106 | }); 107 | }; 108 | 109 | functions.show_fn = function(params, callback) { 110 | if (_.isEqual(_.toLower(_.head(params.options)), 'help')) { 111 | return callback(null, { 112 | statusCode: 200, 113 | body: 'Use this command to show a functions deployment history. Usage: /fn show fn ' 114 | }); 115 | } 116 | 117 | if (_.size(params.options) != 2) { 118 | return callback(null, { 119 | statusCode: 200, 120 | body: 'Invalid format. Usage: /fn show fn ' 121 | }); 122 | } 123 | const short_name = _.toLower(_.head(params.options)); 124 | 125 | dao.get_deployments_per_fn({ 126 | short_name: short_name 127 | }, function(err, data) { 128 | if (err) { 129 | return callback(null, { 130 | statusCode: 200, 131 | body: { 132 | message: 'Unable to complete this command. Please try again or check the logs.', 133 | err: err 134 | } 135 | }); 136 | } else { 137 | 138 | // Notify slack 139 | var message = { 140 | response_type: "in_channel", 141 | pretext: 'Deployment History', 142 | attachments: [] 143 | }; 144 | 145 | _.forEach(data.Items, function(item) { 146 | var d = new Date(item.deployment_date); 147 | var seconds = d.getTime() / 1000; 148 | message.attachments.push( 149 | { 150 | "fallback": '', 151 | "color": '#dddddd', 152 | "text": '', 153 | fields: [ 154 | { 155 | title: 'project', 156 | value: item.project_id, 157 | short: true 158 | }, 159 | { 160 | title: 'version', 161 | value: item.build_version, 162 | short: true 163 | }, 164 | { 165 | title: 'user', 166 | value: '@'+item.user_name, 167 | short: true 168 | } 169 | ], 170 | ts: seconds 171 | }); 172 | }); 173 | // TODO if (!_.isNil(data.LastEvaluatedKey)) 174 | 175 | return callback(null, { 176 | statusCode: 200, 177 | body: JSON.stringify(message) 178 | }); 179 | } 180 | }); 181 | }; 182 | 183 | functions.deploy_fn = function(params, callback) { 184 | if (_.isEqual(_.toLower(_.head(params.options)), 'help')) { 185 | return callback(null, { 186 | statusCode: 200, 187 | body: 'Use this command to deploy a function. Usage: /fn deploy fn ' 188 | }); 189 | } 190 | 191 | if (_.size(params.options) != 4) { 192 | return callback(null, { 193 | statusCode: 200, 194 | body: 'Invalid format. Usage: /fn deploy fn ' 195 | }); 196 | } 197 | 198 | const short_name = _.toLower(_.head(params.options)); 199 | params.options = _.drop(params.options); 200 | 201 | const project_id = _.toLower(_.head(params.options)); 202 | params.options = _.drop(params.options); 203 | 204 | const version = _.toLower(_.head(params.options)); 205 | params.options = _.drop(params.options); 206 | 207 | const user_name = _.toLower(_.head(params.options)); 208 | 209 | async.waterfall([ 210 | function(next) { 211 | console.log('get fn'); 212 | dao.get_fn({ 213 | short_name: short_name 214 | }, next); 215 | }, 216 | function(fn_results, next) { 217 | console.log('get build'); 218 | dao.get_build({ 219 | project_id: project_id, 220 | version: version 221 | }, function(err, build_results) { 222 | if (err) return next(err); 223 | next(null, { 224 | fn: fn_results.Item, 225 | build: build_results.Item 226 | }); 227 | }); 228 | }, 229 | function(results, next) { 230 | console.log('deploy lambda'); 231 | console.log(JSON.stringify(results, null, 3)); 232 | lambda.deploy({ 233 | arn: results.fn.arn, 234 | bucket: results.build.bucket, 235 | key: results.build.key 236 | }, function(err, deploy_results) { 237 | if (err) return next(err); 238 | results.deployment = deploy_results; 239 | next(null, results); 240 | }); 241 | }, 242 | function(results, next) { 243 | console.log('save deployment'); 244 | var now = new Date(); 245 | dao.put_deployment({ 246 | fn_short_name: short_name, 247 | deployment_date: now + '', 248 | build_version: version, 249 | project_id: project_id, 250 | lambda_version: results.deployment.Version, 251 | user_name: user_name 252 | }, next); 253 | } 254 | ], function(err, results) { 255 | if (err) { 256 | console.log(err); 257 | return callback(null, { 258 | statusCode: 200, 259 | body: JSON.stringify({ 260 | response_type: "in_channel", 261 | text: 'Unable to complete this command. Please try again or check the logs.' 262 | }) 263 | }); 264 | } else { 265 | return callback(null, { 266 | statusCode: 200, 267 | body: JSON.stringify({ 268 | response_type: "in_channel", 269 | text: project_id+ ', version '+version+' has been deployed to '+short_name+'.' 270 | }) 271 | }); 272 | } 273 | }); 274 | }; 275 | 276 | functions.show_fns = function(params, callback) { 277 | if (_.isEqual(_.toLower(_.head(params.options)), 'help')) { 278 | return callback(null, { 279 | statusCode: 200, 280 | body: 'Use this command to show all functions. Usage: /fn show fns' 281 | }); 282 | } 283 | 284 | dao.get_fns({}, function(err, data) { 285 | if (err) { 286 | console.log(err); 287 | return callback(null, { 288 | statusCode: 200, 289 | body: JSON.stringify({ 290 | response_type: "in_channel", 291 | text: 'Unable to complete this command. Please try again or check the logs.' 292 | }) 293 | }); 294 | } else { 295 | var text = ''; 296 | var div = ''; 297 | _.forEach(data.Items, function(item) { 298 | text += div + item.short_name + ' -> ' + item.arn; 299 | div = '\n'; 300 | }); 301 | // TODO if (!_.isNil(data.LastEvaluatedKey)) 302 | if (_.isEmpty(text)) { 303 | text = 'You have no functions.'; 304 | } else { 305 | text = 'Functions:'+div+text; 306 | } 307 | return callback(null, { 308 | statusCode: 200, 309 | body: JSON.stringify({ 310 | response_type: "in_channel", 311 | text: text 312 | }) 313 | }); 314 | } 315 | }); 316 | }; 317 | 318 | functions.create_project = function(params, callback) { 319 | console.log('handle_slack_commands.create_project'); 320 | if (_.isEqual(_.toLower(_.head(params.options)), 'help')) { 321 | return callback(null, { 322 | statusCode: 200, 323 | body: 'Use this command to create a new build project.' 324 | }); 325 | } 326 | slack.create_dialog(params.trigger_id, function(err, body) { 327 | if (err) { 328 | console.log(err); 329 | return callback(null, { 330 | statusCode: 200, 331 | body: JSON.stringify({ 332 | response_type: "in_channel", 333 | text: 'Unable to complete this command. Please try again or check the logs.' 334 | }) 335 | }); 336 | } else if (!body.ok) { 337 | return callback(null, { 338 | statusCode: 200, 339 | body: body.error 340 | }); 341 | } else { 342 | return callback(null, { 343 | statusCode: 200, 344 | body: '' 345 | }); 346 | } 347 | }); 348 | }; 349 | 350 | functions.delete_project = function(params, callback) { 351 | if (_.isEqual(_.toLower(_.head(params.options)), 'help')) { 352 | return callback(null, { 353 | statusCode: 200, 354 | body: 'Use this command to delete a project. Usage: /fn delete project ' 355 | }); 356 | } 357 | 358 | if (_.size(params.options) != 2) { 359 | return callback(null, { 360 | statusCode: 200, 361 | body: 'Invalid format. Usage: /fn delete project ' 362 | }); 363 | } 364 | 365 | const project_id = _.toLower(_.head(params.options)); 366 | 367 | cloudformation.delete_stack({ 368 | project_id: project_id 369 | }, function(err, data) { 370 | if (err) { 371 | console.log(err); 372 | return callback(null, { 373 | statusCode: 200, 374 | body: JSON.stringify({ 375 | response_type: "in_channel", 376 | text: 'Unable to complete this command. Please try again or check the logs.' 377 | }) 378 | }); 379 | } else { 380 | return callback(null, { 381 | statusCode: 200, 382 | body: JSON.stringify({ 383 | response_type: "in_channel", 384 | text: 'Project is being deleted.' 385 | }) 386 | }); 387 | } 388 | }); 389 | }; 390 | 391 | functions.show_project = function(params, callback) { 392 | if (_.isEqual(_.toLower(_.head(params.options)), 'help')) { 393 | return callback(null, { 394 | statusCode: 200, 395 | body: 'Use this command to show the build history of a project. Usage: /fn show project ' 396 | }); 397 | } 398 | const project_id = _.toLower(_.head(params.options)); 399 | 400 | dao.get_builds_per_project({ 401 | project_id: project_id 402 | }, function(err, data) { 403 | if (err) { 404 | return callback(null, { 405 | statusCode: 200, 406 | body: { 407 | message: 'Unable to complete this command. Please try again or check the logs.', 408 | err: err 409 | } 410 | }); 411 | } else { 412 | 413 | // Notify slack 414 | var message = { 415 | response_type: "in_channel", 416 | pretext: 'Build History', 417 | attachments: [] 418 | }; 419 | 420 | _.forEach(data.Items, function(item) { 421 | var d = new Date(item.build_date); 422 | var seconds = d.getTime() / 1000; 423 | message.attachments.push( 424 | { 425 | "fallback": '', 426 | "color": '#dddddd', 427 | "text": '', 428 | fields: [ 429 | { 430 | title: 'version', 431 | value: item.version, 432 | short: true 433 | }, 434 | { 435 | title: 'commit', 436 | value: _.truncate(item.commit, {length: 6, omission: ''}), 437 | short: true 438 | } 439 | ], 440 | ts: seconds 441 | }); 442 | }); 443 | // TODO if (!_.isNil(data.LastEvaluatedKey)) 444 | 445 | return callback(null, { 446 | statusCode: 200, 447 | body: JSON.stringify(message) 448 | }); 449 | } 450 | }); 451 | }; 452 | 453 | functions.show_projects = function(params, callback) { 454 | if (_.isEqual(_.toLower(_.head(params.options)), 'help')) { 455 | return callback(null, { 456 | statusCode: 200, 457 | body: 'Use this command to show all projects. Usage: /fn show projects' 458 | }); 459 | } 460 | 461 | dao.get_projects({}, function(err, data) { 462 | if (err) { 463 | console.log(err); 464 | return callback(null, { 465 | statusCode: 200, 466 | body: JSON.stringify({ 467 | response_type: "in_channel", 468 | text: 'Unable to complete this command. Please try again or check the logs.' 469 | }) 470 | }); 471 | } else { 472 | var text = ''; 473 | var div = ''; 474 | _.forEach(data.Items, function(item) { 475 | text += div + item.project_id + ' -> ' + item.github_url; 476 | div = '\n'; 477 | }); 478 | // TODO if (!_.isNil(data.LastEvaluatedKey)) 479 | if (_.isEmpty(text)) { 480 | text = 'You have no projects.'; 481 | } else { 482 | text = 'Projects:'+div + text; 483 | } 484 | return callback(null, { 485 | statusCode: 200, 486 | body: JSON.stringify({ 487 | response_type: "in_channel", 488 | text: text 489 | }) 490 | }); 491 | } 492 | }); 493 | }; 494 | 495 | functions.handle = function(body, callback) { 496 | console.log('handle_slack_commands.handle'); 497 | // Is the token valid? 498 | if (!_.isEqual(body.token, process.env.SlackVerificationToken)) { 499 | return callback(null, { 500 | statusCode: 200, 501 | body: 'Invalid token' 502 | }); 503 | } 504 | 505 | // Is the command valid? 506 | if (_.isNil(body.text) || _.isEmpty(body.text)) { 507 | return callback(null, { 508 | statusCode: 200, 509 | body: 'Invalid command' 510 | }); 511 | } 512 | 513 | // Get the command. 514 | var options = _.split(body.text, /[ ]+/); 515 | var command = _.toLower(_.head(options)); 516 | options = _.drop(options); 517 | command = command + ' ' + _.toLower(_.head(options)); 518 | options = _.drop(options); 519 | options.push(body.user_name); 520 | 521 | // Validate the command 522 | if (!_.includes(VALID_COMMANDS, command)) { 523 | return callback(null, { 524 | statusCode: 200, 525 | body: 'Invalid command. Must be one of ['+ _.join(VALID_COMMANDS) +'].' 526 | }); 527 | } 528 | 529 | // Execute the command 530 | functions[_.replace(command, ' ', '_')]( 531 | { 532 | options: options, 533 | trigger_id: body.trigger_id 534 | }, callback); 535 | }; 536 | 537 | module.exports = functions; -------------------------------------------------------------------------------- /app/lib/handle_slack_interactive_components.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const cloudformation = require('./cloudformation'); 5 | const dao = require('./dao'); 6 | const async = require('async'); 7 | 8 | var functions = {}; 9 | 10 | functions.handle_create_project_dialog = function(payload, callback) { 11 | console.log('create_project'); 12 | // "github_repo": "repo", 13 | // "github_branch": "branch", 14 | // "codebuid_compute_type": "BUILD_GENERAL1_SMALL", 15 | // "codebuid_image": "aws/codebuild/eb-nodejs-6.10.0-amazonlinux-64:4.0.0" 16 | //https://github.com/thestackshack/cim 17 | 18 | const github_url = payload.submission.github_repo; 19 | const github_branch = payload.submission.github_branch; 20 | const codebuid_compute_type = payload.submission.codebuid_compute_type; 21 | const codebuid_image = payload.submission.codebuid_image; 22 | const channel = payload.submission.channel; 23 | const user_id = payload.user.id; 24 | const user_name = payload.user.name; 25 | 26 | const github_repo_parts = _.split(_.replace(github_url, 'https://github.com/', ''), '/'); 27 | if (_.size(github_repo_parts) != 2) { 28 | return callback(null, { 29 | statusCode: 200, 30 | body: 'Invalid github repo. Valid format is https://github.com//' 31 | }); 32 | } 33 | const github_owner = github_repo_parts[0]; 34 | const github_repo = github_repo_parts[1]; 35 | const project_id = _.replace(_.join([github_repo, github_branch], '-'), /[^a-z0-9A-Z-]/g, '-'); 36 | 37 | const params = { 38 | project_id: project_id, 39 | github_owner: github_owner, 40 | github_repo: github_repo, 41 | github_branch: github_branch, 42 | codebuid_compute_type: codebuid_compute_type, 43 | codebuid_image: codebuid_image, 44 | channel: channel, 45 | github_url: github_url, 46 | user_id: user_id, 47 | user_name: user_name 48 | }; 49 | 50 | async.waterfall([ 51 | function(next) { 52 | cloudformation.build_stack_up(params, next); 53 | }, 54 | function(next) { 55 | dao.put_project(params, next); 56 | } 57 | ], function(err, results) { 58 | if (err) { 59 | console.log(err); 60 | return callback(null, { 61 | statusCode: 200, 62 | body: JSON.stringify({ 63 | "errors": [ 64 | { 65 | "name": "github_repo", 66 | "error": err 67 | } 68 | ] 69 | }) 70 | }); 71 | } else { 72 | return callback(null, { 73 | statusCode: 200, 74 | body: '' 75 | }); 76 | } 77 | }); 78 | }; 79 | 80 | functions.handle = function(body, callback) { 81 | console.log('slack_interactive_components'); 82 | var payload = JSON.parse(body.payload); 83 | console.log(JSON.stringify(payload, null, 3)); 84 | 85 | // Is the token valid? 86 | if (!_.isEqual(payload.token, process.env.SlackVerificationToken)) { 87 | return callback(null, { 88 | statusCode: 200, 89 | body: 'Invalid token' 90 | }); 91 | } 92 | 93 | // Route to the correct handler function. 94 | if (_.startsWith(payload.callback_id, 'create_project')) { 95 | functions.handle_create_project_dialog(payload, callback); 96 | } else { 97 | return callback(null, { 98 | statusCode: 500, 99 | body: 'Unknown slack interactive request.' 100 | }); 101 | } 102 | }; 103 | 104 | module.exports = functions; 105 | -------------------------------------------------------------------------------- /app/lib/handle_sns.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const slack = require('./slack'); 5 | const dao = require('./dao'); 6 | const async = require('async'); 7 | var s3 = require('./s3'); 8 | 9 | var functions = {}; 10 | 11 | functions.handle = function(event, callback) { 12 | console.log('handle_sns'); 13 | var message_items = _.split(event.Sns.Message, '\n'); 14 | var message = {}; 15 | _.forEach(message_items, function(item) { 16 | const item_parts = _.split(item, '='); 17 | if (!_.isEmpty(item_parts[0])) { 18 | message[item_parts[0]] = _.replace(item_parts[1], /'/g, ''); 19 | } 20 | }); 21 | console.log(message); 22 | if (_.isEqual(message.ResourceStatus, 'CREATE_FAILED') && 23 | _.isEqual(message.ResourceType, 'AWS::CloudFormation::Stack')) { 24 | // Tell user it didn't work. 25 | const project_id = _.replace(message.LogicalResourceId, 'functionci-', ''); 26 | async.waterfall([ 27 | function(next) { 28 | dao.get_project({ 29 | project_id: project_id 30 | }, next); 31 | }, 32 | function(data, next) { 33 | dao.delete_project({ 34 | project_id: project_id 35 | }, function(err, results) { 36 | next(err, data); 37 | }); 38 | }, 39 | function(data, next) { 40 | // Notify slack 41 | var text = 'Project creation failed - '+ data.Item.project_id; 42 | var d = new Date(); 43 | var seconds = d.getTime() / 1000; 44 | var message = { 45 | channel: data.Item.channel, 46 | attachments: [ 47 | { 48 | "fallback": text, 49 | "color": 'danger', 50 | "text": text, 51 | "ts": seconds 52 | } 53 | ] 54 | }; 55 | slack.post_message(message, next); 56 | } 57 | ], callback); 58 | } else if (_.isEqual(message.ResourceStatus, 'DELETE_COMPLETE') && 59 | _.isEqual(message.ResourceType, 'AWS::CloudFormation::Stack')) { 60 | const project_id = _.replace(message.LogicalResourceId, 'functionci-', ''); 61 | async.waterfall([ 62 | function(next) { 63 | dao.get_project({ 64 | project_id: project_id 65 | }, next); 66 | }, 67 | function(data, next) { 68 | dao.delete_project({ 69 | project_id: project_id 70 | }, function(err, results) { 71 | next(err, data); 72 | }); 73 | }, 74 | function(data, next) { 75 | s3.delete_with_prefix({ 76 | bucket: process.env.ArtifactsBucket, 77 | prefix: project_id 78 | }, 79 | function(err, results) { 80 | next(err, data); 81 | }); 82 | }, 83 | function(data, next) { 84 | // Notify slack 85 | var text = 'Project deleted - '+ data.Item.project_id; 86 | var d = new Date(); 87 | var seconds = d.getTime() / 1000; 88 | var message = { 89 | channel: data.Item.channel, 90 | attachments: [ 91 | { 92 | "fallback": text, 93 | "color": 'good', 94 | "text": text, 95 | "ts": seconds 96 | } 97 | ] 98 | }; 99 | slack.post_message(message, next); 100 | } 101 | ], callback); 102 | } else if (_.isEqual(message.ResourceStatus, 'CREATE_COMPLETE') && 103 | _.isEqual(message.ResourceType, 'AWS::CloudFormation::Stack')) { 104 | // Add to DB. 105 | // Tell user it worked. 106 | const project_id = _.replace(message.LogicalResourceId, 'functionci-', ''); 107 | async.waterfall([ 108 | function(next) { 109 | dao.get_project({ 110 | project_id: project_id 111 | }, next); 112 | }, 113 | function(data, next) { 114 | // Notify slack 115 | var text = 'Project created - '+ data.Item.project_id; 116 | var d = new Date(); 117 | var seconds = d.getTime() / 1000; 118 | var message = { 119 | channel: data.Item.channel, 120 | attachments: [ 121 | { 122 | "fallback": text, 123 | "color": 'good', 124 | "text": text, 125 | "ts": seconds 126 | } 127 | ] 128 | }; 129 | slack.post_message(message, next); 130 | } 131 | ], callback); 132 | } else { 133 | callback(); 134 | } 135 | }; 136 | module.exports = functions; -------------------------------------------------------------------------------- /app/lib/lambda.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const lambda = new AWS.Lambda(); 3 | 4 | var functions = {}; 5 | 6 | functions.deploy = function(input, done) { 7 | console.log('deploy'); 8 | 9 | var params = { 10 | FunctionName: input.arn, /* required */ 11 | DryRun: false, 12 | Publish: true, 13 | S3Bucket: input.bucket, 14 | S3Key: input.key 15 | }; 16 | lambda.updateFunctionCode(params, done); 17 | }; 18 | 19 | module.exports = functions; -------------------------------------------------------------------------------- /app/lib/resources/cloudformation.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | 3 | Description: 'FunctionCI CodePipeline/CodeBuild for Lambda' 4 | 5 | Parameters: 6 | FunctionCIStack: 7 | Type: String 8 | Description: FunctionCI Stack Name 9 | GitHubOwner: 10 | Type: String 11 | Description: GitHub repository owner 12 | GitHubRepo: 13 | Type: String 14 | Description: GitHub repository name 15 | GitHubBranch: 16 | Type: String 17 | Description: GitHub repository branch 18 | GitHubToken: 19 | Type: String 20 | Description: GitHub repository OAuth token 21 | CodeBuildComputeType: 22 | Type: String 23 | Description: CodeBuildComputeType 24 | Default: 'BUILD_GENERAL1_SMALL' 25 | CodeBuildImage: 26 | Type: String 27 | Description: CodeBuildImage 28 | Default: 'aws/codebuild/eb-nodejs-6.10.0-amazonlinux-64:4.0.0' 29 | CodeBuildType: 30 | Type: String 31 | Description: CodeBuildType 32 | Default: 'LINUX_CONTAINER' 33 | 34 | Resources: 35 | 36 | 37 | # 38 | # Code Build IAM Role 39 | # 40 | CodeBuildRole: 41 | Type: AWS::IAM::Role 42 | Properties: 43 | AssumeRolePolicyDocument: 44 | Version: 2012-10-17 45 | Statement: 46 | - Effect: Allow 47 | Action: 48 | - sts:AssumeRole 49 | Principal: 50 | Service: 51 | - codebuild.amazonaws.com 52 | Policies: 53 | - PolicyName: ServiceRole 54 | PolicyDocument: 55 | Version: 2012-10-17 56 | Statement: 57 | - Sid: CloudWatchWriteLogsPolicy 58 | Effect: Allow 59 | Action: 60 | - logs:CreateLogGroup 61 | - logs:CreateLogStream 62 | - logs:PutLogEvents 63 | Resource: '*' 64 | - Sid: S3GetObjectPolicy 65 | Effect: Allow 66 | Action: 67 | - s3:GetObject 68 | - s3:GetObjectVersion 69 | Resource: '*' 70 | - Sid: S3PutObjectPolicy 71 | Effect: Allow 72 | Action: 73 | - s3:PutObject 74 | Resource: '*' 75 | 76 | # 77 | # Code Pipeline IAM Role 78 | # 79 | CodePipelineRole: 80 | Type: AWS::IAM::Role 81 | Properties: 82 | AssumeRolePolicyDocument: 83 | Version: 2012-10-17 84 | Statement: 85 | - Effect: Allow 86 | Action: 87 | - sts:AssumeRole 88 | Principal: 89 | Service: 90 | - codepipeline.amazonaws.com 91 | ManagedPolicyArns: 92 | - arn:aws:iam::aws:policy/AdministratorAccess 93 | 94 | # 95 | # Code Build project 96 | # 97 | AppCodeBuild: 98 | Type: AWS::CodeBuild::Project 99 | Properties: 100 | Artifacts: 101 | Type: CODEPIPELINE 102 | Environment: 103 | ComputeType: !Ref CodeBuildComputeType 104 | Image: !Ref CodeBuildImage 105 | Type: !Ref CodeBuildType 106 | Name: 107 | Fn::Join: 108 | - '' 109 | - - Ref: AWS::StackName 110 | - '-code-build' 111 | ServiceRole: !GetAtt CodeBuildRole.Arn 112 | Source: 113 | Type: CODEPIPELINE 114 | BuildSpec: buildspec.yml 115 | TimeoutInMinutes: 5 # must be between 5 minutes and 8 hours 116 | 117 | 118 | # 119 | # Code Pipeline 'master' 120 | # 121 | AppCodePipeline: 122 | Type: AWS::CodePipeline::Pipeline 123 | Properties: 124 | ArtifactStore: 125 | Type: S3 126 | Location: 127 | Fn::ImportValue: 128 | !Sub "${FunctionCIStack}-ArtifactsBucket" 129 | RestartExecutionOnUpdate: true 130 | RoleArn: !GetAtt CodePipelineRole.Arn 131 | Stages: 132 | - Name: Source 133 | Actions: 134 | - Name: Source 135 | ActionTypeId: 136 | Category: Source 137 | Owner: ThirdParty 138 | Version: 1 139 | Provider: GitHub 140 | Configuration: 141 | Owner: !Ref GitHubOwner 142 | Repo: !Ref GitHubRepo 143 | Branch: !Ref GitHubBranch 144 | OAuthToken: !Ref GitHubToken 145 | OutputArtifacts: 146 | - Name: SourceOutput 147 | RunOrder: 1 148 | - Name: Build 149 | Actions: 150 | - Name: BuildAndTest 151 | ActionTypeId: 152 | Category: Build 153 | Owner: AWS 154 | Provider: CodeBuild 155 | Version: 1 156 | Configuration: 157 | ProjectName: !Ref AppCodeBuild 158 | InputArtifacts: 159 | - Name: SourceOutput 160 | OutputArtifacts: 161 | - Name: BuildOutput 162 | - Name: Package 163 | Actions: 164 | - Name: Package 165 | ActionTypeId: 166 | Category: Invoke 167 | Owner: AWS 168 | Provider: Lambda 169 | Version: 1 170 | InputArtifacts: 171 | - Name: BuildOutput 172 | - Name: SourceOutput 173 | Configuration: 174 | FunctionName: 175 | Fn::ImportValue: 176 | !Sub "${FunctionCIStack}-LambdaFunction" 177 | UserParameters: 178 | Ref: AWS::StackName 179 | 180 | # 181 | # CloudWatch Event to trigger lambda for build slack notifications. 182 | # 183 | BuildEventRule: 184 | Type: 'AWS::Events::Rule' 185 | Properties: 186 | Description: 'BuildEventRule' 187 | EventPattern: 188 | source: 189 | - 'aws.codebuild' 190 | detail-type: 191 | - 'CodeBuild Build State Change' 192 | detail: 193 | project-name: 194 | - Fn::Join: 195 | - '' 196 | - - Ref: AWS::StackName 197 | - '-code-build' 198 | build-status: 199 | - 'IN_PROGRESS' 200 | - 'SUCCEEDED' 201 | - 'FAILED' 202 | - 'STOPPED' 203 | State: 'ENABLED' 204 | Targets: 205 | - 206 | Arn: 207 | Fn::ImportValue: 208 | !Sub "${FunctionCIStack}-LambdaFunctionArn" 209 | Id: 'BuildRuleLambdaTarget' 210 | 211 | # 212 | # Permission for CloudWatch to invoke our Lambda 213 | # 214 | PermissionForBuildEventsToInvokeLambda: 215 | Type: 'AWS::Lambda::Permission' 216 | Properties: 217 | FunctionName: 218 | Fn::ImportValue: 219 | !Sub "${FunctionCIStack}-LambdaFunction" 220 | Action: 'lambda:InvokeFunction' 221 | Principal: 'events.amazonaws.com' 222 | SourceArn: !GetAtt BuildEventRule.Arn -------------------------------------------------------------------------------- /app/lib/s3.js: -------------------------------------------------------------------------------- 1 | 2 | const AWS = require('aws-sdk'); 3 | const async = require('async'); 4 | const _ = require('lodash'); 5 | const s3 = new AWS.S3(); 6 | 7 | 8 | var functions = {}; 9 | 10 | functions.copy = function(bucket, source_key, dest_key, done) { 11 | console.log('copy_build_artifact'); 12 | var params = { 13 | Bucket: bucket, 14 | CopySource: '/'+bucket+'/'+source_key, 15 | Key: dest_key 16 | }; 17 | console.log(JSON.stringify(params)); 18 | s3.copyObject(params, done); 19 | }; 20 | 21 | functions.delete_with_prefix = function(input, done) { 22 | console.log('delete_with_prefix'); 23 | var complete = false; 24 | var nextMarker; 25 | async.until(function() { 26 | return complete; 27 | }, function(next) { 28 | const params = { 29 | Bucket: input.bucket, 30 | Prefix: input.prefix, 31 | Marker: nextMarker 32 | }; 33 | console.log(JSON.stringify(params, null, 3)); 34 | s3.listObjects(params, function(err, data) { 35 | if (err) return next(err); 36 | console.log(JSON.stringify(data, null, 3)); 37 | complete = !data.IsTruncated; 38 | if (_.size(data.Contents) > 0) { 39 | var objects = []; 40 | _.forEach(data.Contents, function (content) { 41 | objects.push({ 42 | Key: content.Key 43 | }); 44 | nextMarker = content.Key; 45 | }); 46 | s3.deleteObjects({ 47 | Bucket: input.bucket, 48 | Delete: { 49 | Objects: objects 50 | } 51 | }, function (err, data) { 52 | console.log(JSON.stringify(data, null, 3)); 53 | next(err); 54 | }); 55 | } else { 56 | next(); 57 | } 58 | }); 59 | }, function(err) { 60 | done(err, null); 61 | }); 62 | }; 63 | 64 | module.exports = functions; -------------------------------------------------------------------------------- /app/lib/slack.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | const codebuid = require('./codebuild'); 3 | const _ = require('lodash'); 4 | const async = require('async'); 5 | 6 | var functions = {}; 7 | 8 | functions.channels_list = function(params, done) { 9 | console.log('channels_list'); 10 | var url = 'https://slack.com/api/channels.list'; 11 | var options = { 12 | url: url, 13 | headers: { 14 | 'Authorization':'Bearer '+process.env.SlackBotOAuthToken 15 | }, 16 | json: true, 17 | method: 'POST', 18 | body: { 19 | limit: params.limit 20 | } 21 | }; 22 | if (params.cursor) { 23 | options.body.cursor = cursor; 24 | } 25 | request(options, function(err, response, body) { 26 | done(err, body); 27 | }); 28 | }; 29 | 30 | functions.groups_list = function(params, done) { 31 | console.log('groups_list'); 32 | var url = 'https://slack.com/api/groups.list'; 33 | var options = { 34 | url: url, 35 | headers: { 36 | 'Authorization':'Bearer '+process.env.SlackBotOAuthToken 37 | }, 38 | json: true, 39 | method: 'GET' 40 | }; 41 | request(options, function(err, response, body) { 42 | done(err, body); 43 | }); 44 | }; 45 | 46 | functions.fetch_all_channels = function(params, done) { 47 | console.log('fetch_all_channels'); 48 | async.waterfall([ 49 | async.constant([]), 50 | function(channels, next_waterfall) { 51 | var complete = false; 52 | var params = { 53 | limit: 200 54 | }; 55 | async.until(function() { 56 | return complete; 57 | }, function(next) { 58 | functions.channels_list(params, function(err, results) { 59 | if (err) return next(err); 60 | if (!results.ok) return next(results.error); 61 | 62 | if ((results.response_metadata && results.response_metadata.next_cursor) 63 | && _.size(results.channels) == params.limit) { 64 | params.cursor = results.response_metadata.next_cursor; 65 | } else { 66 | complete = true; 67 | } 68 | 69 | // Add to the channels array. 70 | channels = _.union(channels, _.map(results.channels, function(channel) { 71 | return { 72 | id: channel.id, 73 | name: channel.name 74 | } 75 | })); 76 | 77 | next(); 78 | }); 79 | }, function(err) { 80 | if (err) return done(err); 81 | next_waterfall(null, channels); 82 | }); 83 | }, 84 | function(channels, next) { 85 | functions.groups_list({}, function(err, results) { 86 | if (err) return next(err); 87 | channels = _.union(channels, _.map(results.groups, function(group) { 88 | return { 89 | id: group.id, 90 | name: group.name 91 | } 92 | })); 93 | next(null, channels); 94 | }); 95 | } 96 | ], done); 97 | }; 98 | 99 | functions.post_message = function(params, done) { 100 | 101 | var url = 'https://slack.com/api/chat.postMessage'; 102 | var options = { 103 | url: url, 104 | headers: { 105 | 'Authorization':'Bearer '+process.env.SlackBotOAuthToken 106 | }, 107 | json: true, 108 | method: 'POST', 109 | body: params 110 | }; 111 | console.log(JSON.stringify(options, null, 3)); 112 | request(options, function(err, response, body) { 113 | console.log(JSON.stringify(body, null, 3)); 114 | done(err); 115 | }); 116 | }; 117 | 118 | functions.create_dialog = function(trigger_id, done) { 119 | console.log('slack.create_dialog'); 120 | async.waterfall([ 121 | async.constant({trigger_id: trigger_id}), 122 | function(params, next) { 123 | codebuid.listCuratedEnvironmentImages(function(err, images) { 124 | if (err) return done(err); 125 | params.images = images; 126 | next(null, params); 127 | }); 128 | }, 129 | function(params, next) { 130 | functions.fetch_all_channels({}, function(err, channels) { 131 | if (err) return done(err); 132 | params.channels = channels; 133 | next(null, params); 134 | }); 135 | }, 136 | function(params, next) { 137 | var url = 'https://slack.com/api/dialog.open'; 138 | var options = { 139 | url: url, 140 | headers: { 141 | 'Authorization':'Bearer '+process.env.SlackBotOAuthToken 142 | }, 143 | json:true, 144 | method: 'POST', 145 | body: { 146 | trigger_id: trigger_id, 147 | dialog: { 148 | "callback_id": 'create_project_'+(new Date()).getTime(), 149 | "title": "Create a build project", 150 | "submit_label": "Create", 151 | "elements": [ 152 | { 153 | "type": "text", 154 | "label": "Github Repo", 155 | "name": "github_repo" 156 | }, 157 | { 158 | "type": "text", 159 | "label": "Github Branch", 160 | "name": "github_branch" 161 | }, 162 | { 163 | "label": "CodeBuild Compute Type", 164 | "type": "select", 165 | "name": "codebuid_compute_type", 166 | "placeholder": "Select a CodeBuild compute type", 167 | "value": "BUILD_GENERAL1_SMALL", 168 | "options": [ 169 | { 170 | "label": "build.general1.small", 171 | "value": "BUILD_GENERAL1_SMALL" 172 | }, 173 | { 174 | "label": "build.general1.medium", 175 | "value": "BUILD_GENERAL1_MEDIUM" 176 | }, 177 | { 178 | "label": "build.general1.large", 179 | "value": "BUILD_GENERAL1_LARGE" 180 | } 181 | ] 182 | }, 183 | { 184 | "label": "CodeBuild Image", 185 | "type": "select", 186 | "name": "codebuid_image", 187 | "placeholder": "Select a CodeBuild image", 188 | "value": "aws/codebuild/eb-nodejs-6.10.0-amazonlinux-64:4.0.0", 189 | "options": _.map(params.images, function(image) { 190 | return { 191 | label: image, 192 | value: image 193 | } 194 | }) 195 | }, 196 | { 197 | "label": "Notifications Channel", 198 | "type": "select", 199 | "name": "channel", 200 | "placeholder": "Select a Channel to receive build notifications", 201 | "options": _.map(params.channels, function(channel) { 202 | return { 203 | label: channel.name, 204 | value: channel.id 205 | } 206 | }) 207 | } 208 | ] 209 | } 210 | } 211 | }; 212 | console.log(JSON.stringify(options, null, 3)); 213 | request(options, function(err, response, body) { 214 | console.log(JSON.stringify(body, null, 3)); 215 | next(err, body); 216 | }); 217 | } 218 | ], done); 219 | }; 220 | 221 | module.exports = functions; -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functionci", 3 | "version": "1.0.0", 4 | "description": "FunctionCI is an open source CI/CD Slack Bot for AWS Lambda's.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "dependencies": { 10 | "async": "^2.5.0", 11 | "lodash": "^4.17.4", 12 | "request": "^2.83.0", 13 | "winston": "^2.3.1" 14 | }, 15 | "devDependencies": { 16 | "aws-sdk": "^2.110.0", 17 | "mocha": "^3.5.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/test/codebuild.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var _ = require('lodash'); 3 | var codebuild = require('../lib/codebuild'); 4 | 5 | process.env.SlackVerificationToken = '1234'; 6 | 7 | describe('codebuild', function() { 8 | it('main', function (done) { 9 | codebuild.listCuratedEnvironmentImages(function(err, images) { 10 | console.log(JSON.stringify(images, null, 3)); 11 | done(); 12 | }); 13 | }); 14 | }); -------------------------------------------------------------------------------- /app/test/dao.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var _ = require('lodash'); 3 | var dao = require('../lib/dao'); 4 | 5 | process.env.FunctionCITable = 'FunctionCI'; 6 | 7 | describe('dao', function() { 8 | it('put_project', function (done) { 9 | dao.put_project({ 10 | project_id: 'rgfindl-cim-sh-master', 11 | github_owner: 'rgfindl', 12 | github_repo: 'cim.sh', 13 | github_branch: 'master', 14 | codebuid_compute_type: 'codebuid_compute_type', 15 | codebuid_image: 'codebuid_image' 16 | }, function(err, results) { 17 | console.log(JSON.stringify(results, null, 3)); 18 | console.log(JSON.stringify(err, null, 3)); 19 | done(err); 20 | }); 21 | }); 22 | it('update_project_build_count', function (done) { 23 | dao.update_project_build_count({ 24 | project_id: 'rgfindl-cim-sh-master' 25 | }, function(err, results) { 26 | console.log(JSON.stringify(results, null, 3)); 27 | console.log(JSON.stringify(err, null, 3)); 28 | done(err); 29 | }); 30 | }); 31 | it('get_project', function (done) { 32 | dao.get_project({ 33 | project_id: 'rgfindl-cim-sh-master' 34 | }, function(err, results) { 35 | console.log(JSON.stringify(results, null, 3)); 36 | console.log(JSON.stringify(err, null, 3)); 37 | done(err); 38 | }); 39 | }); 40 | it('put_project', function (done) { 41 | dao.put_project({ 42 | project_id: 'aaa-cim-sh-master', 43 | github_owner: 'rgfindl', 44 | github_repo: 'cim.sh', 45 | github_branch: 'master', 46 | codebuid_compute_type: 'codebuid_compute_type', 47 | codebuid_image: 'codebuid_image' 48 | }, function(err, results) { 49 | console.log(JSON.stringify(results, null, 3)); 50 | console.log(JSON.stringify(err, null, 3)); 51 | done(err); 52 | }); 53 | }); 54 | it('get_projects', function (done) { 55 | dao.get_projects({}, function(err, results) { 56 | console.log(JSON.stringify(results, null, 3)); 57 | console.log(JSON.stringify(err, null, 3)); 58 | done(err); 59 | }); 60 | }); 61 | it('delete_project', function (done) { 62 | dao.delete_project({ 63 | project_id: 'rgfindl-cim-sh-master' 64 | }, function(err, results) { 65 | console.log(JSON.stringify(results, null, 3)); 66 | console.log(JSON.stringify(err, null, 3)); 67 | done(err); 68 | }); 69 | }); 70 | it('delete_project', function (done) { 71 | dao.delete_project({ 72 | project_id: 'aaa-cim-sh-master' 73 | }, function(err, results) { 74 | console.log(JSON.stringify(results, null, 3)); 75 | console.log(JSON.stringify(err, null, 3)); 76 | done(err); 77 | }); 78 | }); 79 | it('put_fn', function (done) { 80 | dao.put_fn({ 81 | short_name: 'api', 82 | arn: 'api-arn' 83 | }, function(err, results) { 84 | console.log(JSON.stringify(results, null, 3)); 85 | console.log(JSON.stringify(err, null, 3)); 86 | done(err); 87 | }); 88 | }); 89 | it('get_fn', function (done) { 90 | dao.get_fn({ 91 | short_name: 'api' 92 | }, function(err, results) { 93 | console.log(JSON.stringify(results, null, 3)); 94 | console.log(JSON.stringify(err, null, 3)); 95 | done(err); 96 | }); 97 | }); 98 | it('get_fns', function (done) { 99 | dao.get_fns({ 100 | }, function(err, results) { 101 | console.log(JSON.stringify(results, null, 3)); 102 | console.log(JSON.stringify(err, null, 3)); 103 | done(err); 104 | }); 105 | }); 106 | it('delete_fn', function (done) { 107 | dao.delete_fn({ 108 | short_name: 'api' 109 | }, function(err, results) { 110 | console.log(JSON.stringify(results, null, 3)); 111 | console.log(JSON.stringify(err, null, 3)); 112 | done(err); 113 | }); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /app/test/index.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var _ = require('lodash'); 3 | var index = require('../index'); 4 | 5 | process.env.SlackVerificationToken = '1234'; 6 | process.env.SlackBotOAuthToken = 'xoxb-xxxxxx'; 7 | 8 | 9 | describe('index', function() { 10 | it('create project', function (done) { 11 | var req = { 12 | path: '/slack/commands', 13 | "body": "token=1234&team_id=T1Y5SBYKV&team_domain=medisprout&channel_id=D1Y75LXV4&channel_name=directmessage&user_id=U1Y73S952&user_name=randyfindley&command=%2Ffn&text=deploy+fn+demo+functionci-demo-master+5&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%2FT1Y5SBYKV%2F268633988183%2Fs2PDnET9JYjecdiqvtvfCGhv&trigger_id=267034568481.66196406675.ec9261de442dedb68a1d3647859f85fa" 14 | }; 15 | index.handler(req, {}, function(err, response) { 16 | //assert.equal(response.statusCode, 200); 17 | // assert.ok(response.body); 18 | // console.log(response.body); 19 | // var body = JSON.parse(response.body); 20 | // assert.ok(body.event); 21 | // assert.ok(body.event.test); 22 | console.log(JSON.stringify(response, null, 3)); 23 | done(err); 24 | }); 25 | }); 26 | // it('dialog', function(done) { 27 | // var req = { 28 | // path: '/slack/interactive-components', 29 | // body: 'payload=%7B%22type%22%3A%22dialog_submission%22%2C%22submission%22%3A%7B%22github_repo%22%3A%22repo%22%2C%22github_branch%22%3A%22branch%22%2C%22codebuid_compute_type%22%3A%22BUILD_GENERAL1_SMALL%22%2C%22codebuid_image%22%3A%22aws%5C%2Fcodebuild%5C%2Feb-nodejs-6.10.0-amazonlinux-64%3A4.0.0%22%7D%2C%22callback_id%22%3A%22create_project_1509900895876%22%2C%22team%22%3A%7B%22id%22%3A%22T1Y5SBYKV%22%2C%22domain%22%3A%22medisprout%22%7D%2C%22user%22%3A%7B%22id%22%3A%22U1Y73S952%22%2C%22name%22%3A%22randyfindley%22%7D%2C%22channel%22%3A%7B%22id%22%3A%22D1Y75LXV4%22%2C%22name%22%3A%22directmessage%22%7D%2C%22action_ts%22%3A%221509901021.230184%22%2C%22token%22%3A%221234%22%7D' 30 | // }; 31 | // index.handler(req, {}, function(err, response) { 32 | // console.log(response); 33 | // done(err); 34 | // }); 35 | // }); 36 | // it('sns', function(done) { 37 | // var req = { 38 | // "Records": [ 39 | // { 40 | // "EventSource": "aws:sns", 41 | // "EventVersion": "1.0", 42 | // "EventSubscriptionArn": "arn:aws:sns:us-east-1:132093761664:functionci-app-topic:4c90cb05-57e2-469d-ad81-e89a9f5de0d6", 43 | // "Sns": { 44 | // "Type": "Notification", 45 | // "MessageId": "2b1ce984-a2c9-591e-93d3-bbfdd0296226", 46 | // "TopicArn": "arn:aws:sns:us-east-1:132093761664:functionci-app-topic", 47 | // "Subject": "AWS CloudFormation Notification", 48 | // "Message": "StackId='arn:aws:cloudformation:us-east-1:132093761664:stack/functionci-serverless-demo-master/877ae110-c2f6-11e7-9cf3-500c217b26c6'\nTimestamp='2017-11-06T13:30:59.384Z'\nEventId='b9e02f70-c2f6-11e7-9b77-500c2866f062'\nLogicalResourceId='functionci-serverless-demo-master'\nNamespace='132093761664'\nPhysicalResourceId='arn:aws:cloudformation:us-east-1:132093761664:stack/functionci-serverless-demo-master/877ae110-c2f6-11e7-9cf3-500c217b26c6'\nPrincipalId='AROAIYU25YZESDP2UYRZU:functionci-app-LambdaFunction-19QAJIOQZW45M'\nResourceProperties='null'\nResourceStatus='CREATE_COMPLETE'\nResourceStatusReason=''\nResourceType='AWS::CloudFormation::Stack'\nStackName='functionci-serverless-demo-master'\nClientRequestToken='null'\n", 49 | // "Timestamp": "2017-11-05T20:12:16.265Z", 50 | // "SignatureVersion": "1", 51 | // "Signature": "bR4vGiVIHhk4JopYMcBnAvisyQDBmyd+CEpNljfh0B+s3T9MKwzJlMudK8i2qZeikAasMKK3j5HKnwQBqjfpSfiNnsnwNARjAFpSIo1Abd7Ef6ddhYCk05lIqJYyC7IMikYvsaahDFGcPzSWoHs6KOcbCF5uJjVAi4pX7XeFFpCM6U8qA8RxJTiQglxo2RAKWttmILbiFDpnCz1Urt3n0Gx6xRSx420sSc0hJmogkzS7M8YoFSjDxfcg/s9kxrdTDYONIsk/QWRcSGNKCyzGxDh/kvIpCDkcWpe2gbpAs74cgyeVqAF13eZcCpqLvwh+Ksh3GluvVkf6/sKn87otcQ==", 52 | // "SigningCertUrl": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-433026a4050d206028891664da859041.pem", 53 | // "UnsubscribeUrl": "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:132093761664:functionci-app-topic:4c90cb05-57e2-469d-ad81-e89a9f5de0d6", 54 | // "MessageAttributes": {} 55 | // } 56 | // } 57 | // ] 58 | // }; 59 | // index.handler(req, {}, function(err, response) { 60 | // console.log(response); 61 | // done(err); 62 | // }); 63 | // // }); 64 | // it('codebuild', function(done) { 65 | // var req = { 66 | // "version": "0", 67 | // "id": "fc545584-970e-991a-1113-46467a09cd04", 68 | // "detail-type": "CodeBuild Build State Change", 69 | // "source": "aws.codebuild", 70 | // "account": "132093761664", 71 | // "time": "2017-11-05T22:01:11Z", 72 | // "region": "us-east-1", 73 | // "resources": [ 74 | // "arn:aws:codebuild:us-east-1:132093761664:build/functionci-thestackshack-serverless-demo-master-code-build:93942698-859e-4fa9-8fde-c062977f6053" 75 | // ], 76 | // "detail": { 77 | // "build-status": "IN_PROGRESS", 78 | // "project-name": "functionci-thestackshack-serverless-demo-master-code-build", 79 | // "build-id": "arn:aws:codebuild:us-east-1:132093761664:build/functionci-thestackshack-serverless-demo-master-code-build:93942698-859e-4fa9-8fde-c062977f6053", 80 | // "additional-information": { 81 | // "artifact": { 82 | // "location": "arn:aws:s3:::functionci-artifacts/functionci-thestacks/BuildOutpu/lrm1vN7" 83 | // }, 84 | // "environment": { 85 | // "image": "aws/codebuild/eb-nodejs-6.10.0-amazonlinux-64:4.0.0", 86 | // "privileged-mode": false, 87 | // "compute-type": "BUILD_GENERAL1_SMALL", 88 | // "type": "LINUX_CONTAINER", 89 | // "environment-variables": [] 90 | // }, 91 | // "timeout-in-minutes": 5, 92 | // "build-complete": false, 93 | // "initiator": "codepipeline/functionci-thestackshack-serverless-demo-master-AppCodePipeline-12OOMAI0OA7MG", 94 | // "build-start-time": "Nov 5, 2017 10:01:11 PM", 95 | // "source": { 96 | // "buildspec": "buildspec.yml", 97 | // "type": "CODEPIPELINE" 98 | // }, 99 | // "source-version": "arn:aws:s3:::functionci-artifacts/functionci-thestacks/SourceOutp/PHoeKv7.zip" 100 | // }, 101 | // "current-phase": "SUBMITTED", 102 | // "current-phase-context": "[]", 103 | // "version": "1" 104 | // } 105 | // }; 106 | // index.handler(req, {}, function(err, response) { 107 | // console.log(response); 108 | // done(err); 109 | // }); 110 | // }); 111 | // var req_failed = { 112 | // "version": "0", 113 | // "id": "e0e9c40b-5720-143c-4667-fb67ef65a366", 114 | // "detail-type": "CodeBuild Build State Change", 115 | // "source": "aws.codebuild", 116 | // "account": "132093761664", 117 | // "time": "2017-11-06T01:35:07Z", 118 | // "region": "us-east-1", 119 | // "resources": [ 120 | // "arn:aws:codebuild:us-east-1:132093761664:build/functionci-thestackshack-serverless-demo-master-code-build:845a17d1-3901-40aa-a169-15f7545eca60" 121 | // ], 122 | // "detail": { 123 | // "build-status": "FAILED", 124 | // "project-name": "functionci-thestackshack-serverless-demo-master-code-build", 125 | // "build-id": "arn:aws:codebuild:us-east-1:132093761664:build/functionci-thestackshack-serverless-demo-master-code-build:845a17d1-3901-40aa-a169-15f7545eca60", 126 | // "additional-information": { 127 | // "artifact": { 128 | // "location": "arn:aws:s3:::functionci-artifacts/functionci-thestacks/BuildOutpu/JU3DLlI" 129 | // }, 130 | // "environment": { 131 | // "image": "aws/codebuild/eb-nodejs-6.10.0-amazonlinux-64:4.0.0", 132 | // "privileged-mode": false, 133 | // "compute-type": "BUILD_GENERAL1_SMALL", 134 | // "type": "LINUX_CONTAINER", 135 | // "environment-variables": [] 136 | // }, 137 | // "timeout-in-minutes": 5, 138 | // "build-complete": true, 139 | // "initiator": "codepipeline/functionci-thestackshack-serverless-demo-master-AppCodePipeline-ZAKCN4G5I28G", 140 | // "build-start-time": "Nov 6, 2017 1:33:58 AM", 141 | // "source": { 142 | // "buildspec": "buildspec.yml", 143 | // "type": "CODEPIPELINE" 144 | // }, 145 | // "source-version": "arn:aws:s3:::functionci-artifacts/functionci-thestacks/SourceOutp/Wk6cJXz.zip", 146 | // "logs": { 147 | // "group-name": "/aws/codebuild/functionci-thestackshack-serverless-demo-master-code-build", 148 | // "stream-name": "845a17d1-3901-40aa-a169-15f7545eca60", 149 | // "deep-link": "https://console.aws.amazon.com/cloudwatch/home?region=us-east-1#logEvent:group=/aws/codebuild/functionci-thestackshack-serverless-demo-master-code-build;stream=845a17d1-3901-40aa-a169-15f7545eca60" 150 | // }, 151 | // "phases": [ 152 | // { 153 | // "phase-context": [], 154 | // "start-time": "Nov 6, 2017 1:33:58 AM", 155 | // "end-time": "Nov 6, 2017 1:33:59 AM", 156 | // "duration-in-seconds": 0, 157 | // "phase-type": "SUBMITTED", 158 | // "phase-status": "SUCCEEDED" 159 | // }, 160 | // { 161 | // "phase-context": [], 162 | // "start-time": "Nov 6, 2017 1:33:59 AM", 163 | // "end-time": "Nov 6, 2017 1:34:55 AM", 164 | // "duration-in-seconds": 56, 165 | // "phase-type": "PROVISIONING", 166 | // "phase-status": "SUCCEEDED" 167 | // }, 168 | // { 169 | // "phase-context": [ 170 | // "YAML_FILE_ERROR: stat /codebuild/output/src424818173/src/buildspec.yml: no such file or directory" 171 | // ], 172 | // "start-time": "Nov 6, 2017 1:34:55 AM", 173 | // "end-time": "Nov 6, 2017 1:35:00 AM", 174 | // "duration-in-seconds": 4, 175 | // "phase-type": "DOWNLOAD_SOURCE", 176 | // "phase-status": "FAILED" 177 | // }, 178 | // { 179 | // "phase-context": [], 180 | // "start-time": "Nov 6, 2017 1:35:00 AM", 181 | // "end-time": "Nov 6, 2017 1:35:06 AM", 182 | // "duration-in-seconds": 5, 183 | // "phase-type": "FINALIZING", 184 | // "phase-status": "SUCCEEDED" 185 | // }, 186 | // { 187 | // "start-time": "Nov 6, 2017 1:35:06 AM", 188 | // "phase-type": "COMPLETED" 189 | // } 190 | // ] 191 | // }, 192 | // "current-phase": "COMPLETED", 193 | // "current-phase-context": "[]", 194 | // "version": "1" 195 | // } 196 | // }; 197 | // }); 198 | // it('codepipeline', function(done) { 199 | // 200 | // var cp = { 201 | // "CodePipeline.job": { 202 | // "id": "eddd25bc-ef53-4440-bee5-f69d804a5e9b", 203 | // "accountId": "132093761664", 204 | // "data": { 205 | // "actionConfiguration": { 206 | // "configuration": { 207 | // "FunctionName": "functionci-app-LambdaFunction-19QAJIOQZW45M", 208 | // "UserParameters": "functionci-functionci-demo-master" 209 | // } 210 | // }, 211 | // "inputArtifacts": [ 212 | // { 213 | // "location": { 214 | // "s3Location": { 215 | // "bucketName": "functionci-artifacts", 216 | // "objectKey": "functionci-functionc/SourceOutp/8ws4keK.zip" 217 | // }, 218 | // "type": "S3" 219 | // }, 220 | // "revision": "e3642fdfabc5088e834519daa0222af4cb1c90e9", 221 | // "name": "SourceOutput" 222 | // }, 223 | // { 224 | // "location": { 225 | // "s3Location": { 226 | // "bucketName": "functionci-artifacts", 227 | // "objectKey": "functionci-functionc/BuildOutpu/0iqXcIU" 228 | // }, 229 | // "type": "S3" 230 | // }, 231 | // "revision": null, 232 | // "name": "BuildOutput" 233 | // } 234 | // ], 235 | // "outputArtifacts": [], 236 | // "artifactCredentials": { 237 | // "secretAccessKey": "hPgQPlLqhfFaw44f82BZVocR7gSWrlyL5IMTIqNB", 238 | // "sessionToken": "AgoGb3JpZ2luEFgaCXVzLWVhc3QtMSKAAhPEloBGV9Vu0jNLSJtSX+ZQzkS2MyAKv/VRjnuicMjRA05jyL5fTkVzjpz559z/n9464gGPnCqwm1hk34iv3dMsPDfuODzAHx10VzK1rSa2vwnGY7X2ySUrWQaIma4zH1YcgPkxkFfIveRUF3Oq8sp3arcVNFBT79HBR9UuzCHWIeIxq2hLn0G9Rq91bOkSgiwj3aJxAUfzOTgwAL788a+FzMObaMMxIL2p24XawsPU98jmrkGUfkSq0K/X7IZXrwI5t3O2KzIkx5+EjSQjfcevc3/9v9j2WaUqA6N9CpUqVrEMMzD70Qat3c5x6oyrpUvZmjfkbheWBSmdtHkt/owqpwUIjf//////////ARABGgwxMzIwOTM3NjE2NjQiDE/rhG51EaVG/liMCyr7BF82XXaiWQZyEhn7zMAOfgPQ6Wa1Vd0eWfElGRJgngL31Hev55FdAfA95xA2k8XsJSfC1LX+b7rYW0huqw/nZw/KOfrNYGsGkmk76lEJotYefhzsQ9Xfq5NZT2LI7htTrMn0REuS1/cbbkDeY6O++MlbUmeib2ha8XkFX/wMlYUgFdCL3WlCj01aV3THSRqMVaAQMCljrfIj2hMnCPXbRqpUQaDSaUmIIdOoMQEhSnpe7/VWeqL6KFAiNmmx+ZLjj/YWTiqSaYpIPB23RvwVgjKPATxhrgSJJdGLTatDItohOeqkfUovEAmJzrVAgP6Ksev8keDoceqkTetBGqAqE37KYw45CPQRP3+oM7AENsxQwcBJpGVgsBnLTlnoiVp9jQ2WXwoU/wLjolVY9/vJwrvKhN+0NVuQmqI0s5GbNmGbyhw+Wv1P+/tosUHm+w9JNplyVZIc7nbYoN+p1utgrmaedbexBiIbmM3XC3ffpFpgYY3v/c/lNTOgYGDzDGxG7GBvXhOiIxuPggoQBt8O4cSOHsmAyeqFGFP3u0oOr3qqt4v0/YWUr8VuJg9Q2r2KQgzVFG2NierDQMwrh2Hue2lcvx5qtjZKj4pNEO/ze3lKEiC4o3XIT4vrRw7L37k61eKgbQ9DFGaeDT9yUtu+EPYhQOShAhZBnkZfSavNbAtO/sYfhJyxK+fVxxBxxd/xe2K5XpLbaobiy2z2EZL/munoedOThNaUAO/8gP5CGPWZJLv+oSEjeAg93gGmpVq8ZfjE9NCpC8ZnFncT94EUJDrvAKVAHAR4+dDbZ5FDpWilZLw7HG25Fp5lu7Rnc29zmW9FQO00EhdGoqhDMKHDhtAF", 239 | // "accessKeyId": "ASIAIWTXGKXK6IE7D7LQ" 240 | // } 241 | // } 242 | // } 243 | // }; 244 | // index.handler(cp, { 245 | // succeed: function(err, response) { 246 | // console.log('succeed'); 247 | // console.log(response); 248 | // done(err); 249 | // }, 250 | // fail: function(err, response) { 251 | // console.log('fail'); 252 | // console.log(response); 253 | // done(err); 254 | // } 255 | // }, function(err, response) { 256 | // console.log(response); 257 | // done(err); 258 | // }); 259 | // }); 260 | }); -------------------------------------------------------------------------------- /app/test/project.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var _ = require('lodash'); 3 | var codebuild = require('../lib/codebuild'); 4 | 5 | process.env.SlackVerificationToken = '1234'; 6 | 7 | describe('project', function() { 8 | it('main', function (done) { 9 | 10 | const github_url = 'https://github.com/rgfindl/functionci-demo.sh'; 11 | const github_branch = 'master'; 12 | 13 | const github_repo_parts = _.split(_.replace(github_url, 'https://github.com/', ''), '/'); 14 | if (_.size(github_repo_parts) != 2) { 15 | done('INvalid'); 16 | } 17 | const github_owner = github_repo_parts[0]; 18 | const github_repo = github_repo_parts[1]; 19 | const project_id = _.replace(_.join([github_repo, github_branch], '-'), /[^a-z0-9A-Z-]/g, '-'); 20 | console.log(project_id); 21 | done(); 22 | }); 23 | }); -------------------------------------------------------------------------------- /app/test/s3.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var _ = require('lodash'); 3 | var s3 = require('../lib/s3'); 4 | 5 | describe('s3', function() { 6 | it('main', function (done) { 7 | s3.delete_with_prefix({ 8 | bucket: 'functionci-artifacts', 9 | prefix: 'functionci-demo-master' 10 | }, 11 | function(err, images) { 12 | console.log(JSON.stringify(images, null, 3)); 13 | done(); 14 | } 15 | ); 16 | }); 17 | }); -------------------------------------------------------------------------------- /app/test/slack.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var _ = require('lodash'); 3 | var slack = require('../lib/slack'); 4 | 5 | process.env.SlackBotOAuthToken = 'xoxb-xxxx'; 6 | 7 | describe('slack', function() { 8 | // it('channels', function (done) { 9 | // slack.fetch_all_channels({}, function(err, channels) { 10 | // //console.log(JSON.stringify(err, null, 3)); 11 | // console.log(JSON.stringify(channels, null, 3)); 12 | // done(); 13 | // }); 14 | // }); 15 | // it('create_dialog', function(done) { 16 | // slack.create_dialog('test', function(err) { 17 | // done(err); 18 | // }); 19 | // }); 20 | it('post_message', function(done) { 21 | 22 | var d = new Date(); 23 | var seconds = d.getTime() / 1000; 24 | var message = { 25 | channel: 'G7V37B2P3', 26 | text: '', 27 | attachments: [ 28 | { 29 | "fallback": 'test', 30 | "color": "#dddddd", 31 | "text": 'test', 32 | "ts": seconds 33 | } 34 | ] 35 | }; 36 | slack.post_message(message, done); 37 | }); 38 | }); -------------------------------------------------------------------------------- /docs/FunctionCI-Build-Project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgfindl/functionci/bd27f52f5701037f206e8a2c8ee479e866f6c047/docs/FunctionCI-Build-Project.png -------------------------------------------------------------------------------- /docs/FunctionCI-Create-Project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgfindl/functionci/bd27f52f5701037f206e8a2c8ee479e866f6c047/docs/FunctionCI-Create-Project.png -------------------------------------------------------------------------------- /docs/FunctionCI-Deploy-Fn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgfindl/functionci/bd27f52f5701037f206e8a2c8ee479e866f6c047/docs/FunctionCI-Deploy-Fn.png -------------------------------------------------------------------------------- /docs/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgfindl/functionci/bd27f52f5701037f206e8a2c8ee479e866f6c047/docs/banner.png -------------------------------------------------------------------------------- /docs/banner.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgfindl/functionci/bd27f52f5701037f206e8a2c8ee479e866f6c047/docs/banner.pxm -------------------------------------------------------------------------------- /docs/build-messages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgfindl/functionci/bd27f52f5701037f206e8a2c8ee479e866f6c047/docs/build-messages.png -------------------------------------------------------------------------------- /docs/create-dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgfindl/functionci/bd27f52f5701037f206e8a2c8ee479e866f6c047/docs/create-dialog.png -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgfindl/functionci/bd27f52f5701037f206e8a2c8ee479e866f6c047/docs/logo.png -------------------------------------------------------------------------------- /docs/show-fn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgfindl/functionci/bd27f52f5701037f206e8a2c8ee479e866f6c047/docs/show-fn.png -------------------------------------------------------------------------------- /docs/show-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgfindl/functionci/bd27f52f5701037f206e8a2c8ee479e866f6c047/docs/show-project.png -------------------------------------------------------------------------------- /docs/slack-lambda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgfindl/functionci/bd27f52f5701037f206e8a2c8ee479e866f6c047/docs/slack-lambda.png -------------------------------------------------------------------------------- /kms/.gitignore: -------------------------------------------------------------------------------- 1 | encrypt.json -------------------------------------------------------------------------------- /kms/README.md: -------------------------------------------------------------------------------- 1 | # KMS Stack 2 | Creates a KMS Key to use when encrypting your secret credentials. 3 | 4 | ## Create Stack 5 | - First update [_cim.yml](_cim.yml) and add the IAM users you wish to have access to encrypt and decrypt. Keep the `root` user. Make sure you use your AWS account ID. 6 | - Create the stack: `cim stack-up`. 7 | - Record the KMS Key ID in the stack output. 8 | 9 | ## Encrypt Secrets 10 | - Install https://github.com/ddffx/kms-cli and setup your AWS environment vars. 11 | - Encrypt each string as outlined below. 12 | - Add the encrypted strings to the [app/_cim.yml](../app/_cim.yml). The format is `${kms.decrypt()}` 13 | 14 | 15 | ### How to Encrypt 16 | Create a file called `encrypt.json` 17 | ``` 18 | { 19 | "keyId" : "", 20 | "plainText": "", 21 | "awsRegion": "", 22 | "awsProfile": "", 3 | "plainText": "", 4 | "awsRegion": "", 5 | "awsProfile": "