├── .editorconfig ├── .eslintrc.json ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .markdownlint.json ├── .nvmrc ├── .nycrc.json ├── LICENSE ├── README.md ├── commitlint.config.js ├── package-lock.json ├── package.json └── src ├── index.js └── tests ├── .eslintrc.json └── index.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | node_modules/@silvermine/standardization/.editorconfig -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "extends": "@silvermine/eslint-config/node" 4 | 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - 10 | uses: actions/checkout@v4 11 | with: 12 | fetch-depth: 0 # Fetch all history 13 | - 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version-file: '.nvmrc' 17 | - run: npm ci 18 | - run: npm run check-node-version 19 | - run: npm run standards 20 | test: 21 | needs: [ build ] 22 | runs-on: ubuntu-latest 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | node-version: [ 16, 20, 'lts/*', 'latest' ] 27 | steps: 28 | - uses: actions/checkout@v4 29 | - 30 | name: Use Node.js ${{ matrix.node-version }} 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | - run: npm ci # Reinstall the dependencies to ensure they install with the current version of node 35 | - run: npm test 36 | - name: Coveralls 37 | uses: coverallsapp/github-action@v1 38 | with: 39 | parallel: true 40 | flag-name: ${{ matrix.node-version }} 41 | finish: 42 | needs: [ test ] 43 | runs-on: ubuntu-latest 44 | steps: 45 | - name: Close parallel build 46 | uses: coverallsapp/github-action@v1 47 | with: 48 | parallel-finished: true 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | coverage 4 | .nyc_output 5 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@silvermine/standardization/.markdownlint.json" 3 | } 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.12.2 2 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/**/*.js" 4 | ], 5 | "extension": [ 6 | ".js" 7 | ], 8 | "reporter": [ 9 | "text-summary", 10 | "html", 11 | "lcov" 12 | ], 13 | "instrument": true, 14 | "sourceMap": true, 15 | "all": true 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Jeremy Thomerson 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless Plugin: Support CloudFront Lambda@Edge 2 | 3 | [![Build Status](https://travis-ci.com/silvermine/serverless-plugin-cloudfront-lambda-edge.svg?branch=master)](https://travis-ci.com/silvermine/serverless-plugin-cloudfront-lambda-edge) 4 | [![Coverage Status](https://coveralls.io/repos/github/silvermine/serverless-plugin-cloudfront-lambda-edge/badge.svg?branch=master)](https://coveralls.io/github/silvermine/serverless-plugin-cloudfront-lambda-edge?branch=master) 5 | [![Dependency Status](https://david-dm.org/silvermine/serverless-plugin-cloudfront-lambda-edge.svg)](https://david-dm.org/silvermine/serverless-plugin-cloudfront-lambda-edge) 6 | [![Dev Dependency Status](https://david-dm.org/silvermine/serverless-plugin-cloudfront-lambda-edge/dev-status.svg)](https://david-dm.org/silvermine/serverless-plugin-cloudfront-lambda-edge#info=devDependencies&view=table) 7 | 8 | 9 | ## What is it? 10 | 11 | This is a plugin for the Serverless framework that adds support for associating a Lambda 12 | function with a CloudFront distribution to take advantage of the Lambda@Edge features of 13 | CloudFront. 14 | 15 | Even though CloudFormation added support for Lambda@Edge via its 16 | [`LambdaFunctionAssociations`][FnAssoc] config object, it would be difficult to define a 17 | CloudFront distribution in your serverless.yml file's resources that links to one of the 18 | functions that you're deploying with Serverless. 19 | 20 | Why? Because the [`LambdaFunctionAssociations`][FnAssoc] array needs a reference to the 21 | Lambda function's _version_ (`AWS::Lambda::Version` resource), not just the function 22 | itself. (The documentation for CloudFormation says "You must specify the ARN of a function 23 | version; you can't specify a Lambda alias or $LATEST."). Serverless creates the version 24 | automatically for you, but the logical ID for it is seemingly random. You'd need that 25 | logical ID to use a `Ref` in your CloudFormation template for the function association. 26 | 27 | This plugin hides all that for you - it uses other features in Serverless to be able to 28 | programmatically determine the function's logical ID and build the reference for you in 29 | the LambdaFunctionAssociations object. It directly modifies your CloudFormation template 30 | before the stack is ever deployed, so that CloudFormation does the heavy lifting for you. 31 | This 2.0 version of the plugin is thus much faster and easier to use than the 1.0 version 32 | (which existed before CloudFormation supported Lambda@Edge). 33 | 34 | 35 | ## How do I use it? 36 | 37 | There are three steps: 38 | 39 | ### Install the Plugin as a Development Dependency 40 | 41 | ```bash 42 | npm install --save-dev --save-exact @silvermine/serverless-plugin-cloudfront-lambda-edge 43 | ``` 44 | 45 | ### Telling Serverless to Use the Plugin 46 | 47 | Simply add this plugin to the list of plugins in your `serverless.yml` file: 48 | 49 | ```yml 50 | plugins: 51 | - '@silvermine/serverless-plugin-cloudfront-lambda-edge' 52 | ``` 53 | 54 | ⚠️ Be careful, **versioning function MUST BE ENABLED**. If you defined `versionFunctions: 55 | false` in your `serverless.yml` function, the plugin will raise an error during 56 | package/deploy saying it could not find output by name. 57 | 58 | ### Configuring Functions to Associate With CloudFront Distributions 59 | 60 | Also in your `serverless.yml` file, you will modify your function definitions to include a 61 | `lambdaAtEdge` property. That property can be an object if you are associating the 62 | function with only a single distribution (or single cache behavior). Or, if you want the 63 | same function associated with multiple distributions or cache behaviors, the property 64 | value can be an array of objects. Whether you define a single object or an array of 65 | objects, the objects all have the same fields, each of which is explained here: 66 | 67 | * **`distribution`** (required): the logical name used in your `Resources` section to 68 | define the CloudFront distribution. 69 | * **`eventType`** (required): a string, one of the four Lambda@Edge event types: 70 | * viewer-request 71 | * origin-request 72 | * viewer-response 73 | * origin-response 74 | * **`pathPattern`** (optional): a string, the path pattern of one of the cache 75 | behaviors in the specified distribution if you want this function to be associated 76 | with a specific cache behavior. If the path pattern is not defined here, the function 77 | will be associated with the default cache behavior for the specified distribution. 78 | * **`includeBody`** (optional): a boolean, `true` if you want to include the body in 79 | the request event your function receives. See [the AWS docs][includeBody] for more 80 | info. 81 | 82 | You can also apply global properties by adding the `lambdaAtEdge` property to your 83 | `custom` section of your `serverless.yml`. **Note:** This section currently only supports 84 | the follow option: 85 | 86 | * **`retain`** (optional): a boolean (default `false`). If you set this value to 87 | `true`, it will set the [DeletionPolicy][DeletionPolicy] of the function resource to 88 | `Retain`. This can be used to avoid the currently-inevitable [CloudFormation stack 89 | deletion failure][ReplicaDeleteFail]. There are at least [two schools of 90 | thought][HandlingCFNFailure] on how to handle this issue. Hopefully AWS will have 91 | this fixed soon. Use at your own discretion. 92 | 93 | For example: 94 | 95 | ```yml 96 | functions: 97 | directoryRootOriginRequestRewriter: 98 | name: '${self:custom.objectPrefix}-directory-root-origin-request-rewriter' 99 | handler: src/DirectoryRootOriginRequestRewriteHandler.handler 100 | memorySize: 128 101 | timeout: 1 102 | lambdaAtEdge: 103 | distribution: 'WebsiteDistribution' 104 | eventType: 'origin-request' 105 | ``` 106 | 107 | Or: 108 | 109 | ```yml 110 | custom: 111 | lambdaAtEdge: 112 | retain: true 113 | 114 | functions: 115 | someImageHandlingFunction: 116 | name: '${self:custom.objectPrefix}-image-handling' 117 | handler: src/ImageSomethingHandler.handler 118 | memorySize: 128 119 | timeout: 1 120 | lambdaAtEdge: 121 | distribution: 'WebsiteDistribution' 122 | eventType: 'viewer-request' 123 | # This must match a path pattern in a cache behavior of the distribution: 124 | pathPattern: 'images/*.jpg' 125 | ``` 126 | 127 | Or: 128 | 129 | ```yml 130 | functions: 131 | someFunction: 132 | name: '${self:custom.objectPrefix}' 133 | handler: src/SomethingHandler.handler 134 | memorySize: 128 135 | timeout: 1 136 | lambdaAtEdge: 137 | - 138 | distribution: 'WebsiteDistribution' 139 | eventType: 'viewer-response' 140 | # This must match a path pattern in a cache behavior of the distribution: 141 | pathPattern: 'images/*.jpg' 142 | - 143 | distribution: 'OtherDistribution' 144 | eventType: 'viewer-response' 145 | ``` 146 | 147 | 148 | ## Example CloudFront Static Site Serverless Config 149 | 150 | Here is an example of a `serverless.yml` file that configures an S3 bucket with a 151 | CloudFront distribution and a Lambda@Edge function: 152 | 153 | ```yml 154 | service: static-site 155 | 156 | custom: 157 | defaultRegion: us-east-1 158 | defaultEnvironmentGroup: dev 159 | region: ${opt:region, self:custom.defaultRegion} 160 | stage: ${opt:stage, env:USER} 161 | objectPrefix: '${self:service}-${self:custom.stage}' 162 | 163 | plugins: 164 | - '@silvermine/serverless-plugin-cloudfront-lambda-edge' 165 | 166 | package: 167 | exclude: 168 | - 'node_modules/**' 169 | 170 | provider: 171 | name: aws 172 | runtime: nodejs6.10 # Because this runs on CloudFront (lambda@edge) it must be 6.10 or greater 173 | region: ${self:custom.region} 174 | stage: ${self:custom.stage} 175 | # Note that Lambda@Edge does not actually support environment variables for lambda 176 | # functions, but the plugin will strip the environment variables from any function 177 | # that has edge configuration on it 178 | environment: 179 | SLS_SVC_NAME: ${self:service} 180 | SLS_STAGE: ${self:custom.stage} 181 | 182 | functions: 183 | directoryRootOriginRequestRewriter: 184 | name: '${self:custom.objectPrefix}-origin-request' 185 | handler: src/DirectoryRootOriginRequestRewriteHandler.handler 186 | memorySize: 128 187 | timeout: 1 188 | lambdaAtEdge: 189 | distribution: 'WebsiteDistribution' 190 | eventType: 'origin-request' 191 | 192 | resources: 193 | Resources: 194 | WebsiteBucket: 195 | Type: 'AWS::S3::Bucket' 196 | Properties: 197 | BucketName: '${self:custom.objectPrefix}' 198 | AccessControl: 'PublicRead' 199 | WebsiteConfiguration: 200 | IndexDocument: 'index.html' 201 | ErrorDocument: 'error.html' 202 | WebsiteDistribution: 203 | Type: 'AWS::CloudFront::Distribution' 204 | Properties: 205 | DistributionConfig: 206 | DefaultCacheBehavior: 207 | TargetOriginId: 'WebsiteBucketOrigin' 208 | ViewerProtocolPolicy: 'redirect-to-https' 209 | DefaultTTL: 600 # ten minutes 210 | MaxTTL: 600 # ten minutes 211 | Compress: true 212 | ForwardedValues: 213 | QueryString: false 214 | Cookies: 215 | Forward: 'none' 216 | DefaultRootObject: 'index.html' 217 | Enabled: true 218 | PriceClass: 'PriceClass_100' 219 | HttpVersion: 'http2' 220 | ViewerCertificate: 221 | CloudFrontDefaultCertificate: true 222 | Origins: 223 | - 224 | Id: 'WebsiteBucketOrigin' 225 | DomainName: { 'Fn::GetAtt': [ 'WebsiteBucket', 'DomainName' ] } 226 | S3OriginConfig: {} 227 | ``` 228 | 229 | And here is an example function that would go with this Serverless template: 230 | 231 | ```js 232 | 'use strict'; 233 | 234 | module.exports = { 235 | 236 | // invoked by CloudFront (origin requests) 237 | handler: function(evt, context, cb) { 238 | var req = evt.Records[0].cf.request; 239 | 240 | if (req.uri && req.uri.length && req.uri.substring(req.uri.length - 1) === '/') { 241 | var uri = req.uri + 'index.html'; 242 | 243 | console.log('changing "%s" to "%s"', req.uri, uri); 244 | req.uri = uri; 245 | } 246 | 247 | cb(null, req); 248 | }, 249 | 250 | }; 251 | ``` 252 | 253 | 254 | ## How do I contribute? 255 | 256 | 257 | We genuinely appreciate external contributions. See [our extensive 258 | documentation][contributing] on how to contribute. 259 | 260 | 261 | ## License 262 | 263 | This software is released under the MIT license. See [the license file](LICENSE) for more 264 | details. 265 | 266 | 267 | [contributing]: https://github.com/silvermine/silvermine-info#contributing 268 | [FnAssoc]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-cachebehavior.html#cfn-cloudfront-distribution-cachebehavior-lambdafunctionassociations 269 | [DeletionPolicy]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-attribute-deletionpolicy.html 270 | [ReplicaDeleteFail]: https://forums.aws.amazon.com/thread.jspa?threadID=260242&tstart=0 271 | [HandlingCFNFailure]: https://github.com/silvermine/serverless-plugin-cloudfront-lambda-edge/pull/19 272 | [includeBody]: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-include-body-access.html 273 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: [ '@silvermine/standardization/commitlint.js' ], 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@silvermine/serverless-plugin-cloudfront-lambda-edge", 3 | "version": "2.2.2", 4 | "description": "Plugin for the SLS 1.x branch to provide support for Lambda@Edge (not currently supported by CloudFormation", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "nyc mocha -- 'src/tests/**/*.test.js'", 8 | "check-node-version": "check-node-version --npm 10.5.0", 9 | "commitlint": "commitlint --from 4fbfbef", 10 | "eslint": "eslint .", 11 | "markdownlint": "markdownlint -c .markdownlint.json '{,!(node_modules)/**/}*.md'", 12 | "standards": "npm run markdownlint && npm run eslint" 13 | }, 14 | "author": "Jeremy Thomerson", 15 | "license": "MIT", 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/silvermine/serverless-plugin-cloudfront-lambda-edge.git" 19 | }, 20 | "keywords": [ 21 | "serverless plugin cloudfront lambda edge", 22 | "serverless", 23 | "cloudfront", 24 | "lambda@ege" 25 | ], 26 | "bugs": { 27 | "url": "https://github.com/silvermine/serverless-plugin-cloudfront-lambda-edge/issues" 28 | }, 29 | "homepage": "https://github.com/silvermine/serverless-plugin-cloudfront-lambda-edge#readme", 30 | "dependencies": { 31 | "class.extend": "0.9.2", 32 | "underscore": "1.13.1" 33 | }, 34 | "devDependencies": { 35 | "@silvermine/eslint-config": "3.0.1", 36 | "@silvermine/standardization": "2.0.0", 37 | "coveralls": "3.0.2", 38 | "eslint": "6.8.0", 39 | "expect.js": "0.3.1", 40 | "mocha": "8.4.0", 41 | "nyc": "15.1.0", 42 | "sinon": "3.2.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('underscore'), 4 | Class = require('class.extend'), 5 | VALID_EVENT_TYPES = [ 'viewer-request', 'origin-request', 'viewer-response', 'origin-response' ]; 6 | 7 | module.exports = Class.extend({ 8 | 9 | init: function(serverless, opts) { 10 | this._serverless = serverless; 11 | this._provider = serverless ? serverless.getProvider('aws') : null; 12 | this._opts = opts; 13 | this._custom = serverless.service ? serverless.service.custom : null; 14 | 15 | if (!this._provider) { 16 | throw new Error('This plugin must be used with AWS'); 17 | } 18 | 19 | this._configureSchema(serverless.configSchemaHandler); 20 | 21 | this.hooks = { 22 | 'aws:package:finalize:mergeCustomProviderResources': this._modifyTemplate.bind(this), 23 | }; 24 | 25 | }, 26 | 27 | _configureSchema: function(handler) { 28 | if (!handler || !_.isFunction(handler.defineCustomProperties) || !_.isFunction(handler.defineFunctionProperties)) { 29 | return; 30 | } 31 | 32 | handler.defineCustomProperties({ 33 | type: 'object', 34 | properties: { 35 | 'lambdaAtEdge': { 36 | type: 'object', 37 | properties: { 38 | retain: { type: 'boolean' }, 39 | }, 40 | }, 41 | }, 42 | }); 43 | 44 | const functionPropertySchema = { 45 | type: 'object', 46 | properties: { 47 | distribution: { type: 'string' }, 48 | eventType: { enum: VALID_EVENT_TYPES }, 49 | pathPattern: { type: 'string' }, 50 | }, 51 | required: [ 'distribution', 'eventType' ], 52 | }; 53 | 54 | handler.defineFunctionProperties('aws', { 55 | properties: { 56 | 'lambdaAtEdge': { 57 | oneOf: [ 58 | { 59 | type: 'array', 60 | items: functionPropertySchema, 61 | }, 62 | functionPropertySchema, 63 | ], 64 | }, 65 | }, 66 | }); 67 | }, 68 | 69 | _modifyTemplate: function() { 70 | var template = this._serverless.service.provider.compiledCloudFormationTemplate; 71 | 72 | this._modifyExecutionRole(template); 73 | this._modifyLambdaFunctionsAndDistributions(this._serverless.service.functions, template); 74 | }, 75 | 76 | _modifyExecutionRole: function(template) { 77 | var assumeRoleUpdated = false; 78 | 79 | if (!template.Resources || !template.Resources.IamRoleLambdaExecution) { 80 | this._serverless.cli.log('WARNING: no IAM role for Lambda execution found - can not modify assume role policy'); 81 | return; 82 | } 83 | 84 | _.each(template.Resources.IamRoleLambdaExecution.Properties.AssumeRolePolicyDocument.Statement, function(stmt) { 85 | var svc = stmt.Principal.Service; 86 | 87 | if (stmt.Principal && svc && _.contains(svc, 'lambda.amazonaws.com') && !_.contains(svc, 'edgelambda.amazonaws.com')) { 88 | svc.push('edgelambda.amazonaws.com'); 89 | assumeRoleUpdated = true; 90 | this._serverless.cli.log('Updated Lambda assume role policy to allow Lambda@Edge to assume the role'); 91 | } 92 | }.bind(this)); 93 | 94 | // Serverless creates a LogGroup by a specific name, and grants logs:CreateLogStream 95 | // and logs:PutLogEvents permissions to the function. However, on a replicated 96 | // function, AWS will name the log groups differently, so the Serverless-created 97 | // permissions will not work. Thus, we must give the function permission to create 98 | // log groups and streams, as well as put log events. 99 | // 100 | // Since we don't have control over the naming of the log group, we let this 101 | // function have permission to create and use a log group by any name. 102 | // See http://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/iam-identity-based-access-control-cwl.html 103 | template.Resources.IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement.push({ 104 | Effect: 'Allow', 105 | Action: [ 106 | 'logs:CreateLogGroup', 107 | 'logs:CreateLogStream', 108 | 'logs:PutLogEvents', 109 | 'logs:DescribeLogStreams', 110 | ], 111 | Resource: 'arn:aws:logs:*:*:*', 112 | }); 113 | 114 | if (!assumeRoleUpdated) { 115 | this._serverless.cli.log('WARNING: was unable to update the Lambda assume role policy to allow Lambda@Edge to assume the role'); 116 | } 117 | }, 118 | 119 | _modifyLambdaFunctionsAndDistributions: function(functions, template) { 120 | _.chain(functions) 121 | .pick(_.property('lambdaAtEdge')) // `pick` is used like `filter`, but for objects 122 | .each(function(fnDef, fnName) { 123 | var lambdaAtEdge = fnDef.lambdaAtEdge; 124 | 125 | if (_.isArray(lambdaAtEdge)) { 126 | _.each(lambdaAtEdge, this._handleSingleFunctionAssociation.bind(this, template, fnDef, fnName)); 127 | } else { 128 | this._handleSingleFunctionAssociation(template, fnDef, fnName, lambdaAtEdge); 129 | } 130 | }.bind(this)); 131 | }, 132 | 133 | _handleSingleFunctionAssociation: function(template, fnDef, fnName, lambdaAtEdge) { 134 | var fnLogicalName = this._provider.naming.getLambdaLogicalId(fnName), 135 | pathPattern = lambdaAtEdge.pathPattern, 136 | outputName = this._provider.naming.getLambdaVersionOutputLogicalId(fnName), 137 | distName = lambdaAtEdge.distribution, 138 | fnObj = template.Resources[fnLogicalName], 139 | fnProps = template.Resources[fnLogicalName].Properties, 140 | evtType = lambdaAtEdge.eventType, 141 | includeBody = lambdaAtEdge.includeBody || false, 142 | output = template.Outputs[outputName], 143 | dist = template.Resources[distName], 144 | retainFunctions = this._custom && this._custom.lambdaAtEdge && (this._custom.lambdaAtEdge.retain === true), 145 | distConfig, cacheBehavior, fnAssociations, versionLogicalID; 146 | 147 | if (!_.contains(VALID_EVENT_TYPES, evtType)) { 148 | throw new Error('"' + evtType + '" is not a valid event type, must be one of: ' + VALID_EVENT_TYPES.join(', ')); 149 | } 150 | 151 | if (!dist) { 152 | throw new Error('Could not find resource with logical name "' + distName + '"'); 153 | } 154 | 155 | if (dist.Type !== 'AWS::CloudFront::Distribution') { 156 | throw new Error('Resource with logical name "' + distName + '" is not type AWS::CloudFront::Distribution'); 157 | } 158 | 159 | versionLogicalID = (output ? output.Value.Ref : null); 160 | 161 | if (!versionLogicalID) { 162 | throw new Error('Could not find output by name of "' + outputName + '" or value from it to use version ARN'); 163 | } 164 | 165 | if (fnProps && fnProps.Environment && fnProps.Environment.Variables) { 166 | this._serverless.cli.log( 167 | 'Removing ' + 168 | _.size(fnProps.Environment.Variables) + 169 | ' environment variables from function "' + 170 | fnLogicalName + 171 | '" because Lambda@Edge does not support environment variables' 172 | ); 173 | 174 | delete fnProps.Environment.Variables; 175 | 176 | if (_.isEmpty(fnProps.Environment)) { 177 | delete fnProps.Environment; 178 | } 179 | } 180 | 181 | if (retainFunctions) { 182 | fnObj.DeletionPolicy = 'Retain'; 183 | } 184 | 185 | distConfig = dist.Properties.DistributionConfig; 186 | 187 | if (pathPattern) { 188 | cacheBehavior = _.findWhere(distConfig.CacheBehaviors, { PathPattern: pathPattern }); 189 | 190 | if (!cacheBehavior) { 191 | throw new Error('Could not find cache behavior in "' + distName + '" with path pattern "' + pathPattern + '"'); 192 | } 193 | } else { 194 | cacheBehavior = distConfig.DefaultCacheBehavior; 195 | } 196 | 197 | fnAssociations = cacheBehavior.LambdaFunctionAssociations; 198 | 199 | if (!_.isArray(fnAssociations)) { 200 | fnAssociations = cacheBehavior.LambdaFunctionAssociations = []; 201 | } 202 | 203 | fnAssociations.push({ 204 | EventType: evtType, 205 | IncludeBody: includeBody, 206 | LambdaFunctionARN: { Ref: versionLogicalID }, 207 | }); 208 | 209 | this._serverless.cli.log( 210 | 'Added "' + evtType + '" Lambda@Edge association for version "' + 211 | versionLogicalID + '" to distribution "' + distName + '"' + 212 | (pathPattern ? ' (path pattern "' + pathPattern + '")' : '') + 213 | (includeBody ? ' (IncludeBody)' : '') 214 | ); 215 | }, 216 | 217 | }); 218 | -------------------------------------------------------------------------------- /src/tests/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "extends": "@silvermine/eslint-config/node-tests" 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/tests/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('underscore'), 4 | expect = require('expect.js'), 5 | Plugin = require('../index.js'), 6 | sinon = require('sinon'); // eslint-disable-line no-unused-vars 7 | 8 | function stubServerless() { 9 | return { 10 | getProvider: function() { 11 | return {}; 12 | }, 13 | cli: { 14 | log: _.noop, 15 | consoleLog: _.noop, 16 | printDot: _.noop, 17 | }, 18 | configSchemaHandler: { 19 | defineCustomProperties: _.noop, 20 | defineFunctionProperties: _.noop, 21 | }, 22 | }; 23 | } 24 | 25 | describe('serverless-plugin-cloudfront-lambda-edge', function() { 26 | var plugin; // eslint-disable-line no-unused-vars 27 | 28 | beforeEach(function() { 29 | plugin = new Plugin(stubServerless(), {}); 30 | }); 31 | 32 | describe('TODO', function() { 33 | 34 | it('needs to be tested', function() { 35 | expect(1).to.eql(1); 36 | }); 37 | 38 | }); 39 | 40 | }); 41 | --------------------------------------------------------------------------------