├── .github └── workflows │ ├── nodejs.yml │ └── npmpublish.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── buildUtils └── generateRegionsAZMapping.js ├── package-lock.json ├── package.json ├── src ├── AZMap.json ├── ArnResolver.js ├── convertNode.js ├── data │ └── CfnResourceTypeToArnSchemeMap.js ├── index.js ├── nodeEvaluator.js ├── nodeTypes │ ├── ArrayNode.js │ ├── ConditionNode.js │ ├── FnAnd.js │ ├── FnEqualsNode.js │ ├── FnFindInMapNode.js │ ├── FnGetAZsNode.js │ ├── FnGetAttNode.js │ ├── FnIf.js │ ├── FnJoinNode.js │ ├── FnNot.js │ ├── FnOr.js │ ├── FnSelect.js │ ├── FnSplit.js │ ├── FnSub.js │ ├── Node.js │ ├── ObjectNode.js │ ├── PropertyConditionNode.js │ ├── RefNode.js │ ├── ResolveFromMapNode.js │ ├── ResourceNode.js │ ├── ResourcesNode.js │ └── index.js ├── parameterHelper.js └── wrappingHelpers.js └── test ├── ArnResolver.test.js ├── nodeEvaluator.functional.test.js ├── nodeTypes ├── ArrayNode.test.js ├── ConditionNode.test.js ├── FnAnd.test.js ├── FnEqualsNode.test.js ├── FnFindInMapNode.test.js ├── FnGetAZsNode.test.js ├── FnGetAttNode.test.js ├── FnIf.test.js ├── FnJoinNode.test.js ├── FnNot.test.js ├── FnOr.test.js ├── FnSelect.test.js ├── FnSplit.test.js ├── FnSubNode.test.js ├── ObjectNode.test.js ├── PropertyConditionNode.test.js ├── RefNode.test.js └── ResolveFromMapNode.test.js ├── testData └── stack1 │ ├── expected │ ├── expected-us-east-1-beta.json │ ├── expected-us-east-1-prod.json │ └── expected-us-west-2-prod.json │ ├── params │ ├── params-us-east-1-beta.json │ ├── params-us-east-1-prod.json │ └── params-us-west-2-prod.json │ └── template.json └── testUtils.js /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [8.x, 10.x, 12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: npm install, build, and test 21 | run: | 22 | npm ci 23 | npm run build --if-present 24 | npm test 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 12 15 | - run: npm ci 16 | - run: npm test 17 | 18 | publish-npm: 19 | needs: build 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v1 23 | - uses: actions/setup-node@v1 24 | with: 25 | node-version: 12 26 | registry-url: https://registry.npmjs.org/ 27 | - run: npm ci 28 | - run: npm publish 29 | env: 30 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 31 | 32 | # publish-gpr: 33 | # needs: build 34 | # runs-on: ubuntu-latest 35 | # steps: 36 | # - uses: actions/checkout@v1 37 | # - uses: actions/setup-node@v1 38 | # with: 39 | # node-version: 12 40 | # registry-url: https://npm.pkg.github.com/ 41 | # scope: '@your-github-username' 42 | # - run: npm ci 43 | # - run: npm publish 44 | # env: 45 | # NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | coverage 4 | coverage.lcov 5 | cfn-resolver-lib-*.tgz 6 | .DS_Store 7 | buildUtils/tempCreds.json 8 | buildUtils/tempCreds-cn.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | coverage 4 | coverage.lcov 5 | .gitignore 6 | .github 7 | .travis.yml 8 | cfn-resolver-lib-*.tgz 9 | buildUtils/tempCreds.json 10 | buildUtils/tempCreds-cn.json 11 | .vscode/launch.json 12 | test 13 | buildUtils -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 4 | - 10 5 | - 8 6 | 7 | install: 8 | - npm install 9 | - npm install -g codecov 10 | script: 11 | - npm run coverage 12 | - npm run report-coverage -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}/src/index.js" 15 | }, 16 | { 17 | "type": "node", 18 | "request": "launch", 19 | "name": "generateAzs", 20 | "skipFiles": [ 21 | "/**" 22 | ], 23 | "program": "${workspaceFolder}/buildUtils/generateRegionsAZMapping.js" 24 | }, 25 | { 26 | "type": "node", 27 | "request": "launch", 28 | "name": "Mocha all tests", 29 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 30 | "args": [ 31 | "${workspaceRoot}/test/**/*.test.js" 32 | ], 33 | "cwd": "${workspaceRoot}", 34 | "internalConsoleOptions": "openOnSessionStart" 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2019, Robert Essig 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cfn-resolver-lib 2 | [![Build Status](https://travis-ci.com/robessog/cfn-resolver-lib.svg?branch=master)](https://travis-ci.com/robessog/cfn-resolver-lib) 3 | [![codecov](https://codecov.io/gh/robessog/cfn-resolver-lib/branch/master/graph/badge.svg)](https://codecov.io/gh/robessog/cfn-resolver-lib) 4 | ![depend bot](https://badgen.net/dependabot/robessog/cfn-resolver-lib?icon=dependabot) 5 | [![npm version](https://badge.fury.io/js/cfn-resolver-lib.svg)](https://badge.fury.io/js/cfn-resolver-lib) 6 | 7 | 8 | JavaScript library that resolves and evaluates values in [AWS CloudFormation](https://aws.amazon.com/cloudformation/) templates based on the provided stack parameters and produces the JS object representation of the resolved CFN template. 9 | 10 | Did you ever had to debug what's wrong with your AWS CloudFormation template and why your stack deployment fails? Your YAML/JSON could contain some logic with all kinds of nested [intrinsic functions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html) and CFN [pseudo parameters](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/pseudo-parameter-reference.html) and sometimes this can get even more complex when you use a tool (e.g. [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/home.html)) that generates the file for you. 11 | 12 | If you have more than couple of these in your templates it is quite time consuming to figure out which exactly caused the deployment to fail. This simple tool ([cfn-resolver-lib]((https://www.npmjs.com/package/cfn-resolver-lib)) and [cfn-resolver-cli]((https://www.npmjs.com/package/cfn-resolver-cli))) tries to mitigate the issue by evaluating these logic and provide the final exact values that will be used in deployment time. 13 | 14 | [cfn-resolver](https://www.npmjs.com/package/cfn-resolver-cli) can help you 15 | * understand your CFN template better 16 | * troubleshoot CloudFormation deployment issues faster 17 | * secure your IaC with unit tests that assert on exact values before actually deploying anything 18 | * e.g. your unit test now can assert that the `s3_reader` IAM user has access to `prod-uswest2-redshift-log` S3 bucket in `us-west-2` region in your `prod` stack. 19 | 20 | ## CLI and Examples 21 | Check out [cfn-resolver-cli](https://github.com/robessog/cfn-resolver-cli#readme) 22 | 23 | ## How to use? 24 | 25 | Install the npm package 26 | ``` 27 | npm i cfn-resolver-lib 28 | ``` 29 | Write your JavaScript code: 30 | ```js 31 | const NodeEvaluator = require('cfn-resolver-lib'); 32 | 33 | const stackParameters = { 34 | RefResolvers: 35 | { 36 | "AWS::Region": "us-west-2", 37 | "AWS::Partition": "aws", 38 | "AWS::AccountId": "000000111111", 39 | "Stage": "prod", 40 | "AWS::StackId": "MyEvaluatedFakeStackUsWest2" 41 | } 42 | }; 43 | 44 | const resolvedObj = new NodeEvaluator(cloufFormationTemplateDeseralizedObj, stackParameters).evaluateNodes(); 45 | ``` 46 | 47 | Alternative usage: create NodeEvaluator instance once and reuse multiple times for different parameter sets: 48 | ```js 49 | const NodeEvaluator = require('cfn-resolver-lib'); 50 | 51 | const nodeEvaluator = new NodeEvaluator(cloufFormationTemplateDeseralizedObj); 52 | 53 | const stackParameters1 = { 54 | RefResolvers: 55 | { 56 | "AWS::Region": "us-west-2", 57 | "AWS::Partition": "aws", 58 | "AWS::AccountId": "000000111111", 59 | "Stage": "prod", 60 | "AWS::StackId": "MyEvaluatedFakeStackUsWest2" 61 | } 62 | }; 63 | 64 | const stackParameters2 = { 65 | RefResolvers: 66 | { 67 | "AWS::Region": "us-east-1", 68 | "AWS::Partition": "aws", 69 | "AWS::AccountId": "000000112222", 70 | "Stage": "beta", 71 | "AWS::StackId": "MyEvaluatedFakeStackUsEast1" 72 | } 73 | }; 74 | 75 | const resolvedObj1 = nodeEvaluator.evaluateNodes(stackParameters1); 76 | const resolvedObj2 = nodeEvaluator.evaluateNodes(stackParameters2); 77 | 78 | ``` 79 | 80 | 81 | 82 | ## Extensibility & Customization 83 | You can pass additional resolver maps to the `NodeEvaluator` instance, just like `RefResolvers` to customize or **override** the built-in behaviour. 84 | ### Fn::GetAtt resolution 85 | By default the tool tries to resolve the attributes of resources that are defined within the template itself, but you have the opportunity to override the behaviour for specific cases. 86 | Just define the Fn::GetAtt resolver map for custom attribute resolution: 87 | 88 | ```js 89 | { 90 | "Fn::GetAttResolvers": { 91 | MyResourceLogicalId1: { 92 | "AttribeteKey1": "TheOverridenAttributeValue" 93 | } 94 | } 95 | } 96 | ``` 97 | 98 | #### ARN resolution 99 | With `Fn::GetAtt` you can refer to ARN of an other resource defined in the template. 100 | The tool supports ARN resolution for some of the most common AWS CloudFormation resource types (Lambda function, SQS queue, SNS topic, S3 bucket, DyanmoDB Table, etc), but user can provide additional ARN shemas to `NodeEvaulator` intance: 101 | 102 | ```js 103 | { 104 | "ArnSchemas": { 105 | "AWS::DynamoDB::Table": "arn:${Partition}:dynamodb:${Region}:${Account}:table/${TableName}" 106 | } 107 | } 108 | 109 | ``` 110 | The `${Partition}`, `${Region}` and `${Region}` placeholders will be resolved by using the stack parameters. The last placeholder of the arn schema (in the above example `${TableName}`) will be resolved from the attribute from the resource (if both the resource and its attribute can be found in the template). 111 | 112 | 113 | ### Fn::ImportValue resolvers 114 | Define your Fn::ImportValue resolvers in the parameter map as the following: 115 | ```js 116 | { 117 | "Fn::ImportValueResolvers": { 118 | "OtherStacksExportedKey1": "MyFakeImportedValue1" 119 | } 120 | } 121 | ``` 122 | 123 | 124 | ## Supported Features 125 | 126 | * [Condition Functions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html) 127 | * [Fn::And](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html#intrinsic-function-reference-conditions-and) 128 | * [Fn::Equals](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html#intrinsic-function-reference-conditions-equals) 129 | * [Fn::If](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html#intrinsic-function-reference-conditions-if) 130 | * [Fn::Not](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html#intrinsic-function-reference-conditions-not) 131 | * [Fn::Or](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-conditions.html#intrinsic-function-reference-conditions-or) 132 | * [Fn::FindInMap](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-findinmap.html) 133 | * [Fn::GetAtt](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html) 134 | * [Fn::GetAZs](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getavailabilityzones.html) 135 | * [Fn::Join](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-join.html) 136 | * [Fn::Select](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-select.html) 137 | * [Fn::Split](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-split.html) 138 | * [Fn::Sub](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html) (at the moment only key-value map subtitution is supported) 139 | * [Fn::ImportValue](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/) 140 | * [Ref](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-ref.html) 141 | 142 | 143 | ### Unsported Features 144 | * [Fn::Transform](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-transform.html) 145 | 146 | ## Roadmap 147 | * Enchance [Fn::Sub](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html) to work with template parameter names, resource logical IDs, resource attributes 148 | intrinsic-function-reference-importvalue.html 149 | * Support [Fn::Base64](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-base64.html) 150 | * Support [Fn::Cidr](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-cidr.html) 151 | * Add linter/debugging features by identified valudation errors and warnings found during template evaluation (e.g. like [cfn-lint](https://www.npmjs.com/package/cfn-lint)) 152 | 153 | ## Contribution 154 | Feel free to implement any missing features or fix bugs. In any case don't forget to add unit tests. -------------------------------------------------------------------------------- /buildUtils/generateRegionsAZMapping.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const fsExtra = require('fs-extra'); 3 | const yargs = require('yargs'); 4 | 5 | const argv = yargs 6 | .option('defaultRegion', { 7 | alias: 'r', 8 | describe: 'provide a path to input file', 9 | }) 10 | .option('credentialsFile', { 11 | alias: 'c', 12 | describe: 'credentials file' 13 | }) 14 | .option('output', { 15 | alias: 'o', 16 | describe: 'output file' 17 | }).option('verbose', { 18 | alias: 'v', 19 | type: 'boolean', 20 | description: 'Run with verbose logging' 21 | }) 22 | .help() 23 | .argv 24 | 25 | const tempCreds = JSON.parse(fs.readFileSync(argv.credentialsFile, 'utf8')); 26 | 27 | const getEc2Client = (region) => { 28 | const AWS = require('aws-sdk'); 29 | 30 | AWS.config.region = region; 31 | const creds = new AWS.Credentials(tempCreds.credentials.accessKeyId, tempCreds.credentials.secretAccessKey, tempCreds.credentials.sessionToken); 32 | AWS.config.credentials = creds; 33 | const ec2 = new AWS.EC2(); 34 | return ec2; 35 | } 36 | 37 | const asyncForEach = async (array, callback) => { 38 | for (let index = 0; index < array.length; index++) { 39 | await callback(array[index], index, array); 40 | } 41 | } 42 | 43 | const regionToAZmap = {}; 44 | 45 | const callApisSync = async () => { 46 | const regionsResponse = await getEc2Client(argv.defaultRegion).describeRegions().promise(); 47 | 48 | console.log(regionsResponse); 49 | 50 | await asyncForEach(regionsResponse.Regions, async (region) => { 51 | const regionName = region.RegionName; 52 | console.log(`Querying: ${regionName}`) 53 | const azData = await getEc2Client(regionName).describeAvailabilityZones().promise(); 54 | regionToAZmap[regionName] = []; 55 | azData.AvailabilityZones.forEach((az) => { 56 | regionToAZmap[regionName].push(az.ZoneName) 57 | }); 58 | }); 59 | 60 | console.log(regionToAZmap); 61 | 62 | const mergeBase = fs.existsSync(argv.output) ? JSON.parse(fs.readFileSync(argv.output)) : {}; 63 | console.log("Base map: "); 64 | console.log(mergeBase); 65 | 66 | const mergedMap = { ...mergeBase, ...regionToAZmap }; 67 | fsExtra.outputFileSync(argv.output, JSON.stringify(mergedMap, null, 2)); 68 | }; 69 | 70 | callApisSync(); 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cfn-resolver-lib", 3 | "version": "1.1.8", 4 | "description": "Library that resolves AWS Cloudformation templates with exact values", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "mocha --recursive", 8 | "coverage": "nyc --all --reporter cobertura --reporter text --report-dir ./coverage npm run test", 9 | "report-coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov", 10 | "generateAzList": "node ./buildUtils/generateRegionsAZMapping.js", 11 | "generateAzList-classic": "node ./buildUtils/generateRegionsAZMapping.js -c ./buildUtils/tempCreds.json -r us-east-1 -o ./src/AZMap.json", 12 | "generateAzList-cn": "node ./buildUtils/generateRegionsAZMapping.js -c ./buildUtils/tempCreds-cn.json -r cn-north-1 -o ./src/AZMap.json" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/robessog/cfn-resolver-lib.git" 17 | }, 18 | "keywords": [ 19 | "CFN", 20 | "CloudFormation", 21 | "AWS", 22 | "resolve", 23 | "debug", 24 | "troubleshoot", 25 | "test" 26 | ], 27 | "author": "robessog", 28 | "license": "ISC", 29 | "dependencies": { 30 | "lodash": "^4.17.15", 31 | "traverse": "^0.6.6" 32 | }, 33 | "devDependencies": { 34 | "aws-sdk": "^2.590.0", 35 | "chai": "^4.2.0", 36 | "codecov": "^3.6.1", 37 | "fs-extra": "^8.1.0", 38 | "mocha": "^6.2.2", 39 | "nyc": "^15.0.0", 40 | "yargs": "^15.0.2" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/AZMap.json: -------------------------------------------------------------------------------- 1 | { 2 | "eu-north-1": [ 3 | "eu-north-1a", 4 | "eu-north-1b", 5 | "eu-north-1c" 6 | ], 7 | "ap-south-1": [ 8 | "ap-south-1a", 9 | "ap-south-1b", 10 | "ap-south-1c" 11 | ], 12 | "eu-west-3": [ 13 | "eu-west-3a", 14 | "eu-west-3b", 15 | "eu-west-3c" 16 | ], 17 | "eu-west-2": [ 18 | "eu-west-2a", 19 | "eu-west-2b", 20 | "eu-west-2c" 21 | ], 22 | "eu-west-1": [ 23 | "eu-west-1a", 24 | "eu-west-1b", 25 | "eu-west-1c" 26 | ], 27 | "ap-northeast-3": [ 28 | "ap-northeast-3a" 29 | ], 30 | "ap-northeast-2": [ 31 | "ap-northeast-2a", 32 | "ap-northeast-2b", 33 | "ap-northeast-2c" 34 | ], 35 | "ap-northeast-1": [ 36 | "ap-northeast-1a", 37 | "ap-northeast-1c", 38 | "ap-northeast-1d" 39 | ], 40 | "sa-east-1": [ 41 | "sa-east-1a", 42 | "sa-east-1b", 43 | "sa-east-1c" 44 | ], 45 | "ca-central-1": [ 46 | "ca-central-1a", 47 | "ca-central-1b" 48 | ], 49 | "ap-southeast-1": [ 50 | "ap-southeast-1a", 51 | "ap-southeast-1b", 52 | "ap-southeast-1c" 53 | ], 54 | "ap-southeast-2": [ 55 | "ap-southeast-2a", 56 | "ap-southeast-2b", 57 | "ap-southeast-2c" 58 | ], 59 | "eu-central-1": [ 60 | "eu-central-1a", 61 | "eu-central-1b", 62 | "eu-central-1c" 63 | ], 64 | "us-east-1": [ 65 | "us-east-1a", 66 | "us-east-1b", 67 | "us-east-1c", 68 | "us-east-1d", 69 | "us-east-1e", 70 | "us-east-1f" 71 | ], 72 | "us-east-2": [ 73 | "us-east-2a", 74 | "us-east-2b", 75 | "us-east-2c" 76 | ], 77 | "us-west-1": [ 78 | "us-west-1a", 79 | "us-west-1b" 80 | ], 81 | "us-west-2": [ 82 | "us-west-2a", 83 | "us-west-2b", 84 | "us-west-2c", 85 | "us-west-2d" 86 | ], 87 | "cn-north-1": [ 88 | "cn-north-1a", 89 | "cn-north-1b" 90 | ], 91 | "cn-northwest-1": [ 92 | "cn-northwest-1a", 93 | "cn-northwest-1b", 94 | "cn-northwest-1c" 95 | ] 96 | } -------------------------------------------------------------------------------- /src/ArnResolver.js: -------------------------------------------------------------------------------- 1 | class ArnResolver { 2 | constructor(defaultArnSchemaMap, userDefinedArnSchemaMap, refResolvers) { 3 | this.defaultArnSchemaMap = defaultArnSchemaMap; 4 | this.userDefinedArnSchemaMap = userDefinedArnSchemaMap; 5 | this.refResolvers = refResolvers; 6 | } 7 | 8 | getResolvedArn(resourceNode) { 9 | let result = undefined; 10 | const arnSchema = this.findArnSchema(resourceNode); 11 | if (arnSchema) { 12 | let arn = this.replaceStackParams(arnSchema); 13 | const placeHolderAttrName = this.getResourceNamePlaceholderAttrName(arn); 14 | const arnSchemePlaceHolderAttrPath = [placeHolderAttrName]; 15 | if (resourceNode.isPropertyDefined(arnSchemePlaceHolderAttrPath)) { 16 | const attrValue = resourceNode.getResolvedProperyValue(arnSchemePlaceHolderAttrPath); 17 | result = arn.replace("${" + placeHolderAttrName + "}", attrValue); 18 | } 19 | } 20 | return result; 21 | } 22 | 23 | replaceStackParams(arnTemplate) { 24 | let arn = arnTemplate.replace('${Account}', this.refResolvers["AWS::AccountId"]); 25 | arn = arn.replace('${Partition}', this.refResolvers["AWS::Partition"]); 26 | arn = arn.replace('${Region}', this.refResolvers["AWS::Region"]); 27 | return arn; 28 | } 29 | 30 | findArnSchema(resourceNode) { 31 | const resourceType = resourceNode.getType(); 32 | return this.userDefinedArnSchemaMap[resourceType] || this.defaultArnSchemaMap[resourceType]; 33 | } 34 | 35 | getResourceNamePlaceholderAttrName(arnSchema) { 36 | const arn = arnSchema.replace('${Account}', '').replace('${Partition}', '').replace('${Region}', ''); 37 | const str = arn.split("${")[1]; 38 | const result = str.substring(0, str.length - 1); 39 | return result; 40 | } 41 | } 42 | 43 | module.exports = ArnResolver; -------------------------------------------------------------------------------- /src/convertNode.js: -------------------------------------------------------------------------------- 1 | const { 2 | FnEqualsNode, 3 | FnFindInMapNode, 4 | FnJoinNode, 5 | FnSub, 6 | FnSplit, 7 | FnSelect, 8 | FnGetAZsNode, 9 | RefNode, 10 | ObjectNode, 11 | FnOr, 12 | FnAnd, 13 | FnNot, 14 | FnIf, 15 | ConditionNode, 16 | PropertyConditionNode, 17 | FnGetAttNode, 18 | ArrayNode, 19 | ResourcesNode, 20 | ResourceNode, 21 | ResolveFromMapNode 22 | } = require('./nodeTypes'); 23 | const _ = require('lodash') 24 | const azMapping = require('./AZMap.json'); 25 | const defaultArnSchemeMap = require('./data/CfnResourceTypeToArnSchemeMap'); 26 | const ArnResolver = require('./ArnResolver'); 27 | 28 | const convertNode = (node, nodeAccessor, srcObj, params, convRoot, enableVerboseLogging) => { 29 | const getAttResolvers = params["Fn::GetAttResolvers"] || {}; 30 | const userDefinedArnSchemas = params["ArnSchemas"] || {}; 31 | const importValueResolvers = params["Fn::ImportValueResolvers"] || {}; 32 | const refResolvers = params.refResolvers; 33 | 34 | const arnResolver = new ArnResolver(defaultArnSchemeMap, userDefinedArnSchemas, refResolvers); 35 | 36 | switch (nodeAccessor.key) { 37 | case "Resources": 38 | return new ResourcesNode(node, nodeAccessor, enableVerboseLogging, arnResolver); 39 | case "Fn::FindInMap": 40 | return new FnFindInMapNode(node, nodeAccessor, enableVerboseLogging, srcObj.Mappings); 41 | case "Fn::Join": 42 | return new FnJoinNode(node, nodeAccessor, enableVerboseLogging); 43 | case "Fn::Sub": 44 | return new FnSub(node, nodeAccessor, enableVerboseLogging, refResolvers); 45 | case "Fn::Split": 46 | return new FnSplit(node, nodeAccessor, enableVerboseLogging); 47 | case "Fn::Select": 48 | return new FnSelect(node, nodeAccessor, enableVerboseLogging); 49 | case "Fn::Equals": 50 | return new FnEqualsNode(node, nodeAccessor, enableVerboseLogging); 51 | case "Fn::And": 52 | return new FnAnd(node, nodeAccessor, enableVerboseLogging); 53 | case "Fn::Or": 54 | return new FnOr(node, nodeAccessor, enableVerboseLogging); 55 | case "Fn::Not": 56 | return new FnNot(node, nodeAccessor, enableVerboseLogging); 57 | case "Fn::If": 58 | return new FnIf(node, nodeAccessor, enableVerboseLogging, convRoot.wrappedObject.Conditions); 59 | case "Fn::GetAZs": 60 | return new FnGetAZsNode(node, nodeAccessor, enableVerboseLogging, azMapping, refResolvers["AWS::Region"]); 61 | case "Condition": 62 | if (nodeAccessor.path.length >= 3 && nodeAccessor.path[2] == "Properties") { 63 | return new PropertyConditionNode(node, nodeAccessor, enableVerboseLogging); 64 | } 65 | return new ConditionNode(node, nodeAccessor, enableVerboseLogging, convRoot.wrappedObject.Conditions); 66 | case "Ref": 67 | return new RefNode(node, nodeAccessor, enableVerboseLogging, refResolvers); 68 | case "Fn::ImportValue": 69 | return new ResolveFromMapNode(node, nodeAccessor, enableVerboseLogging, importValueResolvers); 70 | case "Fn::GetAtt": 71 | return new FnGetAttNode(node, nodeAccessor, enableVerboseLogging, getAttResolvers, convRoot); 72 | } 73 | 74 | if (_.isArray(nodeAccessor.node)) { 75 | return new ArrayNode(node, nodeAccessor, enableVerboseLogging); 76 | } 77 | // if it is direct child of "Resources" node, we create a ResourceNode 78 | if (nodeAccessor.parent && nodeAccessor.parent.key === "Resources") { 79 | return new ResourceNode(node, nodeAccessor, enableVerboseLogging); 80 | } 81 | return new ObjectNode(node, nodeAccessor, enableVerboseLogging); 82 | }; 83 | 84 | exports.convertNode = convertNode; -------------------------------------------------------------------------------- /src/data/CfnResourceTypeToArnSchemeMap.js: -------------------------------------------------------------------------------- 1 | // Search ARNs from: https://github.com/aws-cloudformation/cfn-python-lint/blob/master/src/cfnlint/data/AdditionalSpecs/Policies.json 2 | // AWS CloudFormation doc: https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#genref-aws-service-namesspaces 3 | 4 | const defaultArnSchemeMap = { 5 | "AWS::Lambda::Function": "arn:${Partition}:lambda:${Region}:${Account}:function:${FunctionName}", 6 | "AWS::SNS::Topic": "arn:${Partition}:sns:${Region}:${Account}:${TopicName}", 7 | "AWS::SQS::Queue": "arn:${Partition}:sqs:${Region}:${Account}:${QueueName}", 8 | "AWS::CloudWatch::Alarm": "arn:${Partition}:cloudwatch:${Region}:${Account}:alarm:${AlarmName}", 9 | "AWS::EC2::Subnet": "arn:${Partition}:ec2:${Region}:${Account}:subnet/${SubnetId}", 10 | "AWS::EC2::VPC": "arn:${Partition}:ec2:${Region}:${Account}:vpc/${VpcId}", 11 | "AWS::S3::Bucket": "arn:${Partition}:s3:::${BucketName}", 12 | "AWS::EC2::SecurityGroup": "arn:${Partition}:ec2:${Region}:${Account}:security-group/${SecurityGroupId}", 13 | "AWS::DynamoDB::Table": "arn:${Partition}:dynamodb:${Region}:${Account}:table/${TableName}" 14 | // "AWS::SNS::Subscription": "", // not found maybe because it has a generated guid suffix 15 | // "AWS::Events::Rule": "", 16 | // "AWS::IAM::Role": "", 17 | // "AWS::IAM::Policy": "", 18 | // "AWS::KMS::Key": "", 19 | // "AWS::IAM::ManagedPolicy": "", 20 | }; 21 | 22 | module.exports = defaultArnSchemeMap; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const NodeEvaluator = require("./nodeEvaluator"); 2 | 3 | module.exports = NodeEvaluator; -------------------------------------------------------------------------------- /src/nodeEvaluator.js: -------------------------------------------------------------------------------- 1 | const traverse = require('traverse'); 2 | const { convertNode } = require("./convertNode"); 3 | const { getFieldValueAtWrappedPath } = require('./wrappingHelpers'); 4 | const { getParameterDefaults } = require('./parameterHelper'); 5 | 6 | class NodeEvaluator { 7 | constructor(srcObj, params = {}, enableVerboseLogging = false) { 8 | this.srcObj = srcObj; 9 | this.params = params || {}; 10 | this.enableVerboseLogging = enableVerboseLogging; 11 | } 12 | 13 | evaluateNodes(overrideParams = {}) { 14 | let convRoot = {}; 15 | 16 | const theseParams = Object.assign({}, this.params, overrideParams); 17 | const defaultRefParams = getParameterDefaults(this.srcObj.Parameters); 18 | // keeping backward compatibility because of typo in previos versions 19 | theseParams.refResolvers = Object.assign({}, defaultRefParams, theseParams.RefResolevers, theseParams.RefResolveres, theseParams.RefResolvers); 20 | 21 | const self = this; 22 | traverse(this.srcObj).forEach(function (x) { 23 | const convNode = convertNode(x, this, self.srcObj, theseParams, convRoot, self.enableVerboseLogging); 24 | if (this.isRoot) { 25 | convRoot = convNode; 26 | } 27 | 28 | if (this.parent) { 29 | let convParent; 30 | if (this.parent.isRoot) { 31 | convParent = convRoot; 32 | } else { 33 | convParent = getFieldValueAtWrappedPath(convRoot, this.parent.path); 34 | } 35 | convParent.addChild(this.key, convNode); 36 | } 37 | }); 38 | 39 | let evaluatedObj = {}; 40 | 41 | convRoot.directDependencies.forEach((depNode) => { 42 | evaluatedObj[depNode.nodeAccessor.key] = depNode.evaluate(); 43 | }); 44 | 45 | return evaluatedObj; 46 | } 47 | 48 | // The only reason for this API is to keep backward compatibility because of a typo in previous versions 49 | evaulateNodes() { 50 | return this.evaluateNodes(); 51 | } 52 | } 53 | 54 | module.exports = NodeEvaluator; -------------------------------------------------------------------------------- /src/nodeTypes/ArrayNode.js: -------------------------------------------------------------------------------- 1 | const Node = require('./Node'); 2 | 3 | class ArrayNode extends Node { 4 | constructor(node, nodeAccessor, enableVerboseLogging){ 5 | super(node, nodeAccessor, enableVerboseLogging) 6 | this.directDependencies = []; 7 | } 8 | 9 | shouldReplaceParent(){ 10 | return true; 11 | } 12 | 13 | addChild(key, child){ 14 | this.directDependencies[key] = child; 15 | } 16 | 17 | get wrappedObject() { 18 | return this.directDependencies; 19 | } 20 | 21 | evaluateResultedArray(array) { 22 | // simple array 23 | return array; 24 | } 25 | 26 | evaluate() { 27 | super.log("Eval: ", this.nodeAccessor.path.join('/')); 28 | if(this.isLeaf){ 29 | super.log("Leaf: ", this.node, this.nodeAccessor.path.join('/')); 30 | } 31 | 32 | const result = [] 33 | this.directDependencies.forEach( (dep) => { 34 | const depRes = dep.evaluate(); 35 | result[dep.nodeAccessor.key] = depRes; 36 | }); 37 | 38 | super.log("Array evaluated: "); 39 | super.log(result); 40 | 41 | return this.evaluateResultedArray(result); 42 | } 43 | } 44 | 45 | module.exports = ArrayNode; -------------------------------------------------------------------------------- /src/nodeTypes/ConditionNode.js: -------------------------------------------------------------------------------- 1 | const ObjectNode = require('./ObjectNode'); 2 | 3 | class ConditionNode extends ObjectNode { 4 | constructor(node, nodeAccessor, enableVerboseLogging, convConditions) { 5 | super(node, nodeAccessor, enableVerboseLogging); 6 | this.convConditions = convConditions; 7 | } 8 | 9 | shouldReplaceParent(){ 10 | return true; 11 | } 12 | 13 | evaluate(){ 14 | let result = this.node; 15 | if(this.convConditions.wrappedObject.hasOwnProperty(this.node)){ 16 | result = this.convConditions.wrappedObject[this.node].evaluate(); 17 | } 18 | // might not needed 19 | else if(this.hasSingleDependency){ 20 | result = this.directDependencies[0].evaluate(); 21 | } 22 | 23 | super.log("Evalulated ConditionNode: " + this.node); 24 | super.log(result); 25 | return result; 26 | } 27 | } 28 | 29 | module.exports = ConditionNode; -------------------------------------------------------------------------------- /src/nodeTypes/FnAnd.js: -------------------------------------------------------------------------------- 1 | const ArrayNode = require('./ArrayNode'); 2 | const _ = require('lodash'); 3 | 4 | class FnAnd extends ArrayNode { 5 | constructor(node, nodeAccessor, enableVerboseLogging) { 6 | super(node, nodeAccessor, enableVerboseLogging); 7 | } 8 | 9 | evaluateResultedArray(array) { 10 | let result = true; 11 | array.forEach((boolVal) => { 12 | if(!_.isBoolean(boolVal)) { 13 | throw "Array should only contain booleans"; 14 | } 15 | result = result && boolVal; 16 | }); 17 | return result; 18 | } 19 | } 20 | 21 | module.exports = FnAnd; -------------------------------------------------------------------------------- /src/nodeTypes/FnEqualsNode.js: -------------------------------------------------------------------------------- 1 | 2 | const ArrayNode = require('./ArrayNode'); 3 | 4 | class FnEqualsNode extends ArrayNode { 5 | constructor(node, nodeAccessor, enableVerboseLogging) { 6 | super(node, nodeAccessor, enableVerboseLogging); 7 | } 8 | 9 | evaluateResultedArray(array) { 10 | const val1 = array[0]; 11 | const val2 = array[1]; 12 | 13 | // in theory at this point we only have primitive values (strings, numbers, booleans) 14 | // so simple === comparesion should sufficient 15 | // but we can make it even better to deep equal by comparing the serialized JSON strings 16 | return (JSON.stringify(val1) === JSON.stringify(val2)); 17 | } 18 | } 19 | 20 | module.exports = FnEqualsNode; -------------------------------------------------------------------------------- /src/nodeTypes/FnFindInMapNode.js: -------------------------------------------------------------------------------- 1 | const ArrayNode = require('./ArrayNode'); 2 | class FnFindInMapNode extends ArrayNode { 3 | constructor(node, nodeAccessor, enableVerboseLogging, mappings) { 4 | super(node, nodeAccessor, enableVerboseLogging); 5 | this.mappings = mappings; 6 | } 7 | 8 | evaluate() { 9 | let result = this.node; 10 | const mappingName = this.directDependencies[0].evaluate(); 11 | const level1Key = this.directDependencies[1].evaluate(); 12 | const level2Key = this.directDependencies[2].evaluate(); 13 | if (!this.mappings.hasOwnProperty(mappingName)) { 14 | console.warn("Not found mapping: " + mappingName); 15 | } 16 | else if (!this.mappings[mappingName].hasOwnProperty(level1Key)) { 17 | console.warn("Not foud direct child " + level1Key + " in map: " + mappingName); 18 | } 19 | if (!this.mappings[mappingName][level1Key].hasOwnProperty(level2Key)) { 20 | console.warn("Not foud second level map key " + level2Key + " in mapping: " + mappingName + "." + level1Key); 21 | } 22 | else { 23 | result = this.mappings[mappingName][level1Key][level2Key]; 24 | } 25 | 26 | super.log("FnFindInMapNode evaluated: "); 27 | super.log(result); 28 | return result; 29 | } 30 | } 31 | 32 | module.exports = FnFindInMapNode; 33 | -------------------------------------------------------------------------------- /src/nodeTypes/FnGetAZsNode.js: -------------------------------------------------------------------------------- 1 | const ObjectNode = require('./ObjectNode'); 2 | 3 | class FnGetAZsNode extends ObjectNode { 4 | constructor(node, nodeAccessor, enableVerboseLogging, azMapping, currentRegion) { 5 | super(node, nodeAccessor, enableVerboseLogging); 6 | this.azMapping = azMapping; 7 | this.currentRegion = currentRegion; 8 | } 9 | 10 | shouldReplaceParent() { 11 | return true; 12 | } 13 | 14 | evaluate() { 15 | let result = this.node; 16 | let regionName = this.currentRegion; 17 | if (!this.isLeaf) { 18 | regionName = this.directDependencies[0].evaluate(); 19 | } 20 | result = this.azMapping[regionName]; 21 | super.log("FnGetAZsNode evaluated: "); 22 | super.log(result); 23 | return result; 24 | } 25 | } 26 | 27 | module.exports = FnGetAZsNode; -------------------------------------------------------------------------------- /src/nodeTypes/FnGetAttNode.js: -------------------------------------------------------------------------------- 1 | const ArrayNode = require('./ArrayNode'); 2 | 3 | class FnGetAttNode extends ArrayNode { 4 | constructor(node, nodeAccessor, enableVerboseLogging, getAttrResolvers, convRoot) { 5 | super(node, nodeAccessor, enableVerboseLogging); 6 | this.getAttrResolvers = getAttrResolvers; 7 | this.convRoot = convRoot; 8 | 9 | this.convertedResources = convRoot.wrappedObject.Resources; 10 | } 11 | 12 | evaluate() { 13 | let result = this.node; 14 | const resourceLogicalId = this.directDependencies[0].evaluate(); 15 | const attrPath = this.directDependencies[1].evaluate(); 16 | 17 | const resource = this.convertedResources ? this.convertedResources.findWrappedResource(resourceLogicalId): undefined; 18 | 19 | // Handle ARN resolutions if resource is present in the template 20 | if (attrPath === "Arn" && resource) { 21 | const resolvedArn = this.convertedResources.getResolvedArn(resourceLogicalId); 22 | result = resolvedArn || result; 23 | } // Try to resolve the attribute value from the current template (e.g. MySqs.QueuName) 24 | else if (resource && resource.isPropertyDefinedOnObjectPath(attrPath)) { 25 | result = resource.getResolvedProperyValueOnObjectPath(attrPath); 26 | } 27 | 28 | // Override values from the provided Fn::GetAttResolvers 29 | if (!this.getAttrResolvers[resourceLogicalId]) { 30 | console.warn("Fn::GetAttResolvers not found in params file: " + resourceLogicalId); 31 | } 32 | else if (!this.getAttrResolvers[resourceLogicalId][attrPath]) { 33 | console.warn("Fn::GetAttResolvers not found in params file: " + resourceLogicalId + "." + attrPath); 34 | } else { 35 | result = this.getAttrResolvers[resourceLogicalId][attrPath]; 36 | } 37 | 38 | super.log("FnGetAttNode evaluated: "); 39 | super.log(result); 40 | return result; 41 | } 42 | } 43 | 44 | module.exports = FnGetAttNode; -------------------------------------------------------------------------------- /src/nodeTypes/FnIf.js: -------------------------------------------------------------------------------- 1 | const ArrayNode = require('./ArrayNode'); 2 | 3 | class FnIf extends ArrayNode { 4 | constructor(node, nodeAccessor, enableVerboseLogging, convConditions) { 5 | super(node, nodeAccessor, enableVerboseLogging); 6 | this.convConditions = convConditions; 7 | } 8 | 9 | evaluate() { 10 | let result = this.node; 11 | if (this.directDependencies.length >= 3) { 12 | const conditionName = this.directDependencies[0].evaluate(); 13 | const evaluatedCondition = this.convConditions.wrappedObject[conditionName].evaluate(); 14 | const valueIfTrue = this.directDependencies[1].evaluate(); 15 | const valueIfFalse = this.directDependencies[2].evaluate(); 16 | result = evaluatedCondition ? valueIfTrue : valueIfFalse; 17 | } 18 | 19 | super.log("FnIf evaluated: "); 20 | super.log(result); 21 | return result; 22 | } 23 | } 24 | 25 | module.exports = FnIf; -------------------------------------------------------------------------------- /src/nodeTypes/FnJoinNode.js: -------------------------------------------------------------------------------- 1 | const ArrayNode = require('./ArrayNode'); 2 | 3 | class FnJoinNode extends ArrayNode { 4 | constructor(node, nodeAccessor, enableVerboseLogging) { 5 | super(node, nodeAccessor, enableVerboseLogging); 6 | } 7 | 8 | evaluate() { 9 | let result = this.node; 10 | if (this.directDependencies.length >= 2) { 11 | const separator = this.directDependencies[0].evaluate(); 12 | const items = this.directDependencies[1].evaluate(); 13 | result = items.join(separator); 14 | } 15 | 16 | super.log("FnJoinNode evailated: "); 17 | super.log(result); 18 | return result; 19 | } 20 | } 21 | 22 | module.exports = FnJoinNode; 23 | -------------------------------------------------------------------------------- /src/nodeTypes/FnNot.js: -------------------------------------------------------------------------------- 1 | const ArrayNode = require('./ArrayNode'); 2 | const _ = require('lodash'); 3 | 4 | class FnNot extends ArrayNode { 5 | constructor(node, nodeAccessor, enableVerboseLogging) { 6 | super(node, nodeAccessor, enableVerboseLogging); 7 | } 8 | 9 | evaluate(){ 10 | let result = this.node; // by default 11 | if(this.hasSingleDependency){ 12 | result = !this.directDependencies[0].evaluate(); 13 | } 14 | super.log("Evaluate FnNot: "); 15 | super.log(result); 16 | return result; 17 | } 18 | } 19 | 20 | module.exports = FnNot; -------------------------------------------------------------------------------- /src/nodeTypes/FnOr.js: -------------------------------------------------------------------------------- 1 | const ArrayNode = require('./ArrayNode'); 2 | const _ = require('lodash'); 3 | 4 | class FnOr extends ArrayNode { 5 | constructor(node, nodeAccessor, enableVerboseLogging) { 6 | super(node, nodeAccessor, enableVerboseLogging); 7 | } 8 | 9 | evaluateResultedArray(array) { 10 | let result = false; 11 | array.forEach((boolVal) => { 12 | if(!_.isBoolean(boolVal)) { 13 | throw "Array should only contain booleans"; 14 | } 15 | result = result || boolVal; 16 | }); 17 | return result; 18 | } 19 | } 20 | 21 | module.exports = FnOr; -------------------------------------------------------------------------------- /src/nodeTypes/FnSelect.js: -------------------------------------------------------------------------------- 1 | const ArrayNode = require('./ArrayNode'); 2 | const _ = require('lodash'); 3 | 4 | class FnSelect extends ArrayNode { 5 | constructor(node, nodeAccessor, enableVerboseLogging) { 6 | super(node, nodeAccessor, enableVerboseLogging); 7 | } 8 | 9 | evaluate() { 10 | let result = this.node; 11 | if(this.directDependencies.length == 2){ 12 | let index = this.directDependencies[0].evaluate(); 13 | const listOfObjects = this.directDependencies[1].evaluate(); 14 | if(_.isArray(listOfObjects) && index < listOfObjects.length){ 15 | result = listOfObjects[index]; 16 | } else{ 17 | const error = `Index ${index} is out of bound.`; 18 | console.warn(error); 19 | throw error; 20 | } 21 | } 22 | 23 | super.log("FnSelect evaluated: "); 24 | super.log(result); 25 | return result; 26 | } 27 | } 28 | 29 | module.exports = FnSelect; -------------------------------------------------------------------------------- /src/nodeTypes/FnSplit.js: -------------------------------------------------------------------------------- 1 | const ArrayNode = require('./ArrayNode'); 2 | const _ = require('lodash'); 3 | 4 | class FnSplit extends ArrayNode { 5 | constructor(node, nodeAccessor, enableVerboseLogging) { 6 | super(node, nodeAccessor, enableVerboseLogging); 7 | } 8 | 9 | evaluate() { 10 | let result = this.node; 11 | if(this.directDependencies.length == 2){ 12 | const delimiter = this.directDependencies[0].evaluate(); 13 | const strToSplit = this.directDependencies[1].evaluate(); 14 | if( ("" + delimiter).length < 1) { 15 | console.warn("delimiter is empty"); 16 | throw "Delimiter is invalid: empty string"; 17 | } 18 | result = strToSplit.split(delimiter); 19 | } 20 | 21 | super.log("FnSplit evaluated: "); 22 | super.log(result); 23 | return result; 24 | } 25 | } 26 | 27 | module.exports = FnSplit; -------------------------------------------------------------------------------- /src/nodeTypes/FnSub.js: -------------------------------------------------------------------------------- 1 | const ArrayNode = require('./ArrayNode'); 2 | 3 | // TODO: Add support for 4 | // - template parameter names 5 | // - resource logical IDs 6 | // - resource attributes 7 | // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html 8 | class FnSub extends ArrayNode { 9 | constructor(node, nodeAccessor, enableVerboseLogging, resolverMap) { 10 | super(node, nodeAccessor, enableVerboseLogging); 11 | this.resolverMap = resolverMap; 12 | } 13 | 14 | evaluate() { 15 | let result = this.node; 16 | let templateStr; 17 | let dictionary = {}; 18 | if(this.isLeaf) { 19 | templateStr = this.node; 20 | dictionary = this.resolverMap; 21 | } else if (this.directDependencies.length == 2) { 22 | templateStr = this.directDependencies[0].evaluate(); 23 | dictionary = this.directDependencies[1].evaluate(); 24 | } 25 | if(templateStr) { 26 | result = templateStr.replace(/\$\{([^}]+)\}/g, function(fullMatch, groupMatch) { 27 | if (dictionary.hasOwnProperty(groupMatch)) { 28 | return dictionary[groupMatch]; 29 | } 30 | return fullMatch; 31 | }); 32 | } 33 | 34 | super.log("FnSub evaluated: "); 35 | super.log(result); 36 | return result; 37 | } 38 | } 39 | 40 | module.exports = FnSub; -------------------------------------------------------------------------------- /src/nodeTypes/Node.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | class Node { 4 | constructor(node, nodeAccessor, enableVerboseLogging) { 5 | this.node = node; 6 | this.nodeAccessor = nodeAccessor; 7 | this.directDependencies = []; 8 | this.enableVerboseLogging = enableVerboseLogging; 9 | } 10 | 11 | log(...str) { 12 | if (this.enableVerboseLogging) { 13 | console.log(...str); 14 | } 15 | } 16 | 17 | get wrappedObject() { 18 | throw 'Method needs to be implemented by child class' 19 | } 20 | 21 | shouldReplaceParent() { 22 | return false; 23 | } 24 | 25 | get isLeaf() { 26 | return this.directDependencies.length === 0; 27 | } 28 | 29 | get hasSingleDependency() { 30 | return this.directDependencies.length === 1; 31 | } 32 | 33 | evaluate() { 34 | throw "Should not happen, child class implementations should handle this case"; 35 | } 36 | 37 | /* Find an ancestor wrapped node with object path (e.g. ["FooSrvQueue" , "Properties", "QueueName") */ 38 | findWrappedAncestorByPathArray(pathArray) { 39 | if (!this.hasAncestorOnPath(pathArray)) { 40 | throw `ancestor with ${pathArray} not found`; 41 | } 42 | 43 | let result = this; 44 | for (let i = 0; i < pathArray.length; i++) { 45 | result = result.wrappedObject[pathArray[i]]; 46 | } 47 | return result; 48 | } 49 | 50 | /* Find an ancestor wrapped node with object path (e.g. FooSrvQueue.Properties.QueueName)*/ 51 | findWrappedAncestorByObjectPath(objectPathString) { 52 | this.findWrappedAncestorByPathArray(getPathArrayFromObjectPath(objectPathString)); 53 | } 54 | 55 | hasAncestorOnPath(pathArray) { 56 | if (!_.isArray(pathArray)) { 57 | throw "pathArray should be an array"; 58 | } 59 | let currentNode = this; 60 | for (let i = 0; i < pathArray.length; i++) { 61 | const pathItem = pathArray[i]; 62 | if (!currentNode.wrappedObject.hasOwnProperty(pathItem)) { 63 | return false; 64 | } 65 | currentNode = currentNode.wrappedObject[pathItem]; 66 | } 67 | return true; 68 | } 69 | } 70 | 71 | module.exports = Node; -------------------------------------------------------------------------------- /src/nodeTypes/ObjectNode.js: -------------------------------------------------------------------------------- 1 | const Node = require('./Node'); 2 | 3 | class ObjectNode extends Node { 4 | constructor(node, nodeAccessor, enableVerboseLogging){ 5 | super(node, nodeAccessor, enableVerboseLogging); 6 | this.directDependencies = []; 7 | this.wrappedObj = undefined; 8 | } 9 | 10 | addChild(key, child){ 11 | if(!this.wrappedObj){ 12 | this.wrappedObj = {}; 13 | } 14 | 15 | this.wrappedObj[key] = child; 16 | this.directDependencies.push(child); 17 | } 18 | 19 | get wrappedObject() { 20 | return this.wrappedObj; 21 | } 22 | 23 | evaluate(){ 24 | super.log("Eval: ", this.nodeAccessor.path.join('/')); 25 | let result = this.node; // by default or simple value (number, string) 26 | 27 | if(this.isLeaf){ 28 | super.log("Leaf: ", this.node, this.nodeAccessor.path.join('/')); 29 | } 30 | else if(this.hasSingleDependency && this.directDependencies[0].shouldReplaceParent()){ 31 | result = this.directDependencies[0].evaluate(); 32 | } 33 | else { 34 | result = {}; 35 | this.directDependencies.forEach( (dep) => { 36 | result[dep.nodeAccessor.key] = dep.evaluate(); 37 | }); 38 | } 39 | 40 | super.log("ObjectNode evaluated: "); 41 | super.log(result); 42 | return result; 43 | } 44 | } 45 | 46 | module.exports = ObjectNode; -------------------------------------------------------------------------------- /src/nodeTypes/PropertyConditionNode.js: -------------------------------------------------------------------------------- 1 | const ObjectNode = require('./ObjectNode'); 2 | 3 | class PropertyConditionNode extends ObjectNode { 4 | constructor(node, nodeAccessor, enableVerboseLogging) { 5 | super(node, nodeAccessor, enableVerboseLogging); 6 | } 7 | 8 | shouldReplaceParent(){ 9 | return false; 10 | } 11 | 12 | evaluate(){ 13 | let result = this.node; 14 | if(this.hasSingleDependency) { 15 | result = {}; 16 | result[this.directDependencies[0].nodeAccessor.key] = this.directDependencies[0].evaluate(); 17 | } 18 | 19 | super.log("Evalulated PropertyConditionNode: " + this.node); 20 | super.log(result); 21 | return result; 22 | } 23 | } 24 | 25 | module.exports = PropertyConditionNode; -------------------------------------------------------------------------------- /src/nodeTypes/RefNode.js: -------------------------------------------------------------------------------- 1 | const ResolveFromMapNode = require('./ResolveFromMapNode'); 2 | 3 | class RefNode extends ResolveFromMapNode { 4 | constructor(node, nodeAccessor, enableVerboseLogging, resolverMap) { 5 | super(node, nodeAccessor, enableVerboseLogging, resolverMap); 6 | } 7 | } 8 | 9 | module.exports = RefNode; -------------------------------------------------------------------------------- /src/nodeTypes/ResolveFromMapNode.js: -------------------------------------------------------------------------------- 1 | const ObjectNode = require('./ObjectNode'); 2 | 3 | // Generic node class that can resolve values from an input map. 4 | // Used for Ref and Fn::ImportValue resolution. 5 | class ResolveFromMapNode extends ObjectNode { 6 | constructor(node, nodeAccessor, enableVerboseLogging, resolverMap) { 7 | super(node, nodeAccessor, enableVerboseLogging); 8 | this.resolverMap = resolverMap; 9 | } 10 | 11 | shouldReplaceParent(){ 12 | return true; 13 | } 14 | 15 | canResolve(str) { 16 | return Object.keys(this.resolverMap).includes(str); 17 | } 18 | 19 | resolveFromMap(str){ 20 | if(this.canResolve(str)){ 21 | return this.resolverMap[str]; 22 | } 23 | return str; 24 | } 25 | 26 | evaluate() { 27 | let result = this.node; // by default 28 | if (this.canResolve(this.node)) { 29 | result = this.resolveFromMap(this.node); 30 | } 31 | 32 | super.log("ResolveFromMapNode evaluated: "); 33 | super.log(result); 34 | return result; 35 | } 36 | } 37 | 38 | module.exports = ResolveFromMapNode; -------------------------------------------------------------------------------- /src/nodeTypes/ResourceNode.js: -------------------------------------------------------------------------------- 1 | const ObjectNode = require('./ObjectNode'); 2 | const { getPathArrayFromObjectPath } = require('../wrappingHelpers'); 3 | 4 | class ResourceNode extends ObjectNode { 5 | constructor(node, nodeAccessor, enableVerboseLogging) { 6 | super(node, nodeAccessor, enableVerboseLogging); 7 | } 8 | 9 | getType() { 10 | return this.findWrappedAncestorByPathArray(["Type"]).evaluate(); 11 | } 12 | 13 | isPropertyDefined(pathArray) { 14 | return this.hasAncestorOnPath(["Properties", ...pathArray]); 15 | } 16 | 17 | isPropertyDefinedOnObjectPath(objectPathStr) { 18 | return this.isPropertyDefined([...getPathArrayFromObjectPath(objectPathStr)]); 19 | } 20 | 21 | getProperyNode(pathArray) { 22 | return this.findWrappedAncestorByPathArray(["Properties", ...pathArray]); 23 | } 24 | 25 | getResolvedProperyValueOnObjectPath(objectPathStr) { 26 | return this.getResolvedProperyValue([...getPathArrayFromObjectPath(objectPathStr)]); 27 | } 28 | 29 | getResolvedProperyValue(pathArray) { 30 | const attrNode = this.getProperyNode(pathArray); 31 | const evaluatedAttrValue = attrNode.evaluate(); 32 | return evaluatedAttrValue; 33 | } 34 | } 35 | 36 | module.exports = ResourceNode; -------------------------------------------------------------------------------- /src/nodeTypes/ResourcesNode.js: -------------------------------------------------------------------------------- 1 | const ObjectNode = require('./ObjectNode'); 2 | 3 | class ResourcesNode extends ObjectNode { 4 | constructor(node, nodeAccessor, enableVerboseLogging, arnResolver) { 5 | super(node, nodeAccessor, enableVerboseLogging); 6 | this.arnResolver = arnResolver; 7 | } 8 | 9 | findWrappedResource(resourceLogicalId) { 10 | return this.wrappedObject ? this.wrappedObject[resourceLogicalId] : undefined; 11 | } 12 | 13 | getResolvedArn(resourceLogicalId) { 14 | const resourceNode = this.findWrappedAncestorByPathArray([resourceLogicalId]); 15 | return this.arnResolver.getResolvedArn(resourceNode); 16 | } 17 | } 18 | 19 | module.exports = ResourcesNode; -------------------------------------------------------------------------------- /src/nodeTypes/index.js: -------------------------------------------------------------------------------- 1 | module.exports.FnEqualsNode = require('./FnEqualsNode'); 2 | module.exports.FnFindInMapNode = require("./FnFindInMapNode"); 3 | module.exports.FnJoinNode = require("./FnJoinNode"); 4 | module.exports.FnSub = require("./FnSub"); 5 | module.exports.FnSelect = require("./FnSelect"); 6 | module.exports.FnSplit = require("./FnSplit"); 7 | module.exports.RefNode = require("./RefNode"); 8 | module.exports.ObjectNode = require("./ObjectNode"); 9 | module.exports.FnOr = require("./FnOr"); 10 | module.exports.FnAnd = require("./FnAnd"); 11 | module.exports.FnNot = require("./FnNot"); 12 | module.exports.FnIf = require("./FnIf"); 13 | module.exports.FnGetAZsNode = require("./FnGetAZsNode"); 14 | module.exports.ConditionNode = require("./ConditionNode"); 15 | module.exports.PropertyConditionNode = require("./PropertyConditionNode"); 16 | module.exports.FnGetAttNode = require("./FnGetAttNode"); 17 | module.exports.ArrayNode = require("./ArrayNode"); 18 | module.exports.ResourcesNode = require("./ResourcesNode"); 19 | module.exports.ResourceNode = require("./ResourceNode"); 20 | module.exports.ResolveFromMapNode = require('./ResolveFromMapNode'); -------------------------------------------------------------------------------- /src/parameterHelper.js: -------------------------------------------------------------------------------- 1 | const DEFAULT = 'Default'; 2 | 3 | // Returns parameters with default values specified in cloudformation template 4 | function getParameterDefaults(parameters) { 5 | const results = {}; 6 | for(const [paramName, paramDefinition] of Object.entries(parameters)) { 7 | if(paramDefinition.hasOwnProperty(DEFAULT)) { 8 | results[paramName] = paramDefinition[DEFAULT]; 9 | } 10 | } 11 | return results; 12 | } 13 | 14 | module.exports = { 15 | getParameterDefaults 16 | } -------------------------------------------------------------------------------- /src/wrappingHelpers.js: -------------------------------------------------------------------------------- 1 | const getFieldValueAtWrappedPath = (node, path) => { 2 | let result = node; 3 | for (let i = 0; i < path.length; i++) { 4 | result = result.wrappedObject[path[i]]; 5 | } 6 | return result; 7 | }; 8 | 9 | const getPathArrayFromObjectPath = (objectPath) => { 10 | return objectPath.split('.'); 11 | } 12 | 13 | module.exports = { 14 | getFieldValueAtWrappedPath, 15 | getPathArrayFromObjectPath 16 | }; -------------------------------------------------------------------------------- /test/ArnResolver.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | const ArnResolver = require('../src/ArnResolver'); 4 | const { getMockNode } = require('./testUtils'); 5 | 6 | const testRefResolvers = { 7 | "AWS::AccountId": "123123123123", 8 | "AWS::Partition": "aws", 9 | "AWS::Region": "us-east-2" 10 | }; 11 | const TEST_QUEUE_NAME = "testSqs-beta"; 12 | const mockQueueNameNode = getMockNode("QueueName", TEST_QUEUE_NAME); 13 | const convertedQueue = { 14 | wrappedObject: { 15 | Properties: { 16 | wrappedObject: { 17 | QueueName: mockQueueNameNode 18 | } 19 | }, 20 | Type: "AWS::SQS::Queue", 21 | }, 22 | isPropertyDefinedOnObjectPath: () => true, 23 | getResolvedProperyValueOnObjectPath: () => TEST_QUEUE_NAME, 24 | getType: () => "AWS::SQS::Queue", 25 | isPropertyDefined: () => true, 26 | getResolvedProperyValue: () => TEST_QUEUE_NAME 27 | }; 28 | 29 | const TEST_BUCKET_NAME = "test-bucket-beta"; 30 | const mockBucketNameNode = getMockNode("BucketName", TEST_BUCKET_NAME); 31 | const convertedBucket= { 32 | wrappedObject: { 33 | Properties: { 34 | wrappedObject: { 35 | BucketName: mockBucketNameNode 36 | } 37 | }, 38 | Type: "AWS::S3::Bucket", 39 | }, 40 | isPropertyDefinedOnObjectPath: () => true, 41 | getResolvedProperyValueOnObjectPath: () => TEST_BUCKET_NAME, 42 | getType: () => "AWS::S3::Bucket", 43 | isPropertyDefined: () => true, 44 | getResolvedProperyValue: () => TEST_BUCKET_NAME 45 | }; 46 | 47 | describe('ArnResolver', () => { 48 | 49 | const target = new ArnResolver({ 50 | "AWS::S3::Bucket": "arn:${Partition}:s3:::${BucketName}" 51 | }, { 52 | "AWS::SQS::Queue": "arn:${Partition}:sqs:${Region}:${Account}:${QueueName}" 53 | }, testRefResolvers); 54 | 55 | it('resolves build-in arn schema', () => { 56 | const actual = target.getResolvedArn(convertedQueue); 57 | expect(actual).to.be.deep.equal("arn:aws:sqs:us-east-2:123123123123:testSqs-beta"); 58 | }); 59 | 60 | it('resolves user-defined arn schema', () => { 61 | const actual = target.getResolvedArn(convertedBucket); 62 | expect(actual).to.be.deep.equal("arn:aws:s3:::test-bucket-beta"); 63 | }); 64 | 65 | it('user-defined schama can override built-in schema', () => { 66 | const target = new ArnResolver({ 67 | "AWS::SQS::Queue": "incorrectBuiltInArnSchema" 68 | }, { 69 | "AWS::SQS::Queue": "arn:${Partition}:sqs:${Region}:${Account}:${QueueName}" 70 | }, testRefResolvers); 71 | 72 | const actual = target.getResolvedArn(convertedQueue); 73 | expect(actual).to.be.deep.equal("arn:aws:sqs:us-east-2:123123123123:testSqs-beta"); 74 | }); 75 | }); -------------------------------------------------------------------------------- /test/nodeEvaluator.functional.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | const NodeEvaluator = require('../src/index'); 4 | 5 | const enableLogging = false 6 | 7 | 8 | const getExpectedObject = (templateStackName, paramsQaulifier) => { 9 | const expectedContent = require(`./testData/${templateStackName}/expected/expected-${paramsQaulifier}.json`); 10 | return expectedContent; 11 | } 12 | 13 | const getNewTargetInstance = (templateStackName, paramsQaulifier) => { 14 | const templatePath = `./testData/${templateStackName}/template.json`; 15 | const srcObject = require(templatePath); 16 | const paramsPath = `./testData/${templateStackName}/params/params-${paramsQaulifier}.json`; 17 | const paramsObject = require(paramsPath); 18 | return new NodeEvaluator(srcObject, paramsObject, enableLogging); 19 | } 20 | 21 | describe('NodeEvaluator', () => { 22 | it('evaluate stack1 example in us-east-1 Prod', () => { 23 | const methodParams = ["stack1", "us-east-1-prod"]; 24 | const target = getNewTargetInstance(...methodParams); 25 | const actual = target.evaluateNodes(); 26 | const expected = getExpectedObject(...methodParams) 27 | expect(actual).to.be.deep.equal(expected); 28 | }); 29 | 30 | it('evaluate stack1 example in us-east-1 Prod testing backward compatibility method "evaluateNodes()"', () => { 31 | const methodParams = ["stack1", "us-east-1-prod"]; 32 | const target = getNewTargetInstance(...methodParams); 33 | const actual = target.evaulateNodes(); 34 | const expected = getExpectedObject(...methodParams) 35 | expect(actual).to.be.deep.equal(expected); 36 | }); 37 | 38 | it('evaluate stack1 example in us-east-1 Beta', () => { 39 | const methodParams = ["stack1", "us-east-1-beta"]; 40 | const target = getNewTargetInstance(...methodParams); 41 | const actual = target.evaluateNodes(); 42 | const expected = getExpectedObject(...methodParams) 43 | expect(actual).to.be.deep.equal(expected); 44 | }); 45 | 46 | it('evaluate stack1 example in us-west-2 Prod', () => { 47 | const methodParams = ["stack1", "us-west-2-prod"]; 48 | const target = getNewTargetInstance(...methodParams); 49 | const actual = target.evaluateNodes(); 50 | const expected = getExpectedObject(...methodParams) 51 | expect(actual).to.be.deep.equal(expected); 52 | }); 53 | 54 | it('evaluate stack1 example in us-east-1 Prod using override params', () => { 55 | const [templateStackName, paramsQaulifier] = methodParams = ["stack1", "us-east-1-prod"]; 56 | const templatePath = `./testData/${templateStackName}/template.json`; 57 | const srcObject = require(templatePath); 58 | const paramsPath = `./testData/${templateStackName}/params/params-${paramsQaulifier}.json`; 59 | const paramsObject = require(paramsPath); 60 | const target = new NodeEvaluator(srcObject); 61 | const actual = target.evaluateNodes(paramsObject); 62 | const expected = getExpectedObject(...methodParams) 63 | expect(actual).to.be.deep.equal(expected); 64 | }); 65 | 66 | // TODO: run on these valid test data: https://github.com/martysweet/cfn-lint/tree/master/testData/valid/json 67 | }); -------------------------------------------------------------------------------- /test/nodeTypes/ArrayNode.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | const { addChildToNode, mockNode, mockNodeAccessor } = require('../testUtils'); 4 | const { ArrayNode } = require('../../src/nodeTypes') 5 | 6 | describe('ArrayNode', () => { 7 | 8 | let target; 9 | 10 | beforeEach(()=> { 11 | target = new ArrayNode(mockNode, mockNodeAccessor, false); 12 | }) 13 | 14 | it('evaluate shallow values', () => { 15 | addChildToNode(target, 0, "item0"); 16 | addChildToNode(target, 1, "item1"); 17 | addChildToNode(target, 2, "item2"); 18 | 19 | const actual = target.evaluate(); 20 | 21 | expect(actual).to.deep.equal(["item0", "item1", "item2"]) 22 | }); 23 | 24 | it('evaluate nested arrays', () => { 25 | addChildToNode(target, 0, "item0"); 26 | const nestedArray = [ 9, 8, "testValue" ] 27 | addChildToNode(target, 1, nestedArray); 28 | addChildToNode(target, 2, "item2"); 29 | 30 | const actual = target.evaluate(); 31 | 32 | expect(actual).to.deep.equal(["item0", nestedArray , "item2"]) 33 | }); 34 | 35 | it('evaluate empty arrays', () => { 36 | const actual = target.evaluate(); 37 | 38 | expect(actual).to.deep.equal([]) 39 | }); 40 | }); -------------------------------------------------------------------------------- /test/nodeTypes/ConditionNode.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | const { addChildToNode, mockNode, mockNodeAccessor } = require('../testUtils'); 4 | const { ConditionNode } = require('../../src/nodeTypes') 5 | 6 | 7 | const convertedConditions = { 8 | wrappedObject: { 9 | False: { evaluate: () => false }, 10 | True: { evaluate: () => true }, 11 | IsRegionIAD: { evaluate: () => true }, 12 | IsRegionPDX: { evaluate: () => false } 13 | } 14 | }; 15 | 16 | describe('ConditionNode', () => { 17 | 18 | let target; 19 | 20 | 21 | it('evaluate False condition', () => { 22 | target = new ConditionNode("False", mockNodeAccessor, false, convertedConditions); 23 | const actual = target.evaluate(); 24 | assert.equal(actual, false); 25 | }); 26 | 27 | it('evaluate True condition', () => { 28 | target = new ConditionNode("True", mockNodeAccessor, false, convertedConditions); 29 | const actual = target.evaluate(); 30 | assert.equal(actual, true); 31 | }); 32 | 33 | it('evaluate IsRegionIAD condition', () => { 34 | target = new ConditionNode("IsRegionIAD", mockNodeAccessor, false, convertedConditions); 35 | const actual = target.evaluate(); 36 | assert.equal(actual, true); 37 | }); 38 | 39 | it('evaluate IsRegionPDX condition', () => { 40 | target = new ConditionNode("IsRegionPDX", mockNodeAccessor, false, convertedConditions); 41 | const actual = target.evaluate(); 42 | assert.equal(actual, false); 43 | }); 44 | }); -------------------------------------------------------------------------------- /test/nodeTypes/FnAnd.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | const { addChildToNode, mockNode, mockNodeAccessor } = require('../testUtils'); 4 | const { FnAnd } = require('../../src/nodeTypes') 5 | 6 | describe('FnAnd', () => { 7 | 8 | let target; 9 | 10 | beforeEach(()=> { 11 | target = new FnAnd(mockNode, mockNodeAccessor, false); 12 | }) 13 | 14 | it('evaluate multiple true items and single false item to false', () => { 15 | addChildToNode(target, 0, true); 16 | addChildToNode(target, 1, false); 17 | addChildToNode(target, 2, true); 18 | 19 | const actual = target.evaluate(); 20 | 21 | expect(actual).to.deep.equal(false) 22 | }); 23 | 24 | it('evaluate all items true to true', () => { 25 | addChildToNode(target, 0, true); 26 | addChildToNode(target, 1, true); 27 | 28 | const actual = target.evaluate(); 29 | 30 | expect(actual).to.deep.equal(true) 31 | }); 32 | 33 | // TODO: testcase when array is empty or contains only 1 item 34 | }); -------------------------------------------------------------------------------- /test/nodeTypes/FnEqualsNode.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | const { addChildToNode, mockNode, mockNodeAccessor } = require('../testUtils'); 4 | const { FnEqualsNode } = require('../../src/nodeTypes') 5 | 6 | describe('FnEqualsNode', () => { 7 | 8 | let target; 9 | 10 | beforeEach(()=> { 11 | target = new FnEqualsNode(mockNode, mockNodeAccessor, false); 12 | }) 13 | 14 | it('evaluate when not the same strings', () => { 15 | addChildToNode(target, 0, "hello"); 16 | addChildToNode(target, 1, "bello"); 17 | 18 | const actual = target.evaluate(); 19 | 20 | expect(actual).to.deep.equal(false) 21 | }); 22 | 23 | it('evaluate when same strings', () => { 24 | addChildToNode(target, 0, "hello"); 25 | addChildToNode(target, 1, "hello"); 26 | 27 | const actual = target.evaluate(); 28 | 29 | expect(actual).to.deep.equal(true) 30 | }); 31 | 32 | it('evaluate when same object reference', () => { 33 | const sameObject = { myKey: "MyVal" }; 34 | addChildToNode(target, 0, sameObject); 35 | addChildToNode(target, 1, sameObject); 36 | 37 | const actual = target.evaluate(); 38 | 39 | expect(actual).to.deep.equal(true) 40 | }); 41 | 42 | it('evaluate when same object content but different object reference', () => { 43 | addChildToNode(target, 0, { myKey: "MyVal" }); 44 | addChildToNode(target, 1, { myKey: "MyVal" }); 45 | 46 | const actual = target.evaluate(); 47 | 48 | expect(actual).to.deep.equal(true) 49 | }); 50 | 51 | it('evaluate comparing boolean with string: true == "true" should be false', () => { 52 | addChildToNode(target, 0, "true" ); 53 | addChildToNode(target, 1, true); 54 | 55 | const actual = target.evaluate(); 56 | 57 | expect(actual).to.deep.equal(false) 58 | }); 59 | 60 | it('evaluate comparing number with string: 143 == "143" should be false', () => { 61 | addChildToNode(target, 0, "143" ); 62 | addChildToNode(target, 1, 143); 63 | 64 | const actual = target.evaluate(); 65 | 66 | expect(actual).to.deep.equal(false) 67 | }); 68 | 69 | it('evaluate comparing number 0 with falsy value: 0 == false should be false', () => { 70 | addChildToNode(target, 0, 0 ); 71 | addChildToNode(target, 1, false); 72 | 73 | const actual = target.evaluate(); 74 | 75 | expect(actual).to.deep.equal(false) 76 | }); 77 | 78 | // TODO: testcase when array 79 | // - is empty 80 | // - has only 1 item 81 | // - has more then 2 items 82 | }); -------------------------------------------------------------------------------- /test/nodeTypes/FnFindInMapNode.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | const { addChildToNode, mockNode, mockNodeAccessor } = require('../testUtils'); 4 | const { FnFindInMapNode } = require('../../src/nodeTypes') 5 | 6 | const testMapping = { 7 | Mapping1: { 8 | key1: { 9 | key1SubKey1: "true", 10 | key1SubKey2: "false" 11 | }, 12 | key2: { 13 | key2SubKey1: "m5.large", 14 | key2SubKey2: "m5.xlarge" 15 | }, 16 | }, 17 | Mapping2: { 18 | key1: { 19 | key1SubKey1: 2, 20 | key1SubKey2: 5 21 | }, 22 | key2: { 23 | key2SubKey1: true, 24 | key2SubKey2: false 25 | }, 26 | } 27 | }; 28 | 29 | describe('FnFindInMapNode', () => { 30 | 31 | let target; 32 | 33 | beforeEach(()=> { 34 | target = new FnFindInMapNode(mockNode, mockNodeAccessor, false, testMapping); 35 | }) 36 | 37 | it('finds string value in map', () => { 38 | addChildToNode(target, 0, "Mapping1"); 39 | addChildToNode(target, 1, "key2"); 40 | addChildToNode(target, 2, "key2SubKey2"); 41 | 42 | const actual = target.evaluate(); 43 | 44 | expect(actual).to.deep.equal("m5.xlarge") 45 | }); 46 | 47 | it('finds number in map', () => { 48 | addChildToNode(target, 0, "Mapping2"); 49 | addChildToNode(target, 1, "key1"); 50 | addChildToNode(target, 2, "key1SubKey1"); 51 | 52 | const actual = target.evaluate(); 53 | 54 | expect(actual).to.deep.equal(2) 55 | }); 56 | 57 | it('finds boolean in map', () => { 58 | addChildToNode(target, 0, "Mapping2"); 59 | addChildToNode(target, 1, "key2"); 60 | addChildToNode(target, 2, "key2SubKey1"); 61 | 62 | const actual = target.evaluate(); 63 | 64 | expect(actual).to.deep.equal(true) 65 | }); 66 | 67 | it('finds boolean as string ("true") in map', () => { 68 | addChildToNode(target, 0, "Mapping1"); 69 | addChildToNode(target, 1, "key1"); 70 | addChildToNode(target, 2, "key1SubKey1"); 71 | 72 | const actual = target.evaluate(); 73 | 74 | expect(actual).to.deep.equal("true") 75 | }); 76 | 77 | // TODO: testcase when 78 | // - array is empty 79 | // - array has <= 2 item 80 | // - array has > 3 items 81 | // - item is not found in map (e.g. incorrect addressing in different levels) 82 | }); -------------------------------------------------------------------------------- /test/nodeTypes/FnGetAZsNode.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | const { addChildToNode, mockNode, mockNodeAccessor } = require('../testUtils'); 4 | const { FnGetAZsNode } = require('../../src/nodeTypes') 5 | 6 | const azMapping = { 7 | 'us-east-1': [ 8 | 'us-east-1a', 9 | 'us-east-1b', 10 | 'us-east-1c', 11 | 'us-east-1d', 12 | 'us-east-1e', 13 | 'us-east-1f' 14 | ], 15 | "us-west-2": [ 16 | "us-west-2a", 17 | "us-west-2b", 18 | "us-west-2c", 19 | "us-west-2d" 20 | ] 21 | }; 22 | 23 | describe('FnGetAZsNode', () => { 24 | it('finds AZs for current region', () => { 25 | const target = new FnGetAZsNode(mockNode, mockNodeAccessor, false, azMapping, "us-east-1"); 26 | 27 | const actual = target.evaluate(); 28 | 29 | expect(actual).to.deep.equal([ 30 | 'us-east-1a', 31 | 'us-east-1b', 32 | 'us-east-1c', 33 | 'us-east-1d', 34 | 'us-east-1e', 35 | 'us-east-1f' 36 | ]); 37 | }); 38 | 39 | it('finds AZs when Fn::GetAZs is called with region parameter', () => { 40 | const target = new FnGetAZsNode(mockNode, mockNodeAccessor, false, azMapping, "us-east-1"); 41 | addChildToNode(target, 0, "us-west-2"); 42 | const actual = target.evaluate(); 43 | 44 | expect(actual).to.deep.equal([ 45 | "us-west-2a", 46 | "us-west-2b", 47 | "us-west-2c", 48 | "us-west-2d" 49 | ]); 50 | }); 51 | 52 | // TODO: testcase when 53 | // - no AZ found 54 | }); -------------------------------------------------------------------------------- /test/nodeTypes/FnGetAttNode.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | const { addChildToNode, mockNode, mockNodeAccessor, getMockNode } = require('../testUtils'); 4 | const { FnGetAttNode } = require('../../src/nodeTypes') 5 | 6 | const testGetAttResolvers = { 7 | "AuditLogsBucket": { 8 | "Arn": "arn:aws:s3:::us-east-1-beta-redshift-clusters-log" 9 | } 10 | }; 11 | 12 | const TEST_QUEUE_NAME = "testSqs-beta"; 13 | const mockQueueNameNode = getMockNode("QueueName", TEST_QUEUE_NAME); 14 | 15 | const convertedQueue = { 16 | wrappedObject: { 17 | Properties: { 18 | wrappedObject: { 19 | QueueName: mockQueueNameNode 20 | } 21 | }, 22 | Type: "AWS::SQS::Queue" 23 | }, 24 | isPropertyDefinedOnObjectPath: () => true, 25 | getResolvedProperyValueOnObjectPath: () => TEST_QUEUE_NAME 26 | }; 27 | 28 | const mockFindWrappedResource = (logicalId) => { 29 | switch (logicalId) { 30 | case "FooSrvQueue": 31 | return convertedQueue; 32 | } 33 | } 34 | 35 | const existingResourceMockConvRoot = { 36 | wrappedObject: { 37 | Resources: { 38 | hasAncestorOnPath: () => true, 39 | findWrappedAncestorByPathArray: () => mockQueueNameNode, 40 | wrappedObject: { 41 | FooSrvQueue: convertedQueue 42 | }, 43 | findWrappedResource: mockFindWrappedResource, 44 | getResolvedArn: () => "arn:aws:sqs:us-east-2:123123123123:testSqs-beta" 45 | } 46 | } 47 | }; 48 | 49 | const attResolverTestMockConvRoot = { 50 | wrappedObject: { 51 | Resources: { 52 | hasAncestorOnPath: () => false, 53 | findWrappedAncestorByPathArray: () => undefined, 54 | wrappedObject: { 55 | FooSrvQueue: convertedQueue 56 | }, 57 | findWrappedResource: () => undefined 58 | } 59 | } 60 | }; 61 | 62 | describe('FnGetAttNode', () => { 63 | 64 | const createTarget = (mockConvRoot) => { 65 | return new FnGetAttNode(mockNode, 66 | mockNodeAccessor, 67 | false, 68 | testGetAttResolvers, 69 | mockConvRoot, 70 | {} 71 | ); 72 | } 73 | 74 | it('finds single attribute value in template object', () => { 75 | const target = createTarget(existingResourceMockConvRoot); 76 | addChildToNode(target, 0, "FooSrvQueue"); 77 | addChildToNode(target, 1, "QueueName"); 78 | 79 | const actual = target.evaluate(); 80 | 81 | expect(actual).to.deep.equal(TEST_QUEUE_NAME); 82 | }); 83 | 84 | it('finds attribute value in Fn::GetAttResolvers object', () => { 85 | const target = createTarget(attResolverTestMockConvRoot); 86 | addChildToNode(target, 0, "AuditLogsBucket"); 87 | addChildToNode(target, 1, "Arn"); 88 | 89 | const actual = target.evaluate(); 90 | 91 | expect(actual).to.deep.equal("arn:aws:s3:::us-east-1-beta-redshift-clusters-log") 92 | }); 93 | 94 | it('resolves Arn from built in Arn schemas', () => { 95 | const target = createTarget(existingResourceMockConvRoot); 96 | addChildToNode(target, 0, "FooSrvQueue"); 97 | addChildToNode(target, 1, "Arn"); 98 | 99 | const actual = target.evaluate(); 100 | 101 | expect(actual).to.deep.equal("arn:aws:sqs:us-east-2:123123123123:testSqs-beta"); 102 | }); 103 | 104 | // TODO: testcase when 105 | // - array is empty 106 | // - array has < 2 item 107 | // - array has >= 3 items 108 | // - add test for nested attributes lookups from template (e.g. "Foo.Bar") 109 | // - item is not found in Fn::GetAttResolvers (e.g. incorrect addressing in different levels) 110 | }); -------------------------------------------------------------------------------- /test/nodeTypes/FnIf.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | const { addChildToNode, mockNode, mockNodeAccessor } = require('../testUtils'); 4 | const { FnIf } = require('../../src/nodeTypes') 5 | 6 | 7 | const convertedConditions = { 8 | wrappedObject: { 9 | False: { evaluate: () => false }, 10 | True: { evaluate: () => true } 11 | } 12 | }; 13 | 14 | describe('FnIf', () => { 15 | 16 | let target; 17 | 18 | beforeEach(()=> { 19 | target = new FnIf(mockNode, mockNodeAccessor, false, convertedConditions); 20 | }) 21 | 22 | it('evaluate if false', () => { 23 | addChildToNode(target, 0, "False") 24 | addChildToNode(target, 1, "testTrueValue") 25 | addChildToNode(target, 2, "testFalseValue") 26 | const actual = target.evaluate(); 27 | assert.equal(actual, "testFalseValue"); 28 | }); 29 | 30 | it('evaluate if true', () => { 31 | addChildToNode(target, 0, "True") 32 | addChildToNode(target, 1, "testTrueValue") 33 | addChildToNode(target, 2, "testFalseValue") 34 | const actual = target.evaluate(); 35 | assert.equal(actual, "testTrueValue"); 36 | }); 37 | 38 | // TODO: Add test case when 39 | // - array length is not exactly 3 40 | // - condition is not found 41 | }); -------------------------------------------------------------------------------- /test/nodeTypes/FnJoinNode.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | const { addChildToNode, mockNode, mockNodeAccessor } = require('../testUtils'); 4 | const { FnJoinNode } = require('../../src/nodeTypes') 5 | 6 | describe('FnJoin', () => { 7 | 8 | let target; 9 | 10 | beforeEach(()=> { 11 | target = new FnJoinNode(mockNode, mockNodeAccessor, false); 12 | }) 13 | 14 | it('evaluate strings', () => { 15 | addChildToNode(target, 0, "") 16 | addChildToNode(target, 1, [ 17 | "valFirst", 18 | "valMiddle1", 19 | "valMiddle2" 20 | ]); 21 | 22 | 23 | const actual = target.evaluate(); 24 | 25 | assert.equal(actual, "valFirstvalMiddle1valMiddle2"); 26 | }); 27 | 28 | it('evaluate integeres', () => { 29 | addChildToNode(target, 0, "-"); 30 | addChildToNode(target, 1, [ 31 | 1234, 32 | 0, 33 | -99, 34 | ]); 35 | const actual = target.evaluate(); 36 | 37 | assert.equal(actual, "1234-0--99"); 38 | }); 39 | 40 | it('evaluate floats', () => { 41 | addChildToNode(target, 0, "/"); 42 | addChildToNode(target, 1, [ 43 | 0.1920, 44 | 1.2, 45 | -99, 46 | ]); 47 | 48 | const actual = target.evaluate(); 49 | 50 | assert.equal(actual, "0.192/1.2/-99"); 51 | }); 52 | }); -------------------------------------------------------------------------------- /test/nodeTypes/FnNot.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | const { addChildToNode, mockNode, mockNodeAccessor } = require('../testUtils'); 4 | const { FnNot } = require('../../src/nodeTypes') 5 | 6 | describe('FnNot', () => { 7 | 8 | let target; 9 | 10 | beforeEach(()=> { 11 | target = new FnNot(mockNode, mockNodeAccessor, false); 12 | }) 13 | 14 | it('evaluate (FnNot true) to false', () => { 15 | addChildToNode(target, 0, true); 16 | 17 | const actual = target.evaluate(); 18 | 19 | expect(actual).to.deep.equal(false) 20 | }); 21 | 22 | it('evaluate (FnNot false) to true', () => { 23 | addChildToNode(target, 0, false); 24 | 25 | const actual = target.evaluate(); 26 | 27 | expect(actual).to.deep.equal(true) 28 | }); 29 | 30 | // TODO: testcase when array length is not 1 or is not an array 31 | }); -------------------------------------------------------------------------------- /test/nodeTypes/FnOr.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | const { addChildToNode, mockNode, mockNodeAccessor } = require('../testUtils'); 4 | const { FnOr } = require('../../src/nodeTypes') 5 | 6 | describe('FnOr', () => { 7 | 8 | let target; 9 | 10 | beforeEach(()=> { 11 | target = new FnOr(mockNode, mockNodeAccessor, false); 12 | }) 13 | 14 | it('evaluate multiple false items and single true item to true', () => { 15 | addChildToNode(target, 0, false); 16 | addChildToNode(target, 1, true); 17 | addChildToNode(target, 2, false); 18 | 19 | const actual = target.evaluate(); 20 | 21 | expect(actual).to.deep.equal(true) 22 | }); 23 | 24 | it('evaluate all items false to false', () => { 25 | addChildToNode(target, 0, false); 26 | addChildToNode(target, 1, false); 27 | 28 | const actual = target.evaluate(); 29 | 30 | expect(actual).to.deep.equal(false) 31 | }); 32 | 33 | // TODO: testcase when array is empty or contains only 1 item 34 | }); -------------------------------------------------------------------------------- /test/nodeTypes/FnSelect.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | const { addChildToNode, mockNode, mockNodeAccessor } = require('../testUtils'); 4 | const { FnSelect } = require('../../src/nodeTypes') 5 | 6 | describe('FnSelect', () => { 7 | 8 | let target; 9 | 10 | beforeEach(() => { 11 | target = new FnSelect(mockNode, mockNodeAccessor, false); 12 | }) 13 | 14 | it('evaluate single item list', () => { 15 | addChildToNode(target, 0, 0) 16 | addChildToNode(target, 1, [ 12 ]); 17 | 18 | const actual = target.evaluate(); 19 | 20 | expect(actual).to.deep.equal(12); 21 | }); 22 | 23 | it('evaluate when index 0', () => { 24 | addChildToNode(target, 0, 0) 25 | addChildToNode(target, 1, [ "apples", "grapes", "oranges", "mangoes" ]); 26 | 27 | const actual = target.evaluate(); 28 | 29 | expect(actual).to.deep.equal("apples"); 30 | }); 31 | 32 | it('evaluate when index middle', () => { 33 | addChildToNode(target, 0, 2) 34 | addChildToNode(target, 1, [ "apples", "grapes", "oranges", "mangoes" ]); 35 | 36 | const actual = target.evaluate(); 37 | 38 | expect(actual).to.deep.equal("oranges"); 39 | }); 40 | 41 | it('evaluate when index last item', () => { 42 | addChildToNode(target, 0, 3) 43 | addChildToNode(target, 1, [ "apples", "grapes", "oranges", "mangoes" ]); 44 | 45 | const actual = target.evaluate(); 46 | 47 | expect(actual).to.deep.equal("mangoes"); 48 | }); 49 | 50 | it('evaluate when index is out of bound by 1', () => { 51 | addChildToNode(target, 0, 4) 52 | addChildToNode(target, 1, [ "apples", "grapes", "oranges", "mangoes" ]); 53 | 54 | expect( () => target.evaluate()).to.throw("Index 4 is out of bound."); 55 | }); 56 | }); -------------------------------------------------------------------------------- /test/nodeTypes/FnSplit.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | const { addChildToNode, mockNode, mockNodeAccessor } = require('../testUtils'); 4 | const { FnSplit } = require('../../src/nodeTypes') 5 | 6 | describe('FnSplit', () => { 7 | 8 | let target; 9 | 10 | beforeEach(() => { 11 | target = new FnSplit(mockNode, mockNodeAccessor, false); 12 | }) 13 | 14 | it('single char delimiter', () => { 15 | addChildToNode(target, 0, "-") 16 | addChildToNode(target, 1, "hello-world"); 17 | 18 | const actual = target.evaluate(); 19 | 20 | expect(actual).to.deep.equal(["hello", "world"]); 21 | }); 22 | 23 | it('multiple char delimiter', () => { 24 | addChildToNode(target, 0, "::") 25 | addChildToNode(target, 1, "hello::world"); 26 | 27 | const actual = target.evaluate(); 28 | 29 | expect(actual).to.deep.equal(["hello", "world"]); 30 | }); 31 | 32 | it('delimiter is not present in string to split', () => { 33 | addChildToNode(target, 0, ":") 34 | addChildToNode(target, 1, "hello-world"); 35 | 36 | const actual = target.evaluate(); 37 | 38 | expect(actual).to.deep.equal(["hello-world"]); 39 | }); 40 | 41 | it('delimiter is empty string', () => { 42 | addChildToNode(target, 0, "") 43 | addChildToNode(target, 1, "split me"); 44 | 45 | expect( () => target.evaluate()).to.throw("Delimiter is invalid: empty string"); 46 | }); 47 | }); -------------------------------------------------------------------------------- /test/nodeTypes/FnSubNode.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | const { addChildToNode, mockNode, mockNodeAccessor } = require('../testUtils'); 4 | const { FnSub } = require('../../src/nodeTypes') 5 | 6 | describe('FnSub with dictionary', () => { 7 | 8 | let target; 9 | 10 | beforeEach(() => { 11 | target = new FnSub(mockNode, mockNodeAccessor, false); 12 | }) 13 | 14 | it('evaluate single instance', () => { 15 | addChildToNode(target, 0, "hello ${replaceMe} test!") 16 | addChildToNode(target, 1, { replaceMe: "world" }); 17 | 18 | const actual = target.evaluate(); 19 | 20 | expect(actual).to.deep.equal("hello world test!"); 21 | }); 22 | 23 | it('evaluate multiple instance of same key', () => { 24 | addChildToNode(target, 0, "hello ${replaceMe} test! ${replaceMe}") 25 | addChildToNode(target, 1, { replaceMe: "world" }); 26 | 27 | const actual = target.evaluate(); 28 | 29 | expect(actual).to.deep.equal("hello world test! world"); 30 | }); 31 | 32 | it('evaluate multiple instance of same key with 2 dictionary items', () => { 33 | addChildToNode(target, 0, "${placeHolder1} ${placeHolder2}, ${placeHolder1}!") 34 | addChildToNode(target, 1, { 35 | placeHolder1: "Hello", 36 | placeHolder2: "World" 37 | }); 38 | 39 | const actual = target.evaluate(); 40 | 41 | expect(actual).to.deep.equal("Hello World, Hello!"); 42 | }); 43 | 44 | // 45 | // TODO: Add test case when 46 | // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-sub.html 47 | // - Variables can be 48 | // - resource logical IDs 49 | // - resource attributes 50 | // - no value provided in the dictionary 51 | // - recursive replacement? 52 | }); 53 | 54 | describe('FnSub using template parameters', () => { 55 | 56 | let target; 57 | let refResolver; 58 | 59 | beforeEach(() => { 60 | refResolver = {}; 61 | }) 62 | 63 | function givenNodeValue(nodeValue) { 64 | target = new FnSub(nodeValue, mockNodeAccessor, false, refResolver); 65 | } 66 | 67 | function givenParamValue(name, value) { 68 | refResolver[name] = value; 69 | } 70 | 71 | it('evaluate single instance', () => { 72 | givenNodeValue("hello ${replaceMe} test!") 73 | givenParamValue('replaceMe', 'world'); 74 | 75 | const actual = target.evaluate(); 76 | 77 | expect(actual).to.deep.equal("hello world test!"); 78 | }); 79 | 80 | it('evaluate multiple instance of same key', () => { 81 | givenNodeValue("hello ${replaceMe} test! ${replaceMe}") 82 | givenParamValue("replaceMe", "world"); 83 | 84 | const actual = target.evaluate(); 85 | 86 | expect(actual).to.deep.equal("hello world test! world"); 87 | }); 88 | 89 | it('evaluate multiple instance of same key with 2 dictionary items', () => { 90 | givenNodeValue("${placeHolder1} ${placeHolder2}, ${placeHolder1}!") 91 | givenParamValue("placeHolder1", "Hello"); 92 | givenParamValue("placeHolder2", "World"); 93 | 94 | const actual = target.evaluate(); 95 | 96 | expect(actual).to.deep.equal("Hello World, Hello!"); 97 | }); 98 | 99 | }); -------------------------------------------------------------------------------- /test/nodeTypes/ObjectNode.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | const { addChildToNode, mockNode, mockNodeAccessor } = require('../testUtils'); 4 | const { ObjectNode } = require('../../src/nodeTypes') 5 | 6 | describe('ObjectNode', () => { 7 | 8 | let target; 9 | 10 | beforeEach(() => { 11 | target = new ObjectNode(mockNode, mockNodeAccessor, false); 12 | }) 13 | 14 | it('evaluate with single child that needs to replace parent (e.g. RefNode)', () => { 15 | addChildToNode(target, "originalKey", "MyBucketLogicalId", true); 16 | 17 | const actual = target.evaluate(); 18 | 19 | expect(actual).to.deep.equal("MyBucketLogicalId") 20 | }); 21 | 22 | it('evaluate with single child that should not replace parent (e.g. PropertyConditionNode)', () => { 23 | addChildToNode(target, "originalKey", "evaluatedValue", false); 24 | 25 | const actual = target.evaluate(); 26 | 27 | expect(actual).to.deep.equal({ originalKey: "evaluatedValue" }) 28 | }); 29 | 30 | it('evaluate with multiple children', () => { 31 | addChildToNode(target, "originalKey1", "evaluatedValue1"); 32 | addChildToNode(target, "originalKey2", "evaluatedValue2"); 33 | addChildToNode(target, "originalKey3", "evaluatedValue3"); 34 | 35 | const actual = target.evaluate(); 36 | 37 | expect(actual).to.deep.equal( 38 | { 39 | originalKey1: "evaluatedValue1", 40 | originalKey2: "evaluatedValue2", 41 | originalKey3: "evaluatedValue3" 42 | } 43 | ); 44 | }); 45 | }); -------------------------------------------------------------------------------- /test/nodeTypes/PropertyConditionNode.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | const { addChildToNode, mockNode, mockNodeAccessor } = require('../testUtils'); 4 | const { PropertyConditionNode } = require('../../src/nodeTypes') 5 | 6 | describe('PropertyConditionNode', () => { 7 | 8 | let target; 9 | 10 | beforeEach(() => { 11 | target = new PropertyConditionNode(mockNode, mockNodeAccessor, false); 12 | }) 13 | 14 | it('evaluate keeps condition node', () => { 15 | const expected = {"StringEqualsIgnoreCase": { "aws:username" : "johndoe" }}; 16 | addChildToNode(target, "StringEqualsIgnoreCase", { "aws:username" : "johndoe" }); 17 | 18 | const actual = target.evaluate(); 19 | expect(actual).to.deep.equal(expected); 20 | }); 21 | 22 | }); -------------------------------------------------------------------------------- /test/nodeTypes/RefNode.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | const { mockNodeAccessor } = require('../testUtils'); 4 | const { RefNode } = require('../../src/nodeTypes') 5 | 6 | const refResolvers = { 7 | "AWS::Region": "us-east-1", 8 | "AWS::Partition": "aws", 9 | "AWS::AccountId": "666666666666", 10 | "Stage": "beta", 11 | "AWS::StackId": "MyEvaluatedFakeStack" 12 | }; 13 | 14 | describe('RefNode', () => { 15 | 16 | let target; 17 | 18 | it('evaluates predefined AWS::Region parameters', () => { 19 | target = new RefNode("AWS::Region", mockNodeAccessor, false, refResolvers); 20 | const actual = target.evaluate(); 21 | expect(actual).to.deep.equal("us-east-1"); 22 | }); 23 | 24 | it('evaluates LogicalId references', () => { 25 | target = new RefNode("MyS3BucketLogicalId", mockNodeAccessor, false, refResolvers); 26 | const actual = target.evaluate(); 27 | expect(actual).to.deep.equal("MyS3BucketLogicalId"); 28 | }); 29 | }); -------------------------------------------------------------------------------- /test/nodeTypes/ResolveFromMapNode.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | 3 | const { mockNodeAccessor } = require('../testUtils'); 4 | const { ResolveFromMapNode } = require('../../src/nodeTypes') 5 | 6 | const resolverMap = { 7 | "OtherStackExportedKey": "MyFakeValue", 8 | }; 9 | 10 | describe('ResolveFromMapNode', () => { 11 | 12 | let target; 13 | 14 | it('evaluates predefined values from input map', () => { 15 | target = new ResolveFromMapNode("OtherStackExportedKey", mockNodeAccessor, false, resolverMap); 16 | const actual = target.evaluate(); 17 | expect(actual).to.deep.equal("MyFakeValue"); 18 | }); 19 | }); -------------------------------------------------------------------------------- /test/testData/stack1/expected/expected-us-east-1-beta.json: -------------------------------------------------------------------------------- 1 | { 2 | "Conditions": { 3 | "CDKMetadataAvailable": true, 4 | "False": false, 5 | "IsNotUSEAST1": false, 6 | "IsNotProdUsEast1": true, 7 | "IsNotProdUsEast1AndNotUsWest2": true, 8 | "IsNotUSWEST2": true, 9 | "IsRegionUSEAST1": true, 10 | "IsRegionUSWEST2": false, 11 | "IsStageprod": false 12 | }, 13 | "Mappings": { 14 | "CondensedRegionNamesWithCapitalInitialLetter": { 15 | "us-east-1": { 16 | "Name": "UsEast1" 17 | }, 18 | "us-west-2": { 19 | "Name": "UsWest2" 20 | } 21 | }, 22 | "CondensedRegionUpperCase": { 23 | "us-east-1": { 24 | "Name": "USEAST1" 25 | }, 26 | "us-west-2": { 27 | "Name": "USWEST2" 28 | } 29 | }, 30 | "RedshiftAuditLoggingUserArn": { 31 | "us-east-1": { 32 | "UserArn": "arn:aws:iam::222222222222:user/logs" 33 | }, 34 | "us-west-2": { 35 | "UserArn": "arn:aws:iam::333333333333:user/logs" 36 | } 37 | }, 38 | "RedshiftNodeType": { 39 | "us-east-1": { 40 | "beta": "ds2.xlarge", 41 | "prod": "ds2.8xlarge" 42 | }, 43 | "us-west-2": { 44 | "beta": "ds2.xlarge", 45 | "prod": "ds2.xlarge" 46 | } 47 | }, 48 | "RedshiftNumberOfNodes": { 49 | "us-east-1": { 50 | "beta": 2, 51 | "prod": 4 52 | }, 53 | "us-west-2": { 54 | "beta": 2, 55 | "prod": 8 56 | } 57 | }, 58 | "RegionMap": { 59 | "us-east-1": { 60 | "condensedName": "useast1" 61 | }, 62 | "us-west-2": { 63 | "condensedName": "uswest2" 64 | } 65 | }, 66 | "StageNameWithCapitalInitialLetter": { 67 | "Name": { 68 | "beta": "Beta", 69 | "prod": "Prod" 70 | } 71 | } 72 | }, 73 | "Outputs": { 74 | "StackArn": { 75 | "Description": "Resource ARN of the Stack", 76 | "Value": "MyEvaluatedFakeStackUsEast1" 77 | } 78 | }, 79 | "Parameters": { 80 | "Stage": { 81 | "AllowedValues": [ 82 | "beta", 83 | "prod" 84 | ], 85 | "Default": "beta", 86 | "Description": "The stage in the CFN pipeline", 87 | "Type": "String" 88 | }, 89 | "Fruit": { 90 | "Default": "Bananas", 91 | "Description": "Favorite kind of fruit", 92 | "Type": "String" 93 | } 94 | }, 95 | "Resources": { 96 | "AuditLogsBucket": { 97 | "Condition": true, 98 | "DeletionPolicy": "Retain", 99 | "Metadata": { 100 | "aws:cdk:path": "FooSrv/AuditLogsBucket/Resource" 101 | }, 102 | "Properties": { 103 | "BucketEncryption": [ 104 | { 105 | "ServerSideEncryptionByDefault": { 106 | "SSEAlgorithm": "AES256" 107 | } 108 | } 109 | ], 110 | "BucketName": "beta-useast1-redshift-log" 111 | }, 112 | "Type": "AWS::S3::Bucket", 113 | "UpdateReplacePolicy": "Retain" 114 | }, 115 | "CDKMetadata": { 116 | "Condition": true, 117 | "Properties": { 118 | "Modules": "aws-cdk=1.15.0,@aws-cdk/assets=1.15.0,@aws-cdk/aws-cloudwatch=1.15.0,@aws-cdk/aws-ec2=1.15.0,@aws-cdk/aws-events=1.15.0,@aws-cdk/aws-iam=1.15.0,@aws-cdk/aws-kms=1.15.0,@aws-cdk/aws-lambda=1.15.0,@aws-cdk/aws-logs=1.15.0,@aws-cdk/aws-redshift=1.15.0,@aws-cdk/aws-s3=1.15.0,@aws-cdk/aws-s3-assets=1.15.0,@aws-cdk/aws-secretsmanager=1.15.0,@aws-cdk/aws-sqs=1.15.0,@aws-cdk/aws-ssm=1.15.0,@aws-cdk/core=1.15.0,@aws-cdk/cx-api=1.15.0,@aws-cdk/region-info=1.15.0,jsii-runtime=Java/1.8.0_231" 119 | }, 120 | "Type": "AWS::CDK::Metadata" 121 | }, 122 | "ExampleWaitHandle": { 123 | "Properties": {}, 124 | "Type": "AWS::CloudFormation::WaitConditionHandle" 125 | }, 126 | "MainClusterCluster": { 127 | "Condition": true, 128 | "DeletionPolicy": "Retain", 129 | "DependsOn": [ 130 | "MainClusterSecret" 131 | ], 132 | "Metadata": { 133 | "aws:cdk:path": "FooSrv/RedshiftMainCluster/MainClusterCluster" 134 | }, 135 | "Properties": { 136 | "AllowVersionUpgrade": true, 137 | "AutomatedSnapshotRetentionPeriod": 30, 138 | "ClusterParameterGroupName": "MainClusterParamGroup", 139 | "ClusterSubnetGroupName": "MainClusterDefaultSubnetGroup", 140 | "ClusterType": "multi-node", 141 | "DBName": "foosrv", 142 | "Encrypted": true, 143 | "LoggingProperties": { 144 | "BucketName": "AuditLogsBucket", 145 | "S3KeyPrefix": "foosrv-cluster/" 146 | }, 147 | "MasterUserPassword": "{{resolve:secretsmanager:MainClusterSecret:SecretString:password::}}", 148 | "MasterUsername": "foosrvroot", 149 | "NodeType": "ds2.xlarge", 150 | "NumberOfNodes": 2, 151 | "Port": 5439, 152 | "PreferredMaintenanceWindow": "fri:07:00-fri:07:30", 153 | "PubliclyAccessible": false, 154 | "VpcSecurityGroupIds": [ "FooSrvRsMainClusterSecurityGroupFakeId" ] 155 | }, 156 | "Type": "AWS::Redshift::Cluster", 157 | "UpdateReplacePolicy": "Retain" 158 | }, 159 | "RedshiftMainClusterVPCSubnet1": { 160 | "Properties": { 161 | "AvailabilityZone": "us-east-1a", 162 | "CidrBlock": "10.0.0.0/20", 163 | "Tags": [ 164 | { 165 | "Key": "Name", 166 | "Value": "FooSrv/RedshiftMainClusterVPC/RedshiftMainClusterVPC/RedshiftMainClusterVPCSubnet1" 167 | } 168 | ], 169 | "VpcId": "RedshiftMainClusterVPC" 170 | }, 171 | "Type": "AWS::EC2::Subnet" 172 | }, 173 | "RedshiftMainClusterVPCSubnet2": { 174 | "Properties": { 175 | "AvailabilityZone": "us-east-1b", 176 | "CidrBlock": "10.0.16.0/20", 177 | "Tags": [ 178 | { 179 | "Key": "Name", 180 | "Value": "FooSrv/RedshiftMainClusterVPC/RedshiftMainClusterVPC/RedshiftMainClusterVPCSubnet2" 181 | } 182 | ], 183 | "VpcId": "RedshiftMainClusterVPC" 184 | }, 185 | "Type": "AWS::EC2::Subnet" 186 | }, 187 | "MainClusterDefaultSubnetGroup": { 188 | "Condition": true, 189 | "Metadata": { 190 | "aws:cdk:path": "FooSrv/RedshiftEnvironmentMainCluster/MainClusterDefaultSubnetGroup" 191 | }, 192 | "Properties": { 193 | "Description": "Subnet group for FooSrv", 194 | "SubnetIds": [ "FooSrvSubnet1FakeId", "FooSrvSubnet2FakeId"] 195 | }, 196 | "Type": "AWS::Redshift::ClusterSubnetGroup" 197 | }, 198 | "MainClusterParamGroup": { 199 | "Condition": true, 200 | "Metadata": { 201 | "aws:cdk:path": "FooSrv/RedshiftEnvironmentMainCluster/MainClusterParamGroup" 202 | }, 203 | "Properties": { 204 | "Description": "Parameters for FooSrv Redshift MainCluster", 205 | "ParameterGroupFamily": "redshift-1.0", 206 | "Parameters": [ 207 | { 208 | "ParameterName": "datestyle", 209 | "ParameterValue": "ISO, MDY" 210 | }, 211 | { 212 | "ParameterName": "enable_user_activity_logging", 213 | "ParameterValue": "true" 214 | }, 215 | { 216 | "ParameterName": "extra_float_digits", 217 | "ParameterValue": "0" 218 | }, 219 | { 220 | "ParameterName": "require_ssl", 221 | "ParameterValue": "true" 222 | }, 223 | { 224 | "ParameterName": "search_path", 225 | "ParameterValue": "public" 226 | }, 227 | { 228 | "ParameterName": "statement_timeout", 229 | "ParameterValue": "0" 230 | }, 231 | { 232 | "ParameterName": "use_fips_ssl", 233 | "ParameterValue": "false" 234 | }, 235 | { 236 | "ParameterName": "wlm_json_configuration", 237 | "ParameterValue": "[{\"query_concurrency\":5}]" 238 | } 239 | ] 240 | }, 241 | "Type": "AWS::Redshift::ClusterParameterGroup" 242 | }, 243 | "MainClusterRole": { 244 | "Condition": true, 245 | "Metadata": { 246 | "aws:cdk:path": "FooSrv/RedshiftEnvironmentMainCluster/MainClusterRole/Resource" 247 | }, 248 | "Properties": { 249 | "AssumeRolePolicyDocument": { 250 | "Statement": [ 251 | { 252 | "Action": "sts:AssumeRole", 253 | "Effect": "Allow", 254 | "Principal": { 255 | "Service": "redshift.amazonaws.com" 256 | } 257 | } 258 | ], 259 | "Version": "2012-10-17" 260 | }, 261 | "RoleName": "MainClusterRoleBetaUsEast1" 262 | }, 263 | "Type": "AWS::IAM::Role" 264 | }, 265 | "MainClusterSecret": { 266 | "Condition": true, 267 | "Metadata": { 268 | "aws:cdk:path": "FooSrv/MainClusterSecret/MainClusterSecretSecret/Resource" 269 | }, 270 | "Properties": { 271 | "Description": "Master secret for FooSrv Redshift MainCluster", 272 | "GenerateSecretString": { 273 | "ExcludePunctuation": true, 274 | "GenerateStringKey": "password", 275 | "IncludeSpace": false, 276 | "PasswordLength": 32, 277 | "SecretStringTemplate": "{\"username\": \"foosrvroot\"}" 278 | }, 279 | "Name": "MainClusterSecret" 280 | }, 281 | "Type": "AWS::SecretsManager::Secret" 282 | }, 283 | "MainClusterWithNoSecretManagerCluster": { 284 | "Condition": false, 285 | "DeletionPolicy": "Retain", 286 | "Metadata": { 287 | "aws:cdk:path": "FooSrv/RedshiftMainClusterWithNoSecretManager/MainClusterWithNoSecretManagerCluster" 288 | }, 289 | "Properties": { 290 | "AllowVersionUpgrade": true, 291 | "AutomatedSnapshotRetentionPeriod": 30, 292 | "ClusterParameterGroupName": "MainClusterParamGroup", 293 | "ClusterSubnetGroupName": "MainClusterDefaultSubnetGroup", 294 | "ClusterType": "multi-node", 295 | "DBName": "foosrv", 296 | "Encrypted": true, 297 | "LoggingProperties": { 298 | "BucketName": "AuditLogsBucket", 299 | "S3KeyPrefix": "foosrv-cluster/" 300 | }, 301 | "MasterUserPassword": "{{resolve:ssm-secure:RedshiftMainClusterMasterPasswordParameter:1}}", 302 | "MasterUsername": "foosrvroot", 303 | "NodeType": "ds2.xlarge", 304 | "NumberOfNodes": 2, 305 | "Port": 5439, 306 | "PreferredMaintenanceWindow": "fri:07:00-fri:07:30", 307 | "PubliclyAccessible": false, 308 | "VpcSecurityGroupIds": [ "FooSrvRsMainClusterSecurityGroupFakeId" ] 309 | }, 310 | "Type": "AWS::Redshift::Cluster", 311 | "UpdateReplacePolicy": "Retain" 312 | }, 313 | "FooSrvAuditLogsBucketPolicy": { 314 | "Condition": true, 315 | "Metadata": { 316 | "aws:cdk:path": "FooSrv/AuditLogsBucket/Policy/Resource" 317 | }, 318 | "Properties": { 319 | "Bucket": "AuditLogsBucket", 320 | "PolicyDocument": { 321 | "Statement": [ 322 | { 323 | "Action": [ 324 | "s3:PutObject*", 325 | "s3:Abort*" 326 | ], 327 | "Effect": "Allow", 328 | "Principal": { 329 | "AWS": "arn:aws:iam::222222222222:user/logs" 330 | }, 331 | "Resource": "arn:aws:s3:::beta-useast1-redshift-log/*" 332 | }, 333 | { 334 | "Action": "s3:GetBucketAcl", 335 | "Effect": "Allow", 336 | "Principal": { 337 | "AWS": "arn:aws:iam::222222222222:user/logs" 338 | }, 339 | "Resource": "arn:aws:s3:::my-unkonwn-random-bucket" 340 | } 341 | ], 342 | "Version": "2012-10-17" 343 | } 344 | }, 345 | "Type": "AWS::S3::BucketPolicy" 346 | }, 347 | "FooSrvRedshiftEnvironmentMainClusterMainClusterRoleDefaultPolicy": { 348 | "Condition": true, 349 | "Metadata": { 350 | "aws:cdk:path": "FooSrv/RedshiftEnvironmentMainCluster/MainClusterRole/DefaultPolicy/Resource" 351 | }, 352 | "Properties": { 353 | "PolicyDocument": { 354 | "Statement": [ 355 | { 356 | "Action": [ 357 | "s3:PutObject*", 358 | "s3:Abort*" 359 | ], 360 | "Effect": "Allow", 361 | "Resource": "arn:aws:s3:::beta-useast1-redshift-log/*" 362 | }, 363 | { 364 | "Action": [ 365 | "s3:GetObject*", 366 | "s3:GetBucket*", 367 | "s3:List*", 368 | "s3:DeleteObject*", 369 | "s3:PutObject*", 370 | "s3:Abort*" 371 | ], 372 | "Effect": "Allow", 373 | "Resource": [ 374 | "arn:aws:s3:::beta-useast1-redshift-log", 375 | "arn:aws:s3:::beta-useast1-redshift-log/*" 376 | ] 377 | } 378 | ], 379 | "Version": "2012-10-17" 380 | }, 381 | "PolicyName": "FooSrvRedshiftEnvironmentMainClusterMainClusterRoleDefaultPolicy", 382 | "Roles": [ 383 | "MainClusterRole" 384 | ] 385 | }, 386 | "Type": "AWS::IAM::Policy" 387 | }, 388 | "FooSrvQueueOldestMessageAgeHighWarningAlarm": { 389 | "Properties": { 390 | "AlarmDescription": "Example Alarm description", 391 | "AlarmName": "TestQueue-Beta.OldestMessageAgeHighWarning", 392 | "ComparisonOperator": "GreaterThanThreshold", 393 | "Dimensions": [ 394 | { 395 | "Name": "QueueName", 396 | "Value": "TestQueue-Beta" 397 | } 398 | ], 399 | "EvaluationPeriods": 1, 400 | "MetricName": "ApproximateAgeOfOldestMessage", 401 | "Namespace": "AWS/SQS", 402 | "Period": 300, 403 | "Statistic": "Maximum", 404 | "Threshold": 600 405 | }, 406 | "Type": "AWS::CloudWatch::Alarm" 407 | }, 408 | "FooSrvQueue": { 409 | "Properties": { 410 | "QueueName": "TestQueue-Beta" 411 | }, 412 | "Type": "AWS::SQS::Queue" 413 | }, 414 | "IdentityPool": { 415 | "Type": "AWS::Cognito::IdentityPool", 416 | "Properties": { 417 | "IdentityPoolName": "my id pool name" 418 | } 419 | }, 420 | "MyDynamoDbTable": { 421 | "Type": "AWS::DynamoDB::Table", 422 | "Properties": { 423 | "TableName": "MyDynamoDbTable-us-east-1-beta" 424 | } 425 | }, 426 | "MyDynamoDbTable2": { 427 | "Type": "AWS::DynamoDB::Table", 428 | "Properties": { 429 | "TableName": "MyDynamoDbTable-2" 430 | } 431 | }, 432 | "MyDynamoDbTable3": { 433 | "Type": "AWS::DynamoDB::Table", 434 | "Properties": { 435 | "TableName": "MyDynamoDbTable-Bananas" 436 | } 437 | } 438 | } 439 | } -------------------------------------------------------------------------------- /test/testData/stack1/expected/expected-us-east-1-prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "Conditions": { 3 | "CDKMetadataAvailable": true, 4 | "False": false, 5 | "IsNotUSEAST1": false, 6 | "IsNotProdUsEast1": false, 7 | "IsNotProdUsEast1AndNotUsWest2": false, 8 | "IsNotUSWEST2": true, 9 | "IsRegionUSEAST1": true, 10 | "IsRegionUSWEST2": false, 11 | "IsStageprod": true 12 | }, 13 | "Mappings": { 14 | "CondensedRegionNamesWithCapitalInitialLetter": { 15 | "us-east-1": { 16 | "Name": "UsEast1" 17 | }, 18 | "us-west-2": { 19 | "Name": "UsWest2" 20 | } 21 | }, 22 | "CondensedRegionUpperCase": { 23 | "us-east-1": { 24 | "Name": "USEAST1" 25 | }, 26 | "us-west-2": { 27 | "Name": "USWEST2" 28 | } 29 | }, 30 | "RedshiftAuditLoggingUserArn": { 31 | "us-east-1": { 32 | "UserArn": "arn:aws:iam::222222222222:user/logs" 33 | }, 34 | "us-west-2": { 35 | "UserArn": "arn:aws:iam::333333333333:user/logs" 36 | } 37 | }, 38 | "RedshiftNodeType": { 39 | "us-east-1": { 40 | "beta": "ds2.xlarge", 41 | "prod": "ds2.8xlarge" 42 | }, 43 | "us-west-2": { 44 | "beta": "ds2.xlarge", 45 | "prod": "ds2.xlarge" 46 | } 47 | }, 48 | "RedshiftNumberOfNodes": { 49 | "us-east-1": { 50 | "beta": 2, 51 | "prod": 4 52 | }, 53 | "us-west-2": { 54 | "beta": 2, 55 | "prod": 8 56 | } 57 | }, 58 | "RegionMap": { 59 | "us-east-1": { 60 | "condensedName": "useast1" 61 | }, 62 | "us-west-2": { 63 | "condensedName": "uswest2" 64 | } 65 | }, 66 | "StageNameWithCapitalInitialLetter": { 67 | "Name": { 68 | "beta": "Beta", 69 | "prod": "Prod" 70 | } 71 | } 72 | }, 73 | "Outputs": { 74 | "StackArn": { 75 | "Description": "Resource ARN of the Stack", 76 | "Value": "MyEvaluatedFakeStackUsEast1" 77 | } 78 | }, 79 | "Parameters": { 80 | "Stage": { 81 | "AllowedValues": [ 82 | "beta", 83 | "prod" 84 | ], 85 | "Default": "beta", 86 | "Description": "The stage in the CFN pipeline", 87 | "Type": "String" 88 | }, 89 | "Fruit": { 90 | "Default": "Bananas", 91 | "Description": "Favorite kind of fruit", 92 | "Type": "String" 93 | } 94 | }, 95 | "Resources": { 96 | "AuditLogsBucket": { 97 | "Condition": false, 98 | "DeletionPolicy": "Retain", 99 | "Metadata": { 100 | "aws:cdk:path": "FooSrv/AuditLogsBucket/Resource" 101 | }, 102 | "Properties": { 103 | "BucketEncryption": [ 104 | { 105 | "ServerSideEncryptionByDefault": { 106 | "SSEAlgorithm": "AES256" 107 | } 108 | } 109 | ], 110 | "BucketName": "redshift-log" 111 | }, 112 | "Type": "AWS::S3::Bucket", 113 | "UpdateReplacePolicy": "Retain" 114 | }, 115 | "CDKMetadata": { 116 | "Condition": true, 117 | "Properties": { 118 | "Modules": "aws-cdk=1.15.0,@aws-cdk/assets=1.15.0,@aws-cdk/aws-cloudwatch=1.15.0,@aws-cdk/aws-ec2=1.15.0,@aws-cdk/aws-events=1.15.0,@aws-cdk/aws-iam=1.15.0,@aws-cdk/aws-kms=1.15.0,@aws-cdk/aws-lambda=1.15.0,@aws-cdk/aws-logs=1.15.0,@aws-cdk/aws-redshift=1.15.0,@aws-cdk/aws-s3=1.15.0,@aws-cdk/aws-s3-assets=1.15.0,@aws-cdk/aws-secretsmanager=1.15.0,@aws-cdk/aws-sqs=1.15.0,@aws-cdk/aws-ssm=1.15.0,@aws-cdk/core=1.15.0,@aws-cdk/cx-api=1.15.0,@aws-cdk/region-info=1.15.0,jsii-runtime=Java/1.8.0_231" 119 | }, 120 | "Type": "AWS::CDK::Metadata" 121 | }, 122 | "ExampleWaitHandle": { 123 | "Properties": {}, 124 | "Type": "AWS::CloudFormation::WaitConditionHandle" 125 | }, 126 | "MainClusterCluster": { 127 | "Condition": false, 128 | "DeletionPolicy": "Retain", 129 | "DependsOn": [ 130 | "MainClusterSecret" 131 | ], 132 | "Metadata": { 133 | "aws:cdk:path": "FooSrv/RedshiftMainCluster/MainClusterCluster" 134 | }, 135 | "Properties": { 136 | "AllowVersionUpgrade": true, 137 | "AutomatedSnapshotRetentionPeriod": 30, 138 | "ClusterParameterGroupName": "MainClusterParamGroup", 139 | "ClusterSubnetGroupName": "MainClusterDefaultSubnetGroup", 140 | "ClusterType": "multi-node", 141 | "DBName": "foosrv", 142 | "Encrypted": true, 143 | "LoggingProperties": { 144 | "BucketName": "AuditLogsBucket", 145 | "S3KeyPrefix": "foosrv-cluster/" 146 | }, 147 | "MasterUserPassword": "{{resolve:secretsmanager:MainClusterSecret:SecretString:password::}}", 148 | "MasterUsername": "foosrvroot", 149 | "NodeType": "ds2.8xlarge", 150 | "NumberOfNodes": 4, 151 | "Port": 5439, 152 | "PreferredMaintenanceWindow": "fri:07:00-fri:07:30", 153 | "PubliclyAccessible": false, 154 | "VpcSecurityGroupIds": [ "FooSrvRsMainClusterSecurityGroupFakeId" ] 155 | }, 156 | "Type": "AWS::Redshift::Cluster", 157 | "UpdateReplacePolicy": "Retain" 158 | }, 159 | "MainClusterDefaultSubnetGroup": { 160 | "Condition": false, 161 | "Metadata": { 162 | "aws:cdk:path": "FooSrv/RedshiftEnvironmentMainCluster/MainClusterDefaultSubnetGroup" 163 | }, 164 | "Properties": { 165 | "Description": "Subnet group for FooSrv", 166 | "SubnetIds": [ "FooSrvSubnet1FakeId", "FooSrvSubnet2FakeId"] 167 | }, 168 | "Type": "AWS::Redshift::ClusterSubnetGroup" 169 | }, 170 | "MainClusterParamGroup": { 171 | "Condition": false, 172 | "Metadata": { 173 | "aws:cdk:path": "FooSrv/RedshiftEnvironmentMainCluster/MainClusterParamGroup" 174 | }, 175 | "Properties": { 176 | "Description": "Parameters for FooSrv Redshift MainCluster", 177 | "ParameterGroupFamily": "redshift-1.0", 178 | "Parameters": [ 179 | { 180 | "ParameterName": "datestyle", 181 | "ParameterValue": "ISO, MDY" 182 | }, 183 | { 184 | "ParameterName": "enable_user_activity_logging", 185 | "ParameterValue": "true" 186 | }, 187 | { 188 | "ParameterName": "extra_float_digits", 189 | "ParameterValue": "0" 190 | }, 191 | { 192 | "ParameterName": "require_ssl", 193 | "ParameterValue": "true" 194 | }, 195 | { 196 | "ParameterName": "search_path", 197 | "ParameterValue": "public" 198 | }, 199 | { 200 | "ParameterName": "statement_timeout", 201 | "ParameterValue": "0" 202 | }, 203 | { 204 | "ParameterName": "use_fips_ssl", 205 | "ParameterValue": "false" 206 | }, 207 | { 208 | "ParameterName": "wlm_json_configuration", 209 | "ParameterValue": "[{\"query_concurrency\":5}]" 210 | } 211 | ] 212 | }, 213 | "Type": "AWS::Redshift::ClusterParameterGroup" 214 | }, 215 | "MainClusterRole": { 216 | "Condition": false, 217 | "Metadata": { 218 | "aws:cdk:path": "FooSrv/RedshiftEnvironmentMainCluster/MainClusterRole/Resource" 219 | }, 220 | "Properties": { 221 | "AssumeRolePolicyDocument": { 222 | "Statement": [ 223 | { 224 | "Action": "sts:AssumeRole", 225 | "Effect": "Allow", 226 | "Principal": { 227 | "Service": "redshift.amazonaws.com" 228 | } 229 | } 230 | ], 231 | "Version": "2012-10-17" 232 | }, 233 | "RoleName": "MainClusterRoleProdUsEast1" 234 | }, 235 | "Type": "AWS::IAM::Role" 236 | }, 237 | "MainClusterSecret": { 238 | "Condition": true, 239 | "Metadata": { 240 | "aws:cdk:path": "FooSrv/MainClusterSecret/MainClusterSecretSecret/Resource" 241 | }, 242 | "Properties": { 243 | "Description": "Master secret for FooSrv Redshift MainCluster", 244 | "GenerateSecretString": { 245 | "ExcludePunctuation": true, 246 | "GenerateStringKey": "password", 247 | "IncludeSpace": false, 248 | "PasswordLength": 32, 249 | "SecretStringTemplate": "{\"username\": \"foosrvroot\"}" 250 | }, 251 | "Name": "MainClusterSecret" 252 | }, 253 | "Type": "AWS::SecretsManager::Secret" 254 | }, 255 | "MainClusterWithNoSecretManagerCluster": { 256 | "Condition": false, 257 | "DeletionPolicy": "Retain", 258 | "Metadata": { 259 | "aws:cdk:path": "FooSrv/RedshiftMainClusterWithNoSecretManager/MainClusterWithNoSecretManagerCluster" 260 | }, 261 | "Properties": { 262 | "AllowVersionUpgrade": true, 263 | "AutomatedSnapshotRetentionPeriod": 30, 264 | "ClusterParameterGroupName": "MainClusterParamGroup", 265 | "ClusterSubnetGroupName": "MainClusterDefaultSubnetGroup", 266 | "ClusterType": "multi-node", 267 | "DBName": "foosrv", 268 | "Encrypted": true, 269 | "LoggingProperties": { 270 | "BucketName": "AuditLogsBucket", 271 | "S3KeyPrefix": "foosrv-cluster/" 272 | }, 273 | "MasterUserPassword": "{{resolve:ssm-secure:RedshiftMainClusterMasterPasswordParameter:1}}", 274 | "MasterUsername": "foosrvroot", 275 | "NodeType": "ds2.8xlarge", 276 | "NumberOfNodes": 4, 277 | "Port": 5439, 278 | "PreferredMaintenanceWindow": "fri:07:00-fri:07:30", 279 | "PubliclyAccessible": false, 280 | "VpcSecurityGroupIds": [ "FooSrvRsMainClusterSecurityGroupFakeId" ] 281 | }, 282 | "Type": "AWS::Redshift::Cluster", 283 | "UpdateReplacePolicy": "Retain" 284 | }, 285 | "RedshiftMainClusterVPCSubnet1": { 286 | "Properties": { 287 | "AvailabilityZone": "us-east-1a", 288 | "CidrBlock": "10.0.0.0/20", 289 | "Tags": [ 290 | { 291 | "Key": "Name", 292 | "Value": "FooSrv/RedshiftMainClusterVPC/RedshiftMainClusterVPC/RedshiftMainClusterVPCSubnet1" 293 | } 294 | ], 295 | "VpcId": "RedshiftMainClusterVPC" 296 | }, 297 | "Type": "AWS::EC2::Subnet" 298 | }, 299 | "RedshiftMainClusterVPCSubnet2": { 300 | "Properties": { 301 | "AvailabilityZone": "us-east-1b", 302 | "CidrBlock": "10.0.16.0/20", 303 | "Tags": [ 304 | { 305 | "Key": "Name", 306 | "Value": "FooSrv/RedshiftMainClusterVPC/RedshiftMainClusterVPC/RedshiftMainClusterVPCSubnet2" 307 | } 308 | ], 309 | "VpcId": "RedshiftMainClusterVPC" 310 | }, 311 | "Type": "AWS::EC2::Subnet" 312 | }, 313 | "FooSrvAuditLogsBucketPolicy": { 314 | "Condition": false, 315 | "Metadata": { 316 | "aws:cdk:path": "FooSrv/AuditLogsBucket/Policy/Resource" 317 | }, 318 | "Properties": { 319 | "Bucket": "AuditLogsBucket", 320 | "PolicyDocument": { 321 | "Statement": [ 322 | { 323 | "Action": [ 324 | "s3:PutObject*", 325 | "s3:Abort*" 326 | ], 327 | "Effect": "Allow", 328 | "Principal": { 329 | "AWS": "arn:aws:iam::222222222222:user/logs" 330 | }, 331 | "Resource": "arn:aws:s3:::redshift-log/*" 332 | }, 333 | { 334 | "Action": "s3:GetBucketAcl", 335 | "Effect": "Allow", 336 | "Principal": { 337 | "AWS": "arn:aws:iam::222222222222:user/logs" 338 | }, 339 | "Resource": "arn:aws:s3:::my-unkonwn-random-bucket" 340 | } 341 | ], 342 | "Version": "2012-10-17" 343 | } 344 | }, 345 | "Type": "AWS::S3::BucketPolicy" 346 | }, 347 | "FooSrvRedshiftEnvironmentMainClusterMainClusterRoleDefaultPolicy": { 348 | "Condition": false, 349 | "Metadata": { 350 | "aws:cdk:path": "FooSrv/RedshiftEnvironmentMainCluster/MainClusterRole/DefaultPolicy/Resource" 351 | }, 352 | "Properties": { 353 | "PolicyDocument": { 354 | "Statement": [ 355 | { 356 | "Action": [ 357 | "s3:PutObject*", 358 | "s3:Abort*" 359 | ], 360 | "Effect": "Allow", 361 | "Resource": "arn:aws:s3:::redshift-log/*" 362 | }, 363 | { 364 | "Action": [ 365 | "s3:GetObject*", 366 | "s3:GetBucket*", 367 | "s3:List*", 368 | "s3:DeleteObject*", 369 | "s3:PutObject*", 370 | "s3:Abort*" 371 | ], 372 | "Effect": "Allow", 373 | "Resource": [ 374 | "arn:aws:s3:::redshift-log", 375 | "arn:aws:s3:::redshift-log/*" 376 | ] 377 | } 378 | ], 379 | "Version": "2012-10-17" 380 | }, 381 | "PolicyName": "FooSrvRedshiftEnvironmentMainClusterMainClusterRoleDefaultPolicy", 382 | "Roles": [ 383 | "MainClusterRole" 384 | ] 385 | }, 386 | "Type": "AWS::IAM::Policy" 387 | }, 388 | "FooSrvQueueOldestMessageAgeHighWarningAlarm": { 389 | "Properties": { 390 | "AlarmDescription": "Example Alarm description", 391 | "AlarmName": "TestQueue-Prod.OldestMessageAgeHighWarning", 392 | "ComparisonOperator": "GreaterThanThreshold", 393 | "Dimensions": [ 394 | { 395 | "Name": "QueueName", 396 | "Value": "TestQueue-Prod" 397 | } 398 | ], 399 | "EvaluationPeriods": 1, 400 | "MetricName": "ApproximateAgeOfOldestMessage", 401 | "Namespace": "AWS/SQS", 402 | "Period": 300, 403 | "Statistic": "Maximum", 404 | "Threshold": 600 405 | }, 406 | "Type": "AWS::CloudWatch::Alarm" 407 | }, 408 | "FooSrvQueue": { 409 | "Properties": { 410 | "QueueName": "TestQueue-Prod" 411 | }, 412 | "Type": "AWS::SQS::Queue" 413 | }, 414 | "IdentityPool": { 415 | "Type": "AWS::Cognito::IdentityPool", 416 | "Properties": { 417 | "IdentityPoolName": "my id pool name" 418 | } 419 | }, 420 | "MyDynamoDbTable": { 421 | "Type": "AWS::DynamoDB::Table", 422 | "Properties": { 423 | "TableName": "MyDynamoDbTable-us-east-1-prod" 424 | } 425 | }, 426 | "MyDynamoDbTable2": { 427 | "Type": "AWS::DynamoDB::Table", 428 | "Properties": { 429 | "TableName": "MyDynamoDbTable-2" 430 | } 431 | }, 432 | "MyDynamoDbTable3": { 433 | "Type": "AWS::DynamoDB::Table", 434 | "Properties": { 435 | "TableName": "MyDynamoDbTable-Oranges" 436 | } 437 | } 438 | } 439 | } -------------------------------------------------------------------------------- /test/testData/stack1/expected/expected-us-west-2-prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "Conditions": { 3 | "CDKMetadataAvailable": true, 4 | "False": false, 5 | "IsNotUSEAST1": true, 6 | "IsNotProdUsEast1": true, 7 | "IsNotProdUsEast1AndNotUsWest2": false, 8 | "IsNotUSWEST2": false, 9 | "IsRegionUSEAST1": false, 10 | "IsRegionUSWEST2": true, 11 | "IsStageprod": true 12 | }, 13 | "Mappings": { 14 | "CondensedRegionNamesWithCapitalInitialLetter": { 15 | "us-east-1": { 16 | "Name": "UsEast1" 17 | }, 18 | "us-west-2": { 19 | "Name": "UsWest2" 20 | } 21 | }, 22 | "CondensedRegionUpperCase": { 23 | "us-east-1": { 24 | "Name": "USEAST1" 25 | }, 26 | "us-west-2": { 27 | "Name": "USWEST2" 28 | } 29 | }, 30 | "RedshiftAuditLoggingUserArn": { 31 | "us-east-1": { 32 | "UserArn": "arn:aws:iam::222222222222:user/logs" 33 | }, 34 | "us-west-2": { 35 | "UserArn": "arn:aws:iam::333333333333:user/logs" 36 | } 37 | }, 38 | "RedshiftNodeType": { 39 | "us-east-1": { 40 | "beta": "ds2.xlarge", 41 | "prod": "ds2.8xlarge" 42 | }, 43 | "us-west-2": { 44 | "beta": "ds2.xlarge", 45 | "prod": "ds2.xlarge" 46 | } 47 | }, 48 | "RedshiftNumberOfNodes": { 49 | "us-east-1": { 50 | "beta": 2, 51 | "prod": 4 52 | }, 53 | "us-west-2": { 54 | "beta": 2, 55 | "prod": 8 56 | } 57 | }, 58 | "RegionMap": { 59 | "us-east-1": { 60 | "condensedName": "useast1" 61 | }, 62 | "us-west-2": { 63 | "condensedName": "uswest2" 64 | } 65 | }, 66 | "StageNameWithCapitalInitialLetter": { 67 | "Name": { 68 | "beta": "Beta", 69 | "prod": "Prod" 70 | } 71 | } 72 | }, 73 | "Outputs": { 74 | "StackArn": { 75 | "Description": "Resource ARN of the Stack", 76 | "Value": "MyEvaluatedFakeStackUsWest2" 77 | } 78 | }, 79 | "Parameters": { 80 | "Stage": { 81 | "AllowedValues": [ 82 | "beta", 83 | "prod" 84 | ], 85 | "Default": "beta", 86 | "Description": "The stage in the CFN pipeline", 87 | "Type": "String" 88 | }, 89 | "Fruit": { 90 | "Default": "Bananas", 91 | "Description": "Favorite kind of fruit", 92 | "Type": "String" 93 | } 94 | }, 95 | "Resources": { 96 | "AuditLogsBucket": { 97 | "Condition": true, 98 | "DeletionPolicy": "Retain", 99 | "Metadata": { 100 | "aws:cdk:path": "FooSrv/AuditLogsBucket/Resource" 101 | }, 102 | "Properties": { 103 | "BucketEncryption": [ 104 | { 105 | "ServerSideEncryptionByDefault": { 106 | "SSEAlgorithm": "AES256" 107 | } 108 | } 109 | ], 110 | "BucketName": "prod-uswest2-redshift-log" 111 | }, 112 | "Type": "AWS::S3::Bucket", 113 | "UpdateReplacePolicy": "Retain" 114 | }, 115 | "CDKMetadata": { 116 | "Condition": true, 117 | "Properties": { 118 | "Modules": "aws-cdk=1.15.0,@aws-cdk/assets=1.15.0,@aws-cdk/aws-cloudwatch=1.15.0,@aws-cdk/aws-ec2=1.15.0,@aws-cdk/aws-events=1.15.0,@aws-cdk/aws-iam=1.15.0,@aws-cdk/aws-kms=1.15.0,@aws-cdk/aws-lambda=1.15.0,@aws-cdk/aws-logs=1.15.0,@aws-cdk/aws-redshift=1.15.0,@aws-cdk/aws-s3=1.15.0,@aws-cdk/aws-s3-assets=1.15.0,@aws-cdk/aws-secretsmanager=1.15.0,@aws-cdk/aws-sqs=1.15.0,@aws-cdk/aws-ssm=1.15.0,@aws-cdk/core=1.15.0,@aws-cdk/cx-api=1.15.0,@aws-cdk/region-info=1.15.0,jsii-runtime=Java/1.8.0_231" 119 | }, 120 | "Type": "AWS::CDK::Metadata" 121 | }, 122 | "ExampleWaitHandle": { 123 | "Properties": {}, 124 | "Type": "AWS::CloudFormation::WaitConditionHandle" 125 | }, 126 | "MainClusterCluster": { 127 | "Condition": false, 128 | "DeletionPolicy": "Retain", 129 | "DependsOn": [ 130 | "MainClusterSecret" 131 | ], 132 | "Metadata": { 133 | "aws:cdk:path": "FooSrv/RedshiftMainCluster/MainClusterCluster" 134 | }, 135 | "Properties": { 136 | "AllowVersionUpgrade": true, 137 | "AutomatedSnapshotRetentionPeriod": 30, 138 | "ClusterParameterGroupName": "MainClusterParamGroup", 139 | "ClusterSubnetGroupName": "MainClusterDefaultSubnetGroup", 140 | "ClusterType": "multi-node", 141 | "DBName": "foosrv", 142 | "Encrypted": true, 143 | "LoggingProperties": { 144 | "BucketName": "AuditLogsBucket", 145 | "S3KeyPrefix": "foosrv-cluster/" 146 | }, 147 | "MasterUserPassword": "{{resolve:secretsmanager:MainClusterSecret:SecretString:password::}}", 148 | "MasterUsername": "foosrvroot", 149 | "NodeType": "ds2.xlarge", 150 | "NumberOfNodes": 8, 151 | "Port": 5439, 152 | "PreferredMaintenanceWindow": "fri:07:00-fri:07:30", 153 | "PubliclyAccessible": false, 154 | "VpcSecurityGroupIds": [ 155 | "FooSrvRsMainClusterSecurityGroupFakeId" 156 | ] 157 | }, 158 | "Type": "AWS::Redshift::Cluster", 159 | "UpdateReplacePolicy": "Retain" 160 | }, 161 | "MainClusterDefaultSubnetGroup": { 162 | "Condition": true, 163 | "Metadata": { 164 | "aws:cdk:path": "FooSrv/RedshiftEnvironmentMainCluster/MainClusterDefaultSubnetGroup" 165 | }, 166 | "Properties": { 167 | "Description": "Subnet group for FooSrv", 168 | "SubnetIds": [ "FooSrvSubnet1FakeId", "FooSrvSubnet2FakeId"] 169 | }, 170 | "Type": "AWS::Redshift::ClusterSubnetGroup" 171 | }, 172 | "MainClusterParamGroup": { 173 | "Condition": true, 174 | "Metadata": { 175 | "aws:cdk:path": "FooSrv/RedshiftEnvironmentMainCluster/MainClusterParamGroup" 176 | }, 177 | "Properties": { 178 | "Description": "Parameters for FooSrv Redshift MainCluster", 179 | "ParameterGroupFamily": "redshift-1.0", 180 | "Parameters": [ 181 | { 182 | "ParameterName": "datestyle", 183 | "ParameterValue": "ISO, MDY" 184 | }, 185 | { 186 | "ParameterName": "enable_user_activity_logging", 187 | "ParameterValue": "true" 188 | }, 189 | { 190 | "ParameterName": "extra_float_digits", 191 | "ParameterValue": "0" 192 | }, 193 | { 194 | "ParameterName": "require_ssl", 195 | "ParameterValue": "true" 196 | }, 197 | { 198 | "ParameterName": "search_path", 199 | "ParameterValue": "public" 200 | }, 201 | { 202 | "ParameterName": "statement_timeout", 203 | "ParameterValue": "0" 204 | }, 205 | { 206 | "ParameterName": "use_fips_ssl", 207 | "ParameterValue": "false" 208 | }, 209 | { 210 | "ParameterName": "wlm_json_configuration", 211 | "ParameterValue": "[{\"query_concurrency\":5}]" 212 | } 213 | ] 214 | }, 215 | "Type": "AWS::Redshift::ClusterParameterGroup" 216 | }, 217 | "MainClusterRole": { 218 | "Condition": true, 219 | "Metadata": { 220 | "aws:cdk:path": "FooSrv/RedshiftEnvironmentMainCluster/MainClusterRole/Resource" 221 | }, 222 | "Properties": { 223 | "AssumeRolePolicyDocument": { 224 | "Statement": [ 225 | { 226 | "Action": "sts:AssumeRole", 227 | "Effect": "Allow", 228 | "Principal": { 229 | "Service": "redshift.amazonaws.com" 230 | } 231 | } 232 | ], 233 | "Version": "2012-10-17" 234 | }, 235 | "RoleName": "MainClusterRoleProdUsWest2" 236 | }, 237 | "Type": "AWS::IAM::Role" 238 | }, 239 | "MainClusterSecret": { 240 | "Condition": false, 241 | "Metadata": { 242 | "aws:cdk:path": "FooSrv/MainClusterSecret/MainClusterSecretSecret/Resource" 243 | }, 244 | "Properties": { 245 | "Description": "Master secret for FooSrv Redshift MainCluster", 246 | "GenerateSecretString": { 247 | "ExcludePunctuation": true, 248 | "GenerateStringKey": "password", 249 | "IncludeSpace": false, 250 | "PasswordLength": 32, 251 | "SecretStringTemplate": "{\"username\": \"foosrvroot\"}" 252 | }, 253 | "Name": "MainClusterSecret" 254 | }, 255 | "Type": "AWS::SecretsManager::Secret" 256 | }, 257 | "MainClusterWithNoSecretManagerCluster": { 258 | "Condition": true, 259 | "DeletionPolicy": "Retain", 260 | "Metadata": { 261 | "aws:cdk:path": "FooSrv/RedshiftMainClusterWithNoSecretManager/MainClusterWithNoSecretManagerCluster" 262 | }, 263 | "Properties": { 264 | "AllowVersionUpgrade": true, 265 | "AutomatedSnapshotRetentionPeriod": 30, 266 | "ClusterParameterGroupName": "MainClusterParamGroup", 267 | "ClusterSubnetGroupName": "MainClusterDefaultSubnetGroup", 268 | "ClusterType": "multi-node", 269 | "DBName": "foosrv", 270 | "Encrypted": true, 271 | "LoggingProperties": { 272 | "BucketName": "AuditLogsBucket", 273 | "S3KeyPrefix": "foosrv-cluster/" 274 | }, 275 | "MasterUserPassword": "{{resolve:ssm-secure:RedshiftMainClusterMasterPasswordParameter:1}}", 276 | "MasterUsername": "foosrvroot", 277 | "NodeType": "ds2.xlarge", 278 | "NumberOfNodes": 8, 279 | "Port": 5439, 280 | "PreferredMaintenanceWindow": "fri:07:00-fri:07:30", 281 | "PubliclyAccessible": false, 282 | "VpcSecurityGroupIds": [ "FooSrvRsMainClusterSecurityGroupFakeId" ] 283 | }, 284 | "Type": "AWS::Redshift::Cluster", 285 | "UpdateReplacePolicy": "Retain" 286 | }, 287 | "RedshiftMainClusterVPCSubnet1": { 288 | "Properties": { 289 | "AvailabilityZone": "us-west-2a", 290 | "CidrBlock": "10.0.0.0/20", 291 | "Tags": [ 292 | { 293 | "Key": "Name", 294 | "Value": "FooSrv/RedshiftMainClusterVPC/RedshiftMainClusterVPC/RedshiftMainClusterVPCSubnet1" 295 | } 296 | ], 297 | "VpcId": "RedshiftMainClusterVPC" 298 | }, 299 | "Type": "AWS::EC2::Subnet" 300 | }, 301 | "RedshiftMainClusterVPCSubnet2": { 302 | "Properties": { 303 | "AvailabilityZone": "us-west-2b", 304 | "CidrBlock": "10.0.16.0/20", 305 | "Tags": [ 306 | { 307 | "Key": "Name", 308 | "Value": "FooSrv/RedshiftMainClusterVPC/RedshiftMainClusterVPC/RedshiftMainClusterVPCSubnet2" 309 | } 310 | ], 311 | "VpcId": "RedshiftMainClusterVPC" 312 | }, 313 | "Type": "AWS::EC2::Subnet" 314 | }, 315 | "FooSrvAuditLogsBucketPolicy": { 316 | "Condition": true, 317 | "Metadata": { 318 | "aws:cdk:path": "FooSrv/AuditLogsBucket/Policy/Resource" 319 | }, 320 | "Properties": { 321 | "Bucket": "AuditLogsBucket", 322 | "PolicyDocument": { 323 | "Statement": [ 324 | { 325 | "Action": [ 326 | "s3:PutObject*", 327 | "s3:Abort*" 328 | ], 329 | "Effect": "Allow", 330 | "Principal": { 331 | "AWS": "arn:aws:iam::333333333333:user/logs" 332 | }, 333 | "Resource": "arn:aws:s3:::prod-uswest2-redshift-log/*" 334 | }, 335 | { 336 | "Action": "s3:GetBucketAcl", 337 | "Effect": "Allow", 338 | "Principal": { 339 | "AWS": "arn:aws:iam::333333333333:user/logs" 340 | }, 341 | "Resource": "arn:aws:s3:::my-unkonwn-random-bucket" 342 | } 343 | ], 344 | "Version": "2012-10-17" 345 | } 346 | }, 347 | "Type": "AWS::S3::BucketPolicy" 348 | }, 349 | "FooSrvRedshiftEnvironmentMainClusterMainClusterRoleDefaultPolicy": { 350 | "Condition": true, 351 | "Metadata": { 352 | "aws:cdk:path": "FooSrv/RedshiftEnvironmentMainCluster/MainClusterRole/DefaultPolicy/Resource" 353 | }, 354 | "Properties": { 355 | "PolicyDocument": { 356 | "Statement": [ 357 | { 358 | "Action": [ 359 | "s3:PutObject*", 360 | "s3:Abort*" 361 | ], 362 | "Effect": "Allow", 363 | "Resource": "arn:aws:s3:::prod-uswest2-redshift-log/*" 364 | }, 365 | { 366 | "Action": [ 367 | "s3:GetObject*", 368 | "s3:GetBucket*", 369 | "s3:List*", 370 | "s3:DeleteObject*", 371 | "s3:PutObject*", 372 | "s3:Abort*" 373 | ], 374 | "Effect": "Allow", 375 | "Resource": [ 376 | "arn:aws:s3:::prod-uswest2-redshift-log", 377 | "arn:aws:s3:::prod-uswest2-redshift-log/*" 378 | ] 379 | } 380 | ], 381 | "Version": "2012-10-17" 382 | }, 383 | "PolicyName": "FooSrvRedshiftEnvironmentMainClusterMainClusterRoleDefaultPolicy", 384 | "Roles": [ 385 | "MainClusterRole" 386 | ] 387 | }, 388 | "Type": "AWS::IAM::Policy" 389 | }, 390 | "FooSrvQueueOldestMessageAgeHighWarningAlarm": { 391 | "Properties": { 392 | "AlarmDescription": "Example Alarm description", 393 | "AlarmName": "TestQueue-Prod.OldestMessageAgeHighWarning", 394 | "ComparisonOperator": "GreaterThanThreshold", 395 | "Dimensions": [ 396 | { 397 | "Name": "QueueName", 398 | "Value": "TestQueue-Prod" 399 | } 400 | ], 401 | "EvaluationPeriods": 1, 402 | "MetricName": "ApproximateAgeOfOldestMessage", 403 | "Namespace": "AWS/SQS", 404 | "Period": 300, 405 | "Statistic": "Maximum", 406 | "Threshold": 600 407 | }, 408 | "Type": "AWS::CloudWatch::Alarm" 409 | }, 410 | "FooSrvQueue": { 411 | "Properties": { 412 | "QueueName": "TestQueue-Prod" 413 | }, 414 | "Type": "AWS::SQS::Queue" 415 | }, 416 | "IdentityPool": { 417 | "Type": "AWS::Cognito::IdentityPool", 418 | "Properties": { 419 | "IdentityPoolName": "my id pool name" 420 | } 421 | }, 422 | "MyDynamoDbTable": { 423 | "Type": "AWS::DynamoDB::Table", 424 | "Properties": { 425 | "TableName": "MyDynamoDbTable-us-west-2-prod" 426 | } 427 | }, 428 | "MyDynamoDbTable2": { 429 | "Type": "AWS::DynamoDB::Table", 430 | "Properties": { 431 | "TableName": "MyDynamoDbTable-2" 432 | } 433 | }, 434 | "MyDynamoDbTable3": { 435 | "Type": "AWS::DynamoDB::Table", 436 | "Properties": { 437 | "TableName": "MyDynamoDbTable-Apples" 438 | } 439 | } 440 | } 441 | } -------------------------------------------------------------------------------- /test/testData/stack1/params/params-us-east-1-beta.json: -------------------------------------------------------------------------------- 1 | { 2 | "RefResolvers": { 3 | "AWS::Region": "us-east-1", 4 | "AWS::Partition": "aws", 5 | "AWS::AccountId": "123123123123", 6 | "Stage": "beta", 7 | "AWS::StackId": "MyEvaluatedFakeStackUsEast1" 8 | }, 9 | "Fn::GetAttResolvers": { 10 | "MyUnknownResource1": { 11 | "Arn": "arn:aws:s3:::my-unkonwn-random-bucket" 12 | } 13 | }, 14 | "Fn::ImportValueResolvers": { 15 | "RedshiftMainClusterSecurityGroup": "FooSrvRsMainClusterSecurityGroupFakeId", 16 | "RedshiftMainClusterSubnet1": "FooSrvSubnet1FakeId", 17 | "RedshiftMainClusterSubnet2": "FooSrvSubnet2FakeId" 18 | }, 19 | "ArnSchemas": { 20 | "AWS::S3::Bucket": "arn:${Partition}:s3:::${BucketName}" 21 | } 22 | } -------------------------------------------------------------------------------- /test/testData/stack1/params/params-us-east-1-prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "RefResolvers": { 3 | "AWS::Region": "us-east-1", 4 | "AWS::Partition": "aws", 5 | "AWS::AccountId": "666666666666", 6 | "Stage": "prod", 7 | "AWS::StackId": "MyEvaluatedFakeStackUsEast1", 8 | "Fruit": "Oranges" 9 | }, 10 | "Fn::ImportValueResolvers": { 11 | "RedshiftMainClusterSecurityGroup": "FooSrvRsMainClusterSecurityGroupFakeId", 12 | "RedshiftMainClusterSubnet1": "FooSrvSubnet1FakeId", 13 | "RedshiftMainClusterSubnet2": "FooSrvSubnet2FakeId" 14 | }, 15 | "Fn::GetAttResolvers": { 16 | "MyUnknownResource1": { 17 | "Arn": "arn:aws:s3:::my-unkonwn-random-bucket" 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /test/testData/stack1/params/params-us-west-2-prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "RefResolvers": 3 | { 4 | "AWS::Region": "us-west-2", 5 | "AWS::Partition": "aws", 6 | "AWS::AccountId": "000000111111", 7 | "Stage": "prod", 8 | "AWS::StackId": "MyEvaluatedFakeStackUsWest2", 9 | "Fruit": "Apples" 10 | }, 11 | "Fn::ImportValueResolvers": { 12 | "RedshiftMainClusterSecurityGroup": "FooSrvRsMainClusterSecurityGroupFakeId", 13 | "RedshiftMainClusterSubnet1": "FooSrvSubnet1FakeId", 14 | "RedshiftMainClusterSubnet2": "FooSrvSubnet2FakeId" 15 | }, 16 | "Fn::GetAttResolvers": { 17 | "MyUnknownResource1": { 18 | "Arn": "arn:aws:s3:::my-unkonwn-random-bucket" 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /test/testData/stack1/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Conditions": { 3 | "CDKMetadataAvailable": { 4 | "Fn::Or": [ 5 | { 6 | "Fn::Or": [ 7 | { 8 | "Fn::Equals": [ 9 | { 10 | "Ref": "AWS::Region" 11 | }, 12 | "ap-east-1" 13 | ] 14 | }, 15 | { 16 | "Fn::Equals": [ 17 | { 18 | "Ref": "AWS::Region" 19 | }, 20 | "ap-northeast-1" 21 | ] 22 | }, 23 | { 24 | "Fn::Equals": [ 25 | { 26 | "Ref": "AWS::Region" 27 | }, 28 | "ap-northeast-2" 29 | ] 30 | }, 31 | { 32 | "Fn::Equals": [ 33 | { 34 | "Ref": "AWS::Region" 35 | }, 36 | "ap-south-1" 37 | ] 38 | }, 39 | { 40 | "Fn::Equals": [ 41 | { 42 | "Ref": "AWS::Region" 43 | }, 44 | "ap-southeast-1" 45 | ] 46 | }, 47 | { 48 | "Fn::Equals": [ 49 | { 50 | "Ref": "AWS::Region" 51 | }, 52 | "ap-southeast-2" 53 | ] 54 | }, 55 | { 56 | "Fn::Equals": [ 57 | { 58 | "Ref": "AWS::Region" 59 | }, 60 | "ca-central-1" 61 | ] 62 | }, 63 | { 64 | "Fn::Equals": [ 65 | { 66 | "Ref": "AWS::Region" 67 | }, 68 | "cn-north-1" 69 | ] 70 | }, 71 | { 72 | "Fn::Equals": [ 73 | { 74 | "Ref": "AWS::Region" 75 | }, 76 | "cn-northwest-1" 77 | ] 78 | }, 79 | { 80 | "Fn::Equals": [ 81 | { 82 | "Ref": "AWS::Region" 83 | }, 84 | "eu-central-1" 85 | ] 86 | } 87 | ] 88 | }, 89 | { 90 | "Fn::Or": [ 91 | { 92 | "Fn::Equals": [ 93 | { 94 | "Ref": "AWS::Region" 95 | }, 96 | "eu-north-1" 97 | ] 98 | }, 99 | { 100 | "Fn::Equals": [ 101 | { 102 | "Ref": "AWS::Region" 103 | }, 104 | "eu-west-1" 105 | ] 106 | }, 107 | { 108 | "Fn::Equals": [ 109 | { 110 | "Ref": "AWS::Region" 111 | }, 112 | "eu-west-2" 113 | ] 114 | }, 115 | { 116 | "Fn::Equals": [ 117 | { 118 | "Ref": "AWS::Region" 119 | }, 120 | "eu-west-3" 121 | ] 122 | }, 123 | { 124 | "Fn::Equals": [ 125 | { 126 | "Ref": "AWS::Region" 127 | }, 128 | "me-south-1" 129 | ] 130 | }, 131 | { 132 | "Fn::Equals": [ 133 | { 134 | "Ref": "AWS::Region" 135 | }, 136 | "sa-east-1" 137 | ] 138 | }, 139 | { 140 | "Fn::Equals": [ 141 | { 142 | "Ref": "AWS::Region" 143 | }, 144 | "us-east-1" 145 | ] 146 | }, 147 | { 148 | "Fn::Equals": [ 149 | { 150 | "Ref": "AWS::Region" 151 | }, 152 | "us-east-2" 153 | ] 154 | }, 155 | { 156 | "Fn::Equals": [ 157 | { 158 | "Ref": "AWS::Region" 159 | }, 160 | "us-west-1" 161 | ] 162 | }, 163 | { 164 | "Fn::Equals": [ 165 | { 166 | "Ref": "AWS::Region" 167 | }, 168 | "us-west-2" 169 | ] 170 | } 171 | ] 172 | } 173 | ] 174 | }, 175 | "False": { 176 | "Fn::Equals": [ 177 | 1, 178 | 2 179 | ] 180 | }, 181 | "IsNotUSEAST1": { 182 | "Fn::Not": [ 183 | { 184 | "Condition": "IsRegionUSEAST1" 185 | } 186 | ] 187 | }, 188 | "IsNotProdUsEast1": { 189 | "Fn::Not": [ 190 | { 191 | "Fn::And": [ 192 | { 193 | "Condition": "IsStageprod" 194 | }, 195 | { 196 | "Condition": "IsRegionUSEAST1" 197 | } 198 | ] 199 | } 200 | ] 201 | }, 202 | "IsNotProdUsEast1AndNotUsWest2": { 203 | "Fn::And": [ 204 | { 205 | "Condition": "IsNotProdUsEast1" 206 | }, 207 | { 208 | "Condition": "IsNotUSWEST2" 209 | } 210 | ] 211 | }, 212 | "IsNotUSWEST2": { 213 | "Fn::Not": [ 214 | { 215 | "Condition": "IsRegionUSWEST2" 216 | } 217 | ] 218 | }, 219 | "IsRegionUSEAST1": { 220 | "Fn::Equals": [ 221 | { 222 | "Ref": "AWS::Region" 223 | }, 224 | "us-east-1" 225 | ] 226 | }, 227 | "IsRegionUSWEST2": { 228 | "Fn::Equals": [ 229 | { 230 | "Ref": "AWS::Region" 231 | }, 232 | "us-west-2" 233 | ] 234 | }, 235 | "IsStageprod": { 236 | "Fn::Equals": [ 237 | { 238 | "Ref": "Stage" 239 | }, 240 | "prod" 241 | ] 242 | } 243 | }, 244 | "Mappings": { 245 | "CondensedRegionNamesWithCapitalInitialLetter": { 246 | "us-east-1": { 247 | "Name": "UsEast1" 248 | }, 249 | "us-west-2": { 250 | "Name": "UsWest2" 251 | } 252 | }, 253 | "CondensedRegionUpperCase": { 254 | "us-east-1": { 255 | "Name": "USEAST1" 256 | }, 257 | "us-west-2": { 258 | "Name": "USWEST2" 259 | } 260 | }, 261 | "RedshiftAuditLoggingUserArn": { 262 | "us-east-1": { 263 | "UserArn": "arn:aws:iam::222222222222:user/logs" 264 | }, 265 | "us-west-2": { 266 | "UserArn": "arn:aws:iam::333333333333:user/logs" 267 | } 268 | }, 269 | "RedshiftNodeType": { 270 | "us-east-1": { 271 | "beta": "ds2.xlarge", 272 | "prod": "ds2.8xlarge" 273 | }, 274 | "us-west-2": { 275 | "beta": "ds2.xlarge", 276 | "prod": "ds2.xlarge" 277 | } 278 | }, 279 | "RedshiftNumberOfNodes": { 280 | "us-east-1": { 281 | "beta": 2, 282 | "prod": 4 283 | }, 284 | "us-west-2": { 285 | "beta": 2, 286 | "prod": 8 287 | } 288 | }, 289 | "RegionMap": { 290 | "us-east-1": { 291 | "condensedName": "useast1" 292 | }, 293 | "us-west-2": { 294 | "condensedName": "uswest2" 295 | } 296 | }, 297 | "StageNameWithCapitalInitialLetter": { 298 | "Name": { 299 | "beta": "Beta", 300 | "prod": "Prod" 301 | } 302 | } 303 | }, 304 | "Outputs": { 305 | "StackArn": { 306 | "Description": "Resource ARN of the Stack", 307 | "Value": { 308 | "Ref": "AWS::StackId" 309 | } 310 | } 311 | }, 312 | "Parameters": { 313 | "Stage": { 314 | "AllowedValues": [ 315 | "beta", 316 | "prod" 317 | ], 318 | "Default": "beta", 319 | "Description": "The stage in the CFN pipeline", 320 | "Type": "String" 321 | }, 322 | "Fruit": { 323 | "Default": "Bananas", 324 | "Description": "Favorite kind of fruit", 325 | "Type": "String" 326 | } 327 | }, 328 | "Resources": { 329 | "AuditLogsBucket": { 330 | "Condition": "IsNotProdUsEast1", 331 | "DeletionPolicy": "Retain", 332 | "Metadata": { 333 | "aws:cdk:path": "FooSrv/AuditLogsBucket/Resource" 334 | }, 335 | "Properties": { 336 | "BucketEncryption": { 337 | "ServerSideEncryptionConfiguration": [ 338 | { 339 | "ServerSideEncryptionByDefault": { 340 | "SSEAlgorithm": "AES256" 341 | } 342 | } 343 | ] 344 | }, 345 | "BucketName": { 346 | "Fn::Join": [ 347 | "", 348 | [ 349 | "", 350 | { 351 | "Fn::If": [ 352 | "IsRegionUSEAST1", 353 | { 354 | "Fn::If": [ 355 | "IsStageprod", 356 | "", 357 | { 358 | "Fn::Join": [ 359 | "", 360 | [ 361 | { 362 | "Ref": "Stage" 363 | }, 364 | "-", 365 | { 366 | "Fn::FindInMap": [ 367 | "RegionMap", 368 | { 369 | "Ref": "AWS::Region" 370 | }, 371 | "condensedName" 372 | ] 373 | }, 374 | "-" 375 | ] 376 | ] 377 | } 378 | ] 379 | }, 380 | { 381 | "Fn::Join": [ 382 | "", 383 | [ 384 | { 385 | "Ref": "Stage" 386 | }, 387 | "-", 388 | { 389 | "Fn::FindInMap": [ 390 | "RegionMap", 391 | { 392 | "Ref": "AWS::Region" 393 | }, 394 | "condensedName" 395 | ] 396 | }, 397 | "-" 398 | ] 399 | ] 400 | } 401 | ] 402 | }, 403 | "redshift-log" 404 | ] 405 | ] 406 | } 407 | }, 408 | "Type": "AWS::S3::Bucket", 409 | "UpdateReplacePolicy": "Retain" 410 | }, 411 | "CDKMetadata": { 412 | "Condition": "CDKMetadataAvailable", 413 | "Properties": { 414 | "Modules": "aws-cdk=1.15.0,@aws-cdk/assets=1.15.0,@aws-cdk/aws-cloudwatch=1.15.0,@aws-cdk/aws-ec2=1.15.0,@aws-cdk/aws-events=1.15.0,@aws-cdk/aws-iam=1.15.0,@aws-cdk/aws-kms=1.15.0,@aws-cdk/aws-lambda=1.15.0,@aws-cdk/aws-logs=1.15.0,@aws-cdk/aws-redshift=1.15.0,@aws-cdk/aws-s3=1.15.0,@aws-cdk/aws-s3-assets=1.15.0,@aws-cdk/aws-secretsmanager=1.15.0,@aws-cdk/aws-sqs=1.15.0,@aws-cdk/aws-ssm=1.15.0,@aws-cdk/core=1.15.0,@aws-cdk/cx-api=1.15.0,@aws-cdk/region-info=1.15.0,jsii-runtime=Java/1.8.0_231" 415 | }, 416 | "Type": "AWS::CDK::Metadata" 417 | }, 418 | "ExampleWaitHandle": { 419 | "Properties": {}, 420 | "Type": "AWS::CloudFormation::WaitConditionHandle" 421 | }, 422 | "MainClusterCluster": { 423 | "Condition": "IsNotProdUsEast1AndNotUsWest2", 424 | "DeletionPolicy": "Retain", 425 | "DependsOn": [ 426 | "MainClusterSecret" 427 | ], 428 | "Metadata": { 429 | "aws:cdk:path": "FooSrv/RedshiftMainCluster/MainClusterCluster" 430 | }, 431 | "Properties": { 432 | "AllowVersionUpgrade": true, 433 | "AutomatedSnapshotRetentionPeriod": 30, 434 | "ClusterParameterGroupName": { 435 | "Ref": "MainClusterParamGroup" 436 | }, 437 | "ClusterSubnetGroupName": { 438 | "Ref": "MainClusterDefaultSubnetGroup" 439 | }, 440 | "ClusterType": "multi-node", 441 | "DBName": "foosrv", 442 | "Encrypted": true, 443 | "LoggingProperties": { 444 | "BucketName": { 445 | "Ref": "AuditLogsBucket" 446 | }, 447 | "S3KeyPrefix": "foosrv-cluster/" 448 | }, 449 | "MasterUserPassword": { 450 | "Fn::Join": [ 451 | "", 452 | [ 453 | "{{resolve:secretsmanager:", 454 | { 455 | "Ref": "MainClusterSecret" 456 | }, 457 | ":SecretString:password::}}" 458 | ] 459 | ] 460 | }, 461 | "MasterUsername": "foosrvroot", 462 | "NodeType": { 463 | "Fn::FindInMap": [ 464 | "RedshiftNodeType", 465 | { 466 | "Ref": "AWS::Region" 467 | }, 468 | { 469 | "Ref": "Stage" 470 | } 471 | ] 472 | }, 473 | "NumberOfNodes": { 474 | "Fn::FindInMap": [ 475 | "RedshiftNumberOfNodes", 476 | { 477 | "Ref": "AWS::Region" 478 | }, 479 | { 480 | "Ref": "Stage" 481 | } 482 | ] 483 | }, 484 | "Port": 5439, 485 | "PreferredMaintenanceWindow": "fri:07:00-fri:07:30", 486 | "PubliclyAccessible": false, 487 | "VpcSecurityGroupIds": [ 488 | { 489 | "Fn::ImportValue": "RedshiftMainClusterSecurityGroup" 490 | } 491 | ] 492 | }, 493 | "Type": "AWS::Redshift::Cluster", 494 | "UpdateReplacePolicy": "Retain" 495 | }, 496 | "MainClusterDefaultSubnetGroup": { 497 | "Condition": "IsNotProdUsEast1", 498 | "Metadata": { 499 | "aws:cdk:path": "FooSrv/RedshiftEnvironmentMainCluster/MainClusterDefaultSubnetGroup" 500 | }, 501 | "Properties": { 502 | "Description": "Subnet group for FooSrv", 503 | "SubnetIds": [ 504 | { 505 | "Fn::ImportValue": "RedshiftMainClusterSubnet1" 506 | }, 507 | { 508 | "Fn::ImportValue": "RedshiftMainClusterSubnet2" 509 | } 510 | ] 511 | }, 512 | "Type": "AWS::Redshift::ClusterSubnetGroup" 513 | }, 514 | "MainClusterParamGroup": { 515 | "Condition": "IsNotProdUsEast1", 516 | "Metadata": { 517 | "aws:cdk:path": "FooSrv/RedshiftEnvironmentMainCluster/MainClusterParamGroup" 518 | }, 519 | "Properties": { 520 | "Description": "Parameters for FooSrv Redshift MainCluster", 521 | "ParameterGroupFamily": "redshift-1.0", 522 | "Parameters": [ 523 | { 524 | "ParameterName": "datestyle", 525 | "ParameterValue": "ISO, MDY" 526 | }, 527 | { 528 | "ParameterName": "enable_user_activity_logging", 529 | "ParameterValue": "true" 530 | }, 531 | { 532 | "ParameterName": "extra_float_digits", 533 | "ParameterValue": "0" 534 | }, 535 | { 536 | "ParameterName": "require_ssl", 537 | "ParameterValue": "true" 538 | }, 539 | { 540 | "ParameterName": "search_path", 541 | "ParameterValue": "public" 542 | }, 543 | { 544 | "ParameterName": "statement_timeout", 545 | "ParameterValue": "0" 546 | }, 547 | { 548 | "ParameterName": "use_fips_ssl", 549 | "ParameterValue": "false" 550 | }, 551 | { 552 | "ParameterName": "wlm_json_configuration", 553 | "ParameterValue": "[{\"query_concurrency\":5}]" 554 | } 555 | ] 556 | }, 557 | "Type": "AWS::Redshift::ClusterParameterGroup" 558 | }, 559 | "MainClusterRole": { 560 | "Condition": "IsNotProdUsEast1", 561 | "Metadata": { 562 | "aws:cdk:path": "FooSrv/RedshiftEnvironmentMainCluster/MainClusterRole/Resource" 563 | }, 564 | "Properties": { 565 | "AssumeRolePolicyDocument": { 566 | "Statement": [ 567 | { 568 | "Action": "sts:AssumeRole", 569 | "Effect": "Allow", 570 | "Principal": { 571 | "Service": "redshift.amazonaws.com" 572 | } 573 | } 574 | ], 575 | "Version": "2012-10-17" 576 | }, 577 | "RoleName": { 578 | "Fn::Join": [ 579 | "", 580 | [ 581 | "MainClusterRole", 582 | { 583 | "Fn::FindInMap": [ 584 | "StageNameWithCapitalInitialLetter", 585 | "Name", 586 | { 587 | "Ref": "Stage" 588 | } 589 | ] 590 | }, 591 | { 592 | "Fn::FindInMap": [ 593 | "CondensedRegionNamesWithCapitalInitialLetter", 594 | { 595 | "Ref": "AWS::Region" 596 | }, 597 | "Name" 598 | ] 599 | } 600 | ] 601 | ] 602 | } 603 | }, 604 | "Type": "AWS::IAM::Role" 605 | }, 606 | "MainClusterSecret": { 607 | "Condition": "IsNotUSWEST2", 608 | "Metadata": { 609 | "aws:cdk:path": "FooSrv/MainClusterSecret/MainClusterSecretSecret/Resource" 610 | }, 611 | "Properties": { 612 | "Description": "Master secret for FooSrv Redshift MainCluster", 613 | "GenerateSecretString": { 614 | "ExcludePunctuation": true, 615 | "GenerateStringKey": "password", 616 | "IncludeSpace": false, 617 | "PasswordLength": 32, 618 | "SecretStringTemplate": "{\"username\": \"foosrvroot\"}" 619 | }, 620 | "Name": "MainClusterSecret" 621 | }, 622 | "Type": "AWS::SecretsManager::Secret" 623 | }, 624 | "MainClusterWithNoSecretManagerCluster": { 625 | "Condition": "IsRegionUSWEST2", 626 | "DeletionPolicy": "Retain", 627 | "Metadata": { 628 | "aws:cdk:path": "FooSrv/RedshiftMainClusterWithNoSecretManager/MainClusterWithNoSecretManagerCluster" 629 | }, 630 | "Properties": { 631 | "AllowVersionUpgrade": true, 632 | "AutomatedSnapshotRetentionPeriod": 30, 633 | "ClusterParameterGroupName": { 634 | "Ref": "MainClusterParamGroup" 635 | }, 636 | "ClusterSubnetGroupName": { 637 | "Ref": "MainClusterDefaultSubnetGroup" 638 | }, 639 | "ClusterType": "multi-node", 640 | "DBName": "foosrv", 641 | "Encrypted": true, 642 | "LoggingProperties": { 643 | "BucketName": { 644 | "Ref": "AuditLogsBucket" 645 | }, 646 | "S3KeyPrefix": "foosrv-cluster/" 647 | }, 648 | "MasterUserPassword": "{{resolve:ssm-secure:RedshiftMainClusterMasterPasswordParameter:1}}", 649 | "MasterUsername": "foosrvroot", 650 | "NodeType": { 651 | "Fn::FindInMap": [ 652 | "RedshiftNodeType", 653 | { 654 | "Ref": "AWS::Region" 655 | }, 656 | { 657 | "Ref": "Stage" 658 | } 659 | ] 660 | }, 661 | "NumberOfNodes": { 662 | "Fn::FindInMap": [ 663 | "RedshiftNumberOfNodes", 664 | { 665 | "Ref": "AWS::Region" 666 | }, 667 | { 668 | "Ref": "Stage" 669 | } 670 | ] 671 | }, 672 | "Port": 5439, 673 | "PreferredMaintenanceWindow": "fri:07:00-fri:07:30", 674 | "PubliclyAccessible": false, 675 | "VpcSecurityGroupIds": [ 676 | { 677 | "Fn::ImportValue": "RedshiftMainClusterSecurityGroup" 678 | } 679 | ] 680 | }, 681 | "Type": "AWS::Redshift::Cluster", 682 | "UpdateReplacePolicy": "Retain" 683 | }, 684 | "FooSrvAuditLogsBucketPolicy": { 685 | "Condition": "IsNotProdUsEast1", 686 | "Metadata": { 687 | "aws:cdk:path": "FooSrv/AuditLogsBucket/Policy/Resource" 688 | }, 689 | "Properties": { 690 | "Bucket": { 691 | "Ref": "AuditLogsBucket" 692 | }, 693 | "PolicyDocument": { 694 | "Statement": [ 695 | { 696 | "Action": [ 697 | "s3:PutObject*", 698 | "s3:Abort*" 699 | ], 700 | "Effect": "Allow", 701 | "Principal": { 702 | "AWS": { 703 | "Fn::FindInMap": [ 704 | "RedshiftAuditLoggingUserArn", 705 | { 706 | "Ref": "AWS::Region" 707 | }, 708 | "UserArn" 709 | ] 710 | } 711 | }, 712 | "Resource": { 713 | "Fn::Join": [ 714 | "", 715 | [ 716 | { 717 | "Fn::GetAtt": [ 718 | "AuditLogsBucket", 719 | "Arn" 720 | ] 721 | }, 722 | "/*" 723 | ] 724 | ] 725 | } 726 | }, 727 | { 728 | "Action": "s3:GetBucketAcl", 729 | "Effect": "Allow", 730 | "Principal": { 731 | "AWS": { 732 | "Fn::FindInMap": [ 733 | "RedshiftAuditLoggingUserArn", 734 | { 735 | "Ref": "AWS::Region" 736 | }, 737 | "UserArn" 738 | ] 739 | } 740 | }, 741 | "Resource": { 742 | "Fn::GetAtt": [ 743 | "MyUnknownResource1", 744 | "Arn" 745 | ] 746 | } 747 | } 748 | ], 749 | "Version": "2012-10-17" 750 | } 751 | }, 752 | "Type": "AWS::S3::BucketPolicy" 753 | }, 754 | "RedshiftMainClusterVPCSubnet1": { 755 | "Properties": { 756 | "AvailabilityZone": { 757 | "Fn::Select": [ 758 | 0, 759 | { 760 | "Fn::GetAZs": "" 761 | } 762 | ] 763 | }, 764 | "CidrBlock": "10.0.0.0/20", 765 | "Tags": [ 766 | { 767 | "Key": "Name", 768 | "Value": "FooSrv/RedshiftMainClusterVPC/RedshiftMainClusterVPC/RedshiftMainClusterVPCSubnet1" 769 | } 770 | ], 771 | "VpcId": { 772 | "Ref": "RedshiftMainClusterVPC" 773 | } 774 | }, 775 | "Type": "AWS::EC2::Subnet" 776 | }, 777 | "RedshiftMainClusterVPCSubnet2": { 778 | "Properties": { 779 | "AvailabilityZone": { 780 | "Fn::Select": [ 781 | 1, 782 | { 783 | "Fn::GetAZs": "" 784 | } 785 | ] 786 | }, 787 | "CidrBlock": "10.0.16.0/20", 788 | "Tags": [ 789 | { 790 | "Key": "Name", 791 | "Value": "FooSrv/RedshiftMainClusterVPC/RedshiftMainClusterVPC/RedshiftMainClusterVPCSubnet2" 792 | } 793 | ], 794 | "VpcId": { 795 | "Ref": "RedshiftMainClusterVPC" 796 | } 797 | }, 798 | "Type": "AWS::EC2::Subnet" 799 | }, 800 | "FooSrvRedshiftEnvironmentMainClusterMainClusterRoleDefaultPolicy": { 801 | "Condition": "IsNotProdUsEast1", 802 | "Metadata": { 803 | "aws:cdk:path": "FooSrv/RedshiftEnvironmentMainCluster/MainClusterRole/DefaultPolicy/Resource" 804 | }, 805 | "Properties": { 806 | "PolicyDocument": { 807 | "Statement": [ 808 | { 809 | "Action": [ 810 | "s3:PutObject*", 811 | "s3:Abort*" 812 | ], 813 | "Effect": "Allow", 814 | "Resource": { 815 | "Fn::Join": [ 816 | "", 817 | [ 818 | { 819 | "Fn::GetAtt": [ 820 | "AuditLogsBucket", 821 | "Arn" 822 | ] 823 | }, 824 | "/*" 825 | ] 826 | ] 827 | } 828 | }, 829 | { 830 | "Action": [ 831 | "s3:GetObject*", 832 | "s3:GetBucket*", 833 | "s3:List*", 834 | "s3:DeleteObject*", 835 | "s3:PutObject*", 836 | "s3:Abort*" 837 | ], 838 | "Effect": "Allow", 839 | "Resource": [ 840 | { 841 | "Fn::GetAtt": [ 842 | "AuditLogsBucket", 843 | "Arn" 844 | ] 845 | }, 846 | { 847 | "Fn::Join": [ 848 | "", 849 | [ 850 | { 851 | "Fn::GetAtt": [ 852 | "AuditLogsBucket", 853 | "Arn" 854 | ] 855 | }, 856 | "/*" 857 | ] 858 | ] 859 | } 860 | ] 861 | } 862 | ], 863 | "Version": "2012-10-17" 864 | }, 865 | "PolicyName": "FooSrvRedshiftEnvironmentMainClusterMainClusterRoleDefaultPolicy", 866 | "Roles": [ 867 | { 868 | "Ref": "MainClusterRole" 869 | } 870 | ] 871 | }, 872 | "Type": "AWS::IAM::Policy" 873 | }, 874 | "FooSrvQueueOldestMessageAgeHighWarningAlarm": { 875 | "Properties": { 876 | "AlarmDescription": "Example Alarm description", 877 | "AlarmName": { 878 | "Fn::Join": [ 879 | ".", 880 | [ 881 | { 882 | "Fn::GetAtt": [ 883 | "FooSrvQueue", 884 | "QueueName" 885 | ] 886 | }, 887 | "OldestMessageAgeHighWarning" 888 | ] 889 | ] 890 | }, 891 | "ComparisonOperator": "GreaterThanThreshold", 892 | "Dimensions": [ 893 | { 894 | "Name": "QueueName", 895 | "Value": { 896 | "Fn::GetAtt": [ 897 | "FooSrvQueue", 898 | "QueueName" 899 | ] 900 | } 901 | } 902 | ], 903 | "EvaluationPeriods": 1, 904 | "MetricName": "ApproximateAgeOfOldestMessage", 905 | "Namespace": "AWS/SQS", 906 | "Period": 300, 907 | "Statistic": "Maximum", 908 | "Threshold": 600 909 | }, 910 | "Type": "AWS::CloudWatch::Alarm" 911 | }, 912 | "FooSrvQueue": { 913 | "Properties": { 914 | "QueueName": { 915 | "Fn::Join": [ 916 | "", 917 | [ 918 | "TestQueue-", 919 | { 920 | "Fn::FindInMap": [ 921 | "StageNameWithCapitalInitialLetter", 922 | "Name", 923 | { 924 | "Ref": "Stage" 925 | } 926 | ] 927 | } 928 | ] 929 | ] 930 | } 931 | }, 932 | "Type": "AWS::SQS::Queue" 933 | }, 934 | "IdentityPool": { 935 | "Type": "AWS::Cognito::IdentityPool", 936 | "Properties": { 937 | "IdentityPoolName": { 938 | "Fn::Join": [ " ", 939 | { "Fn::Split" : [ "-" , "my-id-pool-name" ] } ] } 940 | } 941 | }, 942 | "MyDynamoDbTable": { 943 | "Type": "AWS::DynamoDB::Table", 944 | "Properties": { 945 | "TableName": { "Fn::Sub" : "MyDynamoDbTable-${AWS::Region}-${Stage}" } 946 | } 947 | }, 948 | "MyDynamoDbTable2": { 949 | "Type": "AWS::DynamoDB::Table", 950 | "Properties": { 951 | "TableName": { "Fn::Sub" : [ "MyDynamoDbTable-${index}", { "index": 2 } ] } 952 | } 953 | }, 954 | "MyDynamoDbTable3": { 955 | "Type": "AWS::DynamoDB::Table", 956 | "Properties": { 957 | "TableName": { "Fn::Sub" : "MyDynamoDbTable-${Fruit}" } 958 | } 959 | } 960 | } 961 | } -------------------------------------------------------------------------------- /test/testUtils.js: -------------------------------------------------------------------------------- 1 | const getMockNode = (key, evaluatedValue, shouldReplaceParent = false) => { 2 | return { 3 | nodeAccessor: { 4 | key: key 5 | }, 6 | evaluate() { 7 | return evaluatedValue; 8 | }, 9 | shouldReplaceParent: () => { 10 | return !!shouldReplaceParent; 11 | } 12 | } 13 | } 14 | 15 | const addChildToNode = (node, key, childsEvaluatedValue, shouldReplaceParent = false) => { 16 | node.addChild(key, getMockNode(key, childsEvaluatedValue, shouldReplaceParent)); 17 | } 18 | 19 | const mockNode = { fakeKey: "fakeVal" }; 20 | const mockNodeAccessor = { 21 | path: [ "fakePathSegment1", "fakePathSegment2"] 22 | }; 23 | 24 | module.exports = { 25 | addChildToNode, 26 | mockNode, 27 | mockNodeAccessor, 28 | getMockNode 29 | } --------------------------------------------------------------------------------