├── .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": "