├── .npmrc ├── .eslintignore ├── .esdoc.json ├── .editorconfig ├── .gitignore ├── .eslintrc.yml ├── .travis.yml ├── SECURITY.md ├── .github └── workflows │ ├── ci.yml │ └── codeql-analysis.yml ├── LICENSE ├── package.json ├── README.md ├── test └── index.spec.js ├── src └── index.js └── lib └── index.js /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | coverage/ 4 | -------------------------------------------------------------------------------- /.esdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "./src", 3 | "destination": "doc", 4 | "plugins": [{"name": "esdoc-standard-plugin"}], 5 | "unexportIdentifier": true, 6 | "index": "README.md" 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDEs 2 | .idea/ 3 | .vscode/ 4 | 5 | # NPM 6 | node_modules/ 7 | .npmdebug 8 | npm-debug.log 9 | 10 | # Build artifact 11 | .coverage 12 | coverage 13 | .nyc_output 14 | 15 | # Generated docs 16 | doc/ 17 | 18 | # OS 19 | .DS_Store 20 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: [standard, plugin:mocha/recommended] 3 | plugins: [mocha] 4 | env: 5 | node: true 6 | mocha: true 7 | rules: 8 | space-before-function-paren: ["error", { 9 | "anonymous": "never", 10 | "named": "never", 11 | "asyncArrow": "always" }] 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '12' 4 | cache: yarn 5 | script: npm run build 6 | after_success: npm run docs && npm run ci:coverage 7 | deploy: 8 | provider: pages 9 | skip_cleanup: true 10 | local_dir: doc 11 | github_token: $GITHUB_TOKEN # Set in travis-ci.org dashboard 12 | on: 13 | branch: develop 14 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 2.1.x | :white_check_mark: | 11 | | 2.0.x | :x: | 12 | | 1.x | :x: | 13 | 14 | ## Reporting a Vulnerability 15 | 16 | If you find a vulnerability, please send a message to jacob.e.meacham+oss@gmail.com. I will do my best to respond in a timely manner. 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: '16.x' 20 | - run: npm ci 21 | - run: npm run build 22 | - run: npm run ci:coverage 23 | - name: Coveralls 24 | uses: coverallsapp/github-action@master 25 | with: 26 | github-token: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jacob Meacham 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 | 23 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ develop ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ develop ] 20 | schedule: 21 | - cron: '21 15 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-plugin-aws-resolvers", 3 | "version": "2.1.0", 4 | "description": "Plugin that resolves deployed AWS services into variables", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "lib", 8 | "src" 9 | ], 10 | "engines": { 11 | "node": ">=12.0" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/jacob-meacham/serverless-plugin-aws-resolvers.git" 16 | }, 17 | "keywords": [ 18 | "aws", 19 | "aws lambda", 20 | "amazon", 21 | "amazon web services", 22 | "serverless" 23 | ], 24 | "author": "Matt Sills", 25 | "contributors": [ 26 | { 27 | "name": "Jacob Meacham", 28 | "url": "http://jemonjam.com/" 29 | } 30 | ], 31 | "license": "MIT", 32 | "private": false, 33 | "bugs": { 34 | "url": "https://github.com/jacob-meacham/serverless-plugin-aws-resolvers/issues" 35 | }, 36 | "homepage": "https://github.com/jacob-meacham/serverless-plugin-aws-resolvers", 37 | "scripts": { 38 | "clean": "rimraf lib && mkdir lib", 39 | "test": "cross-env NODE_ENV=test nyc mocha test/", 40 | "test:watch": "mocha --watch test/", 41 | "check": "eslint . && npm run test", 42 | "build:node": "cross-env BABEL_ENV=production babel src --out-dir lib", 43 | "build": "npm run clean && npm run check && npm run build:node", 44 | "docs": "esdoc .", 45 | "docs:deploy": "npm run docs && gh-pages -d doc", 46 | "ci:coverage": "nyc report --reporter=lcov" 47 | }, 48 | "dependencies": { 49 | "aws-sdk": "2.814.0", 50 | "lodash": "^4.17.15", 51 | "winston": "3.4.0" 52 | }, 53 | "devDependencies": { 54 | "@babel/cli": "7.16.8", 55 | "@babel/core": "7.16.7", 56 | "@babel/preset-env": "7.16.8", 57 | "@babel/register": "7.16.9", 58 | "aws-sdk-mock": "5.5.1", 59 | "chai": "4.3.4", 60 | "chai-as-promised": "7.1.1", 61 | "coveralls": "3.1.1", 62 | "cross-env": "7.0.3", 63 | "esdoc": "1.1.0", 64 | "esdoc-standard-plugin": "1.0.0", 65 | "eslint": "7.32.0", 66 | "eslint-config-standard": "16.0.3", 67 | "eslint-plugin-import": "2.25.4", 68 | "eslint-plugin-mocha": "10.0.3", 69 | "eslint-plugin-promise": "6.0.0", 70 | "gh-pages": "3.2.3", 71 | "mocha": "9.1.4", 72 | "nyc": "15.1.0", 73 | "rimraf": "3.0.2", 74 | "serverless": "2.71.0" 75 | }, 76 | "peerDependencies": { 77 | "serverless": ">=1.26.0" 78 | }, 79 | "mocha": { 80 | "require": [ 81 | "@babel/register" 82 | ] 83 | }, 84 | "babel": { 85 | "presets": [ 86 | [ 87 | "@babel/preset-env", 88 | { 89 | "targets": { 90 | "node": 12 91 | } 92 | } 93 | ] 94 | ] 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # serverless-plugin-aws-resolvers 2 | [![serverless](http://public.serverless.com/badges/v3.svg)](http://www.serverless.com) 3 | [![Coverage Status](https://coveralls.io/repos/github/jacob-meacham/serverless-plugin-aws-resolvers/badge.svg?branch=develop)](https://coveralls.io/github/jacob-meacham/serverless-plugin-aws-resolvers?branch=develop) 4 | ![Build Status](https://github.com/jacob-meacham/serverless-plugin-aws-resolvers/actions/workflows/ci.yml/badge.svg) 5 | 6 | A plugin for the serverless framework that resolves deployed AWS services to variables from ESS, RDS, EC2, dynamodb or Kinesis. 7 | 8 | # Usage 9 | ```yaml 10 | custom: 11 | # See https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/ES.html#describeElasticsearchDomain-property 12 | ess: ${aws:ess:my_cluster_name:Endpoint} 13 | # See https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/RDS.html#describeDBInstances-property 14 | rds: ${aws:rds:my_db_name:InstanceCreateTime} 15 | # See https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/RDS.html#describeDBClusters-property 16 | rdsaurora: ${aws:rdsaurora:my_cluster_name:Endpoint} 17 | kinesis: ${aws:kinesis:my_kinesis_stream:StreamARN} 18 | dynamodb: ${aws:dynamodb:my_dynamodb_table:LatestStreamArn} 19 | securityGroup: ${aws:ec2:securityGroup:my_vpc_name-my_group_name:GroupId} 20 | subnet: ${aws:ec2:subnet:my_subnet_name:SubnetId} 21 | vpc: ${aws:ec2:vpc:my_vpc_name:VpcId} 22 | ecs: ${aws:ecs:cache_cluster_name:CacheClusterId} 23 | cf: ${aws:cf:stack_name`_`logical_resource_id:PhysicalResourceId} 24 | apigateway: ${aws:apigateway:my_api_name:id} 25 | apigatewayv2: ${aws:apigatewayv2:my_api_name:ApiId} 26 | ``` 27 | 28 | Given a service, a key, and a property, this plugin will resolve the variable directly from AWS. This uses the IAM role of the executor of the serverless binary. 29 | 30 | This plugin also exposes a command to resolve a variable `sls resolveAwsKey --k aws:ess:my_cluster_name:Endpoint` 31 | 32 | See our [webpage](https://jacob-meacham.github.io/serverless-plugin-aws-resolvers/) for full documentation. 33 | 34 | ## Array access 35 | 36 | To access values in arrays (for example the ElastiCache Endpoint in CacheNodes), the `variableSyntax` of serverless needs to be amended. 37 | 38 | ```yaml 39 | provider: 40 | variableSyntax: "\\${([ ~:a-zA-Z0-9._\\'\",\\-\\/\\(\\)\\[\\]]+?)}" 41 | 42 | environment: 43 | ECS_ADDRESS: ${aws:ecs:ecs-instance:CacheNodes[0].Endpoint.Address} 44 | ``` 45 | 46 | # Configurations 47 | 48 | This plugin has one available configuration option at the moment. 49 | 50 | ```yaml 51 | custom: 52 | awsResolvers: 53 | strict: true 54 | ``` 55 | 56 | Disabling strict mode allows values of non-existing infrastructure to be overwritten by other values. This is especially useful when the serverless configuration also contains the CloudFormation template to create this infrastructure. On the first run the value would not be available and would prevent the template from being applied. 57 | 58 | Values can be overwritten like this: 59 | 60 | ```yaml 61 | custom: 62 | awsResolvers: 63 | strict: false 64 | rds: ${aws:rds:my_db_name:InstanceCreateTime, 'not created yet'} 65 | ``` 66 | 67 | # Version History 68 | * 2.1.0 69 | - Broad dependency update 70 | - Add Aurora clusters (thanks @kschusternetformic) 71 | * 2.0.2 72 | - Dependency security update 73 | - Fix deprecation warning (Thanks @dnicolson) 74 | * 2.0.1 75 | - Correctly depend on node >= 12 76 | * 2.0.0 77 | - Large dependency upgrade, remove babel runtime dependency. 78 | * 1.4.0 79 | - Add ability to get CF Physical Resource ID (thanks @supaggregator) 80 | * 1.3.3 81 | - Update versions for dependabot secruity vulnerabilities 82 | * 1.3.2 83 | - Fix security issue with lodash (thanks @rmbl) 84 | * 1.3.1 85 | - Add support for elasticache resources (thanks @rmbl) 86 | * 1.3.0 87 | - Add a strict mode flag and don't error on non-existing infrastructure when strict mode is not on (thanks @rmbl) 88 | * 1.2.1 89 | - Allow object access for the variable name (thanks @rmbl) 90 | * 1.2.0 91 | - Add support for DynamoDB stream ARN (thanks @geronimo-iia) 92 | * 1.1.0 93 | - Add support for EC2 resources (thanks @kevgliss) 94 | * 1.0.0 95 | - Initial release 96 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by msills on 4/12/17. 3 | */ 4 | 5 | import AWS from 'aws-sdk-mock' 6 | import assert from 'assert' 7 | import { expect } from 'chai' 8 | import Serverless from 'serverless' 9 | import ServerlessAWSResolvers from '../src' 10 | 11 | describe('ServerlessAWSResolvers', function() { 12 | const DEFAULT_VALUE = 'MY_VARIABLE_NAME' 13 | 14 | const CONFIGS = { 15 | KINESIS: { scope: 'kinesis', service: 'Kinesis', method: 'describeStream', topLevel: 'StreamDescription' }, 16 | DYNAMODB: { scope: 'dynamodb', service: 'DynamoDB', method: 'describeTable', topLevel: 'Table' }, 17 | ECS: { 18 | scope: 'ecs', 19 | service: 'ElastiCache', 20 | method: 'describeCacheClusters', 21 | topLevel: 'CacheClusters', 22 | testKey: 'testKey', 23 | testValue: 'test-value', 24 | serviceValue: [{ testKey: 'test-value' }] 25 | }, 26 | ESS: { scope: 'ess', service: 'ES', method: 'describeElasticsearchDomain', topLevel: 'DomainStatus' }, 27 | RDS: { 28 | scope: 'rds', 29 | service: 'RDS', 30 | method: 'describeDBInstances', 31 | topLevel: 'DBInstances', 32 | testKey: 'testKey', 33 | testValue: 'test-value', 34 | serviceValue: [{ testKey: 'test-value' }] 35 | }, 36 | RDSCHILDVAL: { 37 | scope: 'rds', 38 | service: 'RDS', 39 | method: 'describeDBInstances', 40 | topLevel: 'DBInstances', 41 | testKey: 'testKey.testChild', 42 | testValue: 'test-value', 43 | serviceValue: [{ testKey: { testChild: 'test-value' } }] 44 | }, 45 | RDSAURORA: { 46 | scope: 'rdsaurora', 47 | service: 'RDS', 48 | method: 'describeDBClusters', 49 | topLevel: 'DBClusters', 50 | testKey: 'testKey.testChild', 51 | testValue: 'test-value', 52 | serviceValue: [{ testKey: { testChild: 'test-value' } }] 53 | }, 54 | EC2SecurityGroup: { 55 | scope: 'ec2', 56 | service: 'EC2', 57 | method: 'describeSecurityGroups', 58 | topLevel: 'SecurityGroups', 59 | subService: 'securityGroup', 60 | testKey: 'testKey', 61 | testValue: 'test-value', 62 | serviceValue: [{ testKey: 'test-value' }] 63 | }, 64 | EC2VPC: { 65 | scope: 'ec2', 66 | service: 'EC2', 67 | method: 'describeVpcs', 68 | topLevel: 'Vpcs', 69 | subService: 'vpc', 70 | testKey: 'testKey', 71 | testValue: 'test-value', 72 | serviceValue: [{ testKey: 'test-value', VpcId: 'test-value' }] 73 | }, 74 | EC2Subnet: { 75 | scope: 'ec2', 76 | service: 'EC2', 77 | method: 'describeSubnets', 78 | topLevel: 'Subnets', 79 | subService: 'subnet', 80 | testKey: 'testKey', 81 | testValue: 'test-value', 82 | serviceValue: [{ testKey: 'test-value' }] 83 | }, 84 | CLOUDFORMATION: { 85 | scope: 'cf', 86 | service: 'CloudFormation', 87 | method: 'describeStackResource', 88 | topLevel: 'StackResourceDetail', 89 | testKey: 'testKey', 90 | testValue: 'test-value' 91 | }, 92 | APIGATEWAY: { 93 | scope: 'apigateway', 94 | service: 'APIGateway', 95 | method: 'getRestApis', 96 | topLevel: 'items', 97 | testKey: 'testKey', 98 | testValue: 'test-value', 99 | serviceValue: [{ name: 'test-key', testKey: 'test-value' }] 100 | }, 101 | APIGATEWAYV2: { 102 | scope: 'apigatewayv2', 103 | service: 'ApiGatewayV2', 104 | method: 'getApis', 105 | topLevel: 'Items', 106 | testKey: 'testKey', 107 | testValue: 'test-value', 108 | serviceValue: [{ Name: 'test-key', testKey: 'test-value' }] 109 | } 110 | } 111 | 112 | afterEach(function() { 113 | AWS.restore() 114 | }) 115 | 116 | async function createFakeServerless() { 117 | const sls = new Serverless() 118 | sls.service.provider.region = 'us-east-2' 119 | 120 | // Attach the plugin 121 | sls.pluginManager.addPlugin(ServerlessAWSResolvers) 122 | await sls.init() 123 | return sls 124 | } 125 | 126 | async function testResolve({ scope, service, method, topLevel, subService, testKey, testValue, serviceValue }) { 127 | testKey = testKey || 'TEST_KEY' 128 | testValue = testValue || 'TEST_VALUE' 129 | if (!serviceValue) { 130 | serviceValue = {} 131 | serviceValue[testKey] = testValue 132 | } 133 | 134 | const serverless = await createFakeServerless() 135 | 136 | AWS.mock(service, method, (params, callback) => { 137 | const result = {} 138 | result[topLevel] = serviceValue 139 | callback(null, result) 140 | }) 141 | 142 | if (subService) { 143 | serverless.service.custom.myVariable = `\${aws:${scope}:${subService}:test-key:${testKey}}` 144 | } else if (service === 'CloudFormation') { 145 | serverless.service.custom.myVariable = `\${aws:${scope}:test-key1_test-key2:${testKey}}` 146 | } else { 147 | serverless.service.custom.myVariable = `\${aws:${scope}:test-key:${testKey}}` 148 | } 149 | 150 | await serverless.variables.populateService() 151 | assert.equal(serverless.service.custom.myVariable, testValue) 152 | } 153 | 154 | async function testNotFound({ scope, service, method }) { 155 | const serverless = await createFakeServerless() 156 | 157 | AWS.mock(service, method, (params, callback) => { 158 | callback(new Error('Not found')) 159 | }) 160 | 161 | serverless.service.custom.myVariable = `\${aws:${scope}:TEST_KEY}` 162 | expect(serverless.variables.populateService).to.throw(Error) 163 | } 164 | 165 | async function testResolveStrictFallback({ scope, service, method, subService, testKey }) { 166 | const serverless = await createFakeServerless() 167 | 168 | serverless.service.custom.awsResolvers = { 169 | strict: true 170 | } 171 | if (subService) { 172 | serverless.service.custom.myVariable = `\${aws:${scope}:${subService}:test-key:${testKey}, 'test'}` 173 | } else if (service === 'CloudFormation') { 174 | serverless.service.custom.myVariable = `\${aws:${scope}:test-key1_test-key2:${testKey}, 'test'}` 175 | } else { 176 | serverless.service.custom.myVariable = `\${aws:${scope}:test-key:${testKey}, 'test'}` 177 | } 178 | 179 | AWS.mock(service, method, (params, callback) => { 180 | callback(new Error('Not found')) 181 | }) 182 | 183 | expect(serverless.variables.populateService).to.throw(Error) 184 | } 185 | 186 | async function testResolveFallback({ scope, service, method, subService, testKey }) { 187 | const serverless = await createFakeServerless() 188 | 189 | AWS.mock(service, method, (params, callback) => { 190 | callback(new Error('Not found')) 191 | }) 192 | 193 | serverless.service.custom.awsResolvers = { 194 | strict: false 195 | } 196 | if (subService) { 197 | serverless.service.custom.myVariable = `\${aws:${scope}:${subService}:test-key:not-set, 'test'}` 198 | } else if (service === 'CloudFormation') { 199 | serverless.service.custom.myVariable = `\${aws:${scope}:test-key1_test-key2:not-set, 'test'}` 200 | } else { 201 | serverless.service.custom.myVariable = `\${aws:${scope}:test-key:not-set, 'test'}` 202 | } 203 | 204 | await serverless.variables.populateService() 205 | assert.equal(serverless.service.custom.myVariable, 'test') 206 | } 207 | 208 | it('should pass through non-AWS variables', async function() { 209 | const serverless = await createFakeServerless() 210 | serverless.service.custom.myVar = DEFAULT_VALUE 211 | await serverless.variables.populateService() 212 | assert.equal(serverless.service.custom.myVar, DEFAULT_VALUE) 213 | }) 214 | 215 | // eslint-disable-next-line mocha/no-setup-in-describe 216 | for (const service of Object.keys(CONFIGS)) { 217 | it(`should resolve ${service}`, async function() { 218 | testResolve(CONFIGS[service]) 219 | }) 220 | it(`should throw for ${service} not found`, async function() { 221 | testNotFound(CONFIGS[service]) 222 | }) 223 | it(`should not resolve fallback value for ${service} in strict mode`, async function() { 224 | testResolveStrictFallback(CONFIGS[service]) 225 | }) 226 | it(`should resolve fallback value for ${service} in non-strict mode`, async function() { 227 | testResolveFallback(CONFIGS[service]) 228 | }) 229 | } 230 | 231 | it('should throw for keys that are not present', async function() { 232 | const serverless = await createFakeServerless() 233 | 234 | AWS.mock('Kinesis', 'describeStream', (params, callback) => { 235 | callback(null, { StreamDescription: { StreamARN: DEFAULT_VALUE } }) 236 | }) 237 | 238 | serverless.service.custom.foo = '${aws:kinesis:test-stream:BAD_KEY}' // eslint-disable-line 239 | expect(serverless.variables.populateService).to.throw(Error) 240 | }) 241 | }) 242 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import AWS from 'aws-sdk' 3 | import winston from 'winston' 4 | 5 | const AWS_PREFIX = 'aws' 6 | 7 | /** 8 | * @param key the name of the ElastiCache cluster to resolve 9 | * @param awsParameters parameters to pass to the AWS.ElastiCache constructor 10 | * @returns {Promise} 11 | * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/ElastiCache.html#describeCacheClusters-property 12 | */ 13 | async function getECSValue(key, awsParameters) { 14 | winston.debug(`Resolving ElastiCache cluster with name ${key}`) 15 | const ecs = new AWS.ElastiCache({ ...awsParameters, apiVersion: '2015-02-02' }) 16 | const result = await ecs.describeCacheClusters({ CacheClusterId: key, ShowCacheNodeInfo: true }).promise() 17 | if (!result || !result.CacheClusters.length) { 18 | throw new Error(`Could not find ElastiCache cluster with name ${key}`) 19 | } 20 | 21 | return result.CacheClusters[0] 22 | } 23 | 24 | /** 25 | * @param key the name of the ElasticSearch cluster to resolve 26 | * @param awsParameters parameters to pass to the AWS.ES constructor 27 | * @returns {Promise} 28 | * @see http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/ES.html#describeElasticsearchDomain-property 29 | */ 30 | async function getESSValue(key, awsParameters) { 31 | winston.debug(`Resolving ElasticSearch cluster with name ${key}`) 32 | const ess = new AWS.ES({ ...awsParameters, apiVersion: '2015-01-01' }) 33 | const result = await ess.describeElasticsearchDomain({ DomainName: key }).promise() 34 | if (!result || !result.DomainStatus) { 35 | throw new Error(`Could not find ElasticSearch cluster with name ${key}`) 36 | } 37 | 38 | return result.DomainStatus 39 | } 40 | 41 | /** 42 | * @param key the name of the security group to resolve 43 | * @param awsParameters parameters to pass to the AWS.EC2 constructor 44 | * @returns {Promise} 45 | * @see http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/EC2.html#describeSecurityGroups-property 46 | */ 47 | async function getEC2Value(key, awsParameters) { 48 | const ec2 = new AWS.EC2({ ...awsParameters, apiVersion: '2015-01-01' }) 49 | 50 | const values = key.split(':') 51 | 52 | if (values[0] === 'vpc') { 53 | return getVPCValue(values[1], awsParameters) 54 | } else if (values[0] === 'subnet') { 55 | return getSubnetValue(values[1], awsParameters) 56 | } else if (values[0] === 'securityGroup') { 57 | const groupValues = values[1].split('-') 58 | const vpc = await getVPCValue(groupValues[0], awsParameters) 59 | const result = await ec2.describeSecurityGroups( 60 | { 61 | Filters: [{ Name: 'group-name', Values: [groupValues[1]] }, 62 | { Name: 'vpc-id', Values: [vpc.VpcId] }] 63 | }).promise() 64 | 65 | if (!result || !result.SecurityGroups.length) { 66 | throw new Error(`Could not find security group with name ${groupValues[1]} in ${vpc.VpcId}`) 67 | } 68 | return result.SecurityGroups[0] 69 | } 70 | throw new Error(`Unsupported EC2 value. ${values[0]}`) 71 | } 72 | 73 | /** 74 | * @param key the name of the VPC to resolve 75 | * @param awsParameters parameters to pass to the AWS.EC2 constructor 76 | * @returns {Promise} 77 | * @see http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/EC2.html#describeVpcs-property 78 | */ 79 | async function getVPCValue(key, awsParameters) { 80 | winston.debug(`Resolving vpc with name ${key}`) 81 | const ec2 = new AWS.EC2({ ...awsParameters, apiVersion: '2015-01-01' }) 82 | const result = await ec2.describeVpcs( 83 | { Filters: [{ Name: 'tag-value', Values: [key] }] }).promise() 84 | 85 | if (!result || !result.Vpcs.length) { 86 | throw new Error(`Could not find vpc with name ${key}`) 87 | } 88 | 89 | return result.Vpcs[0] 90 | } 91 | 92 | /** 93 | * @param key the name of the subnet to resolve 94 | * @param awsParameters parameters to pass to the AWS.EC2 constructor 95 | * @returns {Promise} 96 | * @see http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/EC2.html#describeSubnets-property 97 | */ 98 | async function getSubnetValue(key, awsParameters) { 99 | winston.debug(`Resolving subnet with name ${key}`) 100 | const ec2 = new AWS.EC2({ ...awsParameters, apiVersion: '2015-01-01' }) 101 | const result = await ec2.describeSubnets( 102 | { Filters: [{ Name: 'tag-value', Values: [key] }] }).promise() 103 | 104 | if (!result || !result.Subnets.length) { 105 | throw new Error(`Could not find subnet with name ${key}`) 106 | } 107 | 108 | return result.Subnets[0] 109 | } 110 | 111 | /** 112 | * @param key the name of the Kinesis stream to resolve 113 | * @param awsParameters parameters to pass to the AWS.Kinesis constructor 114 | * @returns {Promise} 115 | * @see http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Kinesis.html#describeStream-property 116 | */ 117 | async function getKinesisValue(key, awsParameters) { 118 | winston.debug(`Resolving Kinesis stream with name ${key}`) 119 | const kinesis = new AWS.Kinesis({ ...awsParameters, apiVersion: '2013-12-02' }) 120 | const result = await kinesis.describeStream({ StreamName: key }).promise() 121 | return result.StreamDescription 122 | } 123 | 124 | /** 125 | * @param key the name of the DynamoDb table to resolve 126 | * @param awsParameters parameters to pass to the AWS.DynamoDB constructor 127 | * @returns {Promise} 128 | * @see http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html#describeTable-property 129 | */ 130 | async function getDynamoDbValue(key, awsParameters) { 131 | winston.debug(`Resolving DynamoDB stream with name ${key}`) 132 | const dynamodb = new AWS.DynamoDB({ ...awsParameters, apiVersion: '2012-08-10' }) 133 | const result = await dynamodb.describeTable({ TableName: key }).promise() 134 | return result.Table 135 | } 136 | 137 | /** 138 | * @param key the name of the RDS instance to resolve 139 | * @param awsParameters parameters to pass to the AWS.RDS constructor 140 | * @returns {Promise.} 141 | * @see http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/RDS.html#describeDBInstances-property 142 | */ 143 | async function getRDSValue(key, awsParameters) { 144 | winston.debug(`Resolving RDS database with name ${key}`) 145 | const rds = new AWS.RDS({ ...awsParameters, apiVersion: '2014-10-31' }) 146 | const result = await rds.describeDBInstances({ DBInstanceIdentifier: key }).promise() 147 | if (!result) { 148 | throw new Error(`Could not find any databases with identifier ${key}`) 149 | } 150 | // Parse out the instances 151 | const instances = result.DBInstances 152 | 153 | if (instances.length !== 1) { 154 | throw new Error(`Expected exactly one DB instance for key ${key}. Got ${Object.keys(instances)}`) 155 | } 156 | 157 | return instances[0] 158 | } 159 | 160 | /** 161 | * @param key the name of the RDS Aurora cluster to resolve 162 | * @param awsParameters parameters to pass to the AWS.RDS constructor 163 | * @returns {Promise.} 164 | * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/RDS.html#describeDBClusters-property 165 | */ 166 | async function getRDSAuroraValue(key, awsParameters) { 167 | winston.debug(`Resolving RDS Aurora cluster with name ${key}`) 168 | const rds = new AWS.RDS({ ...awsParameters, apiVersion: '2014-10-31' }) 169 | const result = await rds.describeDBClusters({ DBClusterIdentifier: key }).promise() 170 | if (!result) { 171 | throw new Error(`Could not find any RDS Aurora clusters with identifier ${key}`) 172 | } 173 | // Parse out the clusters 174 | const clusters = result.DBClusters 175 | 176 | if (clusters.length !== 1) { 177 | throw new Error(`Expected exactly one RDS Aurora cluster for key ${key}. Got ${Object.keys(clusters)}`) 178 | } 179 | 180 | return clusters[0] 181 | } 182 | 183 | /** 184 | * @param key the concatenated {stackName_logicalResourceId} of the CloudFormation to resolve physicalResourceId 185 | * @param awsParameters parameters to pass to the AWS.CF constructor 186 | * @returns {Promise.} a promise for the resolved variable 187 | * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CloudFormation.html#describeStackResource-property 188 | */ 189 | async function getCFPhysicalResourceId(key, awsParameters) { 190 | winston.debug(`Resolving a CloudFormation stack's PhysicalResourceId by the concatenated {stackName_logicalResourceId} ${key}`) 191 | const cf = new AWS.CloudFormation({ ...awsParameters, apiVersion: '2014-10-31' }) 192 | const values = key.split('_') 193 | let stackName = '' 194 | let logicalResourceId = '' 195 | if (values.length === 2) { 196 | stackName = values[0] 197 | logicalResourceId = values[1] 198 | } else { 199 | throw new Error(`Invalid format for {CloudFormationStackName}_{PhysicalResourceId}, given: ${key}`) 200 | } 201 | const result = await cf.describeStackResource({ LogicalResourceId: logicalResourceId, StackName: stackName }).promise() 202 | if (!result) { 203 | throw new Error(`Could not find in CloudFormationStack: ${stackName} any PhysicalResourceId associated with LogicalResourceId: ${logicalResourceId}`) 204 | } 205 | 206 | return result.StackResourceDetail 207 | } 208 | 209 | /** 210 | * @param key the name of the APIGateway Api (Rest) 211 | * @param awsParameters parameters to pass to the AWS.ApiGateway constructor 212 | * @returns { Promise. } 213 | * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/APIGateway.html#getRestApis-property 214 | */ 215 | async function getAPIGatewayValue(key, awsParameters) { 216 | winston.debug(`Resolving APIGateway Api with name ${key}`) 217 | 218 | const apigateway = new AWS.APIGateway({ ...awsParameters, apiVersion: '2015-07-09' }) 219 | const apis = await apigateway.getRestApis({}).promise() 220 | 221 | return filterAPIGatewayApi(apis.items, 'name', key) 222 | } 223 | 224 | /** 225 | * @param key the name of the APIGatewayV2 Api (Websocket / HTTP) 226 | * @param awsParameters parameters to pass to the AWS.ApiGatewayV2 constructor 227 | * @returns { Promise. } 228 | * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/ApiGatewayV2.html#getApis-property 229 | */ 230 | async function getAPIGatewayV2Value(key, awsParameters) { 231 | winston.debug(`Resolving ApiGatewayV2 Api with name ${key}`) 232 | 233 | const apigateway = new AWS.ApiGatewayV2({ ...awsParameters, apiVersion: '2018-11-29' }) 234 | const apis = await apigateway.getApis({}).promise() 235 | 236 | return filterAPIGatewayApi(apis.Items, 'Name', key) 237 | } 238 | 239 | /** 240 | * @param apiItems array with APIGateway or APIGatewayV2 objects 241 | * @param nameProperty name of the property to filter on (with key) 242 | * @param key the name of the APIGateway(V2) Api 243 | * @returns { Promise. / Promise. } 244 | * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/APIGateway.html#getRestApis-property 245 | * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/ApiGatewayV2.html#getApis-property 246 | */ 247 | async function filterAPIGatewayApi(apiItems, nameProperty, key) { 248 | if (apiItems.length === 0) { 249 | throw new Error('Could not find any Apis') 250 | } 251 | 252 | const matchingApis = apiItems.filter(api => api[nameProperty] === key) 253 | if (matchingApis.length !== 1) { 254 | throw new Error(`Could not find any Api with name ${key}, found: 255 | ${JSON.stringify(apiItems.map(item => item[nameProperty]))}`) 256 | } 257 | 258 | return matchingApis[0] 259 | } 260 | 261 | const AWS_HANDLERS = { 262 | ecs: getECSValue, 263 | ess: getESSValue, 264 | kinesis: getKinesisValue, 265 | dynamodb: getDynamoDbValue, 266 | rds: getRDSValue, 267 | rdsaurora: getRDSAuroraValue, 268 | ec2: getEC2Value, 269 | cf: getCFPhysicalResourceId, 270 | apigateway: getAPIGatewayValue, 271 | apigatewayv2: getAPIGatewayV2Value 272 | } 273 | 274 | /* eslint-disable no-useless-escape */ 275 | const DEFAULT_AWS_PATTERN = /^aws:\w+:[\w-.]+:[\w.\[\]]+$/ 276 | const SUB_SERVICE_AWS_PATTERN = /^aws:\w+:\w+:[\w-.]+:[\w.\[\]]+$/ 277 | /* eslint-enable no-useless-escape */ 278 | 279 | /** 280 | * @param variableString the variable to resolve 281 | * @param region the AWS region to use 282 | * @param strictMode throw errors if aws can't find value or allow overwrite 283 | * @returns {Promise.} a promise for the resolved variable 284 | * @example const myResolvedVariable = await getValueFromAws('aws:kinesis:my-stream:StreamARN', 'us-east-1') 285 | */ 286 | async function getValueFromAws(variableString, region, strictMode) { 287 | // The format is aws:${service}:${key}:${request} or aws:${service}:${subService}:${key}:${request}. 288 | // eg.: aws:kinesis:stream-name:StreamARN 289 | // Validate the input format 290 | if (!variableString.match(DEFAULT_AWS_PATTERN) && !variableString.match(SUB_SERVICE_AWS_PATTERN)) { 291 | throw new Error(`Invalid AWS format for variable ${variableString}`) 292 | } 293 | 294 | const rest = variableString.split(`${AWS_PREFIX}:`)[1] 295 | for (const service of Object.keys(AWS_HANDLERS)) { 296 | if (rest.startsWith(`${service}:`)) { 297 | const commonParameters = {} 298 | if (region) { 299 | commonParameters.region = region 300 | } 301 | 302 | // Parse out the key and request 303 | const subKey = rest.split(`${service}:`)[1] 304 | 305 | let request = '' 306 | let key = '' 307 | // We are dealing with a subService instead of a standard service 308 | if (variableString.match(SUB_SERVICE_AWS_PATTERN)) { 309 | request = subKey.split(':')[2] 310 | key = subKey.split(':').slice(0, 2).join(':') 311 | } else { 312 | request = subKey.split(':')[1] 313 | key = subKey.split(':')[0] 314 | } 315 | 316 | let description 317 | try { 318 | description = await AWS_HANDLERS[service](key, commonParameters) // eslint-disable-line no-await-in-loop, max-len 319 | } catch (e) { 320 | if (strictMode) { 321 | throw e 322 | } 323 | winston.debug(`Error while resolving ${variableString}: ${e.message}`) 324 | return null 325 | } 326 | 327 | // Validate that the desired property exists 328 | if (!_.has(description, request)) { 329 | throw new Error(`Error resolving ${variableString}. Key '${request}' not found. Candidates are ${Object.keys(description)}`) 330 | } 331 | 332 | return _.get(description, request) 333 | } 334 | } 335 | 336 | throw new TypeError(`Cannot parse AWS type from ${rest}`) 337 | } 338 | 339 | /** 340 | * A plugin for the serverless framework that allows resolution of deployed AWS services into variable names 341 | */ 342 | class ServerlessAWSResolvers { 343 | constructor(serverless, options) { 344 | this.provider = 'aws' 345 | 346 | this.commands = { 347 | resolveAwsKey: { 348 | usage: `Resolves an AWS key (Supported prefixes: ${Object.keys(AWS_HANDLERS)})`, 349 | lifecycleEvents: ['run'], 350 | options: { 351 | key: { 352 | usage: 'The key to resolve', 353 | shortcut: 'k', 354 | type: 'string' 355 | } 356 | } 357 | } 358 | } 359 | 360 | this.hooks = { 361 | 'resolveAwsKey:run': () => getValueFromAws(options.key, serverless.service.provider.region) 362 | .then(JSON.stringify) 363 | .then(_.bind(serverless.cli.log, serverless.cli)) 364 | } 365 | 366 | const delegate = _.bind(serverless.variables.getValueFromSource, serverless.variables) 367 | serverless.variables.getValueFromSource = function getValueFromSource(variableString) { // eslint-disable-line no-param-reassign, max-len 368 | const region = serverless.service.provider.region 369 | const strictMode = _.get(serverless.service.custom, 'awsResolvers.strict', true) 370 | if (!region) { 371 | throw new Error('Cannot hydrate AWS variables without a region') 372 | } 373 | if (variableString.startsWith(`${AWS_PREFIX}:`)) { 374 | return getValueFromAws(variableString, region, strictMode) 375 | } 376 | 377 | return delegate(variableString) 378 | } 379 | } 380 | } 381 | 382 | module.exports = ServerlessAWSResolvers 383 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _lodash = _interopRequireDefault(require("lodash")); 4 | 5 | var _awsSdk = _interopRequireDefault(require("aws-sdk")); 6 | 7 | var _winston = _interopRequireDefault(require("winston")); 8 | 9 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 10 | 11 | const AWS_PREFIX = 'aws'; 12 | /** 13 | * @param key the name of the ElastiCache cluster to resolve 14 | * @param awsParameters parameters to pass to the AWS.ElastiCache constructor 15 | * @returns {Promise} 16 | * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/ElastiCache.html#describeCacheClusters-property 17 | */ 18 | 19 | async function getECSValue(key, awsParameters) { 20 | _winston.default.debug(`Resolving ElastiCache cluster with name ${key}`); 21 | 22 | const ecs = new _awsSdk.default.ElastiCache({ ...awsParameters, 23 | apiVersion: '2015-02-02' 24 | }); 25 | const result = await ecs.describeCacheClusters({ 26 | CacheClusterId: key, 27 | ShowCacheNodeInfo: true 28 | }).promise(); 29 | 30 | if (!result || !result.CacheClusters.length) { 31 | throw new Error(`Could not find ElastiCache cluster with name ${key}`); 32 | } 33 | 34 | return result.CacheClusters[0]; 35 | } 36 | /** 37 | * @param key the name of the ElasticSearch cluster to resolve 38 | * @param awsParameters parameters to pass to the AWS.ES constructor 39 | * @returns {Promise} 40 | * @see http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/ES.html#describeElasticsearchDomain-property 41 | */ 42 | 43 | 44 | async function getESSValue(key, awsParameters) { 45 | _winston.default.debug(`Resolving ElasticSearch cluster with name ${key}`); 46 | 47 | const ess = new _awsSdk.default.ES({ ...awsParameters, 48 | apiVersion: '2015-01-01' 49 | }); 50 | const result = await ess.describeElasticsearchDomain({ 51 | DomainName: key 52 | }).promise(); 53 | 54 | if (!result || !result.DomainStatus) { 55 | throw new Error(`Could not find ElasticSearch cluster with name ${key}`); 56 | } 57 | 58 | return result.DomainStatus; 59 | } 60 | /** 61 | * @param key the name of the security group to resolve 62 | * @param awsParameters parameters to pass to the AWS.EC2 constructor 63 | * @returns {Promise} 64 | * @see http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/EC2.html#describeSecurityGroups-property 65 | */ 66 | 67 | 68 | async function getEC2Value(key, awsParameters) { 69 | const ec2 = new _awsSdk.default.EC2({ ...awsParameters, 70 | apiVersion: '2015-01-01' 71 | }); 72 | const values = key.split(':'); 73 | 74 | if (values[0] === 'vpc') { 75 | return getVPCValue(values[1], awsParameters); 76 | } else if (values[0] === 'subnet') { 77 | return getSubnetValue(values[1], awsParameters); 78 | } else if (values[0] === 'securityGroup') { 79 | const groupValues = values[1].split('-'); 80 | const vpc = await getVPCValue(groupValues[0], awsParameters); 81 | const result = await ec2.describeSecurityGroups({ 82 | Filters: [{ 83 | Name: 'group-name', 84 | Values: [groupValues[1]] 85 | }, { 86 | Name: 'vpc-id', 87 | Values: [vpc.VpcId] 88 | }] 89 | }).promise(); 90 | 91 | if (!result || !result.SecurityGroups.length) { 92 | throw new Error(`Could not find security group with name ${groupValues[1]} in ${vpc.VpcId}`); 93 | } 94 | 95 | return result.SecurityGroups[0]; 96 | } 97 | 98 | throw new Error(`Unsupported EC2 value. ${values[0]}`); 99 | } 100 | /** 101 | * @param key the name of the VPC to resolve 102 | * @param awsParameters parameters to pass to the AWS.EC2 constructor 103 | * @returns {Promise} 104 | * @see http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/EC2.html#describeVpcs-property 105 | */ 106 | 107 | 108 | async function getVPCValue(key, awsParameters) { 109 | _winston.default.debug(`Resolving vpc with name ${key}`); 110 | 111 | const ec2 = new _awsSdk.default.EC2({ ...awsParameters, 112 | apiVersion: '2015-01-01' 113 | }); 114 | const result = await ec2.describeVpcs({ 115 | Filters: [{ 116 | Name: 'tag-value', 117 | Values: [key] 118 | }] 119 | }).promise(); 120 | 121 | if (!result || !result.Vpcs.length) { 122 | throw new Error(`Could not find vpc with name ${key}`); 123 | } 124 | 125 | return result.Vpcs[0]; 126 | } 127 | /** 128 | * @param key the name of the subnet to resolve 129 | * @param awsParameters parameters to pass to the AWS.EC2 constructor 130 | * @returns {Promise} 131 | * @see http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/EC2.html#describeSubnets-property 132 | */ 133 | 134 | 135 | async function getSubnetValue(key, awsParameters) { 136 | _winston.default.debug(`Resolving subnet with name ${key}`); 137 | 138 | const ec2 = new _awsSdk.default.EC2({ ...awsParameters, 139 | apiVersion: '2015-01-01' 140 | }); 141 | const result = await ec2.describeSubnets({ 142 | Filters: [{ 143 | Name: 'tag-value', 144 | Values: [key] 145 | }] 146 | }).promise(); 147 | 148 | if (!result || !result.Subnets.length) { 149 | throw new Error(`Could not find subnet with name ${key}`); 150 | } 151 | 152 | return result.Subnets[0]; 153 | } 154 | /** 155 | * @param key the name of the Kinesis stream to resolve 156 | * @param awsParameters parameters to pass to the AWS.Kinesis constructor 157 | * @returns {Promise} 158 | * @see http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Kinesis.html#describeStream-property 159 | */ 160 | 161 | 162 | async function getKinesisValue(key, awsParameters) { 163 | _winston.default.debug(`Resolving Kinesis stream with name ${key}`); 164 | 165 | const kinesis = new _awsSdk.default.Kinesis({ ...awsParameters, 166 | apiVersion: '2013-12-02' 167 | }); 168 | const result = await kinesis.describeStream({ 169 | StreamName: key 170 | }).promise(); 171 | return result.StreamDescription; 172 | } 173 | /** 174 | * @param key the name of the DynamoDb table to resolve 175 | * @param awsParameters parameters to pass to the AWS.DynamoDB constructor 176 | * @returns {Promise} 177 | * @see http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html#describeTable-property 178 | */ 179 | 180 | 181 | async function getDynamoDbValue(key, awsParameters) { 182 | _winston.default.debug(`Resolving DynamoDB stream with name ${key}`); 183 | 184 | const dynamodb = new _awsSdk.default.DynamoDB({ ...awsParameters, 185 | apiVersion: '2012-08-10' 186 | }); 187 | const result = await dynamodb.describeTable({ 188 | TableName: key 189 | }).promise(); 190 | return result.Table; 191 | } 192 | /** 193 | * @param key the name of the RDS instance to resolve 194 | * @param awsParameters parameters to pass to the AWS.RDS constructor 195 | * @returns {Promise.} 196 | * @see http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/RDS.html#describeDBInstances-property 197 | */ 198 | 199 | 200 | async function getRDSValue(key, awsParameters) { 201 | _winston.default.debug(`Resolving RDS database with name ${key}`); 202 | 203 | const rds = new _awsSdk.default.RDS({ ...awsParameters, 204 | apiVersion: '2014-10-31' 205 | }); 206 | const result = await rds.describeDBInstances({ 207 | DBInstanceIdentifier: key 208 | }).promise(); 209 | 210 | if (!result) { 211 | throw new Error(`Could not find any databases with identifier ${key}`); 212 | } // Parse out the instances 213 | 214 | 215 | const instances = result.DBInstances; 216 | 217 | if (instances.length !== 1) { 218 | throw new Error(`Expected exactly one DB instance for key ${key}. Got ${Object.keys(instances)}`); 219 | } 220 | 221 | return instances[0]; 222 | } 223 | /** 224 | * @param key the name of the RDS Aurora cluster to resolve 225 | * @param awsParameters parameters to pass to the AWS.RDS constructor 226 | * @returns {Promise.} 227 | * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/RDS.html#describeDBClusters-property 228 | */ 229 | 230 | 231 | async function getRDSAuroraValue(key, awsParameters) { 232 | _winston.default.debug(`Resolving RDS Aurora cluster with name ${key}`); 233 | 234 | const rds = new _awsSdk.default.RDS({ ...awsParameters, 235 | apiVersion: '2014-10-31' 236 | }); 237 | const result = await rds.describeDBClusters({ 238 | DBClusterIdentifier: key 239 | }).promise(); 240 | 241 | if (!result) { 242 | throw new Error(`Could not find any RDS Aurora clusters with identifier ${key}`); 243 | } // Parse out the clusters 244 | 245 | 246 | const clusters = result.DBClusters; 247 | 248 | if (clusters.length !== 1) { 249 | throw new Error(`Expected exactly one RDS Aurora cluster for key ${key}. Got ${Object.keys(clusters)}`); 250 | } 251 | 252 | return clusters[0]; 253 | } 254 | /** 255 | * @param key the concatenated {stackName_logicalResourceId} of the CloudFormation to resolve physicalResourceId 256 | * @param awsParameters parameters to pass to the AWS.CF constructor 257 | * @returns {Promise.} a promise for the resolved variable 258 | * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CloudFormation.html#describeStackResource-property 259 | */ 260 | 261 | 262 | async function getCFPhysicalResourceId(key, awsParameters) { 263 | _winston.default.debug(`Resolving a CloudFormation stack's PhysicalResourceId by the concatenated {stackName_logicalResourceId} ${key}`); 264 | 265 | const cf = new _awsSdk.default.CloudFormation({ ...awsParameters, 266 | apiVersion: '2014-10-31' 267 | }); 268 | const values = key.split('_'); 269 | let stackName = ''; 270 | let logicalResourceId = ''; 271 | 272 | if (values.length === 2) { 273 | stackName = values[0]; 274 | logicalResourceId = values[1]; 275 | } else { 276 | throw new Error(`Invalid format for {CloudFormationStackName}_{PhysicalResourceId}, given: ${key}`); 277 | } 278 | 279 | const result = await cf.describeStackResource({ 280 | LogicalResourceId: logicalResourceId, 281 | StackName: stackName 282 | }).promise(); 283 | 284 | if (!result) { 285 | throw new Error(`Could not find in CloudFormationStack: ${stackName} any PhysicalResourceId associated with LogicalResourceId: ${logicalResourceId}`); 286 | } 287 | 288 | return result.StackResourceDetail; 289 | } 290 | /** 291 | * @param key the name of the APIGateway Api (Rest) 292 | * @param awsParameters parameters to pass to the AWS.ApiGateway constructor 293 | * @returns { Promise. } 294 | * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/APIGateway.html#getRestApis-property 295 | */ 296 | 297 | 298 | async function getAPIGatewayValue(key, awsParameters) { 299 | _winston.default.debug(`Resolving APIGateway Api with name ${key}`); 300 | 301 | const apigateway = new _awsSdk.default.APIGateway({ ...awsParameters, 302 | apiVersion: '2015-07-09' 303 | }); 304 | const apis = await apigateway.getRestApis({}).promise(); 305 | return filterAPIGatewayApi(apis.items, 'name', key); 306 | } 307 | /** 308 | * @param key the name of the APIGatewayV2 Api (Websocket / HTTP) 309 | * @param awsParameters parameters to pass to the AWS.ApiGatewayV2 constructor 310 | * @returns { Promise. } 311 | * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/ApiGatewayV2.html#getApis-property 312 | */ 313 | 314 | 315 | async function getAPIGatewayV2Value(key, awsParameters) { 316 | _winston.default.debug(`Resolving ApiGatewayV2 Api with name ${key}`); 317 | 318 | const apigateway = new _awsSdk.default.ApiGatewayV2({ ...awsParameters, 319 | apiVersion: '2018-11-29' 320 | }); 321 | const apis = await apigateway.getApis({}).promise(); 322 | return filterAPIGatewayApi(apis.Items, 'Name', key); 323 | } 324 | /** 325 | * @param apiItems array with APIGateway or APIGatewayV2 objects 326 | * @param nameProperty name of the property to filter on (with key) 327 | * @param key the name of the APIGateway(V2) Api 328 | * @returns { Promise. / Promise. } 329 | * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/APIGateway.html#getRestApis-property 330 | * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/ApiGatewayV2.html#getApis-property 331 | */ 332 | 333 | 334 | async function filterAPIGatewayApi(apiItems, nameProperty, key) { 335 | if (apiItems.length === 0) { 336 | throw new Error('Could not find any Apis'); 337 | } 338 | 339 | const matchingApis = apiItems.filter(api => api[nameProperty] === key); 340 | 341 | if (matchingApis.length !== 1) { 342 | throw new Error(`Could not find any Api with name ${key}, found: 343 | ${JSON.stringify(apiItems.map(item => item[nameProperty]))}`); 344 | } 345 | 346 | return matchingApis[0]; 347 | } 348 | 349 | const AWS_HANDLERS = { 350 | ecs: getECSValue, 351 | ess: getESSValue, 352 | kinesis: getKinesisValue, 353 | dynamodb: getDynamoDbValue, 354 | rds: getRDSValue, 355 | rdsaurora: getRDSAuroraValue, 356 | ec2: getEC2Value, 357 | cf: getCFPhysicalResourceId, 358 | apigateway: getAPIGatewayValue, 359 | apigatewayv2: getAPIGatewayV2Value 360 | }; 361 | /* eslint-disable no-useless-escape */ 362 | 363 | const DEFAULT_AWS_PATTERN = /^aws:\w+:[\w-.]+:[\w.\[\]]+$/; 364 | const SUB_SERVICE_AWS_PATTERN = /^aws:\w+:\w+:[\w-.]+:[\w.\[\]]+$/; 365 | /* eslint-enable no-useless-escape */ 366 | 367 | /** 368 | * @param variableString the variable to resolve 369 | * @param region the AWS region to use 370 | * @param strictMode throw errors if aws can't find value or allow overwrite 371 | * @returns {Promise.} a promise for the resolved variable 372 | * @example const myResolvedVariable = await getValueFromAws('aws:kinesis:my-stream:StreamARN', 'us-east-1') 373 | */ 374 | 375 | async function getValueFromAws(variableString, region, strictMode) { 376 | // The format is aws:${service}:${key}:${request} or aws:${service}:${subService}:${key}:${request}. 377 | // eg.: aws:kinesis:stream-name:StreamARN 378 | // Validate the input format 379 | if (!variableString.match(DEFAULT_AWS_PATTERN) && !variableString.match(SUB_SERVICE_AWS_PATTERN)) { 380 | throw new Error(`Invalid AWS format for variable ${variableString}`); 381 | } 382 | 383 | const rest = variableString.split(`${AWS_PREFIX}:`)[1]; 384 | 385 | for (const service of Object.keys(AWS_HANDLERS)) { 386 | if (rest.startsWith(`${service}:`)) { 387 | const commonParameters = {}; 388 | 389 | if (region) { 390 | commonParameters.region = region; 391 | } // Parse out the key and request 392 | 393 | 394 | const subKey = rest.split(`${service}:`)[1]; 395 | let request = ''; 396 | let key = ''; // We are dealing with a subService instead of a standard service 397 | 398 | if (variableString.match(SUB_SERVICE_AWS_PATTERN)) { 399 | request = subKey.split(':')[2]; 400 | key = subKey.split(':').slice(0, 2).join(':'); 401 | } else { 402 | request = subKey.split(':')[1]; 403 | key = subKey.split(':')[0]; 404 | } 405 | 406 | let description; 407 | 408 | try { 409 | description = await AWS_HANDLERS[service](key, commonParameters); // eslint-disable-line no-await-in-loop, max-len 410 | } catch (e) { 411 | if (strictMode) { 412 | throw e; 413 | } 414 | 415 | _winston.default.debug(`Error while resolving ${variableString}: ${e.message}`); 416 | 417 | return null; 418 | } // Validate that the desired property exists 419 | 420 | 421 | if (!_lodash.default.has(description, request)) { 422 | throw new Error(`Error resolving ${variableString}. Key '${request}' not found. Candidates are ${Object.keys(description)}`); 423 | } 424 | 425 | return _lodash.default.get(description, request); 426 | } 427 | } 428 | 429 | throw new TypeError(`Cannot parse AWS type from ${rest}`); 430 | } 431 | /** 432 | * A plugin for the serverless framework that allows resolution of deployed AWS services into variable names 433 | */ 434 | 435 | 436 | class ServerlessAWSResolvers { 437 | constructor(serverless, options) { 438 | this.provider = 'aws'; 439 | this.commands = { 440 | resolveAwsKey: { 441 | usage: `Resolves an AWS key (Supported prefixes: ${Object.keys(AWS_HANDLERS)})`, 442 | lifecycleEvents: ['run'], 443 | options: { 444 | key: { 445 | usage: 'The key to resolve', 446 | shortcut: 'k', 447 | type: 'string' 448 | } 449 | } 450 | } 451 | }; 452 | this.hooks = { 453 | 'resolveAwsKey:run': () => getValueFromAws(options.key, serverless.service.provider.region).then(JSON.stringify).then(_lodash.default.bind(serverless.cli.log, serverless.cli)) 454 | }; 455 | 456 | const delegate = _lodash.default.bind(serverless.variables.getValueFromSource, serverless.variables); 457 | 458 | serverless.variables.getValueFromSource = function getValueFromSource(variableString) { 459 | // eslint-disable-line no-param-reassign, max-len 460 | const region = serverless.service.provider.region; 461 | 462 | const strictMode = _lodash.default.get(serverless.service.custom, 'awsResolvers.strict', true); 463 | 464 | if (!region) { 465 | throw new Error('Cannot hydrate AWS variables without a region'); 466 | } 467 | 468 | if (variableString.startsWith(`${AWS_PREFIX}:`)) { 469 | return getValueFromAws(variableString, region, strictMode); 470 | } 471 | 472 | return delegate(variableString); 473 | }; 474 | } 475 | 476 | } 477 | 478 | module.exports = ServerlessAWSResolvers; --------------------------------------------------------------------------------