├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── package-lock.json ├── package.json ├── src ├── aws-compile-servicecatalog.js ├── cf-provision-product-template.json └── display-endpoints.js ├── templates ├── iam │ ├── sc-enduser-iam.yml │ └── sc-serverless-launchrole.yml └── serverless │ ├── sc-portfolio-serverless.yml │ ├── sc-product-serverless-lambda.yml │ ├── sc-provision-serverless.yml │ └── sc-serverless-lambda.yml └── test ├── aws-compile-service-catalog.test.js ├── customTestTemplate-NoStage.json ├── customTestTemplate.json └── testTemplate.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true ; top-most EditorConfig file 2 | 3 | ; Unix-style newlines with a newline ending every file 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | tmp 4 | tmpdirs-serverless 5 | lib/plugins/create/templates/** 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'godaddy', 3 | plugins: [], 4 | rules: {}, 5 | env: { 6 | mocha: true, 7 | jest: true 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ '*' ] 8 | 9 | jobs: 10 | ci: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [12.x, 14.x, 16.x] 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - name: Cache node modules 22 | uses: actions/cache@v2 23 | env: 24 | cache-name: cache-node-modules 25 | with: 26 | # npm cache files are stored in `~/.npm` on Linux/macOS 27 | path: ~/.npm 28 | key: ${{ runner.os }}-build-${{ env.cache-name }}-node${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }} 29 | restore-keys: | 30 | ${{ runner.os }}-build-${{ env.cache-name }}-node${{ matrix.node-version }}- 31 | ${{ runner.os }}-build-${{ env.cache-name }}- 32 | ${{ runner.os }}-build- 33 | ${{ runner.os }}- 34 | - run: npm ci 35 | - run: npm test 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | .serverless 5 | *.orig 6 | .eslintcache 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at gd-aws-collab@godaddy.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | 78 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 GoDaddy Operating Company, LLC. 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 | # serverless-aws-service-catalog 2 | [![serverless](http://public.serverless.com/badges/v3.svg)](http://www.serverless.com) 3 | 4 | A plugin to allow the provisioning of [AWS Service Catalog](https://console.aws.amazon.com/servicecatalog) products with [serverless](http://www.serverless.com) 5 | 6 | **Serverless Framework versions** 7 | 8 | This plugin is now targeted at serverless@2, if you are using serverless@1 use `v1.2.1`. 9 | 10 | ## Install 11 | 12 | `npm install --save-dev serverless-aws-servicecatalog` 13 | 14 | Alternatively you may package the plugin `npm pack` and install it with npm from the tarball. 15 | 16 | Add the plugin to your `serverless.yml` file: 17 | 18 | ```yaml 19 | plugins: 20 | - serverless-aws-servicecatalog 21 | ``` 22 | 23 | ## Sample Configuration 24 | ```yaml 25 | provider: 26 | name: aws 27 | runtime: python2.7 28 | deploymentBucket: serverless-src-1234567890 29 | scProductId: prod-hpzfzam5x5vac 30 | scProductVersion: v1.2 31 | region: us-east-1 32 | stage: dev 33 | tags: 34 | product: 'my api' 35 | provisioningParameters: 36 | EndpointType: REGIONAL 37 | ``` 38 | 39 | 40 | ## Example 41 | There are 2 ways to setup the example, using the launch-stack button or manually from your own S3 bucket. Both methods result in a 42 | AWS CloudFormation stack with outputs that will be used as parameters in the `serverless.yml` config. 43 | 44 | 45 | ### Express Setup using launch-stack 46 | 1. Click the button below to setup your account. 47 | [![CreateStack](https://s3.amazonaws.com/cloudformation-examples/cloudformation-launch-stack.png)](https://console.aws.amazon.com/cloudformation/#/stacks/new?stackName=Serverless-SC-Portfolio-Stack&templateURL=https://s3.amazonaws.com/aws-service-catalog-reference-architectures/serverless/sc-portfolio-serverless.yml) 48 | https://s3.amazonaws.com/aws-service-catalog-reference-architectures/serverless/sc-portfolio-serverless.yml 49 | 50 | 2. Allow end users to deploy: 51 | 52 | - If you are using IAM users for deployment then go to the __ServiceCatalogEndUsers__ parameter, enter a comma delimited list of users to add to the generated group. 53 | 54 | - If you are using role based authentication then supply up to 2 role names in the __LinkedRole1__ and __LinkedRole2__ parameters. 55 | 56 | 3. Click Next, Next and check the acknowledgement checkboxes in the blue Capabilities box at the bottom 57 | 58 | 4. Click Create. Then wait for the stack to complete and go to the "Configure the serveless.yml in your lambda project" section below. 59 | 60 | 61 | 62 | ### Manually Setup using your own S3 bucket 63 | 1. Copy the files from the templates directory to your S3 bucket 64 | 65 | ```shell 66 | aws s3 cp ./custom-serverless-plugins/serverless-aws-service-catalog/templates s3://$S3BUCKET --exclude "*" --include "*.yml" --recursive 67 | ``` 68 | 69 | 2. Create the Cloudformation stack from the portfolio template. To allow end users to deploy you will need to edit the params of the CloudFormation template: 70 | 71 | - If you are using IAM users for deployment then go to the ServiceCatalogEndUsers parameter, enter a comma delimited list of users to add to the generated group. 72 | For this example an IAM user is supplied using the `SERVERLESS_USER` variable 73 | 74 | - If you are using role based authentication then supply up to 2 role names in the LinkedRole1 and LinkedRole2 parameters. 75 | 76 | ```shell 77 | export S3BUCKET=yourBucketName 78 | export SERVERLESS_USER=yourAwsServerlessUser 79 | aws cloudformation create-stack --stack-name Serverless-SC-Portfolio-Stack --template-url "https://s3.amazonaws.com/$S3BUCKET/serverless/sc-portfolio-serverless.yml" --parameters ParameterKey=PorfolioName,ParameterValue=ServerlessPortfolio ParameterKey=RepoRootURL,ParameterValue="https://s3.amazonaws.com/$S3BUCKET/" ParameterKey=ServiceCatalogEndUsers,ParameterValue=$SERVERLESS_USER --capabilities CAPABILITY_NAMED_IAM 80 | ``` 81 | (note: trailing / is required on the RepoRootUrl param) 82 | 83 | 84 | 85 | ### Configure your Serverless Framework project 86 | Regardless of how you deployed the CloudFormation above, you now need to copy the output values from CloudFormation to your `serverless.yml` file. 87 | This is only covering the AWS provider section and assumes you have a complete config for serverless. See the [Serverless Framework examples](https://github.com/serverless/examples) for more details. 88 | 89 | 1. get the output params 90 | a. using the cli 91 | ```shell 92 | aws cloudformation describe-stacks --stack-name Serverless-SC-Portfolio-Stack 93 | ``` 94 | 95 | b. or in the [AWS CloudFormation Console](https://console.aws.amazon.com/cloudformation) 96 | - in CloudFormation, open the Serverless-SC-Portfolio-Stack stack 97 | - expand Outputs 98 | 99 | 2. under provider, enter the settings 100 | - copy ServerlessDeploymentBucket to deploymentBucket 101 | - copy serverlessProductId to scProductId 102 | - copy serverlessProvisioningArtifactNames to scProductVersion 103 | - enter the region, stage, runtime, and any tags as you normally would. 104 | 105 | 106 | ```yaml 107 | provider: 108 | name: aws 109 | runtime: python2.7 110 | stage: dev 111 | deploymentBucket: [deploymentbucket] 112 | scProductId: [serverlessProductId] # Or use scProductName instead if you want to target your template's name 113 | scProductVersion: [serverlessProvisioningArtifactNames] 114 | region: us-east-1 115 | tags: 116 | product: 'my api' 117 | ``` 118 | 119 | ### Deploy 120 | If you have modified the configuration and have your AWS credentials setup according to 121 | [serverless instrcutions](https://serverless.com/framework/docs/providers/aws/guide/credentials/), you can now deploy as you normally would. 122 | 123 | ```shell 124 | serverless deploy -v 125 | ``` 126 | 127 | ## Building a Custom Serverless Service Catalog Product 128 | 129 | ### Custom parameters passed by the plugin 130 | 131 | The plugin passes custom parameters for the following Serverless features: (see sc-serverless-lambda.yml) 132 | 133 | - **Vpc**: supports the standard Serverless vpc configuration in serverless.yml 134 | ```yaml 135 | # serverless.yml 136 | vpc: 137 | securityGroupIds: 138 | - "sg-XXXXXXXX" 139 | subnetIds: 140 | - "subnet-XXXXXXX" 141 | ``` 142 | 143 | ```yaml 144 | # service catalog product yml 145 | VpcSecurityGroups: 146 | Type: CommaDelimitedList 147 | Description: (optional) The list of security group ids of the VPC that needs to be accessed. 148 | Default: "" 149 | VpcSubnetIds: 150 | Type: CommaDelimitedList 151 | Description: (optional) The list of subnet Ids within the VPC that needs access to. 152 | Default: "" 153 | ``` 154 | - **Layers**: supports a list of existing layers 155 | 156 | ```yaml 157 | # serverless.yml 158 | layers: 159 | - arn:aws:lambda:us-east-1:XXXXXXXXXX:layer:node_js_layer:1 160 | ``` 161 | 162 | ```yaml 163 | # service catalog product yml 164 | LambdaLayers: 165 | Type: CommaDelimitedList 166 | Description: "(optional) list of lambda layers for the function" 167 | Default: "" 168 | ``` 169 | 170 | ## Using Custom Service Catalog Parameter Names 171 | 172 | If you already have a service catalog product for Lambdas that has a different naming schema, you can still use that product. You can provide a renaming mapping in your `serverless.yml` file like so: 173 | 174 | ```yaml 175 | provider: 176 | scParameterMapping: 177 | stage: SomeCustomStageName 178 | name: MyLambdaParameterName 179 | ``` 180 | 181 | You can specify any of the following properties to be renamed (shown here with their default values): 182 | 183 | ```yaml 184 | provider: 185 | scParameterMapping: 186 | s3Bucket: S3Bucket 187 | s3Key: S3Key 188 | handler: Handler 189 | name: LambdaName 190 | memorySize: MemorySize 191 | timeout: Timeout 192 | runtime: Runtime 193 | stage: LambdaStage 194 | environmentVariablesJson: EnvironmentVariablesJson 195 | vpcSecurityGroups: VpcSecurityGroups 196 | vpcSubnetIds: VpcSubnetIds 197 | lambdaLayers: LambdaLayers 198 | ``` 199 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | We take security very seriously at GoDaddy. We appreciate your efforts to 4 | responsibly disclose your findings, and will make every effort to acknowledge 5 | your contributions. 6 | 7 | ## Where should I report security issues? 8 | 9 | In order to give the community time to respond and upgrade, we strongly urge you 10 | report all security issues privately. 11 | 12 | To report a security issue in one of our Open Source projects email us directly 13 | at **oss@godaddy.com** and include the word "SECURITY" in the subject line. 14 | 15 | This mail is delivered to our Open Source Security team. 16 | 17 | After the initial reply to your report, the team will keep you informed of the 18 | progress being made towards a fix and announcement, and may ask for additional 19 | information or guidance. 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-aws-servicecatalog", 3 | "version": "2.1.0", 4 | "description": "A plugin to allow the provisioning of AWS Service Catalog products with the Serverless Framework", 5 | "main": "src/aws-compile-servicecatalog.js", 6 | "homepage": "https://github.com/godaddy/serverless-aws-servicecatalog", 7 | "bugs": "https://github.com/godaddy/serverless-aws-servicecatalog/issues", 8 | "keywords": [ 9 | "serverless", 10 | "plugin", 11 | "aws", 12 | "servicecatalog", 13 | "service catalog" 14 | ], 15 | "scripts": { 16 | "test": "mocha", 17 | "test-coverage": "nyc --reporter=lcov mocha", 18 | "posttest": "npm run lint", 19 | "lint": "eslint . --cache" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git@github.com:godaddy/serverless-aws-servicecatalog.git" 24 | }, 25 | "author": "GoDaddy.com Operating Company, LLC", 26 | "maintainers": [ 27 | "John Smey ", 28 | "Jonathan Keslin " 29 | ], 30 | "license": "MIT", 31 | "dependencies": { 32 | "chalk": "^4.1.2" 33 | }, 34 | "devDependencies": { 35 | "chai": "^4.3.6", 36 | "chai-as-promised": "^7.1.1", 37 | "eslint": "^8.8.0", 38 | "eslint-config-godaddy": "^6.0.0", 39 | "eslint-plugin-json": "^3.1.0", 40 | "eslint-plugin-mocha": "^10.0.3", 41 | "mocha": "^9.2.0", 42 | "nyc": "^15.1.0", 43 | "serverless": "^2.72.2", 44 | "sinon": "^13.0.0" 45 | }, 46 | "engines": { 47 | "node": ">=12" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/aws-compile-servicecatalog.js: -------------------------------------------------------------------------------- 1 | /* eslint no-process-exit: 0 */ 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const { displayEndpoints } = require('./display-endpoints'); 6 | 7 | const scParameterMappingDefaults = { 8 | s3Bucket: 'S3Bucket', 9 | s3Key: 'S3Key', 10 | handler: 'Handler', 11 | name: 'LambdaName', 12 | memorySize: 'MemorySize', 13 | timeout: 'Timeout', 14 | runtime: 'Runtime', 15 | stage: 'LambdaStage', 16 | environmentVariablesJson: 'EnvironmentVariablesJson', 17 | vpcSecurityGroups: 'VpcSecurityGroups', 18 | vpcSubnetIds: 'VpcSubnetIds', 19 | lambdaLayers: 'LambdaLayers' 20 | }; 21 | 22 | /** 23 | * @typedef {import('serverless')} Serverless 24 | * @typedef {import('serverless').Options} ServerlessOptions 25 | */ 26 | 27 | class AwsCompileServiceCatalog { 28 | /** 29 | * Construct an instance of the serverless-aws-servicecatalog plugin 30 | * 31 | * @param {Serverless} serverless Serverless instance 32 | * @param {ServerlessOptions} options Options 33 | */ 34 | constructor(serverless, options) { 35 | this.serverless = serverless; 36 | this.options = options; 37 | const servicePath = this.serverless.config.servicePath || ''; 38 | this.packagePath = this.serverless.service.package.path 39 | || path.join(servicePath || '.', '.serverless'); 40 | this.provider = this.serverless.getProvider('aws'); 41 | 42 | // Add custom schema properties to the AWS provider. For reference use https://github.com/ajv-validator/ajv 43 | if (serverless.configSchemaHandler) { 44 | serverless.configSchemaHandler.defineProvider('aws', { 45 | provider: { 46 | properties: { 47 | scProductId: { type: 'string' }, 48 | scProductName: { type: 'string' }, 49 | scProductVersion: { type: 'string' }, 50 | scProductTemplate: { type: 'string' }, 51 | scParameterMapping: { type: 'object' }, 52 | provisioningParameters: { type: 'object' } 53 | } 54 | } 55 | }); 56 | serverless.configSchemaHandler.defineFunctionProperties('aws', { 57 | properties: { 58 | provisioningParameters: { type: 'object' } 59 | } 60 | }); 61 | } 62 | 63 | // key off the ServiceCatalog Product ID or name 64 | if ('scProductId' in this.serverless.service.provider || 65 | 'scProductName' in this.serverless.service.provider) { 66 | this.hooks = { 67 | 'before:package:finalize': this.compileFunctions.bind(this), 68 | 'after:aws:info:displayApiKeys': displayEndpoints.bind(this) 69 | }; 70 | this.serverless.cli.log('AwsCompileServiceCatalog'); 71 | this.clearOtherPlugins(); 72 | } 73 | 74 | const mappingOverrides = this.serverless.service.provider && 75 | this.serverless.service.provider.scParameterMapping || {}; 76 | this.parameterMapping = Object.assign( 77 | {}, 78 | scParameterMappingDefaults, 79 | mappingOverrides 80 | ); 81 | } 82 | 83 | clearOtherPlugins() { 84 | // clear out any other aws plugins 85 | if (this.serverless.pluginManager.hooks['package:compileEvents']) { 86 | this.serverless.pluginManager.hooks['package:compileEvents'].length = 0; 87 | } 88 | if (this.serverless.pluginManager.hooks['package:compileFunctions']) { 89 | this.serverless.pluginManager.hooks['package:compileFunctions'].length = 0; 90 | } 91 | if (this.serverless.pluginManager.hooks['package:setupProviderConfiguration']) { 92 | this.serverless.pluginManager.hooks['package:setupProviderConfiguration'].length = 0; 93 | } 94 | if (this.serverless.pluginManager.hooks['aws:info:displayEndpoints']) { 95 | this.serverless.pluginManager.hooks['aws:info:displayEndpoints'].length = 0; 96 | } 97 | } 98 | 99 | getCfTemplate() { 100 | let templateFile = path.join(__dirname, './cf-provision-product-template.json'); 101 | if ('scProductTemplate' in this.serverless.service.provider 102 | && this.serverless.service.provider.scProductTemplate.length > 0) { 103 | templateFile = this.serverless.service.provider.scProductTemplate; 104 | } 105 | let templateJson; 106 | let parsedTemplate; 107 | try { 108 | // eslint-disable-next-line no-sync 109 | templateJson = fs.readFileSync(templateFile, 'utf8'); 110 | } catch (ex) { 111 | this.serverless.cli.log('error reading template file:: ', templateFile); 112 | process.exit(1); 113 | } 114 | try { 115 | parsedTemplate = JSON.parse(templateJson); 116 | } catch (ex) { 117 | this.serverless.cli.log('error parsing template file'); 118 | process.exit(1); 119 | } 120 | return parsedTemplate; 121 | } 122 | 123 | async compileFunctions() { 124 | const allFunctions = this.serverless.service.getAllFunctions(); 125 | for (const functionName of allFunctions) { 126 | await this.compileFunction(functionName); 127 | } 128 | 129 | return allFunctions; 130 | } 131 | 132 | getParameterName(name) { 133 | return this.parameterMapping[name]; 134 | } 135 | 136 | // eslint-disable-next-line complexity, max-statements 137 | async compileFunction(functionName) { 138 | const newFunction = this.getCfTemplate(); 139 | const setProvisioningParamValue = (key, value) => { 140 | if (!key) { 141 | return; 142 | } 143 | 144 | const index = newFunction.Properties.ProvisioningParameters 145 | .findIndex(kv => kv.Key === key); 146 | if (index === -1) { 147 | this.serverless.cli.log(`object with Key=${key} not found in ProvisioningParameters!`); 148 | return; 149 | } 150 | newFunction.Properties.ProvisioningParameters[index].Value = value; 151 | }; 152 | const functionObject = this.serverless.service.getFunction(functionName); 153 | if ('image' in functionObject) { 154 | throw new this.serverless.classes.Error('serverless-aws-servicecatalog does not support Docker image functions'); 155 | } 156 | functionObject.package = functionObject.package || {}; 157 | 158 | const serviceArtifactFileName = this.provider.naming.getServiceArtifactName(); 159 | const functionArtifactFileName = this.provider.naming.getFunctionArtifactName(functionName); 160 | 161 | let artifactFilePath = functionObject.package.artifact 162 | || this.serverless.service.package.artifact; 163 | if (!artifactFilePath 164 | || (this.serverless.service.package.artifact && !functionObject.package.artifact)) { 165 | let artifactFileName = serviceArtifactFileName; 166 | if (this.serverless.service.package.individually || functionObject.package.individually) { 167 | artifactFileName = functionArtifactFileName; 168 | } 169 | 170 | artifactFilePath = path.join(this.serverless.config.servicePath, 171 | '.serverless', artifactFileName); 172 | } 173 | if (this.serverless.service.provider.deploymentBucket) { 174 | setProvisioningParamValue(this.getParameterName('s3Bucket'), this.serverless.service.provider.deploymentBucket); 175 | } else { 176 | const errorMessage = 'Missing provider.deploymentBucket parameter.' 177 | + ' Please make sure you provide a deployment bucket parameter. SC Provisioned Product cannot create an S3 Bucket.' 178 | + ' Please check the docs for more info'; 179 | throw new this.serverless.classes.Error(errorMessage); 180 | } 181 | 182 | const s3Folder = this.serverless.service.package.artifactDirectoryName; 183 | const s3FileName = artifactFilePath.split(path.sep).pop(); 184 | setProvisioningParamValue(this.getParameterName('s3Key'), `${s3Folder}/${s3FileName}`); 185 | 186 | if (!functionObject.handler) { 187 | const errorMessage = `Missing "handler" property in function "${functionName}".` 188 | + ' Please make sure you point to the correct lambda handler.' 189 | + ' For example: handler.hello.' 190 | + ' Please check the docs for more info'; 191 | throw new this.serverless.classes.Error(errorMessage); 192 | } 193 | 194 | const MemorySize = Number(functionObject.memorySize) 195 | || Number(this.serverless.service.provider.memorySize) 196 | || 1024; 197 | const Timeout = Number(functionObject.timeout) 198 | || Number(this.serverless.service.provider.timeout) 199 | || 6; 200 | const Runtime = functionObject.runtime 201 | || this.serverless.service.provider.runtime 202 | || 'nodejs4.3'; 203 | 204 | setProvisioningParamValue(this.getParameterName('handler'), functionObject.handler); 205 | setProvisioningParamValue(this.getParameterName('name'), functionObject.name); 206 | setProvisioningParamValue(this.getParameterName('memorySize'), MemorySize); 207 | setProvisioningParamValue(this.getParameterName('timeout'), Timeout); 208 | setProvisioningParamValue(this.getParameterName('runtime'), Runtime); 209 | setProvisioningParamValue(this.getParameterName('stage'), this.provider.getStage()); 210 | const serviceProvider = this.serverless.service.provider; 211 | newFunction.Properties.ProvisioningArtifactName = serviceProvider.scProductVersion; 212 | 213 | if (serviceProvider.scProductId) { 214 | newFunction.Properties.ProductId = serviceProvider.scProductId; 215 | } else if (serviceProvider.scProductName) { 216 | delete newFunction.Properties.ProductId; 217 | newFunction.Properties.ProductName = serviceProvider.scProductName; 218 | } else { 219 | const errorMessage = 'Missing scProductId or scProductName on service.' 220 | + ' Please make sure to define one of "scProductId" or "scProductName".' 221 | + ' See documentation for more info.'; 222 | throw new this.serverless.classes.Error(errorMessage); 223 | } 224 | 225 | newFunction.Properties.ProvisionedProductName = `provisionSC-${functionObject.name}`; 226 | 227 | // publish these properties to the platform 228 | this.serverless.service.functions[functionName].memory = MemorySize; 229 | this.serverless.service.functions[functionName].timeout = Timeout; 230 | this.serverless.service.functions[functionName].runtime = Runtime; 231 | 232 | if (functionObject.tags || this.serverless.service.provider.tags) { 233 | const tags = Object.assign( 234 | {}, 235 | this.serverless.service.provider.tags, 236 | functionObject.tags 237 | ); 238 | newFunction.Properties.Tags = Object.keys(tags).map(key => ( 239 | { Key: key, Value: tags[key] })); 240 | } 241 | 242 | if (functionObject.environment || this.serverless.service.provider.environment) { 243 | const environment = Object.assign( 244 | {}, 245 | this.serverless.service.provider.environment || {}, 246 | functionObject.environment || {} 247 | ); 248 | const envKeys = Object.keys(environment); 249 | envKeys.forEach((key) => { 250 | // taken from the bash man pages 251 | if (!key.match(/^[A-Za-z_][a-zA-Z0-9_]*$/)) { 252 | throw new this.serverless.classes.Error(`Invalid characters in environment variable name ${key}`); 253 | } 254 | const value = environment[key]; 255 | if (value === Object(value)) { 256 | const isCFRef = !value.some(v => v !== 'Ref' && !v.startsWith('Fn::')); 257 | if (!isCFRef) { 258 | throw new this.serverless.classes.Error(`Environment variable ${key} must contain string`); 259 | } 260 | } 261 | return true; 262 | }); 263 | newFunction.Properties.ProvisioningParameters.push({ 264 | Key: this.getParameterName('environmentVariablesJson'), 265 | Value: JSON.stringify(environment) 266 | }); 267 | } 268 | if (functionObject.provisioningParameters 269 | || this.serverless.service.provider.provisioningParameters) { 270 | const provisioningParameters = Object.assign( 271 | {}, 272 | this.serverless.service.provider.provisioningParameters, 273 | functionObject.provisioningParameters 274 | ); 275 | let errorMessage = null; 276 | if (Object.entries(provisioningParameters).some(([key, value]) => { 277 | if (newFunction.Properties.ProvisioningParameters.some(p => p.Key === key)) { 278 | errorMessage = `Duplicate provisioning parameter "${key}" found.` 279 | + ' Please make sure that all items listed in "provisioningParameters" are unique.'; 280 | return true; 281 | } 282 | 283 | newFunction.Properties.ProvisioningParameters.push({ 284 | Key: key, 285 | Value: value 286 | }); 287 | return false; 288 | })) { 289 | throw new this.serverless.classes.Error(errorMessage); 290 | } 291 | } 292 | if (!functionObject.vpc) { 293 | functionObject.vpc = {}; 294 | } else { 295 | const vpcSecurityGroups = functionObject.vpc.securityGroupIds 296 | || this.serverless.service.provider.vpc.securityGroupIds; 297 | 298 | const vpcSubnetIds = functionObject.vpc.subnetIds 299 | || this.serverless.service.provider.vpc.subnetIds; 300 | 301 | if (vpcSecurityGroups && vpcSubnetIds) { 302 | newFunction.Properties.ProvisioningParameters.push({ 303 | Key: this.getParameterName('vpcSecurityGroups'), 304 | Value: vpcSecurityGroups.toString() 305 | }); 306 | newFunction.Properties.ProvisioningParameters.push({ 307 | Key: this.getParameterName('vpcSubnetIds'), 308 | Value: vpcSubnetIds.toString() 309 | }); 310 | } 311 | } 312 | 313 | let { layers } = functionObject; 314 | if (!layers || !Array.isArray(layers)) { 315 | ({ layers } = this.serverless.service.provider); 316 | if (!layers || !Array.isArray(layers)) { 317 | layers = null; 318 | } 319 | } 320 | layers = layers && layers.toString(); 321 | if (layers) { 322 | newFunction.Properties.ProvisioningParameters.push({ 323 | Key: this.getParameterName('lambdaLayers'), 324 | Value: layers 325 | }); 326 | } 327 | 328 | // Only leave the provisioning parameters that exist and have a key 329 | newFunction.Properties.ProvisioningParameters = newFunction.Properties.ProvisioningParameters 330 | .filter(item => item && item.Key); 331 | 332 | const functionLogicalId = `${this.provider.naming.getLambdaLogicalId(functionName)}SCProvisionedProduct`; 333 | this.serverless.service.provider.compiledCloudFormationTemplate 334 | .Resources[functionLogicalId] = newFunction; 335 | this.serverless.service.provider 336 | .compiledCloudFormationTemplate.Outputs.ProvisionedProductID = { 337 | Description: 'Provisioned product ID', 338 | Value: { Ref: functionLogicalId } 339 | }; 340 | } 341 | } 342 | 343 | module.exports = AwsCompileServiceCatalog; 344 | -------------------------------------------------------------------------------- /src/cf-provision-product-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "AWS::ServiceCatalog::CloudFormationProvisionedProduct", 3 | 4 | "Properties": { 5 | "ProvisioningParameters": [ 6 | { 7 | "Key": "S3Bucket", 8 | "Value": "ServerlessDeploymentBucket" 9 | }, 10 | { 11 | "Key": "S3Key", 12 | "Value": "S3Key" 13 | }, 14 | { 15 | "Key": "LambdaName", 16 | "Value": "LambdaName" 17 | }, 18 | { 19 | "Key": "LambdaStage", 20 | "Value": "test" 21 | }, 22 | { 23 | "Key": "Handler", 24 | "Value": "Handler" 25 | }, 26 | { 27 | "Key": "Runtime", 28 | "Value": "Runtime" 29 | }, 30 | { 31 | "Key": "MemorySize", 32 | "Value": "MemorySize" 33 | }, 34 | { 35 | "Key": "Timeout", 36 | "Value": "Timeout" 37 | } 38 | ], 39 | "Tags":[ 40 | { 41 | "Key": "doNotShutDown", 42 | "Value": "false" 43 | } 44 | ], 45 | "ProvisioningArtifactName": "ProvisioningArtifactName", 46 | "ProductId": "ProductId", 47 | "ProvisionedProductName": { 48 | "Fn::Sub": "provisionServerless-${LambdaName}" 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/display-endpoints.js: -------------------------------------------------------------------------------- 1 | /* eslint max-nested-callbacks: 0 */ 2 | 3 | /** @type{import('chalk').Chalk} */ 4 | // @ts-ignore 5 | const chalk = require('chalk'); 6 | 7 | module.exports = { 8 | async displayEndpoints() { 9 | this.serverless.cli.consoleLog(chalk.yellow('endpoints:')); 10 | const allFunctions = this.serverless.service.getAllFunctions(); 11 | 12 | for (const functionName of allFunctions) { 13 | await displayEndpoint.call(this, functionName); 14 | } 15 | 16 | return allFunctions; 17 | } 18 | }; 19 | 20 | async function displayEndpoint(functionName) { 21 | const functionObject = this.serverless.service.getFunction(functionName); 22 | const deployedName = functionObject.name; 23 | 24 | const result = await this.provider.request('Lambda', 25 | 'getFunction', { 26 | FunctionName: deployedName 27 | }); 28 | const stack = await this.provider.request('CloudFormation', 29 | 'describeStacks', { 30 | StackName: result.Tags['aws:cloudformation:stack-name'] 31 | }); 32 | const outputs = stack.Stacks[0].Outputs; 33 | const serviceEndpointOutputRegex = this.provider.naming 34 | .getServiceEndpointRegex(); 35 | outputs.filter(x => x.OutputKey.match(serviceEndpointOutputRegex)) 36 | .forEach((x) => { 37 | const endpoint = x.OutputValue; 38 | functionObject.events.forEach((event) => { 39 | if (event.http) { 40 | let method; 41 | let path; 42 | if (typeof event.http === 'object') { 43 | method = event.http.method.toUpperCase(); 44 | path = event.http.path; 45 | } else { 46 | [method, path] = event.http.split(' '); 47 | method = method.toUpperCase(); 48 | } 49 | this.serverless.cli.consoleLog(` ${method} - ${endpoint}/${path}`); 50 | } 51 | }); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /templates/iam/sc-enduser-iam.yml: -------------------------------------------------------------------------------- 1 | Description: "ServiceCatalog End User policy and group (fdp-1p4dlgcp7)" 2 | Resources: 3 | SCEnduserGroup: 4 | Type: AWS::IAM::Group 5 | Properties: 6 | GroupName: ServiceCatalogEndUsers 7 | ManagedPolicyArns: 8 | - arn:aws:iam::aws:policy/ServiceCatalogEndUserAccess 9 | Path: / 10 | SCEnduserPolicy: 11 | Type: AWS::IAM::Policy 12 | Properties: 13 | PolicyDocument: 14 | Version: "2012-10-17" 15 | Statement: 16 | - Effect: "Allow" 17 | Action: 18 | - "servicecatalog:ProvisionProduct" 19 | - "ec2:CreateNetworkInterface" 20 | - "ec2:DeleteNetworkInterface" 21 | - "ec2:DescribeNetworkInterfaces" 22 | Resource: "*" 23 | PolicyName: ServiceCatalogEndUsers-AdditionalPermissions 24 | Groups: 25 | - !Ref SCEnduserGroup 26 | Outputs: 27 | EndUserGroupArn: 28 | Value: !GetAtt SCEnduserGroup.Arn 29 | EndUserGroupName: 30 | Value: !Ref SCEnduserGroup 31 | 32 | -------------------------------------------------------------------------------- /templates/iam/sc-serverless-launchrole.yml: -------------------------------------------------------------------------------- 1 | Description: "ServiceCatalog Serverless Launch Role. (fdp-1p5s1035k)" 2 | Resources: 3 | SCServerlessLaunchRole: 4 | Type: 'AWS::IAM::Role' 5 | Properties: 6 | RoleName: SCServerlessLaunchRole 7 | AssumeRolePolicyDocument: 8 | Version: 2012-10-17 9 | Statement: 10 | - Effect: Allow 11 | Principal: 12 | Service: 13 | - servicecatalog.amazonaws.com 14 | Action: 15 | - 'sts:AssumeRole' 16 | Path: / 17 | Policies: 18 | - PolicyName: SCLaunchPolicy 19 | PolicyDocument: 20 | Version: 2012-10-17 21 | Statement: 22 | - Sid: SCLaunchPolicySID 23 | Action: 24 | - "apigateway:*" 25 | - "cloudformation:CancelUpdateStack" 26 | - "cloudformation:ContinueUpdateRollback" 27 | - "cloudformation:CreateChangeSet" 28 | - "cloudformation:CreateStack" 29 | - "cloudformation:CreateUploadBucket" 30 | - "cloudformation:DeleteStack" 31 | - "cloudformation:Describe*" 32 | - "cloudformation:EstimateTemplateCost" 33 | - "cloudformation:ExecuteChangeSet" 34 | - "cloudformation:Get*" 35 | - "cloudformation:List*" 36 | - "cloudformation:PreviewStackUpdate" 37 | - "cloudformation:UpdateStack" 38 | - "cloudformation:UpdateTerminationProtection" 39 | - "cloudformation:ValidateTemplate" 40 | - "dynamodb:CreateTable" 41 | - "dynamodb:DeleteTable" 42 | - "dynamodb:DescribeTable" 43 | - "ec2:AttachInternetGateway" 44 | - "ec2:AuthorizeSecurityGroupIngress" 45 | - "ec2:CreateInternetGateway" 46 | - "ec2:CreateNetworkAcl" 47 | - "ec2:CreateNetworkAclEntry" 48 | - "ec2:CreateNetworkInterface" 49 | - "ec2:CreateRouteTable" 50 | - "ec2:CreateSecurityGroup" 51 | - "ec2:CreateSubnet" 52 | - "ec2:CreateTags" 53 | - "ec2:CreateVpc" 54 | - "ec2:DeleteInternetGateway" 55 | - "ec2:DeleteNetworkAcl" 56 | - "ec2:DeleteNetworkAclEntry" 57 | - "ec2:DeleteNetworkInterface" 58 | - "ec2:DescribeNetworkInterfaces" 59 | - "ec2:DeleteRouteTable" 60 | - "ec2:DeleteSecurityGroup" 61 | - "ec2:DeleteSubnet" 62 | - "ec2:DeleteVpc" 63 | - "ec2:Describe*" 64 | - "ec2:DetachInternetGateway" 65 | - "ec2:ModifyVpcAttribute" 66 | - "events:DeleteRule" 67 | - "events:DescribeRule" 68 | - "events:ListRuleNamesByTarget" 69 | - "events:ListRules" 70 | - "events:ListTargetsByRule" 71 | - "events:PutRule" 72 | - "events:PutTargets" 73 | - "events:RemoveTargets" 74 | - "iam:CreateRole" 75 | - "iam:DeleteRole" 76 | - "iam:DeleteRolePolicy" 77 | - "iam:GetRole" 78 | - "iam:PassRole" 79 | - "iam:PutRolePolicy" 80 | - "iot:CreateTopicRule" 81 | - "iot:DeleteTopicRule" 82 | - "iot:DisableTopicRule" 83 | - "iot:EnableTopicRule" 84 | - "iot:ReplaceTopicRule" 85 | - "kinesis:CreateStream" 86 | - "kinesis:DeleteStream" 87 | - "kinesis:DescribeStream" 88 | - "lambda:*" 89 | - "logs:CreateLogGroup" 90 | - "logs:DeleteLogGroup" 91 | - "logs:DescribeLogGroups" 92 | - "logs:DescribeLogStreams" 93 | - "logs:FilterLogEvents" 94 | - "logs:GetLogEvents" 95 | - "s3:CreateBucket" 96 | - "s3:DeleteBucket" 97 | - "s3:DeleteBucketPolicy" 98 | - "s3:DeleteObject" 99 | - "s3:DeleteObjectVersion" 100 | - "s3:GetObject" 101 | - "s3:GetObjectVersion" 102 | - "s3:ListAllMyBuckets" 103 | - "s3:ListBucket" 104 | - "s3:PutBucketNotification" 105 | - "s3:PutBucketPolicy" 106 | - "s3:PutBucketTagging" 107 | - "s3:PutBucketWebsite" 108 | - "s3:PutEncryptionConfiguration" 109 | - "s3:PutObject" 110 | - "sns:CreateTopic" 111 | - "sns:DeleteTopic" 112 | - "sns:GetSubscriptionAttributes" 113 | - "sns:GetTopicAttributes" 114 | - "sns:ListSubscriptions" 115 | - "sns:ListSubscriptionsByTopic" 116 | - "sns:ListTopics" 117 | - "sns:SetSubscriptionAttributes" 118 | - "sns:SetTopicAttributes" 119 | - "sns:Subscribe" 120 | - "sns:Unsubscribe" 121 | - "states:CreateStateMachine" 122 | - "states:DeleteStateMachine" 123 | Resource: '*' 124 | Effect: "Allow" 125 | Outputs: 126 | LaunchRoleArn: 127 | Value: !GetAtt SCServerlessLaunchRole.Arn 128 | LaunchRoleName: 129 | Value: !Ref SCServerlessLaunchRole 130 | -------------------------------------------------------------------------------- /templates/serverless/sc-portfolio-serverless.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: '2010-09-09' 3 | Description: Serverless Portfolio for Service Catalog. (fdp-1p5s1036d) 4 | Metadata: 5 | AWS::CloudFormation::Interface: 6 | ParameterGroups: 7 | - Label: 8 | default: Portfolio Information 9 | Parameters: 10 | - PorfolioName 11 | - PortfolioProvider 12 | - PorfolioDescription 13 | - Label: 14 | default: IAM Settings 15 | Parameters: 16 | - LaunchRoleName 17 | - LinkedRole1 18 | - LinkedRole2 19 | - ServiceCatalogEndUsers 20 | - CreateEndUsersGroup 21 | - Label: 22 | default: Product Settings 23 | Parameters: 24 | - RepoRootURL 25 | Parameters: 26 | PortfolioProvider: 27 | Type: String 28 | Description: Provider Name 29 | Default: IT Services 30 | PorfolioName: 31 | Type: String 32 | Description: Portfolio Name 33 | Default: Service Catalog Serverless Reference Architecture 34 | PorfolioDescription: 35 | Type: String 36 | Description: Portfolio Description 37 | Default: Service Catalog Portfolio that contains reference architecture products 38 | for Serverless Framework. 39 | LaunchRoleName: 40 | Type: String 41 | Description: Name of the launch constraint role for Serverless products. leave 42 | this blank to create the role. 43 | Default: '' 44 | LinkedRole1: 45 | Type: String 46 | Description: "(Optional) The name of a role which can execute products in this 47 | portfolio." 48 | Default: '' 49 | LinkedRole2: 50 | Type: String 51 | Description: "(Optional) The name of a second role which can execute products 52 | in this portfolio." 53 | Default: '' 54 | RepoRootURL: 55 | Type: String 56 | Description: Root url for the repo containing the product templates. 57 | Default: 'https://s3.amazonaws.com/aws-service-catalog-reference-architectures/' 58 | CreateEndUsersGroup: 59 | Type: String 60 | Description: Select Yes to Create the ServiceCatalogEndUsers IAM group. No if 61 | you have already created the group 62 | AllowedValues: 63 | - 'Yes' 64 | - 'No' 65 | Default: 'Yes' 66 | ServiceCatalogEndUsers: 67 | Type: CommaDelimitedList 68 | Description: (Optional) Users to add ServiceCatalogEndUsers. 69 | Default: '' 70 | Conditions: 71 | CreateLaunchConstraint: 72 | Fn::Equals: 73 | - Ref: LaunchRoleName 74 | - '' 75 | CondCreateEndUsersGroup: 76 | Fn::Equals: 77 | - Ref: CreateEndUsersGroup 78 | - 'Yes' 79 | CondHasEndUsers: 80 | Fn::Not: 81 | - Fn::Equals: 82 | - Fn::Join: 83 | - '' 84 | - Ref: ServiceCatalogEndUsers 85 | - '' 86 | CondLinkRole1: 87 | Fn::Not: 88 | - Fn::Equals: 89 | - Ref: LinkedRole1 90 | - '' 91 | CondLinkRole2: 92 | Fn::Not: 93 | - Fn::Equals: 94 | - Ref: LinkedRole2 95 | - '' 96 | Resources: 97 | serverlessFrameworkDeploymentBucket: 98 | Type: AWS::S3::Bucket 99 | Properties : 100 | BucketName: !Sub "serverless-src-${AWS::AccountId}" 101 | SCServerlessPortfolio: 102 | Type: AWS::ServiceCatalog::Portfolio 103 | Properties: 104 | ProviderName: 105 | Ref: PortfolioProvider 106 | Description: 107 | Ref: PorfolioDescription 108 | DisplayName: 109 | Ref: PorfolioName 110 | addrole1: 111 | Type: AWS::ServiceCatalog::PortfolioPrincipalAssociation 112 | Condition: CondLinkRole1 113 | Properties: 114 | PrincipalARN: 115 | Fn::Sub: arn:aws:iam::${AWS::AccountId}:role/${LinkedRole1} 116 | PortfolioId: 117 | Ref: SCServerlessPortfolio 118 | PrincipalType: IAM 119 | addrole2: 120 | Type: AWS::ServiceCatalog::PortfolioPrincipalAssociation 121 | Condition: CondLinkRole2 122 | Properties: 123 | PrincipalARN: 124 | Fn::Sub: arn:aws:iam::${AWS::AccountId}:role/${LinkedRole2} 125 | PortfolioId: 126 | Ref: SCServerlessPortfolio 127 | PrincipalType: IAM 128 | LaunchConstraintRole: 129 | Type: AWS::CloudFormation::Stack 130 | Condition: CreateLaunchConstraint 131 | Properties: 132 | TemplateURL: 133 | Fn::Sub: "${RepoRootURL}iam/sc-serverless-launchrole.yml" 134 | TimeoutInMinutes: 5 135 | stackServiceCatalogEndUsers: 136 | Type: AWS::CloudFormation::Stack 137 | Condition: CondCreateEndUsersGroup 138 | Properties: 139 | TemplateURL: 140 | Fn::Sub: "${RepoRootURL}iam/sc-enduser-iam.yml" 141 | TimeoutInMinutes: 5 142 | LinkEndusers: 143 | Type: AWS::ServiceCatalog::PortfolioPrincipalAssociation 144 | Properties: 145 | PrincipalARN: 146 | Fn::If: 147 | - CondCreateEndUsersGroup 148 | - Fn::GetAtt: 149 | - stackServiceCatalogEndUsers 150 | - Outputs.EndUserGroupArn 151 | - Fn::Sub: arn:aws:iam::${AWS::AccountId}:group/ServiceCatalogEndUsers 152 | PortfolioId: 153 | Ref: SCServerlessPortfolio 154 | PrincipalType: IAM 155 | AddUsersToGroup: 156 | Type: AWS::IAM::UserToGroupAddition 157 | Condition: CondHasEndUsers 158 | Properties: 159 | GroupName: 160 | !GetAtt 161 | - stackServiceCatalogEndUsers 162 | - Outputs.EndUserGroupName 163 | Users: 164 | Ref: ServiceCatalogEndUsers 165 | DependsOn: stackServiceCatalogEndUsers 166 | serverlessproduct: 167 | Type: AWS::CloudFormation::Stack 168 | Properties: 169 | Parameters: 170 | PortfolioProvider: 171 | Ref: PortfolioProvider 172 | LaunchConstraintARN: 173 | Fn::If: 174 | - CreateLaunchConstraint 175 | - Fn::GetAtt: 176 | - LaunchConstraintRole 177 | - Outputs.LaunchRoleArn 178 | - Fn::Sub: arn:aws:iam::${AWS::AccountId}:role/${LaunchRoleName} 179 | PortfolioId: 180 | Ref: SCServerlessPortfolio 181 | RepoRootURL: 182 | Ref: RepoRootURL 183 | TemplateURL: 184 | Fn::Sub: "${RepoRootURL}serverless/sc-product-serverless-lambda.yml" 185 | TimeoutInMinutes: 5 186 | Outputs: 187 | PortfolioId: 188 | Description: The Service Catalog portfolioId 189 | Value: 190 | Ref: SCServerlessPortfolio 191 | LaunchConstraintRoleARN: 192 | Condition: CreateLaunchConstraint 193 | Value: 194 | Fn::GetAtt: 195 | - LaunchConstraintRole 196 | - Outputs.LaunchRoleArn 197 | LaunchConstraintRoleName: 198 | Condition: CreateLaunchConstraint 199 | Value: 200 | Fn::GetAtt: 201 | - LaunchConstraintRole 202 | - Outputs.LaunchRoleName 203 | serverlessProductId: 204 | Description: The Service Catalog Lambda productId. 205 | Value: 206 | Fn::GetAtt: 207 | - serverlessproduct 208 | - Outputs.ProductId 209 | serverlessProvisioningArtifactIds: 210 | Value: 211 | Fn::GetAtt: 212 | - serverlessproduct 213 | - Outputs.ProvisioningArtifactIds 214 | serverlessProvisioningArtifactNames: 215 | Value: 216 | Fn::GetAtt: 217 | - serverlessproduct 218 | - Outputs.ProvisioningArtifactNames 219 | PortfolioProvider: 220 | Description: The name of the portfolio admin 221 | Value: 222 | Ref: PortfolioProvider 223 | ServerlessDeploymentBucket: 224 | Description: The bucket housing the lambda code for deployment 225 | Value: !Ref serverlessFrameworkDeploymentBucket 226 | -------------------------------------------------------------------------------- /templates/serverless/sc-product-serverless-lambda.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: '2010-09-09' 3 | Description: AWS Lambda Service Catalog product (fdp-1p5rtpgls) 4 | Parameters: 5 | PortfolioProvider: 6 | Type: String 7 | Description: Owner and Distributor Name 8 | LaunchConstraintARN: 9 | Type: String 10 | Description: ARN of the launch constraint role for EC2 products. 11 | PortfolioId: 12 | Type: String 13 | Description: The ServiceCatalog portfolio this product will be attached to. 14 | RepoRootURL: 15 | Type: String 16 | Description: Root url for the repo containing the product templates. 17 | Resources: 18 | lambdaproduct: 19 | Type: AWS::ServiceCatalog::CloudFormationProduct 20 | Properties: 21 | Name: Amazon Lambda APIGateway Endpoint 22 | Description: This product builds one Amazon Lambda function and APIGateway endpoint 23 | Owner: 24 | Ref: PortfolioProvider 25 | Distributor: 26 | Ref: PortfolioProvider 27 | SupportDescription: Operations Team 28 | SupportEmail: support@yourcompany.com 29 | AcceptLanguage: en 30 | SupportUrl: http://helpdesk.yourcompany.com 31 | ProvisioningArtifactParameters: 32 | - Description: baseline version 33 | Info: 34 | LoadTemplateFromURL: 35 | Fn::Sub: "${RepoRootURL}serverless/sc-serverless-lambda.yml" 36 | Name: v1.2 37 | Associatelambda: 38 | Type: AWS::ServiceCatalog::PortfolioProductAssociation 39 | Properties: 40 | PortfolioId: 41 | Ref: PortfolioId 42 | ProductId: 43 | Ref: lambdaproduct 44 | constraintlambda: 45 | Type: AWS::ServiceCatalog::LaunchRoleConstraint 46 | DependsOn: Associatelambda 47 | Properties: 48 | PortfolioId: 49 | Ref: PortfolioId 50 | ProductId: 51 | Ref: lambdaproduct 52 | RoleArn: 53 | Ref: LaunchConstraintARN 54 | Description: 55 | Ref: LaunchConstraintARN 56 | Outputs: 57 | ProductId: 58 | Value: 59 | Ref: lambdaproduct 60 | ProvisioningArtifactIds: 61 | Value: 62 | Fn::GetAtt: 63 | - lambdaproduct 64 | - ProvisioningArtifactIds 65 | ProvisioningArtifactNames: 66 | Value: 67 | Fn::GetAtt: 68 | - lambdaproduct 69 | - ProvisioningArtifactNames 70 | -------------------------------------------------------------------------------- /templates/serverless/sc-provision-serverless.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: '2010-09-09' 3 | Description: ServiceCatalog Resource provisioning template for Serverless application 4 | (fdp-1p5rtpgld) 5 | Parameters: 6 | ProductId: 7 | Type: String 8 | ProvisioningArtifactName: 9 | Type: String 10 | Description: The product's version name 11 | Default: v1.0 12 | LambdaName: 13 | Type: String 14 | LambdaStage: 15 | Type: String 16 | Description: test, dev, prod, ... 17 | S3Bucket: 18 | Type: String 19 | S3Key: 20 | Type: String 21 | Description: The full key of the lambda package path in S3 without the bucket 22 | name. 23 | Handler: 24 | Type: String 25 | Default: wsgi.handler 26 | Runtime: 27 | Type: String 28 | Default: python2.7 29 | AllowedValues: 30 | - nodejs 31 | - nodejs4.3 32 | - nodejs6.10 33 | - nodejs8.10 34 | - nodejs4.3-edge 35 | - java8 36 | - python2.7 37 | - python3.6 38 | - dotnetcore1.0 39 | - dotnetcore2.0 40 | - dotnetcore2.1 41 | - go1.x 42 | MemorySize: 43 | Type: Number 44 | Default: 1024 45 | Timeout: 46 | Type: Number 47 | Default: 5 48 | Resources: 49 | SCprovisionServerless: 50 | Type: AWS::ServiceCatalog::CloudFormationProvisionedProduct 51 | Properties: 52 | ProvisioningParameters: 53 | - Key: S3Bucket 54 | Value: 55 | Ref: S3Bucket 56 | - Key: S3Key 57 | Value: 58 | Ref: S3Key 59 | - Key: LambdaName 60 | Value: 61 | Ref: LambdaName 62 | - Key: LambdaStage 63 | Value: 64 | Ref: LambdaStage 65 | - Key: Handler 66 | Value: 67 | Ref: Handler 68 | - Key: Runtime 69 | Value: 70 | Ref: Runtime 71 | - Key: MemorySize 72 | Value: 73 | Ref: MemorySize 74 | - Key: Timeout 75 | Value: 76 | Ref: Timeout 77 | ProductId: 78 | Ref: ProductId 79 | ProvisionedProductName: 80 | Fn::Sub: provisionServerless-${LambdaName} 81 | Outputs: 82 | CloudformationStackArn: 83 | Description: The Cloudformation stack that was created for the product 84 | Value: 85 | Fn::GetAtt: 86 | - SCprovisionServerless 87 | - CloudformationStackArn 88 | ProvisionedProductID: 89 | Description: Provisioned product ID 90 | Value: 91 | Ref: SCprovisionServerless 92 | -------------------------------------------------------------------------------- /templates/serverless/sc-serverless-lambda.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: '2010-09-09' 3 | Description: ServiceCatalog Resource template for Serverless application (fdp-1p5rtpglk) 4 | Parameters: 5 | LambdaName: 6 | Type: String 7 | LambdaStage: 8 | Type: String 9 | Description: test, dev, prod, ... 10 | S3Bucket: 11 | Type: String 12 | S3Key: 13 | Type: String 14 | Description: The full key of the lambda package path in S3 without the bucket 15 | name. 16 | Handler: 17 | Type: String 18 | Default: wsgi.handler 19 | Runtime: 20 | Type: String 21 | Default: python2.7 22 | AllowedValues: 23 | - nodejs 24 | - nodejs4.3 25 | - nodejs6.10 26 | - nodejs8.10 27 | - nodejs4.3-edge 28 | - java8 29 | - python2.7 30 | - python3.6 31 | - dotnetcore1.0 32 | - dotnetcore2.0 33 | - dotnetcore2.1 34 | - go1.x 35 | MemorySize: 36 | Type: Number 37 | Default: 1024 38 | Timeout: 39 | Type: Number 40 | Default: 6 41 | CustomDomainName: 42 | Type: String 43 | Description: (optional) Custom Domain Name 44 | Default: "" 45 | CustomDomainAcmCertificateId: 46 | Type: String 47 | Description: (optional) ID of the ACM Certificate to use for the Custom Domain Name 48 | Default: "" 49 | DesiredBasePath: 50 | Type: String 51 | Description: (optional) Base Path for Custom Domains 52 | Default: "" 53 | EnvironmentVariablesJson: 54 | Type: String 55 | Default: "{}" 56 | Description: (optional) Pass environment variables to the Lambda function. This has to be a JSON escaped string. 57 | LambdaLayers: 58 | Type: CommaDelimitedList 59 | Description: "(optional) list of lambda layers for the function" 60 | Default: "" 61 | LambdaVersionSHA256: 62 | Type: String 63 | Default: '' 64 | VpcSecurityGroups: 65 | Type: CommaDelimitedList 66 | Description: (optional) The list of security group ids of the VPC that needs to be accessed. 67 | Default: "" 68 | VpcSubnetIds: 69 | Type: CommaDelimitedList 70 | Description: (optional) The list of subnet Ids within the VPC that needs access to. 71 | Default: "" 72 | Conditions: 73 | UsingVPC: !And 74 | - !Not [ !Equals [ !Join [ "", !Ref VpcSubnetIds ], "" ] ] 75 | - !Not [ !Equals [ !Join [ "", !Ref VpcSecurityGroups ], "" ] ] 76 | HasLayers: !Not [ !Equals [ !Join [ "", !Ref LambdaLayers ], "" ] ] 77 | UsingCustomDomain: !And 78 | - !Not [ !Equals [ !Ref CustomDomainName, "" ] ] 79 | - !Not [ !Equals [ !Ref CustomDomainAcmCertificateId, "" ] ] 80 | Resources: 81 | AppLogGroup: 82 | Type: AWS::Logs::LogGroup 83 | Properties: 84 | LogGroupName: 85 | Fn::Sub: "/aws/lambda/${LambdaName}" 86 | IamRoleLambdaExecution: 87 | Type: AWS::IAM::Role 88 | Properties: 89 | AssumeRolePolicyDocument: 90 | Version: '2012-10-17' 91 | Statement: 92 | - Effect: Allow 93 | Principal: 94 | Service: 95 | - lambda.amazonaws.com 96 | Action: 97 | - sts:AssumeRole 98 | Policies: 99 | - PolicyName: 100 | Fn::Sub: "${LambdaStage}-${LambdaName}-lambda" 101 | PolicyDocument: 102 | Version: '2012-10-17' 103 | Statement: 104 | - Effect: Allow 105 | Action: 106 | - ec2:CreateNetworkInterface 107 | - ec2:DeleteNetworkInterface 108 | - ec2:DescribeNetworkInterfaces 109 | Resource: 110 | - "*" 111 | - Effect: Allow 112 | Action: 113 | - logs:CreateLogStream 114 | Resource: 115 | - Fn::Sub: arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${LambdaName}:* 116 | - Effect: Allow 117 | Action: 118 | - logs:PutLogEvents 119 | Resource: 120 | - Fn::Sub: arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${LambdaName}:*:* 121 | Path: "/" 122 | RoleName: 123 | Fn::Sub: "${LambdaName}-${LambdaStage}-${AWS::Region}-lambdaRole" 124 | AppLambdaFunction: 125 | Type: AWS::Lambda::Function 126 | Properties: 127 | Code: 128 | S3Bucket: 129 | Ref: S3Bucket 130 | S3Key: 131 | Ref: S3Key 132 | FunctionName: 133 | Fn::Sub: "${LambdaName}" 134 | Handler: 135 | Ref: Handler 136 | MemorySize: 137 | Ref: MemorySize 138 | Role: 139 | Fn::GetAtt: 140 | - IamRoleLambdaExecution 141 | - Arn 142 | Runtime: 143 | Ref: Runtime 144 | Timeout: 145 | Ref: Timeout 146 | VpcConfig: !If 147 | - UsingVPC 148 | - SecurityGroupIds: !Ref VpcSecurityGroups 149 | SubnetIds: !Ref VpcSubnetIds 150 | - !Ref "AWS::NoValue" 151 | Layers: !If [ HasLayers, !Ref LambdaLayers, !Ref "AWS::NoValue" ] 152 | DependsOn: 153 | - AppLogGroup 154 | - IamRoleLambdaExecution 155 | ApiGatewayRestApi: 156 | Type: AWS::ApiGateway::RestApi 157 | Properties: 158 | Name: 159 | Fn::Sub: "${LambdaStage}-${LambdaName}" 160 | EndpointConfiguration: 161 | Types: 162 | - EDGE 163 | ApiGatewayResourceProxyVar: 164 | Type: AWS::ApiGateway::Resource 165 | Properties: 166 | ParentId: 167 | Fn::GetAtt: 168 | - ApiGatewayRestApi 169 | - RootResourceId 170 | PathPart: "{proxy+}" 171 | RestApiId: 172 | Ref: ApiGatewayRestApi 173 | ApiGatewayMethodAny: 174 | Type: AWS::ApiGateway::Method 175 | Properties: 176 | HttpMethod: ANY 177 | RequestParameters: {} 178 | ResourceId: 179 | Fn::GetAtt: 180 | - ApiGatewayRestApi 181 | - RootResourceId 182 | RestApiId: 183 | Ref: ApiGatewayRestApi 184 | ApiKeyRequired: false 185 | AuthorizationType: NONE 186 | Integration: 187 | IntegrationHttpMethod: POST 188 | Type: AWS_PROXY 189 | Uri: 190 | Fn::Sub: 191 | - arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${lambdaarn}/invocations 192 | - lambdaarn: 193 | Fn::GetAtt: 194 | - AppLambdaFunction 195 | - Arn 196 | MethodResponses: [] 197 | ApiGatewayMethodProxyVarAny: 198 | Type: AWS::ApiGateway::Method 199 | Properties: 200 | HttpMethod: ANY 201 | RequestParameters: {} 202 | ResourceId: 203 | Ref: ApiGatewayResourceProxyVar 204 | RestApiId: 205 | Ref: ApiGatewayRestApi 206 | ApiKeyRequired: false 207 | AuthorizationType: NONE 208 | Integration: 209 | IntegrationHttpMethod: POST 210 | Type: AWS_PROXY 211 | Uri: 212 | Fn::Sub: 213 | - arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${lambdaarn}/invocations 214 | - lambdaarn: 215 | Fn::GetAtt: 216 | - AppLambdaFunction 217 | - Arn 218 | MethodResponses: [] 219 | ApiGatewayDeployment: 220 | Type: AWS::ApiGateway::Deployment 221 | Properties: 222 | RestApiId: 223 | Ref: ApiGatewayRestApi 224 | StageName: 225 | Ref: LambdaStage 226 | DependsOn: 227 | - ApiGatewayMethodAny 228 | - ApiGatewayMethodProxyVarAny 229 | CustomDomain: 230 | Type: AWS::ApiGateway::DomainName 231 | Condition: UsingCustomDomain 232 | Properties: 233 | DomainName: !Ref CustomDomainName 234 | EndpointConfiguration: 235 | Types: 236 | - EDGE 237 | RegionalCertificateArn: !Ref "AWS::NoValue" 238 | CertificateArn: !Sub "arn:aws:acm:${AWS::Region}:${AWS::AccountId}:certificate/${CustomDomainAcmCertificateId}" 239 | CustomDomainBasePathMapping: 240 | Type: AWS::ApiGateway::BasePathMapping 241 | Condition: UsingCustomDomain 242 | Properties: 243 | BasePath: !Ref DesiredBasePath 244 | DomainName: !Ref CustomDomainName 245 | RestApiId: !Ref ApiGatewayRestApi 246 | Stage: !Ref LambdaStage 247 | DependsOn: 248 | - ApiGatewayDeployment 249 | - CustomDomain 250 | AppLambdaPermissionApiGateway: 251 | Type: AWS::Lambda::Permission 252 | Properties: 253 | FunctionName: 254 | Fn::GetAtt: 255 | - AppLambdaFunction 256 | - Arn 257 | Action: lambda:InvokeFunction 258 | Principal: 259 | Fn::Sub: apigateway.${AWS::URLSuffix} 260 | SourceArn: 261 | Fn::Sub: arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGatewayRestApi}/*/* 262 | Outputs: 263 | ServiceEndpoint: 264 | Description: URL of the service endpoint 265 | Value: 266 | Fn::Sub: https://${ApiGatewayRestApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${LambdaStage} 267 | Export: 268 | Name: 269 | Fn::Sub: "${LambdaName}-ServiceEndpoint" 270 | -------------------------------------------------------------------------------- /test/aws-compile-service-catalog.test.js: -------------------------------------------------------------------------------- 1 | /* eslint no-sync: 0, max-nested-callbacks: 0 */ 2 | 3 | const path = require('path'); 4 | const os = require('os'); 5 | const crypto = require('crypto'); 6 | const chai = require('chai'); 7 | const sinon = require('sinon'); 8 | const Serverless = require('serverless'); 9 | const AwsProvider = require('serverless/lib/plugins/aws/provider'); 10 | const AwsCompileServiceCatalog = require('../src/aws-compile-servicecatalog'); 11 | 12 | chai.use(require('chai-as-promised')); 13 | 14 | const { expect } = chai; 15 | const getTmpDirPath = () => path.join(os.tmpdir(), 16 | 'tmpdirs-serverless', 'serverless', crypto.randomBytes(8).toString('hex')); 17 | describe('AwsCompileFunctions', () => { 18 | let serverless; 19 | let testProvider; 20 | let awsCompileServiceCatalog; 21 | const functionNameHello = 'testHello'; 22 | const functionNameBye = 'testBye'; 23 | const productNameHello = 'TestHelloLambdaFunctionSCProvisionedProduct'; 24 | const productNameBye = 'TestByeLambdaFunctionSCProvisionedProduct'; 25 | const testEnvironment = { 26 | DEV: 'dev', 27 | TEST: 'test' 28 | }; 29 | const testVpc = { 30 | securityGroupIds: ['group1', 'group2'], 31 | subnetIds: ['subnet1', 'subnet2'] 32 | }; 33 | 34 | // eslint-disable-next-line max-statements 35 | const setup = (providerProps) => { 36 | const options = { stage: 'dev', region: 'us-east-1' }; 37 | const serviceArtifact = 'new-service.zip'; 38 | const individualArtifact = 'test.zip'; 39 | 40 | testProvider = { 41 | deploymentBucket: 'test-bucket', 42 | scProdcutVersion: 'v1.0', 43 | scProductId: 'prod-testid' 44 | }; 45 | if (providerProps) { 46 | testProvider = { ...testProvider, ...providerProps }; 47 | } 48 | serverless = new Serverless(options); 49 | serverless.service.provider = { ...serverless.service.provider, ...testProvider }; 50 | serverless.setProvider('aws', new AwsProvider(serverless, options)); 51 | serverless.cli = new serverless.classes.CLI(); 52 | awsCompileServiceCatalog = new AwsCompileServiceCatalog(serverless, options); 53 | awsCompileServiceCatalog.serverless.service.provider.compiledCloudFormationTemplate = { 54 | Resources: {}, 55 | Outputs: {} 56 | }; 57 | awsCompileServiceCatalog.packagePath = getTmpDirPath(); 58 | // The contents of the test artifacts need to be predictable so the hashes stay the same 59 | serverless.utils.writeFileSync(path.join(awsCompileServiceCatalog.packagePath, 60 | serviceArtifact), 'foobar'); 61 | serverless.utils.writeFileSync(path.join(awsCompileServiceCatalog.packagePath, 62 | individualArtifact), 'barbaz'); 63 | awsCompileServiceCatalog.serverless.service.service = 'new-service'; 64 | awsCompileServiceCatalog.serverless.service.package.artifactDirectoryName = 'somedir'; 65 | awsCompileServiceCatalog.serverless.service.package 66 | .artifact = path.join(awsCompileServiceCatalog.packagePath, serviceArtifact); 67 | awsCompileServiceCatalog.serverless.service.functions = {}; 68 | awsCompileServiceCatalog.serverless.service.functions[functionNameHello] = { 69 | name: 'test-hello', 70 | package: { 71 | artifact: path.join(awsCompileServiceCatalog.packagePath, 72 | individualArtifact) 73 | }, 74 | handler: 'handler.hello', 75 | environment: testEnvironment, 76 | vpc: testVpc 77 | }; 78 | awsCompileServiceCatalog.serverless.service.functions[functionNameBye] = { 79 | name: 'test-bye', 80 | package: { 81 | artifact: path.join(awsCompileServiceCatalog.packagePath, 82 | individualArtifact) 83 | }, 84 | handler: 'handler.bye' 85 | }; 86 | }; 87 | 88 | afterEach(function () { 89 | sinon.restore(); 90 | serverless = null; 91 | testProvider = null; 92 | awsCompileServiceCatalog = null; 93 | }); 94 | 95 | describe('#constructor()', () => { 96 | it('should set the provider variable to an instance of AwsProvider', () => { 97 | setup(); 98 | expect(awsCompileServiceCatalog.provider).to.be.instanceof(AwsProvider); 99 | }); 100 | }); 101 | describe('#compileFunctions()', () => { 102 | it('should set the functionResource properties', () => { 103 | setup(); 104 | return expect(awsCompileServiceCatalog.compileFunctions()).to.be.fulfilled 105 | .then(() => { 106 | const functionResource = awsCompileServiceCatalog.serverless.service.provider 107 | .compiledCloudFormationTemplate.Resources[productNameHello]; 108 | expect(functionResource.Type).to.equal('AWS::ServiceCatalog::CloudFormationProvisionedProduct'); 109 | expect(functionResource.Properties.ProductId).to.equal(testProvider.scProductId); 110 | expect(functionResource.Properties.ProvisionedProductName).to.equal('provisionSC-test-hello'); 111 | }); 112 | }); 113 | it('should pass the environment parameters as json', () => { 114 | setup(); 115 | return expect(awsCompileServiceCatalog.compileFunctions()).to.be.fulfilled 116 | .then(() => { 117 | const functionResource = awsCompileServiceCatalog.serverless.service.provider 118 | .compiledCloudFormationTemplate.Resources[productNameHello]; 119 | const envParams = functionResource.Properties.ProvisioningParameters.find(k => k.Key === 'EnvironmentVariablesJson'); 120 | expect(envParams.Value).to.equal(JSON.stringify(testEnvironment)); 121 | }); 122 | }); 123 | it('should reject invalid environment keys', () => { 124 | setup(); 125 | awsCompileServiceCatalog.serverless.service.functions[functionNameHello].environment = { 126 | 'OK': 'value1', 127 | 'FOO$@~': 'value2' 128 | }; 129 | expect(awsCompileServiceCatalog.compileFunctions()).to.be.rejectedWith('Invalid characters in environment variable FOO$@~'); 130 | }); 131 | it('should set the vpc parameters', () => { 132 | setup(); 133 | return expect(awsCompileServiceCatalog.compileFunctions()).to.be.fulfilled 134 | .then(() => { 135 | const functionResource = awsCompileServiceCatalog.serverless.service.provider 136 | .compiledCloudFormationTemplate.Resources[productNameHello]; 137 | const securityParam = functionResource.Properties.ProvisioningParameters.find(k => k.Key === 'VpcSecurityGroups'); 138 | expect(securityParam.Value).to.equal(testVpc.securityGroupIds.toString()); 139 | const subnetParam = functionResource.Properties.ProvisioningParameters.find(k => k.Key === 'VpcSubnetIds'); 140 | expect(subnetParam.Value).to.equal(testVpc.subnetIds.toString()); 141 | }); 142 | }); 143 | it('should reject invalid environment key values', () => { 144 | setup(); 145 | awsCompileServiceCatalog.serverless.service.functions[functionNameHello].environment = { 146 | OK: 'value1', 147 | OK_CFRef: ['Ref'], 148 | OK_FN: ['Fn::GetAtt'], 149 | NOT_OK: ['foo'] 150 | }; 151 | expect(awsCompileServiceCatalog.compileFunctions()).to.be.rejectedWith('Environment variable NOT_OK must contain string'); 152 | }); 153 | it('should override the template when the template', () => { 154 | const providerProps = { 155 | scProductTemplate: path.join(__dirname, './testTemplate.json') 156 | }; 157 | const customParam = { 158 | Key: 'CustomParam', 159 | Value: 'CustomValue' 160 | }; 161 | setup(providerProps); 162 | return expect(awsCompileServiceCatalog.compileFunctions()).to.be.fulfilled 163 | .then(() => { 164 | const functionResource = awsCompileServiceCatalog.serverless.service.provider 165 | .compiledCloudFormationTemplate.Resources[productNameHello]; 166 | expect(functionResource.Type).to.equal('AWS::ServiceCatalog::CloudFormationProvisionedProduct'); 167 | const param = functionResource.Properties.ProvisioningParameters.find(k => k.Key === 'CustomParam'); 168 | expect(param).to.deep.equal(customParam); 169 | }); 170 | }); 171 | it('should set the handle multiple handlers', () => { 172 | setup(); 173 | return expect(awsCompileServiceCatalog.compileFunctions()).to.be.fulfilled 174 | .then(() => { 175 | let functionResource = awsCompileServiceCatalog.serverless.service.provider 176 | .compiledCloudFormationTemplate.Resources[productNameHello]; 177 | expect(functionResource.Properties.ProvisionedProductName).to.equal('provisionSC-test-hello'); 178 | functionResource = awsCompileServiceCatalog.serverless.service.provider 179 | .compiledCloudFormationTemplate.Resources[productNameBye]; 180 | expect(functionResource.Properties.ProvisionedProductName).to.equal('provisionSC-test-bye'); 181 | }); 182 | }); 183 | it('should set Layers when specified', () => { 184 | setup(); 185 | awsCompileServiceCatalog.serverless.service.functions[functionNameHello] 186 | .layers = ['arn:aws:xxx:*:*']; 187 | return expect(awsCompileServiceCatalog.compileFunctions()).to.be.fulfilled 188 | .then(() => { 189 | const functionResource = awsCompileServiceCatalog.serverless.service.provider 190 | .compiledCloudFormationTemplate.Resources[productNameHello]; 191 | const securityParam = functionResource.Properties.ProvisioningParameters.find(k => k.Key === 'LambdaLayers'); 192 | expect(securityParam.Value).to.equal('arn:aws:xxx:*:*'); 193 | }); 194 | }); 195 | 196 | it('should use scProductId when present', () => { 197 | setup({ 198 | scProductName: 'foo' 199 | }); 200 | return expect(awsCompileServiceCatalog.compileFunctions()).to.be.fulfilled 201 | .then(() => { 202 | const functionResource = awsCompileServiceCatalog.serverless.service.provider 203 | .compiledCloudFormationTemplate.Resources[productNameHello]; 204 | expect(functionResource.Properties.ProductId).to.equal('prod-testid'); 205 | expect(functionResource.Properties.ProductName).to.not.exist; 206 | }); 207 | }); 208 | 209 | it('should use scProductName when present and no scProductId is specified', () => { 210 | setup({ 211 | scProductId: void 0, 212 | scProductName: 'foo' 213 | }); 214 | return expect(awsCompileServiceCatalog.compileFunctions()).to.be.fulfilled 215 | .then(() => { 216 | const functionResource = awsCompileServiceCatalog.serverless.service.provider 217 | .compiledCloudFormationTemplate.Resources[productNameHello]; 218 | expect(functionResource.Properties.ProductId).to.not.exist; 219 | expect(functionResource.Properties.ProductName).to.equal('foo'); 220 | }); 221 | }); 222 | 223 | it('should reject when neither scProductId nor scProductName are specified', () => { 224 | setup({ 225 | scProductId: void 0 226 | }); 227 | return expect(awsCompileServiceCatalog.compileFunctions()) 228 | .to.be.rejectedWith('Missing scProductId or scProductName on service.'); 229 | }); 230 | 231 | describe('#scParameterMapping', function () { 232 | it('can provide alternate SC Paramter Names', function () { 233 | const customMapping = { 234 | s3Bucket: 'CustomS3Bucket', 235 | s3Key: 'CustomS3Key', 236 | handler: 'CustomHandler', 237 | name: 'CustomLambdaName', 238 | memorySize: 'CustomMemorySize', 239 | timeout: 'CustomTimeout', 240 | runtime: 'CustomRuntime', 241 | stage: 'CustomLambdaStage', 242 | environmentVariablesJson: 'CustomEnvironmentVariablesJson', 243 | vpcSecurityGroups: 'CustomVpcSecurityGroups', 244 | vpcSubnetIds: 'CustomVpcSubnetIds', 245 | lambdaLayers: 'CustomLambdaLayers' 246 | }; 247 | 248 | setup({ 249 | scProductTemplate: path.join(__dirname, './customTestTemplate.json'), 250 | scParameterMapping: customMapping 251 | }); 252 | awsCompileServiceCatalog.serverless.service.functions[functionNameHello] 253 | .layers = ['arn:aws:xxx:*:*']; 254 | 255 | return expect(awsCompileServiceCatalog.compileFunctions()).to.be.fulfilled 256 | .then(() => { 257 | const parameters = awsCompileServiceCatalog.serverless.service.provider 258 | .compiledCloudFormationTemplate.Resources[productNameHello].Properties.ProvisioningParameters; 259 | Object.values(customMapping).forEach(overriddenKey => { 260 | const value = parameters.find(k => k.Key === overriddenKey); 261 | expect(value, `Mapping for ${ overriddenKey } doesn't exist in ${ JSON.stringify(parameters, null, 2) }`).to.exist; 262 | }); 263 | }); 264 | 265 | }); 266 | 267 | it('can drop some SC Paramter Names', function () { 268 | const customMapping = { 269 | stage: '' 270 | }; 271 | 272 | setup({ 273 | scProductTemplate: path.join(__dirname, './customTestTemplate-NoStage.json'), 274 | scParameterMapping: customMapping 275 | }); 276 | 277 | const logSpy = sinon.spy(serverless.cli, 'log'); 278 | 279 | return expect(awsCompileServiceCatalog.compileFunctions()).to.be.fulfilled 280 | .then(() => { 281 | const parameters = awsCompileServiceCatalog.serverless.service.provider 282 | .compiledCloudFormationTemplate.Resources[productNameHello].Properties.ProvisioningParameters; 283 | 284 | // Make sure we didn't log any errors 285 | expect(logSpy.callCount).to.equal(0); 286 | 287 | // Make sure we don't have the stage 288 | const hasStage = parameters.find(k => k.Key && /[sS]tage/.test(k.Key)); 289 | expect(hasStage).to.not.be.ok; 290 | }); 291 | }); 292 | }); 293 | }); 294 | }); 295 | -------------------------------------------------------------------------------- /test/customTestTemplate-NoStage.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "AWS::ServiceCatalog::CloudFormationProvisionedProduct", 3 | 4 | "Properties": { 5 | "ProvisioningParameters": [ 6 | { 7 | "Key": "S3Bucket", 8 | "Value": "ServerlessDeploymentBucket" 9 | }, 10 | { 11 | "Key": "S3Key", 12 | "Value": "S3Key" 13 | }, 14 | { 15 | "Key": "LambdaName", 16 | "Value": "LambdaName" 17 | }, 18 | { 19 | "Key": "Handler", 20 | "Value": "Handler" 21 | }, 22 | { 23 | "Key": "Runtime", 24 | "Value": "Runtime" 25 | }, 26 | { 27 | "Key": "MemorySize", 28 | "Value": "MemorySize" 29 | }, 30 | { 31 | "Key": "Timeout", 32 | "Value": "Timeout" 33 | }, 34 | { 35 | "Key": "CustomParam", 36 | "Value": "CustomValue" 37 | } 38 | ], 39 | "ProvisioningArtifactName": "ProvisioningArtifactName", 40 | "ProductId": "ProductId", 41 | "ProvisionedProductName": { 42 | "Fn::Sub": "provisionServerless-${LambdaName}" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/customTestTemplate.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "AWS::ServiceCatalog::CloudFormationProvisionedProduct", 3 | 4 | "Properties": { 5 | "ProvisioningParameters": [ 6 | { 7 | "Key": "CustomS3Bucket", 8 | "Value": "ServerlessDeploymentBucket" 9 | }, 10 | { 11 | "Key": "CustomS3Key", 12 | "Value": "S3Key" 13 | }, 14 | { 15 | "Key": "CustomLambdaName", 16 | "Value": "LambdaName" 17 | }, 18 | { 19 | "Key": "CustomLambdaStage", 20 | "Value": "test" 21 | }, 22 | { 23 | "Key": "CustomHandler", 24 | "Value": "Handler" 25 | }, 26 | { 27 | "Key": "CustomRuntime", 28 | "Value": "Runtime" 29 | }, 30 | { 31 | "Key": "CustomMemorySize", 32 | "Value": "MemorySize" 33 | }, 34 | { 35 | "Key": "CustomTimeout", 36 | "Value": "Timeout" 37 | }, 38 | { 39 | "Key": "CustomCustomParam", 40 | "Value": "CustomValue" 41 | } 42 | ], 43 | "ProvisioningArtifactName": "ProvisioningArtifactName", 44 | "ProductId": "ProductId", 45 | "ProvisionedProductName": { 46 | "Fn::Sub": "provisionServerless-${LambdaName}" 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/testTemplate.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "AWS::ServiceCatalog::CloudFormationProvisionedProduct", 3 | 4 | "Properties": { 5 | "ProvisioningParameters": [ 6 | { 7 | "Key": "S3Bucket", 8 | "Value": "ServerlessDeploymentBucket" 9 | }, 10 | { 11 | "Key": "S3Key", 12 | "Value": "S3Key" 13 | }, 14 | { 15 | "Key": "LambdaName", 16 | "Value": "LambdaName" 17 | }, 18 | { 19 | "Key": "LambdaStage", 20 | "Value": "test" 21 | }, 22 | { 23 | "Key": "Handler", 24 | "Value": "Handler" 25 | }, 26 | { 27 | "Key": "Runtime", 28 | "Value": "Runtime" 29 | }, 30 | { 31 | "Key": "MemorySize", 32 | "Value": "MemorySize" 33 | }, 34 | { 35 | "Key": "Timeout", 36 | "Value": "Timeout" 37 | }, 38 | { 39 | "Key": "CustomParam", 40 | "Value": "CustomValue" 41 | } 42 | ], 43 | "ProvisioningArtifactName": "ProvisioningArtifactName", 44 | "ProductId": "ProductId", 45 | "ProvisionedProductName": { 46 | "Fn::Sub": "provisionServerless-${LambdaName}" 47 | } 48 | } 49 | } 50 | --------------------------------------------------------------------------------