├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── atom-bad.png ├── buildspec_merge.yaml ├── cfn-linter.png ├── lib └── atom-cfn-lint.js ├── package-lock.json ├── package.json └── spec ├── .eslintrc.js ├── atom-cfn-lint-spec.js └── fixtures ├── append_rules └── PropertiesTagsRequired.py ├── override_spec └── required.json └── templates ├── append.yaml ├── bad.json ├── bad.yaml ├── good.json ├── good.yaml ├── not_cloudformation.json ├── not_cloudformation.yaml └── override.yaml /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | ### Project specific config ### 2 | language: generic 3 | 4 | env: 5 | global: 6 | - APM_TEST_PACKAGES="" 7 | - ATOM_LINT_WITH_BUNDLED_NODE="true" 8 | 9 | matrix: 10 | - ATOM_CHANNEL=stable 11 | - ATOM_CHANNEL=beta 12 | 13 | os: 14 | - linux 15 | - osx 16 | 17 | ### Generic setup follows ### 18 | script: 19 | - sudo pip2 install cfn-lint 20 | - curl -s -O https://raw.githubusercontent.com/atom/ci/master/build-package.sh 21 | - chmod u+x build-package.sh 22 | - ./build-package.sh 23 | 24 | notifications: 25 | email: 26 | on_success: never 27 | on_failure: change 28 | 29 | branches: 30 | only: 31 | - master 32 | 33 | git: 34 | depth: 10 35 | 36 | sudo: false 37 | 38 | dist: trusty 39 | 40 | addons: 41 | apt: 42 | packages: 43 | - build-essential 44 | - fakeroot 45 | - git 46 | - libsecret-1-dev 47 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/awslabs/aws-cfn-lint-atom/issues), or [recently closed](https://github.com/awslabs/aws-cfn-lint-atom/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/aws-cfn-lint-atom/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/awslabs/aws-cfn-lint-atom/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | AWS Cfn Lint Atom 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## AWS Cfn Lint Atom 2 | Provides IDE specific integration to [cfn-lint](https://github.com/awslabs/cfn-python-lint) 3 | 4 | ![Preview](cfn-linter.png) 5 | 6 | ## Requires 7 | #### cfn-lint 8 | For atom-cfn-lint to work you need to have cfn-lint installed. 9 | ```pip install cfn-lint``` 10 | 11 | ## Verify Installation 12 | #### JSON/YAML Templates 13 | Atom-cfn-lint will work with JSON and YAML files but the goal is to only scan CloudFormation templates. As a result we look for `AWSTemplateFormatVersion` being defined in the file. CloudFormation only requires `Resources` to be defined but this is too generic for assuming that the file is a CloudFormation template. 14 | 15 | ##### Example 16 | You can use the following example to verify that the installation was successful: 17 | ```YAML 18 | AWSTemplateFormatVersion: "2010-09-09" 19 | Resources: 20 | # Unsupported Type "AWS::EC2::Instance1" 21 | MyEC2Instance1: 22 | Type: "AWS::EC2::Instance1" 23 | ``` 24 | 25 | Result (`Type` is marked as an error): 26 | 27 | ![Result](atom-bad.png) 28 | 29 | See [Templates](spec/fixtures/templates/) for more examples. 30 | 31 | ## Contribute 32 | 33 | The code for this plugin can be found on GitHub at [awslabs/aws-cfn-lint-atom](https://github.com/awslabs/aws-cfn-lint-atom) 34 | 35 | ## License 36 | 37 | This library is licensed under the Apache 2.0 License. 38 | -------------------------------------------------------------------------------- /atom-bad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-cloudformation/cfn-lint-atom/60f95f6a74c989c19e8fbe46cd884efaf71ef1df/atom-bad.png -------------------------------------------------------------------------------- /buildspec_merge.yaml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | phases: 3 | build: 4 | commands: 5 | - docker run -i --rm -v "${PWD}:/cfnlint" -w /cfnlint leipert/atom-apm sh -c "apm install && \ 6 | start-stop-daemon --start --pidfile /tmp/xvfb_99.pid --make-pidfile \ 7 | --background --exec /usr/bin/Xvfb -- :99 -screen 0 1024x768x24 -ac \ 8 | +extension GLX +extension RANDR +render -noreset && \ 9 | sleep 3 && \ 10 | export DISPLAY=:99 && \ 11 | apm test" 12 | - docker run -i --rm -v "${PWD}:/cfnlint" -w /cfnlint python:3.6-alpine3.7 sh -c "pip install tox;tox -e py36,pylint36" 13 | -------------------------------------------------------------------------------- /cfn-linter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-cloudformation/cfn-lint-atom/60f95f6a74c989c19e8fbe46cd884efaf71ef1df/cfn-linter.png -------------------------------------------------------------------------------- /lib/atom-cfn-lint.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | /* 4 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"). 7 | You may not use this file except in compliance with the License. 8 | A copy of the License is located at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | or in the "license" file accompanying this file. This file is distributed 13 | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 14 | express or implied. See the License for the specific language governing 15 | permissions and limitations under the License. 16 | */ 17 | 18 | // eslint-disable-next-line import/no-extraneous-dependencies, import/extensions 19 | import { CompositeDisposable } from 'atom' 20 | 21 | let helpers 22 | 23 | 24 | const loadDeps = () => { 25 | if (!helpers) { 26 | helpers = require('atom-linter') 27 | } 28 | } 29 | 30 | // Internal variables 31 | const idleCallbacks = new Set() 32 | 33 | const makeIdleCallback = (work) => { 34 | let callbackId 35 | const callBack = () => { 36 | idleCallbacks.delete(callbackId) 37 | work() 38 | } 39 | callbackId = window.requestIdleCallback(callBack) 40 | idleCallbacks.add(callbackId) 41 | } 42 | 43 | const scheduleIdleTasks = () => { 44 | const linterAtomCfnLintInstallPeerPackages = () => { 45 | require('atom-package-deps').install('atom-cfn-lint') 46 | } 47 | const linterAtomCfnLintStartWorker = () => { 48 | loadDeps() 49 | } 50 | 51 | if (!atom.inSpecMode()) { 52 | makeIdleCallback(linterAtomCfnLintInstallPeerPackages) 53 | makeIdleCallback(linterAtomCfnLintStartWorker) 54 | } 55 | } 56 | 57 | module.exports = { 58 | config: { 59 | cfnLintExecutablePath: { 60 | title: 'Cfn-Lint Executable Path', 61 | type: 'string', 62 | description: 'Path to Cfn-Lint executable (e.g. /usr/bin/cfn-lint) if not in shell env path.', 63 | default: 'cfn-lint', 64 | }, 65 | ignoreRules: { 66 | title: 'Ignore Rules', 67 | type: 'array', 68 | default: [], 69 | items: { 70 | type: 'string' 71 | }, 72 | description: 'Ignore Rules (space deliminated)' 73 | }, 74 | appendRules: { 75 | title: 'Append Rules Directory', 76 | type: 'array', 77 | default: [], 78 | items: { 79 | type: 'string' 80 | }, 81 | description: 'Append Rules Directory (space deliminated)' 82 | }, 83 | overrideSpecPath: { 84 | title: 'Override Spec file path', 85 | type: 'string', 86 | description: '(Optional) Path to an override specfile json file', 87 | default: '' 88 | } 89 | }, 90 | // activate linter 91 | activate() { 92 | this.subscriptions = new CompositeDisposable() 93 | 94 | this.subscriptions.add( 95 | atom.config.observe( 96 | 'atom-cfn-lint.cfnLintExecutablePath', 97 | (value) => { this.cfnLintExecutablePath = value } 98 | ), 99 | atom.config.observe( 100 | 'atom-cfn-lint.ignoreRules', 101 | (value) => { this.ignoreRules = value } 102 | ), 103 | atom.config.observe( 104 | 'atom-cfn-lint.appendRules', 105 | (value) => { this.appendRules = value } 106 | ), 107 | atom.config.observe( 108 | 'atom-cfn-lint.overrideSpecPath', 109 | (value) => { this.overrideSpecPath = value } 110 | ), 111 | ) 112 | 113 | scheduleIdleTasks() 114 | }, 115 | 116 | deactivate() { 117 | idleCallbacks.forEach(callbackID => window.cancelIdleCallback(callbackID)) 118 | idleCallbacks.clear() 119 | this.subscriptions.dispose() 120 | }, 121 | 122 | provideLinter() { 123 | return { 124 | name: 'Cfn-Lint', 125 | grammarScopes: ['source.yaml', 'source.json'], 126 | scope: 'file', 127 | lintsOnChange: false, 128 | lint: (activeEditor) => { 129 | // setup variables 130 | if (!atom.workspace.isTextEditor(activeEditor)) { 131 | // If we somehow get fed an invalid TextEditor just immediately return 132 | return null 133 | } 134 | 135 | const file = activeEditor.getPath() 136 | if (!file) { 137 | // The editor currently has no path, we can't report messages back to 138 | // Linter so just return null 139 | return null 140 | } 141 | 142 | loadDeps() 143 | 144 | const isCfnRegex = new RegExp('"?AWSTemplateFormatVersion"?') 145 | 146 | let isCfn = false 147 | activeEditor.buffer.buffer.getLines().forEach((line) => { 148 | if (isCfnRegex.exec(line)) { 149 | isCfn = true 150 | } 151 | }) 152 | 153 | function emptyArray() { 154 | return new Promise((resolve) => { 155 | resolve([]) 156 | }) 157 | } 158 | 159 | if (!(isCfn)) { 160 | return emptyArray().then(() => []) 161 | } 162 | 163 | // parseable output is required 164 | let args = ['--format', 'json'] 165 | 166 | // add file to check 167 | args = args.concat(['--template', file]) 168 | args = args.concat(['--ignore-bad-template']) 169 | 170 | let i 171 | if (Array.isArray(this.ignoreRules) && this.ignoreRules.length) { 172 | for (i = 0; i < this.ignoreRules.length; i += 1) { 173 | args = args.concat(['--ignore-checks']) 174 | args = args.concat([this.ignoreRules[i]]) 175 | } 176 | } 177 | 178 | if (Array.isArray(this.appendRules) && this.appendRules.length) { 179 | for (i = 0; i < this.appendRules.length; i += 1) { 180 | args = args.concat(['--append-rules']) 181 | args = args.concat([this.appendRules[i]]) 182 | } 183 | } 184 | 185 | if (this.overrideSpecPath) { 186 | args = args.concat(['--override-spec']) 187 | args = args.concat([this.overrideSpecPath]) 188 | } 189 | 190 | // initialize variable for linter return here for either linter output or errors 191 | const toReturn = [] 192 | return helpers.exec(this.cfnLintExecutablePath, args, { cwd: require('path').dirname(file), ignoreExitCode: true }).then((output) => { 193 | JSON.parse(output).forEach((match) => { 194 | const linenumber = parseInt(match.Location.Start.LineNumber, 10) - 1 195 | const columnnumber = parseInt(match.Location.Start.ColumnNumber, 10) - 1 196 | const linenumberend = parseInt(match.Location.End.LineNumber, 10) - 1 197 | const columnnumberend = parseInt(match.Location.End.ColumnNumber, 10) - 1 198 | 199 | let sourceUrl = '' 200 | 201 | // Rule sources are added in version 0.3.3 of cfn-lint 202 | if (Object.prototype.hasOwnProperty.call(match.Rule, 'Source')) { 203 | sourceUrl = match.Rule.Source 204 | } 205 | 206 | 207 | // Map the linter severity into the Atom linter values (error, warning, info) 208 | // See: https://steelbrain.me/linter/types/linter-message-v2.html 209 | let linterSeverity 210 | 211 | switch (match.Level) { 212 | case 'Informational': 213 | linterSeverity = 'info' 214 | break 215 | case 'Warning': 216 | linterSeverity = 'warning' 217 | break 218 | default: 219 | linterSeverity = 'error' 220 | } 221 | 222 | toReturn.push({ 223 | severity: linterSeverity, 224 | excerpt: match.Message, 225 | url: sourceUrl, 226 | location: { 227 | file, 228 | position: [[linenumber, columnnumber], [linenumberend, columnnumberend]], 229 | }, 230 | }) 231 | }) 232 | 233 | return toReturn 234 | }) 235 | .catch((error) => { 236 | console.log(error.message) 237 | atom.notifications.addError( 238 | 'An unexpected error with cloudformation, cfn-lint, atom-cfn-lint, atom, linter, and/or your playbook, has occurred.', 239 | { 240 | detail: error.message 241 | } 242 | ) 243 | return toReturn 244 | }) 245 | } 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atom-cfn-lint", 3 | "version": "0.6.2", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "atom-linter": { 8 | "version": "5.0.2", 9 | "resolved": "https://registry.npmjs.org/atom-linter/-/atom-linter-5.0.2.tgz", 10 | "integrity": "sha1-2eilBobexQskkoIG0J1uZEM8+i8=", 11 | "requires": { 12 | "named-js-regexp": "1.3.4", 13 | "sb-exec": "2.0.5", 14 | "sb-promisify": "1.3.0", 15 | "tmp": "0.0.33" 16 | } 17 | }, 18 | "atom-package-deps": { 19 | "version": "4.6.2", 20 | "resolved": "https://registry.npmjs.org/atom-package-deps/-/atom-package-deps-4.6.2.tgz", 21 | "integrity": "sha512-GOcCULZPzpcFfnHo9Oz5fT/EaArFHNs84E4rp/Nox0/GlS1UYkEF44FRdgD+7TxAudRfQAXE0a8wh0GealCXZg==", 22 | "requires": { 23 | "atom-package-path": "1.1.0", 24 | "sb-fs": "3.0.0", 25 | "semver": "5.6.0" 26 | } 27 | }, 28 | "atom-package-path": { 29 | "version": "1.1.0", 30 | "resolved": "https://registry.npmjs.org/atom-package-path/-/atom-package-path-1.1.0.tgz", 31 | "integrity": "sha1-tR/tvADnyM5SI9DYA9t6P09pYU8=", 32 | "requires": { 33 | "sb-callsite": "1.1.2" 34 | } 35 | }, 36 | "consistent-env": { 37 | "version": "1.3.1", 38 | "resolved": "https://registry.npmjs.org/consistent-env/-/consistent-env-1.3.1.tgz", 39 | "integrity": "sha1-9oI018afxt2WVviuI0Kc4EmbZfs=", 40 | "requires": { 41 | "lodash.uniq": "4.5.0" 42 | } 43 | }, 44 | "is-utf8": { 45 | "version": "0.2.1", 46 | "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", 47 | "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" 48 | }, 49 | "lodash.uniq": { 50 | "version": "4.5.0", 51 | "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", 52 | "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" 53 | }, 54 | "named-js-regexp": { 55 | "version": "1.3.4", 56 | "resolved": "https://registry.npmjs.org/named-js-regexp/-/named-js-regexp-1.3.4.tgz", 57 | "integrity": "sha512-wvbB+afegWlmc3/bXd+S1DDLPGcyARWVvYCOntQwBRhKgwSZewk8zolExzyLrXT70xhFkmRYQGigXFaTgtHqjA==" 58 | }, 59 | "os-tmpdir": { 60 | "version": "1.0.2", 61 | "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", 62 | "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" 63 | }, 64 | "sb-callsite": { 65 | "version": "1.1.2", 66 | "resolved": "https://registry.npmjs.org/sb-callsite/-/sb-callsite-1.1.2.tgz", 67 | "integrity": "sha1-KBkftm1k46PukghKlakPy1ECJDs=" 68 | }, 69 | "sb-exec": { 70 | "version": "2.0.5", 71 | "resolved": "https://registry.npmjs.org/sb-exec/-/sb-exec-2.0.5.tgz", 72 | "integrity": "sha1-HLKu5+8GpmY3nT2K+J3uriVJj1g=", 73 | "requires": { 74 | "consistent-env": "1.3.1", 75 | "sb-npm-path": "1.0.1" 76 | } 77 | }, 78 | "sb-fs": { 79 | "version": "3.0.0", 80 | "resolved": "https://registry.npmjs.org/sb-fs/-/sb-fs-3.0.0.tgz", 81 | "integrity": "sha1-+9zdMBDoChuOJ0kM7zNgZJdCA7g=", 82 | "requires": { 83 | "sb-promisify": "2.0.2", 84 | "strip-bom-buf": "1.0.0" 85 | }, 86 | "dependencies": { 87 | "sb-promisify": { 88 | "version": "2.0.2", 89 | "resolved": "https://registry.npmjs.org/sb-promisify/-/sb-promisify-2.0.2.tgz", 90 | "integrity": "sha1-QnelR1RIiqlnXYhuNU24lMm9yYE=" 91 | } 92 | } 93 | }, 94 | "sb-memoize": { 95 | "version": "1.0.2", 96 | "resolved": "https://registry.npmjs.org/sb-memoize/-/sb-memoize-1.0.2.tgz", 97 | "integrity": "sha1-EoN1xi3bnMT/qQXQxaWXwZuurY4=" 98 | }, 99 | "sb-npm-path": { 100 | "version": "1.0.1", 101 | "resolved": "https://registry.npmjs.org/sb-npm-path/-/sb-npm-path-1.0.1.tgz", 102 | "integrity": "sha1-k5aH+pl9CWDREDBF6/scRnSvVpE=", 103 | "requires": { 104 | "sb-memoize": "1.0.2", 105 | "sb-promisify": "1.3.0" 106 | } 107 | }, 108 | "sb-promisify": { 109 | "version": "1.3.0", 110 | "resolved": "https://registry.npmjs.org/sb-promisify/-/sb-promisify-1.3.0.tgz", 111 | "integrity": "sha1-Ovbx+p/8gz8U3oaRbu/B9VmxsFE=" 112 | }, 113 | "semver": { 114 | "version": "5.6.0", 115 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", 116 | "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" 117 | }, 118 | "strip-bom-buf": { 119 | "version": "1.0.0", 120 | "resolved": "https://registry.npmjs.org/strip-bom-buf/-/strip-bom-buf-1.0.0.tgz", 121 | "integrity": "sha1-HLRar1dTD0yvhsf3UXnSyaUd1XI=", 122 | "requires": { 123 | "is-utf8": "0.2.1" 124 | } 125 | }, 126 | "tmp": { 127 | "version": "0.0.33", 128 | "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", 129 | "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", 130 | "requires": { 131 | "os-tmpdir": "1.0.2" 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atom-cfn-lint", 3 | "main": "./lib/atom-cfn-lint", 4 | "version": "0.7.0", 5 | "description": "Validate CloudFormation yaml/json templates against the CloudFormation spec and additional checks. Includes checking valid values for resource properties and best practices.", 6 | "keywords": [ 7 | "linter", 8 | "AWS", 9 | "CloudFormation" 10 | ], 11 | "repository": "https://github.com/awslabs/aws-cfn-lint-atom", 12 | "license": "Apache-2.0", 13 | "engines": { 14 | "atom": ">=1.0.0 <2.0.0" 15 | }, 16 | "dependencies": { 17 | "atom-linter": "^5.0.2", 18 | "atom-package-deps": "^4.6.0" 19 | }, 20 | "package-deps": [ 21 | "linter" 22 | ], 23 | "scripts": { 24 | "test": "apm test", 25 | "lint": "eslint lib spec" 26 | }, 27 | "providedServices": { 28 | "linter": { 29 | "versions": { 30 | "2.0.0": "provideLinter" 31 | } 32 | } 33 | }, 34 | "readmeFilename": "README.md", 35 | "devDependencies": { 36 | "babel-eslint": "^10.0.1", 37 | "eslint": "^5.13.0", 38 | "eslint-config-airbnb-base": "^13.1.0", 39 | "eslint-plugin-import": "^2.16.0", 40 | "jasmine-fix": "^1.3.0" 41 | }, 42 | "eslintConfig": { 43 | "extends": "airbnb-base", 44 | "rules": { 45 | "no-console": "off", 46 | "semi": [ 47 | "error", 48 | "never" 49 | ], 50 | "comma-dangle": "off", 51 | "global-require": "off", 52 | "import/no-unresolved": [ 53 | "error", 54 | { 55 | "ignore": [ 56 | "atom" 57 | ] 58 | } 59 | ], 60 | "object-curly-newline": [ 61 | "error", 62 | { 63 | "ObjectExpression": { 64 | "minProperties": 5, 65 | "multiline": true, 66 | "consistent": true 67 | }, 68 | "ObjectPattern": { 69 | "minProperties": 5, 70 | "multiline": true, 71 | "consistent": true 72 | } 73 | } 74 | ] 75 | }, 76 | "globals": { 77 | "atom": "true" 78 | }, 79 | "env": { 80 | "node": true, 81 | "browser": true 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /spec/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globals: { 3 | waitsForPromise: true 4 | }, 5 | env: { 6 | jasmine: true 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /spec/atom-cfn-lint-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | /* 4 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"). 7 | You may not use this file except in compliance with the License. 8 | A copy of the License is located at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | or in the "license" file accompanying this file. This file is distributed 13 | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 14 | express or implied. See the License for the specific language governing 15 | permissions and limitations under the License. 16 | */ 17 | 18 | 19 | // eslint-disable-next-line no-unused-vars 20 | import { it, fit, wait, beforeEach, afterEach } from 'jasmine-fix' 21 | 22 | import * as path from 'path' 23 | 24 | const fixturesDir = path.join(__dirname, 'fixtures') 25 | 26 | const fixtures = { 27 | goodJson: ['templates', 'good.json'], 28 | goodYaml: ['templates', 'good.yaml'], 29 | nonCfnYaml: ['templates', 'not_cloudformation.yaml'], 30 | nonCfnJson: ['templates', 'not_cloudformation.json'], 31 | badJson: ['templates', 'bad.json'], 32 | badYaml: ['templates', 'bad.yaml'], 33 | overrideYaml: ['templates', 'override.yaml'], 34 | overrideSpec: ['override_spec', 'required.json'], 35 | appendYaml: ['templates', 'append.yaml'], 36 | appendRules: ['append_rules'], 37 | } 38 | 39 | const paths = Object.keys(fixtures) 40 | .reduce((accumulator, fixture) => { 41 | const acc = accumulator 42 | acc[fixture] = path.join(fixturesDir, ...(fixtures[fixture])) 43 | return acc 44 | }, {}) 45 | 46 | describe('The CFN Lint provider for Linter', () => { 47 | const { lint } = require('../lib/atom-cfn-lint.js').provideLinter() 48 | 49 | beforeEach(async () => { 50 | atom.workspace.destroyActivePaneItem() 51 | waitsForPromise(() => Promise.all([ 52 | atom.packages.activatePackage('atom-cfn-lint'), 53 | atom.packages.activatePackage('language-json'), 54 | atom.packages.activatePackage('language-yaml'), 55 | ])) 56 | }) 57 | 58 | describe('returns results when lint issues are found', () => { 59 | it('finds something wrong with bad.yaml', async () => { 60 | const editor = await atom.workspace.open(paths.badYaml) 61 | const messages = await lint(editor) 62 | 63 | expect(messages.length).toBe(16) 64 | expect(messages[0].severity).toBeDefined() 65 | expect(messages[0].severity).toEqual('error') 66 | expect(messages[0].excerpt).toBeDefined() 67 | expect(messages[0].location.file).toBeDefined() 68 | expect(messages[0].location.file).toMatch(/.+bad\.yaml$/) 69 | expect(messages[0].location.position).toBeDefined() 70 | expect(messages[1].severity).toBeDefined() 71 | expect(messages[1].severity).toEqual('warning') 72 | expect(messages[1].excerpt).toBeDefined() 73 | expect(messages[1].location.file).toBeDefined() 74 | expect(messages[1].location.file).toMatch(/.+bad\.yaml$/) 75 | expect(messages[1].location.position).toBeDefined() 76 | expect(messages[2].severity).toBeDefined() 77 | expect(messages[2].severity).toEqual('error') 78 | expect(messages[2].excerpt).toBeDefined() 79 | expect(messages[2].location.file).toBeDefined() 80 | expect(messages[2].location.file).toMatch(/.+bad\.yaml$/) 81 | expect(messages[2].location.position).toBeDefined() 82 | expect(messages[3].severity).toBeDefined() 83 | expect(messages[3].severity).toEqual('error') 84 | expect(messages[3].excerpt).toBeDefined() 85 | expect(messages[3].location.file).toBeDefined() 86 | expect(messages[3].location.file).toMatch(/.+bad\.yaml$/) 87 | expect(messages[3].location.position).toBeDefined() 88 | expect(messages[4].severity).toBeDefined() 89 | expect(messages[4].severity).toEqual('error') 90 | expect(messages[4].excerpt).toBeDefined() 91 | expect(messages[4].location.file).toBeDefined() 92 | expect(messages[4].location.file).toMatch(/.+bad\.yaml$/) 93 | expect(messages[4].location.position).toBeDefined() 94 | expect(messages[5].severity).toBeDefined() 95 | expect(messages[5].severity).toEqual('error') 96 | expect(messages[5].excerpt).toBeDefined() 97 | expect(messages[5].location.file).toBeDefined() 98 | expect(messages[5].location.file).toMatch(/.+bad\.yaml$/) 99 | expect(messages[5].location.position).toBeDefined() 100 | expect(messages[6].severity).toBeDefined() 101 | expect(messages[6].severity).toEqual('error') 102 | expect(messages[6].excerpt).toBeDefined() 103 | expect(messages[6].location.file).toBeDefined() 104 | expect(messages[6].location.file).toMatch(/.+bad\.yaml$/) 105 | expect(messages[6].location.position).toBeDefined() 106 | expect(messages[7].severity).toBeDefined() 107 | expect(messages[7].severity).toEqual('error') 108 | expect(messages[7].excerpt).toBeDefined() 109 | expect(messages[7].location.file).toBeDefined() 110 | expect(messages[7].location.file).toMatch(/.+bad\.yaml$/) 111 | expect(messages[7].location.position).toBeDefined() 112 | expect(messages[8].severity).toBeDefined() 113 | expect(messages[8].severity).toEqual('error') 114 | expect(messages[8].excerpt).toBeDefined() 115 | expect(messages[8].location.file).toBeDefined() 116 | expect(messages[8].location.file).toMatch(/.+bad\.yaml$/) 117 | expect(messages[8].location.position).toBeDefined() 118 | expect(messages[9].severity).toBeDefined() 119 | expect(messages[9].severity).toEqual('error') 120 | expect(messages[9].excerpt).toBeDefined() 121 | expect(messages[9].location.file).toBeDefined() 122 | expect(messages[9].location.file).toMatch(/.+bad\.yaml$/) 123 | expect(messages[9].location.position).toBeDefined() 124 | expect(messages[10].severity).toBeDefined() 125 | expect(messages[10].severity).toEqual('error') 126 | expect(messages[10].excerpt).toBeDefined() 127 | expect(messages[10].location.file).toBeDefined() 128 | expect(messages[10].location.file).toMatch(/.+bad\.yaml$/) 129 | expect(messages[10].location.position).toBeDefined() 130 | expect(messages[11].severity).toBeDefined() 131 | expect(messages[11].severity).toEqual('error') 132 | expect(messages[11].excerpt).toBeDefined() 133 | expect(messages[11].location.file).toBeDefined() 134 | expect(messages[11].location.file).toMatch(/.+bad\.yaml$/) 135 | expect(messages[11].location.position).toBeDefined() 136 | expect(messages[12].severity).toBeDefined() 137 | expect(messages[12].severity).toEqual('error') 138 | expect(messages[12].excerpt).toBeDefined() 139 | expect(messages[12].location.file).toBeDefined() 140 | expect(messages[12].location.file).toMatch(/.+bad\.yaml$/) 141 | expect(messages[12].location.position).toBeDefined() 142 | }) 143 | 144 | it('finds something wrong with bad.json', async () => { 145 | const editor = await atom.workspace.open(paths.badJson) 146 | const messages = await lint(editor) 147 | 148 | expect(messages.length).toBe(23) 149 | expect(messages[0].severity).toBeDefined() 150 | expect(messages[0].severity).toEqual('error') 151 | expect(messages[0].excerpt).toBeDefined() 152 | expect(messages[0].location.file).toBeDefined() 153 | expect(messages[0].location.file).toMatch(/.+bad\.json/) 154 | expect(messages[0].location.position).toBeDefined() 155 | }) 156 | }) 157 | 158 | describe('doesnt return failures on good files', () => { 159 | it('it doesn\'t return results for disabled rule for good.yaml', async () => { 160 | const editor = await atom.workspace.open(paths.goodYaml) 161 | const messages = await lint(editor) 162 | 163 | expect(messages.length).toBe(0) 164 | }) 165 | 166 | it('it doesn\'t return results for disabled rule for good.json', async () => { 167 | const editor = await atom.workspace.open(paths.goodYaml) 168 | const messages = await lint(editor) 169 | 170 | expect(messages.length).toBe(0) 171 | }) 172 | }) 173 | 174 | describe('doesnt check Non CloudFormation Templates', () => { 175 | it('it doesnt find something wrong with not_cloudformation.yaml', async () => { 176 | const editor = await atom.workspace.open(paths.nonCfnYaml) 177 | const messages = await lint(editor) 178 | 179 | expect(messages.length).toBe(0) 180 | }) 181 | 182 | it('it doesnt find something wrong with not_cloudformation.json', async () => { 183 | const editor = await atom.workspace.open(paths.nonCfnJson) 184 | const messages = await lint(editor) 185 | 186 | expect(messages.length).toBe(0) 187 | }) 188 | }) 189 | 190 | describe('disable rule', () => { 191 | beforeEach(async () => { 192 | atom.config.set('atom-cfn-lint.ignoreRules', ['E3012']) 193 | }) 194 | 195 | it('it doesn\'t return results for disabled rule for bad.yaml', async () => { 196 | const editor = await atom.workspace.open(paths.badYaml) 197 | const messages = await lint(editor) 198 | 199 | expect(messages.length).toBe(13) 200 | }) 201 | 202 | it('it doesn\'t return results for disabled rule for bad.json', async () => { 203 | const editor = await atom.workspace.open(paths.badJson) 204 | const messages = await lint(editor) 205 | 206 | expect(messages.length).toBe(1) 207 | }) 208 | }) 209 | 210 | describe('override spec', () => { 211 | beforeEach(async () => { 212 | atom.config.set('atom-cfn-lint.overrideSpecPath', paths.overrideSpec) 213 | }) 214 | 215 | it('it returns errors when overriding the spec for override.yaml', async () => { 216 | const editor = await atom.workspace.open(paths.overrideYaml) 217 | const messages = await lint(editor) 218 | 219 | expect(messages.length).toBe(1) 220 | }) 221 | }) 222 | 223 | describe('append rules', () => { 224 | beforeEach(async () => { 225 | atom.config.set('atom-cfn-lint.appendRules', [paths.appendRules]) 226 | }) 227 | 228 | it('it returns errors when appending rules to append.yaml', async () => { 229 | const editor = await atom.workspace.open(paths.appendYaml) 230 | const messages = await lint(editor) 231 | console.log(messages) 232 | 233 | expect(messages.length).toBe(2) 234 | }) 235 | }) 236 | }) 237 | -------------------------------------------------------------------------------- /spec/fixtures/append_rules/PropertiesTagsRequired.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | software and associated documentation files (the "Software"), to deal in the Software 6 | without restriction, including without limitation the rights to use, copy, modify, 7 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | """ 17 | from cfnlint import CloudFormationLintRule 18 | from cfnlint import RuleMatch 19 | 20 | 21 | class PropertiesTagsRequired(CloudFormationLintRule): 22 | """Check if Tags have required keys""" 23 | id = 'E9000' 24 | shortdesc = 'Tags have correct key values' 25 | description = 'Check Tags for resources' 26 | tags = ['base', 'resources', 'tags'] 27 | 28 | def match(self, cfn): 29 | """Check Tags for required keys""" 30 | 31 | matches = list() 32 | 33 | required_tags = ['CostCenter', 'ApplicationName'] 34 | 35 | all_tags = cfn.search_deep_keys('Tags') 36 | all_tags = [x for x in all_tags if x[0] == 'Resources'] 37 | for all_tag in all_tags: 38 | all_keys = [d.get('Key') for d in all_tag[-1]] 39 | for required_tag in required_tags: 40 | if required_tag not in all_keys: 41 | message = "Missing Tag {0} at {1}" 42 | matches.append( 43 | RuleMatch( 44 | all_tag[:-1], 45 | message.format(required_tag, '/'.join(map(str, all_tag[:-1]))))) 46 | 47 | return matches 48 | -------------------------------------------------------------------------------- /spec/fixtures/override_spec/required.json: -------------------------------------------------------------------------------- 1 | { 2 | "ResourceTypes": { 3 | "AWS::S3::Bucket": { 4 | "Properties": { 5 | "BucketName": { 6 | "Required": true 7 | } 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /spec/fixtures/templates/append.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: "2010-09-09" 3 | Description: > 4 | Basic Template 5 | Parameters: 6 | ApplicationName: 7 | Type: String 8 | Description: Application Name 9 | Environment: 10 | Type: String 11 | Default: Test 12 | Description: Environment 13 | AllowedValues: 14 | - Dev 15 | - Test 16 | TestEnvironment: 17 | Type: String 18 | Default: Test 19 | Description: Another Environment 20 | AllowedValues: 21 | - Dev 22 | - Test 23 | - Prod 24 | BillingContact: 25 | Type: String 26 | Default: billing@example.com 27 | AllowedPattern: '^[A-Za-z0-9._%+-]+@example.com$' 28 | OperationsContact: 29 | Type: String 30 | Default: operations@example.com 31 | AllowedPattern: '^[A-Za-z0-9._%+-]+@example.com$' 32 | Conditions: 33 | isProd: 34 | Fn::Equals: [!Ref Environment, 'Prod'] 35 | Resources: 36 | untaggedInstance: 37 | Type: AWS::EC2::Instance 38 | Properties: 39 | ImageId: ami-123456 40 | myInstance: 41 | Type: AWS::EC2::Instance 42 | Properties: 43 | ImageId: ami-123456 44 | Tags: 45 | - 46 | Key: application 47 | Value: !Ref ApplicationName 48 | - 49 | Key: environment 50 | Value: !Ref Environment 51 | - 52 | Key: environment 53 | Value: !If [isProd, 'Yes', !Ref Environment] 54 | - 55 | Key: environment 56 | Value: !Ref TestEnvironment 57 | - 58 | Key: billing_contact 59 | Value: !Ref BillingContact 60 | - 61 | Key: operations_contact 62 | Value: !Ref OperationsContact 63 | - 64 | Key: security_contact 65 | Value: security@example.c 66 | -------------------------------------------------------------------------------- /spec/fixtures/templates/bad.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "This workload template deploys an ASG behind an ELB load balancer in two private subnets. The cluster is configured to use an S3 bucket for storage **WARNING** This template creates EC2 instances and related resources. You will be billed for the AWS resources used if you create a stack from this template.", 4 | "Metadata": { 5 | "AWS::CloudFormation::Interface": { 6 | "ParameterGroups": [ 7 | { 8 | "Label": { 9 | "default": "Network Configuration" 10 | }, 11 | "Parameters": [ 12 | "VPCID", 13 | "PrivateSubnet1ID", 14 | "PrivateSubnet2ID", 15 | "PublicSubnet1ID", 16 | "PublicSubnet2ID" 17 | ] 18 | }, 19 | { 20 | "Label": { 21 | "default": "Amazon EC2 Configuration" 22 | }, 23 | "Parameters": [ 24 | "KeyPairName", 25 | "BastionSecurityGroupID", 26 | "WorkloadInstanceType" 27 | ] 28 | }, 29 | { 30 | "Label": { 31 | "default": "Workload Nodes Configuration" 32 | }, 33 | "Parameters": [ 34 | "WorkloadNodesMinSize", 35 | "WorkloadNodesMaxSize", 36 | "WorkloadNodesDesiredCapacity", 37 | "OperatorEmail" 38 | ] 39 | }, 40 | { 41 | "Label": { 42 | "default": "Workload Storage Configuration" 43 | }, 44 | "Parameters": [ 45 | "S3BucketName" 46 | ] 47 | }, 48 | { 49 | "Label": { 50 | "default": "AWS Quick Start Configuration" 51 | }, 52 | "Parameters": [ 53 | "QSS3BucketName", 54 | "QSS3KeyPrefix" 55 | ] 56 | } 57 | ], 58 | "ParameterLabels": { 59 | "BastionSecurityGroupID": { 60 | "default": "Bastion Security Group ID" 61 | }, 62 | "KeyPairName": { 63 | "default": "SSH Key Name" 64 | }, 65 | "OperatorEmail": { 66 | "default": "Operator Email" 67 | }, 68 | "PrivateSubnet1ID": { 69 | "default": "Private Subnet 1 ID" 70 | }, 71 | "PrivateSubnet2ID": { 72 | "default": "Private Subnet 2 ID" 73 | }, 74 | "PublicSubnet1ID": { 75 | "default": "Public Subnet 1 ID" 76 | }, 77 | "PublicSubnet2ID": { 78 | "default": "Public Subnet 2 ID" 79 | }, 80 | "QSS3BucketName": { 81 | "default": "Quick Start S3 Bucket Name" 82 | }, 83 | "QSS3KeyPrefix": { 84 | "default": "Quick Start S3 Key Prefix" 85 | }, 86 | "S3BucketName": { 87 | "default": "S3 Bucket Name" 88 | }, 89 | "VPCID": { 90 | "default": "VPC ID" 91 | }, 92 | "WorkloadInstanceType": { 93 | "default": "Workload Servers Instance Type" 94 | }, 95 | "WorkloadNodesDesiredCapacity": { 96 | "default": "Workload Nodes Desired Capacity" 97 | }, 98 | "WorkloadNodesMaxSize": { 99 | "default": "Workload Nodes Max Size" 100 | }, 101 | "WorkloadNodesMinSize": { 102 | "default": "Workload Nodes Min Size" 103 | } 104 | } 105 | } 106 | }, 107 | "Parameters": { 108 | "BastionSecurityGroupID": { 109 | "Description": "ID of the bastion host security group to enable SSH connections (e.g., sg-7f16e910)", 110 | "Type": "AWS::EC2::SecurityGroup::Id" 111 | }, 112 | "KeyPairName": { 113 | "Description": "Name of an existing EC2 key pair. All instances will launch with this key pair.", 114 | "Type": "AWS::EC2::KeyPair::KeyName" 115 | }, 116 | "OperatorEmail": { 117 | "AllowedPattern": "([a-zA-Z0-9_\\-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.)|(([a-zA-Z0-9\\-]+\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?)", 118 | "ConstraintDescription": "Must be a valid email address.", 119 | "Description": "Email address that notifications of any scaling operations will be sent to", 120 | "Type": "String" 121 | }, 122 | "PrivateSubnet1ID": { 123 | "Description": "ID of private subnet 1 in Availability Zone 1 for the Workload (e.g., subnet-a0246dcd)", 124 | "Type": "AWS::EC2::Subnet::Id" 125 | }, 126 | "PrivateSubnet2ID": { 127 | "Description": "ID of private subnet 2 in Availability Zone 2 for the Workload (e.g., subnet-b1f432cd)", 128 | "Type": "AWS::EC2::Subnet::Id" 129 | }, 130 | "PublicSubnet1ID": { 131 | "Description": "ID of public subnet 1 in Availability Zone 1 for the ELB load balancer (e.g., subnet-9bc642ac)", 132 | "Type": "AWS::EC2::Subnet::Id" 133 | }, 134 | "PublicSubnet2ID": { 135 | "Description": "ID of public subnet 2 in Availability Zone 2 for the ELB load balancer (e.g., subnet-e3246d8e)", 136 | "Type": "AWS::EC2::Subnet::Id" 137 | }, 138 | "QSS3BucketName": { 139 | "AllowedPattern": "^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$", 140 | "ConstraintDescription": "Quick Start bucket name can include numbers, lowercase letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen (-).", 141 | "Default": "aws-quickstart", 142 | "Description": "S3 bucket name for the Quick Start assets. This string can include numbers, lowercase letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen (-).", 143 | "Type": "String" 144 | }, 145 | "QSS3KeyPrefix": { 146 | "AllowedPattern": "^[0-9a-zA-Z-/]*$", 147 | "ConstraintDescription": "Quick Start key prefix can include numbers, lowercase letters, uppercase letters, hyphens (-), and forward slash (/).", 148 | "Default": "quickstart-examples/", 149 | "Description": "S3 key prefix for the Quick Start assets. Quick Start key prefix can include numbers, lowercase letters, uppercase letters, hyphens (-), and forward slash (/).", 150 | "Type": "String" 151 | }, 152 | "S3BucketName": { 153 | "AllowedPattern": "^[a-z0-9][a-z0-9-.]*$", 154 | "Default": "type-unique-value-here-in-lowercase", 155 | "Description": "Name of the S3 bucket that will be created for your workload to store data. Enter a unique name that does not include uppercase characters.", 156 | "Type": "String" 157 | }, 158 | "VPCID": { 159 | "Description": "ID of your existing VPC for deployment", 160 | "Type": "AWS::EC2::VPC::Id" 161 | }, 162 | "WorkloadInstanceType": { 163 | "AllowedValues": [ 164 | "t2.large", 165 | "m4.large", 166 | "m4.xlarge", 167 | "m4.2xlarge", 168 | "m4.4xlarge", 169 | "m4.10xlarge", 170 | "m3.medium", 171 | "m3.large", 172 | "m3.xlarge", 173 | "m3.2xlarge", 174 | "c4.large", 175 | "c4.xlarge", 176 | "c4.2xlarge", 177 | "c4.4xlarge", 178 | "c4.8xlarge", 179 | "c3.large", 180 | "c3.xlarge", 181 | "c3.2xlarge", 182 | "c3.4xlarge", 183 | "c3.8xlarge", 184 | "r3.large", 185 | "r3.xlarge" 186 | ], 187 | "ConstraintDescription": "Must contain valid instance type", 188 | "Default": "m4.xlarge", 189 | "Description": "Type of EC2 instance for the Workload instances", 190 | "Type": "String" 191 | }, 192 | "WorkloadNodesDesiredCapacity": { 193 | "Default": "2", 194 | "Description": "The desired capacity for the Workload nodes Auto Scaling group", 195 | "Type": "String" 196 | }, 197 | "WorkloadNodesMaxSize": { 198 | "Default": "4", 199 | "Description": "The maximum size of the Auto Scaling group", 200 | "Type": "String" 201 | }, 202 | "WorkloadNodesMinSize": { 203 | "Default": "2", 204 | "Description": "The minimum size of the Auto Scaling group", 205 | "Type": "String" 206 | } 207 | }, 208 | "Rules": { 209 | "KeyPairsNotEmpty": { 210 | "Assertions": [ 211 | { 212 | "Assert": { 213 | "Fn::Not": [ 214 | { 215 | "Fn::EachMemberEquals": [ 216 | { 217 | "Fn::RefAll": "AWS::EC2::KeyPair::KeyName" 218 | }, 219 | "" 220 | ] 221 | } 222 | ] 223 | }, 224 | "AssertDescription": "All key pair parameters must not be empty" 225 | } 226 | ] 227 | }, 228 | "SubnetsInVPC": { 229 | "Assertions": [ 230 | { 231 | "Assert": { 232 | "Fn::EachMemberIn": [ 233 | { 234 | "Fn::ValueOfAll": [ 235 | "AWS::EC2::Subnet::Id", 236 | "VpcId" 237 | ] 238 | }, 239 | { 240 | "Fn::RefAll": "AWS::EC2::VPC::Id" 241 | } 242 | ] 243 | }, 244 | "AssertDescription": "All subnets must in the VPC" 245 | } 246 | ] 247 | } 248 | }, 249 | "Mappings": { 250 | "AWSAMIRegionMap": { 251 | "AMI": { 252 | "AMZNLINUXHVM": "amzn-ami-hvm-2018.03.0.20180412-x86_64-gp2" 253 | }, 254 | "ap-northeast-1": { 255 | "AMZNLINUXHVM": "ami-28ddc154" 256 | }, 257 | "ap-northeast-2": { 258 | "AMZNLINUXHVM": "ami-efaf0181" 259 | }, 260 | "ap-south-1": { 261 | "AMZNLINUXHVM": "ami-b46f48db" 262 | }, 263 | "ap-southeast-1": { 264 | "AMZNLINUXHVM": "ami-64260718" 265 | }, 266 | "ap-southeast-2": { 267 | "AMZNLINUXHVM": "ami-60a26a02" 268 | }, 269 | "ca-central-1": { 270 | "AMZNLINUXHVM": "ami-2f39bf4b" 271 | }, 272 | "eu-central-1": { 273 | "AMZNLINUXHVM": "ami-1b316af0" 274 | }, 275 | "eu-west-1": { 276 | "AMZNLINUXHVM": "ami-9cbe9be5" 277 | }, 278 | "eu-west-2": { 279 | "AMZNLINUXHVM": "ami-c12dcda6" 280 | }, 281 | "sa-east-1": { 282 | "AMZNLINUXHVM": "ami-f09dcc9c" 283 | }, 284 | "us-east-1": { 285 | "AMZNLINUXHVM": "ami-467ca739" 286 | }, 287 | "us-east-2": { 288 | "AMZNLINUXHVM": "ami-976152f2" 289 | }, 290 | "us-west-1": { 291 | "AMZNLINUXHVM": "ami-46e1f226" 292 | }, 293 | "us-west-2": { 294 | "AMZNLINUXHVM": "ami-6b8cef13" 295 | } 296 | } 297 | }, 298 | "Resources": { 299 | "NotificationTopic": { 300 | "Type": "AWS::SNS::Topic", 301 | "Properties": { 302 | "Subscription": [ 303 | { 304 | "Endpoint": { 305 | "Ref": "OperatorEmail" 306 | }, 307 | "Protocol": "email" 308 | } 309 | ] 310 | } 311 | }, 312 | "ELBSecurityGroup": { 313 | "Type": "AWS::EC2::SecurityGroup", 314 | "Properties": { 315 | "GroupDescription": "Allow access to the ELB", 316 | "VpcId": { 317 | "Ref": "VPCID" 318 | }, 319 | "SecurityGroupIngress": [ 320 | { 321 | "IpProtocol": "tcp", 322 | "FromPort": "80", 323 | "ToPort": "80", 324 | "CidrIp": "0.0.0.0/0" 325 | }, 326 | { 327 | "IpProtocol": "tcp", 328 | "FromPort": "443", 329 | "ToPort": "443", 330 | "CidrIp": "0.0.0.0/0" 331 | } 332 | ] 333 | } 334 | }, 335 | "ElasticLoadBalancer": { 336 | "Type": "AWS::ElasticLoadBalancing::LoadBalancer", 337 | "Properties": { 338 | "Subnets": [ 339 | { 340 | "Ref": "PublicSubnet1ID" 341 | }, 342 | { 343 | "Ref": "PublicSubnet2ID" 344 | } 345 | ], 346 | "SecurityGroups": [ 347 | { 348 | "Ref": "ELBSecurityGroup" 349 | } 350 | ], 351 | "AppCookieStickinessPolicy": [ 352 | { 353 | "CookieName": "dummy", 354 | "PolicyName": "WorkloadCookieStickinessPolicy" 355 | } 356 | ], 357 | "Listeners": [ 358 | { 359 | "LoadBalancerPort": "80", 360 | "InstancePort": "80", 361 | "Protocol": "HTTP", 362 | "PolicyNames": [ 363 | "WorkloadCookieStickinessPolicy" 364 | ] 365 | } 366 | ], 367 | "CrossZone": "true", 368 | "HealthCheck": { 369 | "Target": "TCP:80", 370 | "HealthyThreshold": "2", 371 | "UnhealthyThreshold": "3", 372 | "Interval": "30", 373 | "Timeout": "3" 374 | } 375 | } 376 | }, 377 | "SetupRole": { 378 | "Type": "AWS::IAM::Role", 379 | "Properties": { 380 | "AssumeRolePolicyDocument": { 381 | "Statement": [ 382 | { 383 | "Effect": "Allow", 384 | "Principal": { 385 | "Service": [ 386 | "ec2.amazonaws.com" 387 | ] 388 | }, 389 | "Action": [ 390 | "sts:AssumeRole" 391 | ] 392 | } 393 | ] 394 | }, 395 | "Path": "/", 396 | "Policies": [ 397 | { 398 | "PolicyDocument": { 399 | "Version": "2012-10-17", 400 | "Statement": [ 401 | { 402 | "Action": [ 403 | "s3:GetObject" 404 | ], 405 | "Resource": { 406 | "Fn::Sub": "arn:aws:s3:::${QSS3BucketName}/${QSS3KeyPrefix}*" 407 | }, 408 | "Effect": "Allow" 409 | } 410 | ] 411 | }, 412 | "PolicyName": "aws-quick-start-s3-policy" 413 | }, 414 | { 415 | "PolicyName": "WorkloadSetup", 416 | "PolicyDocument": { 417 | "Statement": [ 418 | { 419 | "Effect": "Allow", 420 | "Action": [ 421 | "cloudwatch:PutMetricData", 422 | "cloudwatch:EnableAlarmActions", 423 | "cloudwatch:PutMetricAlarm" 424 | ], 425 | "Resource": "*" 426 | }, 427 | { 428 | "Effect": "Allow", 429 | "Action": [ 430 | "s3:*" 431 | ], 432 | "Resource": [ 433 | { 434 | "Fn::Join": [ 435 | "", 436 | [ 437 | "arn:aws:s3:::", 438 | { 439 | "Ref": "S3Bucket" 440 | }, 441 | "" 442 | ] 443 | ] 444 | }, 445 | { 446 | "Fn::Join": [ 447 | "", 448 | [ 449 | "arn:aws:s3:::", 450 | { 451 | "Ref": "S3Bucket" 452 | }, 453 | "/*" 454 | ] 455 | ] 456 | } 457 | ] 458 | } 459 | ] 460 | } 461 | } 462 | ] 463 | } 464 | }, 465 | "SetupRoleProfile": { 466 | "Type": "AWS::IAM::InstanceProfile", 467 | "Properties": { 468 | "Path": "/", 469 | "Roles": [ 470 | { 471 | "Ref": "SetupRole" 472 | } 473 | ] 474 | } 475 | }, 476 | "WorkloadSecurityGroup": { 477 | "Type": "AWS::EC2::SecurityGroup", 478 | "Properties": { 479 | "GroupDescription": "Allow access to the Workload instances", 480 | "VpcId": { 481 | "Ref": "VPCID" 482 | }, 483 | "SecurityGroupIngress": [ 484 | { 485 | "IpProtocol": "tcp", 486 | "FromPort": "80", 487 | "ToPort": "80", 488 | "SourceSecurityGroupId": { 489 | "Ref": "ELBSecurityGroup" 490 | } 491 | }, 492 | { 493 | "IpProtocol": "tcp", 494 | "FromPort": "443", 495 | "ToPort": "443", 496 | "SourceSecurityGroupId": { 497 | "Ref": "ELBSecurityGroup" 498 | } 499 | }, 500 | { 501 | "IpProtocol": "tcp", 502 | "FromPort": "22", 503 | "ToPort": "22", 504 | "SourceSecurityGroupId": { 505 | "Ref": "BastionSecurityGroupID" 506 | } 507 | } 508 | ] 509 | } 510 | }, 511 | "WorkloadASLaunchConfig": { 512 | "Type": "AWS::AutoScaling::LaunchConfiguration", 513 | "Metadata": { 514 | "AWS::CloudFormation::Init": { 515 | "configSets": { 516 | "quickstart": [ 517 | "install", 518 | "configure", 519 | "cleanup" 520 | ] 521 | }, 522 | "install": {}, 523 | "configure": {}, 524 | "cleanup": {} 525 | } 526 | }, 527 | "Properties": { 528 | "KeyName": { 529 | "Ref": "KeyPairName" 530 | }, 531 | "ImageId": { 532 | "Fn::FindInMap": [ 533 | "AWSAMIRegionMap", 534 | { 535 | "Ref": "AWS::Region" 536 | }, 537 | "AMZNLINUXHVM" 538 | ] 539 | }, 540 | "InstanceMonitoring": "true", 541 | "IamInstanceProfile": { 542 | "Ref": "SetupRoleProfile" 543 | }, 544 | "InstanceType": { 545 | "Ref": "WorkloadInstanceType" 546 | }, 547 | "SecurityGroups": [ 548 | { 549 | "Ref": "WorkloadSecurityGroup" 550 | } 551 | ], 552 | "UserData": { 553 | "Fn::Base64": { 554 | "Fn::Join": [ 555 | "", 556 | [ 557 | "#!/bin/bash\n", 558 | "export PATH=$PATH:/usr/local/bin\n", 559 | "which pip &> /dev/null\n", 560 | "if [ $? -ne 0 ] ; then\n", 561 | " echo \"PIP NOT INSTALLED\"\n", 562 | " [ `which yum` ] && $(yum install -y epel-release; yum install -y python-pip) && echo \"PIP INSTALLED\"\n", 563 | " [ `which apt-get` ] && apt-get -y update && apt-get -y install python-pip && echo \"PIP INSTALLED\"\n", 564 | "fi\n", 565 | "pip install --upgrade pip &> /dev/null\n", 566 | "pip install awscli --ignore-installed six &> /dev/null\n", 567 | "easy_install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz\n", 568 | "cfn-init --stack ", 569 | { 570 | "Ref": "AWS::StackName" 571 | }, 572 | " --resource WorkloadASLaunchConfig --configsets quickstart --region ", 573 | { 574 | "Ref": "AWS::Region" 575 | }, 576 | "\n", 577 | "# Signal the status from cfn-init\n", 578 | "cfn-signal -e $? --stack ", 579 | { 580 | "Ref": "AWS::StackName" 581 | }, 582 | " --resource WorkloadAutoScalingGroup --region ", 583 | { 584 | "Ref": "AWS::Region" 585 | }, 586 | "\n" 587 | ] 588 | ] 589 | } 590 | } 591 | } 592 | }, 593 | "WorkloadScaleUpPolicy": { 594 | "Type": "AWS::AutoScaling::ScalingPolicy", 595 | "Properties": { 596 | "AdjustmentType": "ChangeInCapacity", 597 | "AutoScalingGroupName": { 598 | "Ref": "WorkloadAutoScalingGroup" 599 | }, 600 | "Cooldown": "300", 601 | "ScalingAdjustment": "1" 602 | } 603 | }, 604 | "WorkloadScaleDownPolicy": { 605 | "Type": "AWS::AutoScaling::ScalingPolicy", 606 | "Properties": { 607 | "AdjustmentType": "ChangeInCapacity", 608 | "AutoScalingGroupName": { 609 | "Ref": "WorkloadAutoScalingGroup" 610 | }, 611 | "Cooldown": "300", 612 | "ScalingAdjustment": "-1" 613 | } 614 | }, 615 | "CPUAlarmHigh": { 616 | "Type": "AWS::CloudWatch::Alarm", 617 | "Properties": { 618 | "AlarmDescription": "Scale-up if CPU > 60% for 5 minutes", 619 | "MetricName": "CPUUtilization", 620 | "Namespace": "AWS/EC2", 621 | "Statistic": "Average", 622 | "Period": "60", 623 | "EvaluationPeriods": "5", 624 | "Threshold": "60", 625 | "AlarmActions": [ 626 | { 627 | "Ref": "WorkloadScaleUpPolicy" 628 | } 629 | ], 630 | "Dimensions": [ 631 | { 632 | "Name": "AutoScalingGroupName", 633 | "Value": { 634 | "Ref": "WorkloadAutoScalingGroup" 635 | } 636 | } 637 | ], 638 | "ComparisonOperator": "GreaterThanThreshold" 639 | } 640 | }, 641 | "CPUAlarmLow": { 642 | "Type": "AWS::CloudWatch::Alarm", 643 | "Properties": { 644 | "AlarmDescription": "Scale-down if CPU < 40% for 30 minutes", 645 | "MetricName": "CPUUtilization", 646 | "Namespace": "AWS/EC2", 647 | "Statistic": "Average", 648 | "Period": "60", 649 | "EvaluationPeriods": "30", 650 | "Threshold": "40", 651 | "AlarmActions": [ 652 | { 653 | "Ref": "WorkloadScaleDownPolicy" 654 | } 655 | ], 656 | "Dimensions": [ 657 | { 658 | "Name": "AutoScalingGroupName", 659 | "Value": { 660 | "Ref": "WorkloadAutoScalingGroup" 661 | } 662 | } 663 | ], 664 | "ComparisonOperator": "LessThanThreshold" 665 | } 666 | }, 667 | "WorkloadAutoScalingGroup": { 668 | "Type": "AWS::AutoScaling::AutoScalingGroup", 669 | "Properties": { 670 | "VPCZoneIdentifier": [ 671 | { 672 | "Ref": "PrivateSubnet1ID" 673 | }, 674 | { 675 | "Ref": "PrivateSubnet2ID" 676 | } 677 | ], 678 | "Cooldown": "600", 679 | "DesiredCapacity": { 680 | "Ref": "WorkloadNodesDesiredCapacity" 681 | }, 682 | "HealthCheckGracePeriod": "600", 683 | "HealthCheckType": "EC2", 684 | "LaunchConfigurationName": { 685 | "Ref": "WorkloadASLaunchConfig" 686 | }, 687 | "LoadBalancerNames": [ 688 | { 689 | "Ref": "ElasticLoadBalancer" 690 | } 691 | ], 692 | "MaxSize": { 693 | "Ref": "WorkloadNodesMaxSize" 694 | }, 695 | "MinSize": { 696 | "Ref": "WorkloadNodesMinSize" 697 | }, 698 | "NotificationConfiguration": { 699 | "TopicARN": { 700 | "Ref": "NotificationTopic" 701 | }, 702 | "NotificationTypes": [ 703 | "autoscaling:EC2_INSTANCE_LAUNCH", 704 | "autoscaling:EC2_INSTANCE_LAUNCH_ERROR", 705 | "autoscaling:EC2_INSTANCE_TERMINATE", 706 | "autoscaling:EC2_INSTANCE_TERMINATE_ERROR", 707 | "autoscaling:TEST_NOTIFICATION" 708 | ] 709 | }, 710 | "Tags": [ 711 | { 712 | "Key": "Name", 713 | "Value": "Workload Server cluster node", 714 | "PropagateAtLaunch": "true" 715 | } 716 | ] 717 | }, 718 | "CreationPolicy": { 719 | "ResourceSignal": { 720 | "Count": { 721 | "Ref": "WorkloadNodesDesiredCapacity" 722 | }, 723 | "Timeout": "PT2H" 724 | } 725 | } 726 | }, 727 | "S3Bucket": { 728 | "Type": "AWS::S3::Bucket", 729 | "Properties": { 730 | "BucketName": { 731 | "Ref": "S3BucketName" 732 | }, 733 | "AccessControl": "BucketOwnerFullControl" 734 | } 735 | } 736 | }, 737 | "Outputs": { 738 | "ELBDNSName": { 739 | "Description": "ELB DNS Name", 740 | "Value": { 741 | "Fn::GetAtt": [ 742 | "ElasticLoadBalancer", 743 | "DNSName" 744 | ] 745 | } 746 | } 747 | } 748 | } 749 | -------------------------------------------------------------------------------- /spec/fixtures/templates/bad.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: A sample template 3 | Errors: 4 | Catch: Missing 5 | Parameters: 6 | myParam: 7 | Type: String 8 | Default: String 9 | Description: String 10 | Resources: 11 | ## Missing Properties 12 | MyEC2Instance1: 13 | Type: "AWS::EC2::Instance1" 14 | ## Fake Properties Key on main level 15 | ## Bad sub properties in BlockDeviceMappings/Ebs and NetworkInterfaces 16 | MyEC2Instance: 17 | Type: "AWS::EC2::Instance" 18 | Properties: 19 | ImageId: "ami-2f726546" 20 | InstanceType: t1.micro 21 | KeyName: 1 22 | FakeKey: MadeYouLook 23 | BlockDeviceMappings: 24 | - 25 | DeviceName: /dev/sdm 26 | Ebs: 27 | VolumeType: io1 28 | Iops: !Ref pIops 29 | DeleteOnTermination: false 30 | VolumeSize: 20 31 | BadSubX2Key: Not valid 32 | NetworkInterfaces: 33 | - DeviceIndex: "1" 34 | BadKey: true 35 | ## Bad Policies property in PolicyDocument 36 | RootRole: 37 | Type: "AWS::IAM::Role" 38 | Properties: 39 | AssumeRolePolicyDocument: 40 | Version: "2012-10-17" 41 | Statement: 42 | - 43 | Effect: "Allow" 44 | Principal: 45 | Service: 46 | - "ec2.amazonaws.com" 47 | Action: 48 | - "sts:AssumeRole" 49 | Path: "/" 50 | Policies: 51 | - 52 | PolicyName: "root" 53 | PolicyDocument1: 54 | Version: "2012-10-17" 55 | Statement: 56 | - 57 | Effect: "Allow" 58 | Action: "*" 59 | Resource: "*" 60 | RolePolicies: 61 | Type: "AWS::IAM::Policy" 62 | Properties: 63 | PolicyName: "root" 64 | PolicyDocument: 65 | Version: "2012-10-17" 66 | Statement: 67 | - 68 | Effect: "Allow" 69 | Action: "*" 70 | Resource: "*" 71 | Roles: 72 | - 73 | Ref: "RootRole" 74 | RootInstanceProfile: 75 | Type: "AWS::IAM::InstanceProfile" 76 | Properties: 77 | Path: "/" 78 | Roles: 79 | - 80 | Ref: "RootRole" 81 | 82 | # Bad Key under HealthCheck 83 | ElasticLoadBalancer: 84 | Type: AWS::ElasticLoadBalancing::LoadBalancer 85 | Properties: 86 | AvailabilityZones: 87 | Fn::GetAZs: '' 88 | Instances: 89 | - Ref: MyEC2Instance 90 | Listeners: 91 | - LoadBalancerPort: '80' 92 | InstancePort: 93 | Ref: WebServerPort 94 | Protocol: HTTP 95 | HealthCheck: 96 | FakeKey: Another fake key 97 | Target: 98 | Fn::Join: 99 | - '' 100 | - - 'HTTP:' 101 | - Ref: WebServerPort 102 | - "/" 103 | HealthyThreshold: '3' 104 | # Int which should be string. (No Error) 105 | UnhealthyThreshold: 5 106 | # Should be boolean (Error) 107 | Interval: True 108 | Timeout: '5' 109 | Outputs: 110 | myOutput: 111 | Value: !GetAtt ElasticLoadBalancer.CanonicalHostedZoneName 112 | myErrorOutput: 113 | Value: !GetAtt ElasticLoadBalancer.DNE 114 | -------------------------------------------------------------------------------- /spec/fixtures/templates/good.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "This workload template deploys an ASG behind an ELB load balancer in two private subnets. The cluster is configured to use an S3 bucket for storage **WARNING** This template creates EC2 instances and related resources. You will be billed for the AWS resources used if you create a stack from this template.", 4 | "Metadata": { 5 | "AWS::CloudFormation::Interface": { 6 | "ParameterGroups": [ 7 | { 8 | "Label": { 9 | "default": "Network Configuration" 10 | }, 11 | "Parameters": [ 12 | "VPCID", 13 | "PrivateSubnet1ID", 14 | "PrivateSubnet2ID", 15 | "PublicSubnet1ID", 16 | "PublicSubnet2ID" 17 | ] 18 | }, 19 | { 20 | "Label": { 21 | "default": "Amazon EC2 Configuration" 22 | }, 23 | "Parameters": [ 24 | "KeyPairName", 25 | "BastionSecurityGroupID", 26 | "WorkloadInstanceType" 27 | ] 28 | }, 29 | { 30 | "Label": { 31 | "default": "Workload Nodes Configuration" 32 | }, 33 | "Parameters": [ 34 | "WorkloadNodesMinSize", 35 | "WorkloadNodesMaxSize", 36 | "WorkloadNodesDesiredCapacity", 37 | "OperatorEmail" 38 | ] 39 | }, 40 | { 41 | "Label": { 42 | "default": "Workload Storage Configuration" 43 | }, 44 | "Parameters": [ 45 | "S3BucketName" 46 | ] 47 | }, 48 | { 49 | "Label": { 50 | "default": "AWS Quick Start Configuration" 51 | }, 52 | "Parameters": [ 53 | "QSS3BucketName", 54 | "QSS3KeyPrefix" 55 | ] 56 | } 57 | ], 58 | "ParameterLabels": { 59 | "BastionSecurityGroupID": { 60 | "default": "Bastion Security Group ID" 61 | }, 62 | "KeyPairName": { 63 | "default": "SSH Key Name" 64 | }, 65 | "OperatorEmail": { 66 | "default": "Operator Email" 67 | }, 68 | "PrivateSubnet1ID": { 69 | "default": "Private Subnet 1 ID" 70 | }, 71 | "PrivateSubnet2ID": { 72 | "default": "Private Subnet 2 ID" 73 | }, 74 | "PublicSubnet1ID": { 75 | "default": "Public Subnet 1 ID" 76 | }, 77 | "PublicSubnet2ID": { 78 | "default": "Public Subnet 2 ID" 79 | }, 80 | "QSS3BucketName": { 81 | "default": "Quick Start S3 Bucket Name" 82 | }, 83 | "QSS3KeyPrefix": { 84 | "default": "Quick Start S3 Key Prefix" 85 | }, 86 | "S3BucketName": { 87 | "default": "S3 Bucket Name" 88 | }, 89 | "VPCID": { 90 | "default": "VPC ID" 91 | }, 92 | "WorkloadInstanceType": { 93 | "default": "Workload Servers Instance Type" 94 | }, 95 | "WorkloadNodesDesiredCapacity": { 96 | "default": "Workload Nodes Desired Capacity" 97 | }, 98 | "WorkloadNodesMaxSize": { 99 | "default": "Workload Nodes Max Size" 100 | }, 101 | "WorkloadNodesMinSize": { 102 | "default": "Workload Nodes Min Size" 103 | } 104 | } 105 | } 106 | }, 107 | "Parameters": { 108 | "BastionSecurityGroupID": { 109 | "Description": "ID of the bastion host security group to enable SSH connections (e.g., sg-7f16e910)", 110 | "Type": "AWS::EC2::SecurityGroup::Id" 111 | }, 112 | "KeyPairName": { 113 | "Description": "Name of an existing EC2 key pair. All instances will launch with this key pair.", 114 | "Type": "AWS::EC2::KeyPair::KeyName" 115 | }, 116 | "OperatorEmail": { 117 | "AllowedPattern": "([a-zA-Z0-9_\\-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.)|(([a-zA-Z0-9\\-]+\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?)", 118 | "ConstraintDescription": "Must be a valid email address.", 119 | "Description": "Email address that notifications of any scaling operations will be sent to", 120 | "Type": "String" 121 | }, 122 | "PrivateSubnet1ID": { 123 | "Description": "ID of private subnet 1 in Availability Zone 1 for the Workload (e.g., subnet-a0246dcd)", 124 | "Type": "AWS::EC2::Subnet::Id" 125 | }, 126 | "PrivateSubnet2ID": { 127 | "Description": "ID of private subnet 2 in Availability Zone 2 for the Workload (e.g., subnet-b1f432cd)", 128 | "Type": "AWS::EC2::Subnet::Id" 129 | }, 130 | "PublicSubnet1ID": { 131 | "Description": "ID of public subnet 1 in Availability Zone 1 for the ELB load balancer (e.g., subnet-9bc642ac)", 132 | "Type": "AWS::EC2::Subnet::Id" 133 | }, 134 | "PublicSubnet2ID": { 135 | "Description": "ID of public subnet 2 in Availability Zone 2 for the ELB load balancer (e.g., subnet-e3246d8e)", 136 | "Type": "AWS::EC2::Subnet::Id" 137 | }, 138 | "QSS3BucketName": { 139 | "AllowedPattern": "^[0-9a-zA-Z]+([0-9a-zA-Z-]*[0-9a-zA-Z])*$", 140 | "ConstraintDescription": "Quick Start bucket name can include numbers, lowercase letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen (-).", 141 | "Default": "aws-quickstart", 142 | "Description": "S3 bucket name for the Quick Start assets. This string can include numbers, lowercase letters, uppercase letters, and hyphens (-). It cannot start or end with a hyphen (-).", 143 | "Type": "String" 144 | }, 145 | "QSS3KeyPrefix": { 146 | "AllowedPattern": "^[0-9a-zA-Z-/]*$", 147 | "ConstraintDescription": "Quick Start key prefix can include numbers, lowercase letters, uppercase letters, hyphens (-), and forward slash (/).", 148 | "Default": "quickstart-examples/", 149 | "Description": "S3 key prefix for the Quick Start assets. Quick Start key prefix can include numbers, lowercase letters, uppercase letters, hyphens (-), and forward slash (/).", 150 | "Type": "String" 151 | }, 152 | "S3BucketName": { 153 | "AllowedPattern": "^[a-z0-9][a-z0-9-.]*$", 154 | "Default": "type-unique-value-here-in-lowercase", 155 | "Description": "Name of the S3 bucket that will be created for your workload to store data. Enter a unique name that does not include uppercase characters.", 156 | "Type": "String" 157 | }, 158 | "VPCID": { 159 | "Description": "ID of your existing VPC for deployment", 160 | "Type": "AWS::EC2::VPC::Id" 161 | }, 162 | "WorkloadInstanceType": { 163 | "AllowedValues": [ 164 | "t2.large", 165 | "m4.large", 166 | "m4.xlarge", 167 | "m4.2xlarge", 168 | "m4.4xlarge", 169 | "m4.10xlarge", 170 | "m3.medium", 171 | "m3.large", 172 | "m3.xlarge", 173 | "m3.2xlarge", 174 | "c4.large", 175 | "c4.xlarge", 176 | "c4.2xlarge", 177 | "c4.4xlarge", 178 | "c4.8xlarge", 179 | "c3.large", 180 | "c3.xlarge", 181 | "c3.2xlarge", 182 | "c3.4xlarge", 183 | "c3.8xlarge", 184 | "r3.large", 185 | "r3.xlarge" 186 | ], 187 | "ConstraintDescription": "Must contain valid instance type", 188 | "Default": "m4.xlarge", 189 | "Description": "Type of EC2 instance for the Workload instances", 190 | "Type": "String" 191 | }, 192 | "WorkloadNodesDesiredCapacity": { 193 | "Default": "2", 194 | "Description": "The desired capacity for the Workload nodes Auto Scaling group", 195 | "Type": "String" 196 | }, 197 | "WorkloadNodesMaxSize": { 198 | "Default": "4", 199 | "Description": "The maximum size of the Auto Scaling group", 200 | "Type": "String" 201 | }, 202 | "WorkloadNodesMinSize": { 203 | "Default": "2", 204 | "Description": "The minimum size of the Auto Scaling group", 205 | "Type": "String" 206 | } 207 | }, 208 | "Rules": { 209 | "KeyPairsNotEmpty": { 210 | "Assertions": [ 211 | { 212 | "Assert": { 213 | "Fn::Not": [ 214 | { 215 | "Fn::EachMemberEquals": [ 216 | { 217 | "Fn::RefAll": "AWS::EC2::KeyPair::KeyName" 218 | }, 219 | "" 220 | ] 221 | } 222 | ] 223 | }, 224 | "AssertDescription": "All key pair parameters must not be empty" 225 | } 226 | ] 227 | }, 228 | "SubnetsInVPC": { 229 | "Assertions": [ 230 | { 231 | "Assert": { 232 | "Fn::EachMemberIn": [ 233 | { 234 | "Fn::ValueOfAll": [ 235 | "AWS::EC2::Subnet::Id", 236 | "VpcId" 237 | ] 238 | }, 239 | { 240 | "Fn::RefAll": "AWS::EC2::VPC::Id" 241 | } 242 | ] 243 | }, 244 | "AssertDescription": "All subnets must in the VPC" 245 | } 246 | ] 247 | } 248 | }, 249 | "Mappings": { 250 | "AWSAMIRegionMap": { 251 | "AMI": { 252 | "AMZNLINUXHVM": "amzn-ami-hvm-2018.03.0.20180412-x86_64-gp2" 253 | }, 254 | "ap-northeast-1": { 255 | "AMZNLINUXHVM": "ami-28ddc154" 256 | }, 257 | "ap-northeast-2": { 258 | "AMZNLINUXHVM": "ami-efaf0181" 259 | }, 260 | "ap-south-1": { 261 | "AMZNLINUXHVM": "ami-b46f48db" 262 | }, 263 | "ap-southeast-1": { 264 | "AMZNLINUXHVM": "ami-64260718" 265 | }, 266 | "ap-southeast-2": { 267 | "AMZNLINUXHVM": "ami-60a26a02" 268 | }, 269 | "ca-central-1": { 270 | "AMZNLINUXHVM": "ami-2f39bf4b" 271 | }, 272 | "eu-central-1": { 273 | "AMZNLINUXHVM": "ami-1b316af0" 274 | }, 275 | "eu-west-1": { 276 | "AMZNLINUXHVM": "ami-9cbe9be5" 277 | }, 278 | "eu-west-2": { 279 | "AMZNLINUXHVM": "ami-c12dcda6" 280 | }, 281 | "sa-east-1": { 282 | "AMZNLINUXHVM": "ami-f09dcc9c" 283 | }, 284 | "us-east-1": { 285 | "AMZNLINUXHVM": "ami-467ca739" 286 | }, 287 | "us-east-2": { 288 | "AMZNLINUXHVM": "ami-976152f2" 289 | }, 290 | "us-west-1": { 291 | "AMZNLINUXHVM": "ami-46e1f226" 292 | }, 293 | "us-west-2": { 294 | "AMZNLINUXHVM": "ami-6b8cef13" 295 | } 296 | } 297 | }, 298 | "Resources": { 299 | "NotificationTopic": { 300 | "Type": "AWS::SNS::Topic", 301 | "Properties": { 302 | "Subscription": [ 303 | { 304 | "Endpoint": { 305 | "Ref": "OperatorEmail" 306 | }, 307 | "Protocol": "email" 308 | } 309 | ] 310 | } 311 | }, 312 | "ELBSecurityGroup": { 313 | "Type": "AWS::EC2::SecurityGroup", 314 | "Properties": { 315 | "GroupDescription": "Allow access to the ELB", 316 | "VpcId": { 317 | "Ref": "VPCID" 318 | }, 319 | "SecurityGroupIngress": [ 320 | { 321 | "IpProtocol": "tcp", 322 | "FromPort": 80, 323 | "ToPort": 80, 324 | "CidrIp": "0.0.0.0/0" 325 | }, 326 | { 327 | "IpProtocol": "tcp", 328 | "FromPort": 443, 329 | "ToPort": 443, 330 | "CidrIp": "0.0.0.0/0" 331 | } 332 | ] 333 | } 334 | }, 335 | "ElasticLoadBalancer": { 336 | "Type": "AWS::ElasticLoadBalancing::LoadBalancer", 337 | "Properties": { 338 | "Subnets": [ 339 | { 340 | "Ref": "PublicSubnet1ID" 341 | }, 342 | { 343 | "Ref": "PublicSubnet2ID" 344 | } 345 | ], 346 | "SecurityGroups": [ 347 | { 348 | "Ref": "ELBSecurityGroup" 349 | } 350 | ], 351 | "AppCookieStickinessPolicy": [ 352 | { 353 | "CookieName": "dummy", 354 | "PolicyName": "WorkloadCookieStickinessPolicy" 355 | } 356 | ], 357 | "Listeners": [ 358 | { 359 | "LoadBalancerPort": "80", 360 | "InstancePort": "80", 361 | "Protocol": "HTTP", 362 | "PolicyNames": [ 363 | "WorkloadCookieStickinessPolicy" 364 | ] 365 | } 366 | ], 367 | "CrossZone": true, 368 | "HealthCheck": { 369 | "Target": "TCP:80", 370 | "HealthyThreshold": "2", 371 | "UnhealthyThreshold": "3", 372 | "Interval": "30", 373 | "Timeout": "3" 374 | } 375 | } 376 | }, 377 | "SetupRole": { 378 | "Type": "AWS::IAM::Role", 379 | "Properties": { 380 | "AssumeRolePolicyDocument": { 381 | "Statement": [ 382 | { 383 | "Effect": "Allow", 384 | "Principal": { 385 | "Service": [ 386 | "ec2.amazonaws.com" 387 | ] 388 | }, 389 | "Action": [ 390 | "sts:AssumeRole" 391 | ] 392 | } 393 | ] 394 | }, 395 | "Path": "/", 396 | "Policies": [ 397 | { 398 | "PolicyDocument": { 399 | "Version": "2012-10-17", 400 | "Statement": [ 401 | { 402 | "Action": [ 403 | "s3:GetObject" 404 | ], 405 | "Resource": { 406 | "Fn::Sub": "arn:aws:s3:::${QSS3BucketName}/${QSS3KeyPrefix}*" 407 | }, 408 | "Effect": "Allow" 409 | } 410 | ] 411 | }, 412 | "PolicyName": "aws-quick-start-s3-policy" 413 | }, 414 | { 415 | "PolicyName": "WorkloadSetup", 416 | "PolicyDocument": { 417 | "Statement": [ 418 | { 419 | "Effect": "Allow", 420 | "Action": [ 421 | "cloudwatch:PutMetricData", 422 | "cloudwatch:EnableAlarmActions", 423 | "cloudwatch:PutMetricAlarm" 424 | ], 425 | "Resource": "*" 426 | }, 427 | { 428 | "Effect": "Allow", 429 | "Action": [ 430 | "s3:*" 431 | ], 432 | "Resource": [ 433 | { 434 | "Fn::Join": [ 435 | "", 436 | [ 437 | "arn:aws:s3:::", 438 | { 439 | "Ref": "S3Bucket" 440 | }, 441 | "" 442 | ] 443 | ] 444 | }, 445 | { 446 | "Fn::Join": [ 447 | "", 448 | [ 449 | "arn:aws:s3:::", 450 | { 451 | "Ref": "S3Bucket" 452 | }, 453 | "/*" 454 | ] 455 | ] 456 | } 457 | ] 458 | } 459 | ] 460 | } 461 | } 462 | ] 463 | } 464 | }, 465 | "SetupRoleProfile": { 466 | "Type": "AWS::IAM::InstanceProfile", 467 | "Properties": { 468 | "Path": "/", 469 | "Roles": [ 470 | { 471 | "Ref": "SetupRole" 472 | } 473 | ] 474 | } 475 | }, 476 | "WorkloadSecurityGroup": { 477 | "Type": "AWS::EC2::SecurityGroup", 478 | "Properties": { 479 | "GroupDescription": "Allow access to the Workload instances", 480 | "VpcId": { 481 | "Ref": "VPCID" 482 | }, 483 | "SecurityGroupIngress": [ 484 | { 485 | "IpProtocol": "tcp", 486 | "FromPort": 80, 487 | "ToPort": 80, 488 | "SourceSecurityGroupId": { 489 | "Ref": "ELBSecurityGroup" 490 | } 491 | }, 492 | { 493 | "IpProtocol": "tcp", 494 | "FromPort": 443, 495 | "ToPort": 443, 496 | "SourceSecurityGroupId": { 497 | "Ref": "ELBSecurityGroup" 498 | } 499 | }, 500 | { 501 | "IpProtocol": "tcp", 502 | "FromPort": 22, 503 | "ToPort": 22, 504 | "SourceSecurityGroupId": { 505 | "Ref": "BastionSecurityGroupID" 506 | } 507 | } 508 | ] 509 | } 510 | }, 511 | "WorkloadASLaunchConfig": { 512 | "Type": "AWS::AutoScaling::LaunchConfiguration", 513 | "Metadata": { 514 | "AWS::CloudFormation::Init": { 515 | "configSets": { 516 | "quickstart": [ 517 | "install", 518 | "configure", 519 | "cleanup" 520 | ] 521 | }, 522 | "install": {}, 523 | "configure": {}, 524 | "cleanup": {} 525 | } 526 | }, 527 | "Properties": { 528 | "KeyName": { 529 | "Ref": "KeyPairName" 530 | }, 531 | "ImageId": { 532 | "Fn::FindInMap": [ 533 | "AWSAMIRegionMap", 534 | { 535 | "Ref": "AWS::Region" 536 | }, 537 | "AMZNLINUXHVM" 538 | ] 539 | }, 540 | "InstanceMonitoring": true, 541 | "IamInstanceProfile": { 542 | "Ref": "SetupRoleProfile" 543 | }, 544 | "InstanceType": { 545 | "Ref": "WorkloadInstanceType" 546 | }, 547 | "SecurityGroups": [ 548 | { 549 | "Ref": "WorkloadSecurityGroup" 550 | } 551 | ], 552 | "UserData": { 553 | "Fn::Base64": { 554 | "Fn::Join": [ 555 | "", 556 | [ 557 | "#!/bin/bash\n", 558 | "export PATH=$PATH:/usr/local/bin\n", 559 | "which pip &> /dev/null\n", 560 | "if [ $? -ne 0 ] ; then\n", 561 | " echo \"PIP NOT INSTALLED\"\n", 562 | " [ `which yum` ] && $(yum install -y epel-release; yum install -y python-pip) && echo \"PIP INSTALLED\"\n", 563 | " [ `which apt-get` ] && apt-get -y update && apt-get -y install python-pip && echo \"PIP INSTALLED\"\n", 564 | "fi\n", 565 | "pip install --upgrade pip &> /dev/null\n", 566 | "pip install awscli --ignore-installed six &> /dev/null\n", 567 | "easy_install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz\n", 568 | "cfn-init --stack ", 569 | { 570 | "Ref": "AWS::StackName" 571 | }, 572 | " --resource WorkloadASLaunchConfig --configsets quickstart --region ", 573 | { 574 | "Ref": "AWS::Region" 575 | }, 576 | "\n", 577 | "# Signal the status from cfn-init\n", 578 | "cfn-signal -e $? --stack ", 579 | { 580 | "Ref": "AWS::StackName" 581 | }, 582 | " --resource WorkloadAutoScalingGroup --region ", 583 | { 584 | "Ref": "AWS::Region" 585 | }, 586 | "\n" 587 | ] 588 | ] 589 | } 590 | } 591 | } 592 | }, 593 | "WorkloadScaleUpPolicy": { 594 | "Type": "AWS::AutoScaling::ScalingPolicy", 595 | "Properties": { 596 | "AdjustmentType": "ChangeInCapacity", 597 | "AutoScalingGroupName": { 598 | "Ref": "WorkloadAutoScalingGroup" 599 | }, 600 | "Cooldown": "300", 601 | "ScalingAdjustment": 1 602 | } 603 | }, 604 | "WorkloadScaleDownPolicy": { 605 | "Type": "AWS::AutoScaling::ScalingPolicy", 606 | "Properties": { 607 | "AdjustmentType": "ChangeInCapacity", 608 | "AutoScalingGroupName": { 609 | "Ref": "WorkloadAutoScalingGroup" 610 | }, 611 | "Cooldown": "300", 612 | "ScalingAdjustment": -1 613 | } 614 | }, 615 | "CPUAlarmHigh": { 616 | "Type": "AWS::CloudWatch::Alarm", 617 | "Properties": { 618 | "AlarmDescription": "Scale-up if CPU > 60% for 5 minutes", 619 | "MetricName": "CPUUtilization", 620 | "Namespace": "AWS/EC2", 621 | "Statistic": "Average", 622 | "Period": 60, 623 | "EvaluationPeriods": 5, 624 | "Threshold": 60, 625 | "AlarmActions": [ 626 | { 627 | "Ref": "WorkloadScaleUpPolicy" 628 | } 629 | ], 630 | "Dimensions": [ 631 | { 632 | "Name": "AutoScalingGroupName", 633 | "Value": { 634 | "Ref": "WorkloadAutoScalingGroup" 635 | } 636 | } 637 | ], 638 | "ComparisonOperator": "GreaterThanThreshold" 639 | } 640 | }, 641 | "CPUAlarmLow": { 642 | "Type": "AWS::CloudWatch::Alarm", 643 | "Properties": { 644 | "AlarmDescription": "Scale-down if CPU < 40% for 30 minutes", 645 | "MetricName": "CPUUtilization", 646 | "Namespace": "AWS/EC2", 647 | "Statistic": "Average", 648 | "Period": 60, 649 | "EvaluationPeriods": 30, 650 | "Threshold": 40, 651 | "AlarmActions": [ 652 | { 653 | "Ref": "WorkloadScaleDownPolicy" 654 | } 655 | ], 656 | "Dimensions": [ 657 | { 658 | "Name": "AutoScalingGroupName", 659 | "Value": { 660 | "Ref": "WorkloadAutoScalingGroup" 661 | } 662 | } 663 | ], 664 | "ComparisonOperator": "LessThanThreshold" 665 | } 666 | }, 667 | "WorkloadAutoScalingGroup": { 668 | "Type": "AWS::AutoScaling::AutoScalingGroup", 669 | "Properties": { 670 | "VPCZoneIdentifier": [ 671 | { 672 | "Ref": "PrivateSubnet1ID" 673 | }, 674 | { 675 | "Ref": "PrivateSubnet2ID" 676 | } 677 | ], 678 | "Cooldown": "600", 679 | "DesiredCapacity": { 680 | "Ref": "WorkloadNodesDesiredCapacity" 681 | }, 682 | "HealthCheckGracePeriod": 600, 683 | "HealthCheckType": "EC2", 684 | "LaunchConfigurationName": { 685 | "Ref": "WorkloadASLaunchConfig" 686 | }, 687 | "LoadBalancerNames": [ 688 | { 689 | "Ref": "ElasticLoadBalancer" 690 | } 691 | ], 692 | "MaxSize": { 693 | "Ref": "WorkloadNodesMaxSize" 694 | }, 695 | "MinSize": { 696 | "Ref": "WorkloadNodesMinSize" 697 | }, 698 | "NotificationConfigurations": [ 699 | { 700 | "TopicARN": { 701 | "Ref": "NotificationTopic" 702 | }, 703 | "NotificationTypes": [ 704 | "autoscaling:EC2_INSTANCE_LAUNCH", 705 | "autoscaling:EC2_INSTANCE_LAUNCH_ERROR", 706 | "autoscaling:EC2_INSTANCE_TERMINATE", 707 | "autoscaling:EC2_INSTANCE_TERMINATE_ERROR", 708 | "autoscaling:TEST_NOTIFICATION" 709 | ] 710 | } 711 | ], 712 | "Tags": [ 713 | { 714 | "Key": "Name", 715 | "Value": "Workload Server cluster node", 716 | "PropagateAtLaunch": true 717 | } 718 | ] 719 | }, 720 | "CreationPolicy": { 721 | "ResourceSignal": { 722 | "Count": { 723 | "Ref": "WorkloadNodesDesiredCapacity" 724 | }, 725 | "Timeout": "PT2H" 726 | } 727 | } 728 | }, 729 | "S3Bucket": { 730 | "Type": "AWS::S3::Bucket", 731 | "Properties": { 732 | "BucketName": { 733 | "Ref": "S3BucketName" 734 | }, 735 | "AccessControl": "BucketOwnerFullControl" 736 | } 737 | } 738 | }, 739 | "Outputs": { 740 | "ELBDNSName": { 741 | "Description": "ELB DNS Name", 742 | "Value": { 743 | "Fn::GetAtt": [ 744 | "ElasticLoadBalancer", 745 | "DNSName" 746 | ] 747 | } 748 | } 749 | } 750 | } 751 | -------------------------------------------------------------------------------- /spec/fixtures/templates/good.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: A sample template 3 | Parameters: 4 | WebServerPort: 5 | Type: String 6 | Default: 80 7 | Description: Web Server Ports 8 | pIops: 9 | Type: Number 10 | Resources: 11 | RootRole: 12 | Type: "AWS::IAM::Role" 13 | Properties: 14 | AssumeRolePolicyDocument: 15 | Version: "2012-10-17" 16 | Statement: 17 | - 18 | Effect: "Allow" 19 | Principal: 20 | Service: 21 | - "ec2.amazonaws.com" 22 | Action: 23 | - "sts:AssumeRole" 24 | Path: "/" 25 | Policies: 26 | - 27 | PolicyName: "root" 28 | PolicyDocument: 29 | Version: "2012-10-17" 30 | Statement: 31 | - 32 | Effect: "Allow" 33 | Action: "*" 34 | Resource: "*" 35 | RolePolicies: 36 | Type: "AWS::IAM::Policy" 37 | Properties: 38 | PolicyName: "root" 39 | PolicyDocument: 40 | Version: "2012-10-17" 41 | Statement: 42 | - 43 | Effect: "Allow" 44 | Action: "*" 45 | Resource: "*" 46 | Roles: 47 | - 48 | Ref: "RootRole" 49 | RootInstanceProfile: 50 | Type: "AWS::IAM::InstanceProfile" 51 | Properties: 52 | Path: "/" 53 | Roles: 54 | - 55 | Ref: "RootRole" 56 | MyEC2Instance: 57 | Type: "AWS::EC2::Instance" 58 | Properties: 59 | ImageId: "ami-2f726546" 60 | InstanceType: t1.micro 61 | KeyName: testkey 62 | BlockDeviceMappings: 63 | - 64 | DeviceName: /dev/sdm 65 | Ebs: 66 | VolumeType: io1 67 | Iops: !Ref pIops 68 | DeleteOnTermination: false 69 | VolumeSize: 20 70 | NetworkInterfaces: 71 | - DeviceIndex: "1" 72 | ElasticLoadBalancer: 73 | Type: AWS::ElasticLoadBalancing::LoadBalancer 74 | Properties: 75 | AvailabilityZones: 76 | Fn::GetAZs: '' 77 | Instances: 78 | - Ref: MyEC2Instance 79 | Listeners: 80 | - LoadBalancerPort: '80' 81 | InstancePort: 82 | Ref: WebServerPort 83 | Protocol: HTTP 84 | HealthCheck: 85 | Target: 86 | Fn::Join: 87 | - '' 88 | - - 'HTTP:' 89 | - Ref: WebServerPort 90 | - "/" 91 | HealthyThreshold: '3' 92 | UnhealthyThreshold: '5' 93 | Interval: '30' 94 | Timeout: '5' 95 | IamPipeline: 96 | Type: "AWS::CloudFormation::Stack" 97 | Properties: 98 | TemplateURL: !Sub 'https://s3.${AWS::Region}.amazonaws.com/ss-vsts-codepipeline-${AWS::Region}/vsts/${AWS::AccountId}/templates/vsts-pipeline/pipeline.yaml' 99 | Parameters: 100 | DeploymentName: iam-pipeline 101 | Deploy: 'auto' 102 | -------------------------------------------------------------------------------- /spec/fixtures/templates/not_cloudformation.json: -------------------------------------------------------------------------------- 1 | { 2 | "NotEven": "A valid CloudFormation template" 3 | } 4 | -------------------------------------------------------------------------------- /spec/fixtures/templates/not_cloudformation.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | NotEven: A valid CloudFormation template 3 | -------------------------------------------------------------------------------- /spec/fixtures/templates/override.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: "2010-09-09" 3 | Description: > 4 | Template to test overriding the spec file 5 | Resources: 6 | myBucket: 7 | Type: AWS::S3::Bucket 8 | Properties: {} 9 | --------------------------------------------------------------------------------