├── .dockerignore ├── .github ├── pull_request_template.md └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── NOTICE.txt ├── README.md ├── TODO.md ├── cfn_clean ├── __init__.py └── yaml_dumper.py ├── cfn_flip ├── __init__.py ├── __main__.py ├── main.py └── yaml_dumper.py ├── cfn_tools ├── __init__.py ├── _config.py ├── json_encoder.py ├── literal.py ├── odict.py ├── yaml_dumper.py └── yaml_loader.py ├── examples ├── clean.json ├── clean.yaml ├── invalid ├── test.json ├── test.yaml ├── test_json_data.json ├── test_json_data.yaml ├── test_json_data_long_line.json ├── test_json_def_string_with_sub.json ├── test_json_state_machine.json ├── test_long.json ├── test_lorem.yaml ├── test_multibyte.json ├── test_multibyte.yaml ├── test_multiline.yaml ├── test_user_data.yaml ├── test_yaml_def_string_with_sub.yaml ├── test_yaml_long_line.yaml └── test_yaml_state_machine.yaml ├── requirements.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── test_clean.py ├── test_cli.py ├── test_config.py ├── test_flip.py ├── test_odict.py ├── test_step_functions_template.py ├── test_tools.py └── test_yaml_patching.py └── tox.ini /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | .gitignore 4 | .travis.yml 5 | Dockerfile 6 | snapcraft.yaml 7 | TODO.md 8 | -------------------------------------------------------------------------------- /.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 you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - '*' 5 | 6 | name: Create release from tag 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.x' 21 | 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install -U pip 25 | pip install -r requirements.txt 26 | pip install setuptools wheel twine 27 | 28 | - name: Build 29 | run: | 30 | python setup.py sdist bdist_wheel 31 | 32 | - name: Publish 33 | env: 34 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 35 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 36 | run: twine upload dist/* 37 | 38 | - name: Create GitHub release 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | run: | 42 | set -x 43 | (echo ${GITHUB_REF##*/}; echo; git cherry -v $(git describe --abbrev=0 HEAD^) | cut -d" " -f3-) > CHANGELOG 44 | assets=() 45 | for f in ./dist/*; do 46 | assets+=("-a" "$f") 47 | done 48 | hub release create "${assets[@]}" -F CHANGELOG "${GITHUB_REF##*/}" 49 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [ master ] 4 | 5 | name: Unit tests 6 | 7 | jobs: 8 | test: 9 | name: Test 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python-version: [3.6, 3.7, 3.8, 3.9, "3.10"] 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install -U pip 26 | pip install -r requirements.txt 27 | pip install tox-gh-actions 28 | 29 | - name: Run tests 30 | run: tox 31 | 32 | - name: Upload coverage reports to Codecov 33 | uses: codecov/codecov-action@v2 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled python modules. 2 | *.pyc 3 | 4 | # Setuptools distribution folder. 5 | /dist/ 6 | 7 | # Python egg metadata, regenerated from source files by setuptools. 8 | /*.egg-info 9 | .eggs/ 10 | 11 | # Test data 12 | .pytest_cache/ 13 | .cache/ 14 | .coverage 15 | .tox/ 16 | /htmlcov/ 17 | .eggs/ 18 | 19 | # IDE 20 | .idea/ 21 | 22 | # Env manager. 23 | .python-version 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-alpine 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY requirements.txt . 6 | RUN pip install --no-cache-dir -r requirements.txt 7 | 8 | COPY . . 9 | RUN pip install --no-cache-dir . 10 | 11 | ENTRYPOINT ["cfn-flip"] 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | AWS CloudFormation Template Flip 2 | Copyright 2016-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **NOTICE:** For CLI usage, AWS CloudFormation Template Flip is now *deprecated*. 2 | 3 | You should use [rain](https://github.com/aws-cloudformation/rain) instead. `rain fmt` can convert CloudFormation templates between JSON and YAML. 4 | 5 | See [the rain fmt documentation](https://aws-cloudformation.github.io/rain/rain_fmt.html) for details. 6 | 7 | This deprecation notice does not affect the API in this package which may continue to be used. 8 | 9 | --- 10 | 11 | [![Build Status](https://github.com/awslabs/aws-cfn-template-flip/actions/workflows/test.yml/badge.svg)](https://github.com/awslabs/aws-cfn-template-flip/actions/workflows/test.yml) 12 | [![PyPI version](https://badge.fury.io/py/cfn-flip.svg)](https://badge.fury.io/py/cfn-flip) 13 | [![Codecov Test Coverage](https://codecov.io/gh/awslabs/aws-cfn-template-flip/branch/master/graphs/badge.svg?style=flat)](https://codecov.io/gh/awslabs/aws-cfn-template-flip) 14 | 15 | # AWS CloudFormation Template Flip 16 | 17 | ## About 18 | 19 | AWS CloudFormation Template Flip is a tool that converts [AWS CloudFormation](https://aws.amazon.com/cloudformation/) templates between [JSON](http://json.org/) and [YAML](http://yaml.org) formats, making use of the YAML format's short function syntax where possible. 20 | 21 | The term "Flip" is inspired by the well-known Unix command-line tool [flip](https://ccrma.stanford.edu/~craig/utility/flip/) which converts text files between Unix, Mac, and MS-DOS formats. 22 | 23 | ## Installation 24 | 25 | AWS CloudFormation Template Flip can be installed using [pip](https://pip.pypa.io/en/stable/): 26 | 27 | ```bash 28 | pip install cfn-flip 29 | ``` 30 | 31 | ## Usage 32 | 33 | AWS CloudFormation Template Flip is both a command line tool and a python library. 34 | 35 | Note that the command line tool is spelled `cfn-flip` with a hyphen, while the python package is `cfn_flip` with an underscore. 36 | 37 | ### Command line tool 38 | 39 | ``` 40 | Usage: cfn-flip [OPTIONS] [INPUT] [OUTPUT] 41 | 42 | AWS CloudFormation Template Flip is a tool that converts AWS 43 | CloudFormation templates between JSON and YAML formats, making use of the 44 | YAML format's short function syntax where possible." 45 | 46 | Options: 47 | -i, --input [json|yaml] Specify the input format. Overrides -j and -y 48 | flags. 49 | -o, --output [json|yaml] Specify the output format. Overrides -j, -y, and 50 | -n flags. 51 | -j, --json Convert to JSON. Assume the input is YAML. 52 | -y, --yaml Convert to YAML. Assume the input is JSON. 53 | -c, --clean Performs some opinionated cleanup on your 54 | template. 55 | -l, --long Use long-form syntax for functions when converting 56 | to YAML. 57 | -n, --no-flip Perform other operations but do not flip the 58 | output format. 59 | --version Show the version and exit. 60 | --help Show this message and exit. 61 | ``` 62 | 63 | 64 | cfn-flip will detect the format of the input template and convert JSON to YAML and YAML to JSON, respectively. 65 | 66 | Examples: 67 | 68 | * Reading from `stdin` and outputting to `stdout`: 69 | 70 | ```bash 71 | cat examples/test.json | cfn-flip 72 | ``` 73 | 74 | * Reading from a file and outputting to `stdout`: 75 | 76 | ```bash 77 | cfn-flip examples/test.yaml 78 | ``` 79 | 80 | * Reading from a file and outputting to another file: 81 | 82 | ```bash 83 | cfn-flip examples/test.json output.yaml 84 | ``` 85 | 86 | * Reading from a file and cleaning up the output 87 | 88 | ```bash 89 | cfn-flip -c examples/test.json 90 | ``` 91 | 92 | ### Python package 93 | 94 | To use AWS CloudFormation Template Flip from your own python projects, import one of the functions `flip`, `to_yaml`, or `to_json` as needed. 95 | 96 | ```python 97 | from cfn_flip import flip, to_yaml, to_json 98 | 99 | """ 100 | All functions expect a string containing serialised data 101 | and return a string containing serialised data 102 | or raise an exception if there is a problem parsing the input 103 | """ 104 | 105 | # flip takes a best guess at the serialisation format 106 | # and returns the opposite, converting json into yaml and vice versa 107 | some_yaml_or_json = flip(some_json_or_yaml) 108 | 109 | # to_json expects serialised yaml as input, and returns serialised json 110 | some_json = to_json(some_yaml) 111 | 112 | # to_yaml expects serialised json as input, and returns serialised yaml 113 | some_yaml = to_yaml(some_json) 114 | 115 | # The clean_up flag performs some opinionated, CloudFormation-specific sanitation of the input 116 | # For example, converting uses of Fn::Join to Fn::Sub 117 | # flip, to_yaml, and to_json all support the clean_up flag 118 | clean_yaml = to_yaml(some_json, clean_up=True) 119 | ``` 120 | 121 | ### Configuration paramters 122 | 123 | You can configure some parameters like: 124 | 125 | `max_col_width`: Maximum columns before breakline. Default value is 200 126 | To change the configuration you can use: 127 | 128 | **Environment Variable** 129 | 130 | Linux/Unix: 131 | `export CFN_MAX_COL_WIDTH=120` 132 | 133 | Windows: `SET CFN_MAX_COL_WIDTH=120` 134 | 135 | **Python** 136 | 137 | ```python 138 | 139 | from cfn_tools._config import config 140 | from cfn_flip import flip, to_yaml, to_json 141 | 142 | """ 143 | All functions expect a string containing serialised data 144 | and return a string containing serialised data 145 | or raise an exception if there is a problem parsing the input 146 | """ 147 | 148 | # Change the default number of columns to break line to 120 149 | config['max_col_width'] = "120" 150 | 151 | # flip takes a best guess at the serialisation format 152 | # and returns the opposite, converting json into yaml and vice versa 153 | some_yaml_or_json = flip(some_json_or_yaml) 154 | 155 | # to_json expects serialised yaml as input, and returns serialised json 156 | some_json = to_json(some_yaml) 157 | 158 | # to_yaml expects serialised json as input, and returns serialised yaml 159 | some_yaml = to_yaml(some_json) 160 | 161 | # The clean_up flag performs some opinionated, CloudFormation-specific sanitation of the input 162 | # For example, converting uses of Fn::Join to Fn::Sub 163 | # flip, to_yaml, and to_json all support the clean_up flag 164 | clean_yaml = to_yaml(some_json, clean_up=True) 165 | 166 | ``` 167 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # To do 2 | 3 | * Use yaml.safe_load 4 | -------------------------------------------------------------------------------- /cfn_clean/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. A copy of the License is located at 6 | 7 | https://aws.amazon.com/apache2.0/ 8 | 9 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and limitations under the License. 12 | """ 13 | 14 | from cfn_tools.odict import ODict 15 | from cfn_tools.literal import LiteralString 16 | from collections.abc import KeysView 17 | 18 | import json 19 | import six 20 | 21 | UNCONVERTED_KEYS = [ 22 | # Resource Type, String Attribute to keep Json 23 | ("AWS::StepFunctions::StateMachine", "DefinitionString") 24 | ] 25 | 26 | 27 | def has_intrinsic_functions(parameter): 28 | intrinsic_functions = ["Fn::Sub", "!Sub", "!GetAtt"] 29 | result = False 30 | if isinstance(parameter, (list, tuple, dict, KeysView)): 31 | for item in parameter: 32 | if item in intrinsic_functions: 33 | result = True 34 | break 35 | return result 36 | 37 | 38 | def convert_join(value): 39 | """ 40 | Fix a Join ;) 41 | """ 42 | 43 | if not isinstance(value, list) or len(value) != 2: 44 | # Cowardly refuse 45 | return value 46 | 47 | sep, parts = value[0], value[1] 48 | 49 | if isinstance(parts, six.string_types): 50 | return parts 51 | 52 | if not isinstance(parts, list): 53 | # This looks tricky, just return the join as it was 54 | return { 55 | "Fn::Join": value, 56 | } 57 | 58 | plain_string = True 59 | 60 | args = ODict() 61 | new_parts = [] 62 | 63 | for part in parts: 64 | part = clean(part) 65 | 66 | if isinstance(part, dict): 67 | plain_string = False 68 | 69 | if "Ref" in part: 70 | new_parts.append("${{{}}}".format(part["Ref"])) 71 | elif "Fn::GetAtt" in part: 72 | params = part["Fn::GetAtt"] 73 | new_parts.append("${{{}}}".format(".".join(params))) 74 | else: 75 | for key, val in args.items(): 76 | # we want to bail if a conditional can evaluate to AWS::NoValue 77 | if isinstance(val, dict): 78 | if "Fn::If" in val and "AWS::NoValue" in str(val["Fn::If"]): 79 | return { 80 | "Fn::Join": value, 81 | } 82 | 83 | if val == part: 84 | param_name = key 85 | break 86 | else: 87 | param_name = "Param{}".format(len(args) + 1) 88 | args[param_name] = part 89 | 90 | new_parts.append("${{{}}}".format(param_name)) 91 | 92 | elif isinstance(part, six.string_types): 93 | new_parts.append(part.replace("${", "${!")) 94 | 95 | else: 96 | # Doing something weird; refuse 97 | return { 98 | "Fn::Join": value 99 | } 100 | 101 | source = sep.join(new_parts) 102 | 103 | if plain_string: 104 | return source 105 | 106 | if args: 107 | return ODict(( 108 | ("Fn::Sub", [source, args]), 109 | )) 110 | 111 | return ODict(( 112 | ("Fn::Sub", source), 113 | )) 114 | 115 | 116 | def clean(source): 117 | """ 118 | Clean up the source: 119 | * Replace use of Fn::Join with Fn::Sub 120 | * Keep json body for specific resource properties 121 | """ 122 | 123 | if isinstance(source, dict): 124 | for key, value in source.items(): 125 | if key == "Fn::Join": 126 | return convert_join(value) 127 | 128 | else: 129 | source[key] = clean(value) 130 | 131 | elif isinstance(source, list): 132 | return [clean(item) for item in source] 133 | 134 | return source 135 | 136 | 137 | def cfn_literal_parser(source): 138 | """ 139 | Sanitize the source: 140 | * Keep json body for specific resource properties 141 | """ 142 | 143 | if isinstance(source, dict): 144 | for key, value in source.items(): 145 | if key == "Type": 146 | for item in UNCONVERTED_KEYS: 147 | if value == item[0]: 148 | # Checking if this resource has "Properties" and the property literal to maintain 149 | # Better check than just try/except KeyError :-) 150 | if source.get("Properties") and source.get("Properties", {}).get(item[1]): 151 | if isinstance(source["Properties"][item[1]], dict) and \ 152 | not has_intrinsic_functions(source["Properties"][item[1]].keys()): 153 | source["Properties"][item[1]] = LiteralString(u"{}".format(json.dumps( 154 | source["Properties"][item[1]], 155 | indent=2, 156 | separators=(',', ': ')) 157 | )) 158 | 159 | else: 160 | source[key] = cfn_literal_parser(value) 161 | 162 | elif isinstance(source, list): 163 | return [cfn_literal_parser(item) for item in source] 164 | 165 | return source 166 | -------------------------------------------------------------------------------- /cfn_clean/yaml_dumper.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. A copy of the License is located at 6 | 7 | http://aws.amazon.com/apache2.0/ 8 | 9 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and limitations under the License. 12 | """ 13 | 14 | from cfn_tools.yaml_dumper import CfnYamlDumper 15 | 16 | 17 | class CleanCfnYamlDumper(CfnYamlDumper): 18 | """ 19 | Format multi-line strings with | 20 | """ 21 | 22 | def represent_scalar(self, tag, value, style=None): 23 | if "\n" in value: 24 | style = "|" 25 | 26 | return super(CleanCfnYamlDumper, self).represent_scalar(tag, value, style) 27 | -------------------------------------------------------------------------------- /cfn_flip/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. A copy of the License is located at 6 | 7 | http://aws.amazon.com/apache2.0/ 8 | 9 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and limitations under the License. 12 | """ 13 | 14 | from .yaml_dumper import get_dumper 15 | from cfn_clean import clean, cfn_literal_parser 16 | from cfn_tools import load_json, load_yaml, dump_json 17 | from cfn_tools._config import config 18 | import yaml 19 | 20 | 21 | def load(template): 22 | """ 23 | Try to guess the input format 24 | """ 25 | 26 | try: 27 | data = load_json(template) 28 | return data, "json" 29 | except ValueError as e: 30 | try: 31 | data = load_yaml(template) 32 | return data, "yaml" 33 | except Exception: 34 | raise e 35 | 36 | 37 | def dump_yaml(data, clean_up=False, long_form=False): 38 | """ 39 | Output some YAML 40 | """ 41 | 42 | return yaml.dump( 43 | data, 44 | Dumper=get_dumper(clean_up, long_form), 45 | default_flow_style=False, 46 | allow_unicode=True, 47 | width=config.max_col_width 48 | ) 49 | 50 | 51 | def to_json(template, clean_up=False): 52 | """ 53 | Assume the input is YAML and convert to JSON 54 | """ 55 | 56 | data, _ = load(template) 57 | 58 | if clean_up: 59 | data = clean(data) 60 | 61 | return dump_json(data) 62 | 63 | 64 | def to_yaml(template, clean_up=False, long_form=False, literal=True): 65 | """ 66 | Assume the input is JSON and convert to YAML 67 | """ 68 | 69 | data, _ = load(template) 70 | 71 | if clean_up: 72 | data = clean(data) 73 | 74 | if literal: 75 | data = cfn_literal_parser(data) 76 | 77 | return dump_yaml(data, clean_up, long_form) 78 | 79 | 80 | def flip(template, in_format=None, out_format=None, clean_up=False, no_flip=False, long_form=False): 81 | """ 82 | Figure out the input format and convert the data to the opposing output format 83 | """ 84 | 85 | # Do we need to figure out the input format? 86 | if not in_format: 87 | # Load the template as JSON? 88 | if (out_format == "json" and no_flip) or (out_format == "yaml" and not no_flip): 89 | in_format = "json" 90 | elif (out_format == "yaml" and no_flip) or (out_format == "json" and not no_flip): 91 | in_format = "yaml" 92 | 93 | # Load the data 94 | if in_format == "json": 95 | data = load_json(template) 96 | elif in_format == "yaml": 97 | data = load_yaml(template) 98 | else: 99 | data, in_format = load(template) 100 | 101 | # Clean up? 102 | if clean_up: 103 | data = clean(data) 104 | 105 | # Figure out the output format 106 | if not out_format: 107 | if (in_format == "json" and no_flip) or (in_format == "yaml" and not no_flip): 108 | out_format = "json" 109 | else: 110 | out_format = "yaml" 111 | 112 | # Finished! 113 | if out_format == "json": 114 | return dump_json(data) 115 | 116 | return dump_yaml(data, clean_up, long_form) 117 | -------------------------------------------------------------------------------- /cfn_flip/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. A copy of the License is located at 6 | 7 | http://aws.amazon.com/apache2.0/ 8 | 9 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and limitations under the License. 12 | """ 13 | 14 | from .main import main 15 | 16 | main() 17 | -------------------------------------------------------------------------------- /cfn_flip/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. A copy of the License is located at 6 | 7 | http://aws.amazon.com/apache2.0/ 8 | 9 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and limitations under the License. 12 | """ 13 | 14 | from . import flip 15 | import click 16 | import sys 17 | 18 | 19 | @click.command() 20 | @click.option("--input", "-i", "in_format", type=click.Choice(["json", "yaml"]), help="Specify the input format. Overrides -j and -y flags.") 21 | @click.option("--output", "-o", "out_format", type=click.Choice(["json", "yaml"]), help="Specify the output format. Overrides -j, -y, and -n flags.") 22 | @click.option("--json", "-j", "out_flag", flag_value="json", help="Convert to JSON. Assume the input is YAML.") 23 | @click.option("--yaml", "-y", "out_flag", flag_value="yaml", help="Convert to YAML. Assume the input is JSON.") 24 | @click.option("--clean", "-c", is_flag=True, help="Performs some opinionated cleanup on your template.") 25 | @click.option("--long", "-l", is_flag=True, help="Use long-form syntax for functions when converting to YAML.") 26 | @click.option("--no-flip", "-n", is_flag=True, help="Perform other operations but do not flip the output format.") 27 | @click.argument("input", type=click.File("r"), default=sys.stdin) 28 | @click.argument("output", type=click.File("w"), default=sys.stdout) 29 | @click.version_option(message='AWS Cloudformation Template Flip, Version %(version)s') 30 | @click.pass_context 31 | def main(ctx, **kwargs): 32 | """ 33 | AWS CloudFormation Template Flip is a tool that converts 34 | AWS CloudFormation templates between JSON and YAML formats, 35 | making use of the YAML format's short function syntax where possible. 36 | """ 37 | in_format = kwargs.pop('in_format') 38 | out_format = kwargs.pop('out_format') or kwargs.pop('out_flag') 39 | no_flip = kwargs.pop('no_flip') 40 | clean = kwargs.pop('clean') 41 | long_form = kwargs.pop('long') 42 | input_file = kwargs.pop('input') 43 | output_file = kwargs.pop('output') 44 | 45 | if not in_format: 46 | if input_file.name.endswith(".json"): 47 | in_format = "json" 48 | elif input_file.name.endswith(".yaml") or input_file.name.endswith(".yml"): 49 | in_format = "yaml" 50 | 51 | if input_file.name == "" and sys.stdin.isatty(): 52 | click.echo(ctx.get_help()) 53 | ctx.exit() 54 | 55 | try: 56 | flipped = flip( 57 | input_file.read(), 58 | in_format=in_format, 59 | out_format=out_format, 60 | clean_up=clean, 61 | no_flip=no_flip, 62 | long_form=long_form 63 | ) 64 | output_file.write(flipped) 65 | except Exception as e: 66 | raise click.ClickException("{}".format(e)) 67 | -------------------------------------------------------------------------------- /cfn_flip/yaml_dumper.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | A copy of the License is located at 7 | 8 | http://aws.amazon.com/apache2.0/ 9 | 10 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and limitations under the License. 13 | """ 14 | 15 | import six 16 | 17 | from cfn_clean.yaml_dumper import CleanCfnYamlDumper 18 | from cfn_tools.literal import LiteralString 19 | from cfn_tools.odict import ODict 20 | from cfn_tools.yaml_dumper import CfnYamlDumper 21 | from cfn_tools._config import config 22 | 23 | TAG_STR = "tag:yaml.org,2002:str" 24 | TAG_MAP = "tag:yaml.org,2002:map" 25 | CONVERTED_SUFFIXES = ["Ref", "Condition"] 26 | 27 | FN_PREFIX = "Fn::" 28 | 29 | # Maximum length of a string before switching to angle-bracket-style representation 30 | STR_MAX_LENGTH_QUOTED = config.max_col_width 31 | # Maximum number of newlines a string can have before switching to pipe-style representation 32 | STR_MAX_LINES_QUOTED = 10 33 | 34 | 35 | class Dumper(CfnYamlDumper): 36 | """ 37 | The standard dumper 38 | """ 39 | 40 | 41 | class CleanDumper(CleanCfnYamlDumper): 42 | """ 43 | Cleans up strings 44 | """ 45 | 46 | 47 | class LongDumper(CfnYamlDumper): 48 | """ 49 | Preserves long-form function syntax 50 | """ 51 | 52 | 53 | class LongCleanDumper(CleanCfnYamlDumper): 54 | """ 55 | Preserves long-form function syntax 56 | """ 57 | 58 | 59 | def literal_unicode_representer(dumper, value): 60 | return dumper.represent_scalar(TAG_STR, value, style='|') 61 | 62 | 63 | def string_representer(dumper, value): 64 | if sum(1 for nl in value if nl in ('\n', '\r')) >= STR_MAX_LINES_QUOTED: 65 | return dumper.represent_scalar(TAG_STR, value, style="|") 66 | 67 | if len(value) >= STR_MAX_LENGTH_QUOTED and '\n' not in value: 68 | return dumper.represent_scalar(TAG_STR, value, style=">") 69 | 70 | if value.startswith("0"): 71 | return dumper.represent_scalar(TAG_STR, value, style="'") 72 | 73 | return dumper.represent_scalar(TAG_STR, value) 74 | 75 | 76 | def fn_representer(dumper, fn_name, value): 77 | tag = "!{}".format(fn_name) 78 | 79 | if tag == "!GetAtt" and isinstance(value, list): 80 | value = ".".join(value) 81 | 82 | if isinstance(value, list): 83 | return dumper.represent_sequence(tag, value) 84 | 85 | if isinstance(value, dict): 86 | return dumper.represent_mapping(tag, value) 87 | 88 | return dumper.represent_scalar(tag, value) 89 | 90 | 91 | def map_representer(dumper, value): 92 | """ 93 | Deal with !Ref style function format and OrderedDict 94 | """ 95 | 96 | value = ODict(value.items()) 97 | 98 | if len(value.keys()) == 1: 99 | key = list(value.keys())[0] 100 | 101 | if key in CONVERTED_SUFFIXES: 102 | return fn_representer(dumper, key, value[key]) 103 | 104 | if key.startswith(FN_PREFIX): 105 | return fn_representer(dumper, key[4:], value[key]) 106 | 107 | return dumper.represent_mapping(TAG_MAP, value, flow_style=False) 108 | 109 | 110 | # Customise our dumpers 111 | Dumper.add_representer(ODict, map_representer) 112 | Dumper.add_representer(six.text_type, string_representer) 113 | Dumper.add_representer(LiteralString, literal_unicode_representer) 114 | CleanDumper.add_representer(LiteralString, literal_unicode_representer) 115 | CleanDumper.add_representer(ODict, map_representer) 116 | 117 | 118 | def get_dumper(clean_up=False, long_form=False): 119 | if clean_up: 120 | if long_form: 121 | return LongCleanDumper 122 | 123 | return CleanDumper 124 | 125 | if long_form: 126 | return LongDumper 127 | 128 | return Dumper 129 | -------------------------------------------------------------------------------- /cfn_tools/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at 5 | 6 | http://aws.amazon.com/apache2.0/ 7 | 8 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 9 | """ 10 | 11 | from .json_encoder import DateTimeAwareJsonEncoder 12 | from .odict import ODict 13 | from .yaml_dumper import CfnYamlDumper 14 | from .yaml_loader import CfnYamlLoader 15 | import json 16 | import yaml 17 | 18 | 19 | def load_json(source): 20 | return json.loads(source, object_pairs_hook=ODict) 21 | 22 | 23 | def dump_json(source): 24 | return json.dumps(source, indent=4, cls=DateTimeAwareJsonEncoder, 25 | separators=(',', ': '), ensure_ascii=False) 26 | 27 | 28 | def load_yaml(source): 29 | return yaml.load(source, Loader=CfnYamlLoader) 30 | 31 | 32 | def dump_yaml(source): 33 | return yaml.dump(source, Dumper=CfnYamlDumper, default_flow_style=False, allow_unicode=True, width=120) 34 | -------------------------------------------------------------------------------- /cfn_tools/_config.py: -------------------------------------------------------------------------------- 1 | """Configuration file for cfn_flip.""" 2 | 3 | import inspect 4 | import logging 5 | import os 6 | from typing import Any, Dict, NamedTuple, Optional, Type 7 | 8 | _logger: logging.Logger = logging.getLogger(__name__) 9 | 10 | 11 | class _ConfigArg(NamedTuple): 12 | dtype: Type 13 | nullable: bool = True 14 | has_default: bool = False 15 | default: Any = None 16 | 17 | 18 | _CONFIG_DEFAULTS: Dict[str, _ConfigArg] = { 19 | "max_col_width": _ConfigArg(dtype=int, nullable=False, has_default=True, default=200)} 20 | 21 | 22 | class _Config: 23 | """Cfn_flip's Configuration class.""" 24 | 25 | # __slots__ = (f"_{attr}" for attr in _CONFIG_DEFAULTS) 26 | 27 | def __init__(self): 28 | for name, conf in _CONFIG_DEFAULTS.items(): 29 | self._load_config(name=name, conf=conf) 30 | 31 | def _load_config(self, name, conf): 32 | env_var_name: str = f"CFN_{name.upper()}" 33 | env_var: Optional[str] = os.getenv(env_var_name) 34 | if env_var is not None: 35 | value = self._apply_type( 36 | name=name, value=env_var, dtype=_CONFIG_DEFAULTS[name].dtype, nullable=_CONFIG_DEFAULTS[name].nullable 37 | ) 38 | self.__setattr__(name, value) 39 | elif conf.has_default is True: 40 | self.__setattr__(name, conf.default) 41 | 42 | def __getattr__(self, item: str) -> Any: 43 | if item in _CONFIG_DEFAULTS: 44 | return super().__getattribute__(f"_{item}") 45 | raise AttributeError 46 | 47 | def __getitem__(self, item: str) -> Any: 48 | return self.__getattr__(item) 49 | 50 | def __setattr__(self, key: str, value: Any) -> Any: 51 | if key not in _CONFIG_DEFAULTS: 52 | raise TypeError( 53 | f"{key} is not a valid configuration. " f"Please use: {list(_CONFIG_DEFAULTS.keys())}" 54 | ) 55 | value = self._apply_type( 56 | name=key, value=value, dtype=_CONFIG_DEFAULTS[key].dtype, nullable=_CONFIG_DEFAULTS[key].nullable 57 | ) 58 | super().__setattr__(f"_{key}", value) 59 | 60 | def reset(self, item: Optional[str] = None) -> None: 61 | """Reset one or all (if None is received) configuration values. 62 | 63 | Parameters 64 | ---------- 65 | item : str, optional 66 | Configuration item name. 67 | 68 | Returns 69 | ------- 70 | None 71 | None. 72 | 73 | Examples 74 | -------- 75 | >>> import cfn_flip 76 | >>> cfn_flip.config.reset("max_col_width") # Reset one specific configuration 77 | >>> cfn_flip.config.reset() # Reset all 78 | 79 | """ 80 | if item is None: 81 | for name, conf in _CONFIG_DEFAULTS.items(): 82 | delattr(self, f"_{name}") 83 | self._load_config(name=name, conf=conf) 84 | else: 85 | delattr(self, f"_{item}") 86 | self._load_config(name=item, conf=_CONFIG_DEFAULTS[item]) 87 | 88 | @staticmethod 89 | def _apply_type(name: str, value: Any, dtype: Type, nullable: bool) -> Any: 90 | if _Config._is_null(value=value) is True: 91 | if nullable is True: 92 | return None 93 | ValueError( 94 | f"{name} configuration does not accept null value." f" Please pass {dtype}." 95 | ) 96 | if isinstance(value, dtype) is False: 97 | value = dtype(value) 98 | return value 99 | 100 | @staticmethod 101 | def _is_null(value: Any) -> bool: 102 | if value is None: 103 | return True 104 | if (isinstance(value, str) is True) and (value.lower() in ("none", "null", "nil")): 105 | return True 106 | return False 107 | 108 | 109 | def apply_configs(function): 110 | """Decorate some function with configs.""" 111 | signature = inspect.signature(function) 112 | args_names = list(signature.parameters.keys()) 113 | valid_configs = [x for x in _CONFIG_DEFAULTS if x in args_names] 114 | 115 | def wrapper(*args, **kwargs): 116 | received_args = signature.bind_partial(*args, **kwargs).arguments 117 | available_configs = [x for x in valid_configs if (x not in received_args) and (hasattr(config, x) is True)] 118 | missing_args = {x: config[x] for x in available_configs} 119 | final_args = {**received_args, **missing_args} 120 | return function(**final_args) 121 | 122 | wrapper.__doc__ = function.__doc__ 123 | wrapper.__name__ = function.__name__ 124 | wrapper.__signature__ = signature 125 | return wrapper 126 | 127 | 128 | config: _Config = _Config() 129 | -------------------------------------------------------------------------------- /cfn_tools/json_encoder.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at 5 | 6 | http://aws.amazon.com/apache2.0/ 7 | 8 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 9 | """ 10 | 11 | from datetime import date, datetime, time 12 | import json 13 | 14 | 15 | class DateTimeAwareJsonEncoder(json.JSONEncoder): 16 | def default(self, obj): 17 | if isinstance(obj, (datetime, date, time)): 18 | return obj.isoformat() 19 | 20 | return json.JSONEncoder.default(self, obj) 21 | -------------------------------------------------------------------------------- /cfn_tools/literal.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at 5 | 6 | http://aws.amazon.com/apache2.0/ 7 | 8 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 9 | """ 10 | 11 | 12 | class LiteralString(str): 13 | """ 14 | Class to represent json literal 15 | """ 16 | pass 17 | -------------------------------------------------------------------------------- /cfn_tools/odict.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at 5 | 6 | http://aws.amazon.com/apache2.0/ 7 | 8 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 9 | """ 10 | 11 | import collections 12 | 13 | 14 | class OdictItems(list): 15 | """ 16 | Helper class to ensure ordering is preserved 17 | """ 18 | 19 | def __init__(self, items): 20 | new_items = [] 21 | 22 | for item in items: 23 | class C(type(item)): 24 | def __lt__(self, *args, **kwargs): 25 | return False 26 | 27 | new_items.append(C(item)) 28 | 29 | return super(OdictItems, self).__init__(new_items) 30 | 31 | def sort(self): 32 | pass 33 | 34 | 35 | class ODict(collections.OrderedDict): 36 | """ 37 | A wrapper for OrderedDict that doesn't allow sorting of keys 38 | """ 39 | 40 | def __init__(self, pairs=[]): 41 | if isinstance(pairs, dict): 42 | # Dicts lose ordering in python<3.6 so disallow them 43 | raise Exception("ODict does not allow construction from a dict") 44 | 45 | super(ODict, self).__init__(pairs) 46 | 47 | def items(self): 48 | old_items = super(ODict, self).items() 49 | return OdictItems(old_items) 50 | -------------------------------------------------------------------------------- /cfn_tools/yaml_dumper.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | A copy of the License is located at 7 | 8 | http://aws.amazon.com/apache2.0/ 9 | 10 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and limitations under the License. 13 | """ 14 | 15 | import re 16 | import six 17 | import yaml 18 | from yaml.emitter import Emitter, ScalarAnalysis 19 | 20 | from .literal import LiteralString 21 | from .odict import ODict 22 | 23 | TAG_MAP = "tag:yaml.org,2002:map" 24 | TAG_STRING = "tag:yaml.org,2002:str" 25 | AWS_ACCOUNT_ID = r"^0[0-9]+$" 26 | 27 | 28 | class CfnEmitter(Emitter): 29 | def analyze_scalar(self, scalar): 30 | # We have Json payloads that we want to show as literal 31 | # This way we can skip the checks that analize_scalar do in the string when you have 32 | # leading_space or leading_break 33 | if not scalar or isinstance(scalar, LiteralString): 34 | return ScalarAnalysis(scalar=scalar, empty=True, multiline=False, 35 | allow_flow_plain=False, allow_block_plain=True, 36 | allow_single_quoted=True, allow_double_quoted=True, 37 | allow_block=True) 38 | return super(CfnEmitter, self).analyze_scalar(scalar) 39 | 40 | 41 | class CfnYamlDumper(yaml.Dumper, CfnEmitter): 42 | """ 43 | Indent block sequences from parent using more common style 44 | (" - entry" vs "- entry"). 45 | Causes fewer problems with validation and tools. 46 | """ 47 | 48 | def increase_indent(self, flow=False, indentless=False): 49 | return super(CfnYamlDumper, self).increase_indent(flow, False) 50 | 51 | def represent_scalar(self, tag, value, style=None): 52 | if re.match(AWS_ACCOUNT_ID, value): 53 | style = "\'" 54 | 55 | if isinstance(value, six.text_type): 56 | if any(eol in value for eol in "\n\r") and style is None: 57 | style = "\"" 58 | 59 | # return super(CfnYamlDumper, self).represent_scalar(TAG_STRING, value, style) 60 | 61 | return super(CfnYamlDumper, self).represent_scalar(tag, value, style) 62 | 63 | 64 | def string_representer(dumper, value): 65 | style = None 66 | 67 | if "\n" in value: 68 | style = "\"" 69 | 70 | return dumper.represent_scalar(TAG_STRING, value, style=style) 71 | 72 | 73 | def map_representer(dumper, value): 74 | """ 75 | Map ODict into ODict to prevent sorting 76 | """ 77 | 78 | return dumper.represent_mapping(TAG_MAP, value) 79 | 80 | 81 | def literal_unicode_representer(dumper, value): 82 | return dumper.represent_scalar(TAG_STRING, value, style='|') 83 | 84 | 85 | # Customise the dumper 86 | CfnYamlDumper.add_representer(ODict, map_representer) 87 | CfnYamlDumper.add_representer(LiteralString, literal_unicode_representer) 88 | CfnYamlDumper.add_representer(six.text_type, string_representer) 89 | -------------------------------------------------------------------------------- /cfn_tools/yaml_loader.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at 5 | 6 | http://aws.amazon.com/apache2.0/ 7 | 8 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 9 | """ 10 | 11 | from .odict import ODict 12 | import six 13 | import yaml 14 | 15 | TAG_MAP = "tag:yaml.org,2002:map" 16 | UNCONVERTED_SUFFIXES = ["Ref", "Condition"] 17 | FN_PREFIX = "Fn::" 18 | 19 | 20 | class CfnYamlLoader(yaml.SafeLoader): 21 | pass 22 | 23 | 24 | def multi_constructor(loader, tag_suffix, node): 25 | """ 26 | Deal with !Ref style function format 27 | """ 28 | 29 | if tag_suffix not in UNCONVERTED_SUFFIXES: 30 | tag_suffix = "{}{}".format(FN_PREFIX, tag_suffix) 31 | 32 | constructor = None 33 | 34 | if tag_suffix == "Fn::GetAtt": 35 | constructor = construct_getatt 36 | elif isinstance(node, yaml.ScalarNode): 37 | constructor = loader.construct_scalar 38 | elif isinstance(node, yaml.SequenceNode): 39 | constructor = loader.construct_sequence 40 | elif isinstance(node, yaml.MappingNode): 41 | constructor = loader.construct_mapping 42 | else: 43 | raise Exception("Bad tag: !{}".format(tag_suffix)) 44 | 45 | return ODict(( 46 | (tag_suffix, constructor(node)), 47 | )) 48 | 49 | 50 | def construct_getatt(node): 51 | """ 52 | Reconstruct !GetAtt into a list 53 | """ 54 | 55 | if isinstance(node.value, six.text_type): 56 | return node.value.split(".", 1) 57 | elif isinstance(node.value, list): 58 | return [s.value for s in node.value] 59 | else: 60 | raise ValueError("Unexpected node type: {}".format(type(node.value))) 61 | 62 | 63 | def construct_mapping(self, node, deep=False): 64 | """ 65 | Use ODict for maps 66 | """ 67 | 68 | mapping = ODict() 69 | 70 | for key_node, value_node in node.value: 71 | key = self.construct_object(key_node, deep=deep) 72 | value = self.construct_object(value_node, deep=deep) 73 | 74 | mapping[key] = value 75 | 76 | return mapping 77 | 78 | 79 | # Customise our loader 80 | CfnYamlLoader.add_constructor(TAG_MAP, construct_mapping) 81 | CfnYamlLoader.add_multi_constructor("!", multi_constructor) 82 | -------------------------------------------------------------------------------- /examples/clean.json: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "Test", 3 | "Foo": { 4 | "Fn::Sub": "The ${cake} is a lie" 5 | }, 6 | "Bar": { 7 | "Fn::Base64": { 8 | "Ref": "Binary" 9 | } 10 | }, 11 | "Baz": { 12 | "Fn::Sub": [ 13 | "The cake is a...\n${CakeStatus}", 14 | { 15 | "CakeStatus": "lie" 16 | } 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/clean.yaml: -------------------------------------------------------------------------------- 1 | Description: Test 2 | Foo: !Sub 'The ${cake} is a lie' 3 | Bar: !Base64 4 | Ref: Binary 5 | Baz: !Sub 6 | - |- 7 | The cake is a... 8 | ${CakeStatus} 9 | - CakeStatus: lie 10 | -------------------------------------------------------------------------------- /examples/invalid: -------------------------------------------------------------------------------- 1 | { 2 | 'Invalid: 3 | } 4 | -------------------------------------------------------------------------------- /examples/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "Test", 3 | "Foo": { 4 | "Fn::Join": [ 5 | " ", 6 | [ 7 | "The", 8 | { 9 | "Ref": "cake" 10 | }, 11 | "is", 12 | "a", 13 | "lie" 14 | ] 15 | ] 16 | }, 17 | "Bar": { 18 | "Fn::Base64": { 19 | "Ref": "Binary" 20 | } 21 | }, 22 | "Baz": { 23 | "Fn::Sub": [ 24 | "The cake is a...\n${CakeStatus}", 25 | { 26 | "CakeStatus": "lie" 27 | } 28 | ] 29 | } 30 | } -------------------------------------------------------------------------------- /examples/test.yaml: -------------------------------------------------------------------------------- 1 | Description: Test 2 | Foo: !Join 3 | - ' ' 4 | - - The 5 | - !Ref 'cake' 6 | - is 7 | - a 8 | - lie 9 | Bar: !Base64 10 | Ref: Binary 11 | Baz: !Sub 12 | - "The cake is a...\n${CakeStatus}" 13 | - CakeStatus: lie 14 | -------------------------------------------------------------------------------- /examples/test_json_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Transform": "AWS::Serverless-2016-10-31", 4 | "Resources": { 5 | "ExampleStateMachineResource": { 6 | "Type": "AWS::StepFunctions::StateMachine", 7 | "Properties": { 8 | "DefinitionString": { 9 | "StartAt": "First State", 10 | "States": { 11 | "First State": { 12 | "Type": "Task", 13 | "Resource": "FunctionARN", 14 | "Next": "Second State" 15 | }, 16 | "Second State": { 17 | "Type": "Task", 18 | "Resource": "FunctionARN", 19 | "End": true 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/test_json_data.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Resources: 4 | ExampleStateMachineResource: 5 | Type: AWS::StepFunctions::StateMachine 6 | Properties: 7 | DefinitionString: |- 8 | { 9 | "StartAt": "First State", 10 | "States": { 11 | "First State": { 12 | "Type": "Task", 13 | "Resource": "FunctionARN", 14 | "Next": "Second State" 15 | }, 16 | "Second State": { 17 | "Type": "Task", 18 | "Resource": "FunctionARN", 19 | "End": true 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/test_json_data_long_line.json: -------------------------------------------------------------------------------- 1 | { 2 | "app.example.com" : { 3 | "(?#Configuration)^/(.*?\\.(fcgi|psgi)/)?(manager\\.html|confs/|$)" : "inGroup(\"admingroup\")", 4 | "(?#Notifications)/(.*?\\.(fcgi|psgi)/)?notifications" : "inGroup(\"admingroup\") or $uid eq \"myuser\"", 5 | "(?#Sessions)/(.*?\\.(fcgi|psgi)/)?sessions" : "inGroup(\"admingroup\") or $uid eq \"myuser\"", 6 | "default" : "inGroup(\"admingroup\") or $uid eq \"myuser\"" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/test_json_def_string_with_sub.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "FeedbackRequestStateMachine": { 4 | "Properties": { 5 | "DefinitionString": { 6 | "Fn::Sub": [ 7 | "\n {\n \"Comment\": \"State machine which awaits until defined date to submit a feedback request\",\n \"StartAt\": \"WaitForDueDate\",\n \"States\": {\n \"WaitForDueDate\": {\n \"Type\": \"Wait\",\n \"TimestampPath\": \"$.plannedAt\",\n \"Next\": \"SubmitFeedbackRequestToSQS\"\n },\n \"SubmitFeedbackRequestToSQS\": {\n \"Type\": \"Task\",\n \"Resource\": \"arn:aws:states:::sqs:sendMessage\",\n \"Parameters\": {\n \"MessageBody.$\": \"$\",\n \"QueueUrl\": \"${feedbackQueueUrl}\"\n },\n \"End\": true\n }\n }\n }\n ", 8 | { 9 | "feedbackQueueUrl": { 10 | "Ref": "FeedbackRequestsQueue" 11 | } 12 | } 13 | ] 14 | }, 15 | "RoleArn": { 16 | "Fn::Join": [ 17 | "/", 18 | [ 19 | { 20 | "Fn::Sub": "arn:aws:iam::${AWS::AccountId}:role" 21 | }, 22 | { 23 | "Ref": "FeedbackRequestStateMachineRole" 24 | } 25 | ] 26 | ] 27 | }, 28 | "StateMachineName": { 29 | "Fn::Sub": "${AWS::StackName}-FeedbackRequestStateMachine" 30 | } 31 | }, 32 | "Type": "AWS::StepFunctions::StateMachine" 33 | }, 34 | "FeedbackRequestStateMachineRole": { 35 | "Properties": { 36 | "AssumeRolePolicyDocument": { 37 | "Statement": [ 38 | { 39 | "Action": [ 40 | "sts:AssumeRole" 41 | ], 42 | "Effect": "Allow", 43 | "Principal": { 44 | "Service": [ 45 | { 46 | "Fn::Sub": "states.${AWS::Region}.amazonaws.com" 47 | } 48 | ] 49 | } 50 | } 51 | ] 52 | }, 53 | "Policies": [ 54 | { 55 | "PolicyDocument": { 56 | "Statement": [ 57 | { 58 | "Action": [ 59 | "sqs:SendMessage" 60 | ], 61 | "Effect": "Allow", 62 | "Resource": { 63 | "Fn::GetAtt": [ 64 | "FeedbackRequestsQueue", 65 | "Arn" 66 | ] 67 | } 68 | } 69 | ] 70 | }, 71 | "PolicyName": "submit_request_to_sqs" 72 | } 73 | ] 74 | }, 75 | "Type": "AWS::IAM::Role" 76 | }, 77 | "FeedbackRequestsQueue": { 78 | "Properties": { 79 | "MessageRetentionPeriod": 172800, 80 | "VisibilityTimeout": 600 81 | }, 82 | "Type": "AWS::SQS::Queue" 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /examples/test_json_state_machine.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion":"2010-09-09", 3 | "Description":"An example template for a Step Functions state machine.", 4 | "Resources":{ 5 | "MyStateMachine":{ 6 | "Type":"AWS::StepFunctions::StateMachine", 7 | "Properties":{ 8 | "StateMachineName":"HelloWorld-StateMachine", 9 | "StateMachineType":"STANDARD", 10 | "DefinitionString":"{\"StartAt\": \"HelloWorld\", \"States\": {\"HelloWorld\": {\"Type\": \"Task\", \"Resource\": \"arn:aws:lambda:us-east-1:111122223333;:function:HelloFunction\", \"End\": true}}}", 11 | "RoleArn":"arn:aws:iam::111122223333:role/service-role/StatesExecutionRole-us-east-1;" 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /examples/test_long.json: -------------------------------------------------------------------------------- 1 | { 2 | "Title": "Test pipe-style representation for multiline strings", 3 | "TooShort": "foo\nbar\nbaz\nquuux", 4 | "LongText": "Two roads diverged in a yellow wood,\nAnd sorry I could not travel both\nAnd be one traveler, long I stood\nAnd looked down one as far as I could\nTo where it bent in the undergrowth: \n\nThen took the other, as just as fair,\nAnd having perhaps the better claim\nBecause it was grassy and wanted wear,\nThough as for that the passing there\nHad worn them really about the same,\n\nAnd both that morning equally lay\nIn leaves no step had trodden black.\nOh, I kept the first for another day! \nYet knowing how way leads on to way\nI doubted if I should ever come back.\n\nI shall be telling this with a sigh\nSomewhere ages and ages hence;\nTwo roads diverged in a wood, and I,\nI took the one less traveled by,\nAnd that has made all the difference", 5 | "WideText": "Two roads diverged in a yellow wood, And sorry I could not travel both And be one traveler, long I stood And looked down one as far as I could To where it bent in the undergrowth: Then took the other, as just as fair, And having perhaps the better claim Because it was grassy and wanted wear, Though as for that the passing there Had worn them really about the same, And both that morning equally lay In leaves no step had trodden black. Oh, I kept the first for another day! Yet knowing how way leads on to way I doubted if I should ever come back. I shall be telling this with a sigh Somewhere ages and ages hence; Two roads diverged in a wood, and I, I took the one less traveled by, And that has made all the difference" 6 | } 7 | -------------------------------------------------------------------------------- /examples/test_lorem.yaml: -------------------------------------------------------------------------------- 1 | Description: | 2 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed egestas est diam, in malesuada odio ultrices non. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam id lorem leo. Proin sit amet sem in erat pellentesque pretium id cursus orci. In ac nulla id nisi tempor accumsan. Curabitur convallis tempus mauris non pharetra. Vivamus sit amet dui sed ligula vehicula hendrerit. 3 | Integer imperdiet nunc ut enim imperdiet, et faucibus erat luctus. Nullam pretium a turpis sed malesuada. Nullam ut arcu a quam laoreet tristique at id ex. Morbi convallis augue a libero elementum, vitae vestibulum lacus accumsan. Nunc ornare iaculis tortor, id accumsan turpis blandit ac. Quisque efficitur cursus malesuada. Nunc a nunc malesuada, aliquet ligula eu, consectetur nisl. 4 | Duis risus nunc, bibendum finibus dapibus at, vehicula sed lectus. Vestibulum molestie leo ante, quis consequat dui pretium eget. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent aliquam risus scelerisque elit rhoncus, in consectetur orci ultricies. Donec lobortis leo erat, a imperdiet risus scelerisque a. Cras nec est neque. Sed nibh orci, feugiat quis finibus at, bibendum non felis. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nunc mollis scelerisque nisi a pellentesque. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Integer at eros dignissim, luctus risus a, convallis tortor. 5 | Pellentesque vitae bibendum justo, non molestie nisi. Ut luctus facilisis nisi ac ullamcorper. Suspendisse orci ex, efficitur eget gravida tempor, semper a libero. Vivamus sit amet orci vitae felis consequat luctus vel vel nibh. Proin facilisis, nunc eu sodales malesuada, ligula urna varius risus, sed posuere magna urna quis dolor. Proin nec euismod libero. Etiam scelerisque vehicula tincidunt. Mauris vulputate ipsum ac eros lobortis, et condimentum mauris auctor. Praesent a felis sit amet augue lacinia interdum. Duis urna quam, consectetur sed pulvinar in, fringilla eu dolor. Pellentesque feugiat vitae turpis vel rutrum. In quis est lacinia, venenatis lorem eget, efficitur ipsum. Mauris efficitur fringilla leo, at accumsan lorem venenatis ullamcorper. Cras pretium tellus est, vel consequat orci gravida ut. 6 | Nam lobortis tellus hendrerit volutpat pharetra. Praesent at interdum sapien. Donec condimentum dui vel sollicitudin malesuada. Nunc vitae tempus ipsum. Proin et efficitur ante. Mauris feugiat fringilla ex, eu tincidunt justo. Praesent in dolor porta, varius arcu non, ornare risus. Ut ullamcorper dui molestie sapien hendrerit vestibulum ut at turpis. Donec maximus nec arcu et accumsan. Integer tempus placerat dui, id congue ante semper at. Mauris tempus felis sed risus convallis, nec suscipit ligula molestie. In ornare nulla quam, et eleifend elit rutrum ac. Morbi interdum, nulla ac elementum rutrum, mauris odio faucibus augue, non feugiat tortor tellus vel turpis. Fusce id sem odio. Praesent non maximus leo, a cursus felis. 7 | -------------------------------------------------------------------------------- /examples/test_multibyte.json: -------------------------------------------------------------------------------- 1 | { 2 | "Description": "僕の名前は成田です。" 3 | } -------------------------------------------------------------------------------- /examples/test_multibyte.yaml: -------------------------------------------------------------------------------- 1 | Description: 僕の名前は成田です。 2 | -------------------------------------------------------------------------------- /examples/test_multiline.yaml: -------------------------------------------------------------------------------- 1 | UserData: 2 | Fn::Base64: !Sub | 3 | write_files: 4 | - path: /etc/yum.repos.d/nix.repo 5 | content: | 6 | [nix] 7 | name=nix Repository -------------------------------------------------------------------------------- /examples/test_user_data.yaml: -------------------------------------------------------------------------------- 1 | UserData: 2 | Fn::Base64: !Sub | 3 | #!/bin/bash -xe 4 | echo "WaitHandle '${WaitHandle}'" 5 | /bin/echo '%password%' | /bin/passwd cloud_user --stdin 6 | yum update -y 7 | yum install python3 -y 8 | su - cloud_user -c 'aws configure set region us-east-1' 9 | su - cloud_user -c 'python3 -m pip install boto3' 10 | /opt/aws/bin/cfn-signal -e $? -r "UserData script complete" '${WaitHandle}' -------------------------------------------------------------------------------- /examples/test_yaml_def_string_with_sub.yaml: -------------------------------------------------------------------------------- 1 | Resources: 2 | FeedbackRequestStateMachine: 3 | Properties: 4 | DefinitionString: !Sub 5 | - "\n {\n \"Comment\": \"State machine which awaits until defined date to submit a feedback request\",\n \"StartAt\": \"WaitForDueDate\",\n \"States\": {\n\ 6 | \ \"WaitForDueDate\": {\n \"Type\": \"Wait\",\n \"TimestampPath\": \"$.plannedAt\",\n \"Next\": \"SubmitFeedbackRequestToSQS\"\ 7 | \n },\n \"SubmitFeedbackRequestToSQS\": {\n \"Type\": \"Task\",\n \"Resource\": \"arn:aws:states:::sqs:sendMessage\",\n \ 8 | \ \"Parameters\": {\n \"MessageBody.$\": \"$\",\n \"QueueUrl\": \"${feedbackQueueUrl}\"\n },\n \ 9 | \ \"End\": true\n }\n }\n }\n " 10 | - feedbackQueueUrl: !Ref 'FeedbackRequestsQueue' 11 | RoleArn: !Join 12 | - / 13 | - - !Sub 'arn:aws:iam::${AWS::AccountId}:role' 14 | - !Ref 'FeedbackRequestStateMachineRole' 15 | StateMachineName: !Sub '${AWS::StackName}-FeedbackRequestStateMachine' 16 | Type: AWS::StepFunctions::StateMachine 17 | FeedbackRequestStateMachineRole: 18 | Properties: 19 | AssumeRolePolicyDocument: 20 | Statement: 21 | - Action: 22 | - sts:AssumeRole 23 | Effect: Allow 24 | Principal: 25 | Service: 26 | - !Sub 'states.${AWS::Region}.amazonaws.com' 27 | Policies: 28 | - PolicyDocument: 29 | Statement: 30 | - Action: 31 | - sqs:SendMessage 32 | Effect: Allow 33 | Resource: !GetAtt 'FeedbackRequestsQueue.Arn' 34 | PolicyName: submit_request_to_sqs 35 | Type: AWS::IAM::Role 36 | FeedbackRequestsQueue: 37 | Properties: 38 | MessageRetentionPeriod: 172800 39 | VisibilityTimeout: 600 40 | Type: AWS::SQS::Queue 41 | -------------------------------------------------------------------------------- /examples/test_yaml_long_line.yaml: -------------------------------------------------------------------------------- 1 | app.example.com: 2 | (?#Configuration)^/(.*?\.(fcgi|psgi)/)?(manager\.html|confs/|$): inGroup("admingroup") 3 | (?#Notifications)/(.*?\.(fcgi|psgi)/)?notifications: inGroup("admingroup") or $uid eq "myuser" 4 | (?#Sessions)/(.*?\.(fcgi|psgi)/)?sessions: inGroup("admingroup") or $uid eq "myuser" 5 | default: inGroup("admingroup") or $uid eq "myuser" -------------------------------------------------------------------------------- /examples/test_yaml_state_machine.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: An example template for a Step Functions state machine. 3 | Resources: 4 | MyStateMachine: 5 | Type: AWS::StepFunctions::StateMachine 6 | Properties: 7 | StateMachineName: HelloWorld-StateMachine 8 | StateMachineType: STANDARD 9 | DefinitionString: '{"StartAt": "HelloWorld", "States": {"HelloWorld": {"Type": "Task", "Resource": "arn:aws:lambda:us-east-1:111122223333;:function:HelloFunction", "End": true}}}' 10 | RoleArn: arn:aws:iam::111122223333:role/service-role/StatesExecutionRole-us-east-1; 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Click 2 | PyYAML 3 | six 4 | tox 5 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at 5 | 6 | http://aws.amazon.com/apache2.0/ 7 | 8 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 9 | """ 10 | 11 | from setuptools import setup, find_packages 12 | 13 | setup( 14 | name="cfn_flip", 15 | version="1.3.0", 16 | description="Convert AWS CloudFormation templates between JSON and YAML formats", 17 | long_description=open("README.md").read(), 18 | long_description_content_type="text/markdown", 19 | url="https://github.com/awslabs/aws-cfn-template-flip", 20 | author="Steve Engledow", 21 | author_email="sengledo@amazon.co.uk", 22 | license="Apache2", 23 | packages=find_packages(exclude=["tests"]), 24 | install_requires=[ 25 | "Click", 26 | "PyYAML>=4.1", 27 | "six", 28 | ], 29 | tests_require=[ 30 | 'pytest>=4.3.0', 31 | 'pytest-cov', 32 | 'pytest-runner' 33 | ], 34 | zip_safe=False, 35 | entry_points={ 36 | "console_scripts": ["cfn-flip=cfn_flip.main:main"], 37 | }, 38 | ) 39 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awslabs/aws-cfn-template-flip/2a1b714f23eec6306c8572938aecb52a31a92f69/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_clean.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. A copy of the License is located at 6 | 7 | http://aws.amazon.com/apache2.0/ 8 | 9 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and limitations under the License. 12 | """ 13 | 14 | from cfn_clean import clean 15 | from cfn_clean.yaml_dumper import CleanCfnYamlDumper 16 | from cfn_tools.odict import ODict 17 | import yaml 18 | 19 | 20 | def test_basic_case(): 21 | """ 22 | As simple as it gets 23 | """ 24 | 25 | source = { 26 | "Fn::Join": [ 27 | " ", 28 | ["The", "cake", "is", "a", "lie"], 29 | ], 30 | } 31 | 32 | expected = "The cake is a lie" 33 | 34 | actual = clean(source) 35 | 36 | assert expected == actual 37 | 38 | 39 | def test_ref(): 40 | """ 41 | Refs should be replaced by ${value} 42 | """ 43 | 44 | source = { 45 | "Fn::Join": [ 46 | " ", 47 | ["The", {"Ref": "Cake"}, "is", "a", "lie"], 48 | ], 49 | } 50 | 51 | expected = { 52 | "Fn::Sub": "The ${Cake} is a lie", 53 | } 54 | 55 | actual = clean(source) 56 | 57 | assert expected == actual 58 | 59 | 60 | def test_get_att(): 61 | """ 62 | Intrinsics should be replaced by parameters to Sub 63 | """ 64 | 65 | source = { 66 | "Fn::Join": [ 67 | " ", 68 | ["The", {"Fn::GetAtt": ["Cake", "Hole"]}, "is", "a", "lie"], 69 | ], 70 | } 71 | 72 | expected = { 73 | "Fn::Sub": "The ${Cake.Hole} is a lie", 74 | } 75 | 76 | actual = clean(source) 77 | 78 | assert expected == actual 79 | 80 | 81 | def test_multi_level_get_att(): 82 | """ 83 | Intrinsics should be replaced by parameters to Sub 84 | """ 85 | 86 | source = { 87 | "Fn::Join": [ 88 | " ", 89 | ["The", {"Fn::GetAtt": ["First", "Second", "Third"]}, "is", "a", "lie"], 90 | ], 91 | } 92 | 93 | expected = { 94 | "Fn::Sub": "The ${First.Second.Third} is a lie", 95 | } 96 | 97 | actual = clean(source) 98 | 99 | assert expected == actual 100 | 101 | 102 | def test_others(): 103 | """ 104 | GetAtt should be replaced by ${Thing.Property} 105 | """ 106 | 107 | source = { 108 | "Fn::Join": [ 109 | " ", 110 | ["The", {"Fn::Base64": "Notreallybase64"}, "is", "a", "lie"], 111 | ], 112 | } 113 | 114 | expected = { 115 | "Fn::Sub": [ 116 | "The ${Param1} is a lie", 117 | { 118 | "Param1": { 119 | "Fn::Base64": "Notreallybase64", 120 | }, 121 | }, 122 | ], 123 | } 124 | 125 | actual = clean(source) 126 | 127 | assert expected == actual 128 | 129 | 130 | def test_in_array(): 131 | """ 132 | Converting Join to Sub should still work when the join is part of a larger array 133 | """ 134 | 135 | source = { 136 | "things": [ 137 | "Just a string", 138 | { 139 | "Fn::Join": [ 140 | " ", 141 | ["The", {"Fn::Base64": "Notreallybase64"}, "is", "a", "lie"], 142 | ], 143 | }, 144 | { 145 | "Another": "thing", 146 | }, 147 | ], 148 | } 149 | 150 | expected = { 151 | "things": [ 152 | "Just a string", 153 | { 154 | "Fn::Sub": [ 155 | "The ${Param1} is a lie", 156 | { 157 | "Param1": { 158 | "Fn::Base64": "Notreallybase64", 159 | }, 160 | }, 161 | ], 162 | }, 163 | { 164 | "Another": "thing", 165 | }, 166 | ], 167 | } 168 | 169 | actual = clean(source) 170 | 171 | assert expected == actual 172 | 173 | 174 | def test_literals(): 175 | """ 176 | Test that existing ${var} in source is respected 177 | """ 178 | 179 | source = { 180 | "Fn::Join": [ 181 | " ", 182 | ["The", "${cake}", "is", "a", "lie"], 183 | ], 184 | } 185 | 186 | expected = "The ${!cake} is a lie" 187 | 188 | actual = clean(source) 189 | 190 | assert expected == actual 191 | 192 | 193 | def test_nested_join(): 194 | """ 195 | Test that a join of joins works correctly 196 | """ 197 | 198 | source = { 199 | "Fn::Join": [ 200 | " ", 201 | ["The", "cake", { 202 | "Fn::Join": [ 203 | " ", 204 | ["is", "a"], 205 | ], 206 | }, "lie"], 207 | ], 208 | } 209 | 210 | expected = "The cake is a lie" 211 | 212 | actual = clean(source) 213 | 214 | assert expected == actual 215 | 216 | 217 | def test_deep_nested_join(): 218 | """ 219 | Test that a join works correctly when inside an intrinsic, inside a join 220 | """ 221 | 222 | source = { 223 | "Fn::Join": [ 224 | " ", 225 | ["The", "cake", "is", "a", { 226 | "Fn::ImportValue": { 227 | "Fn::Join": [ 228 | "-", 229 | [{"Ref": "lieStack"}, "lieValue"], 230 | ] 231 | }, 232 | }], 233 | ], 234 | } 235 | 236 | expected = { 237 | "Fn::Sub": [ 238 | "The cake is a ${Param1}", 239 | { 240 | "Param1": { 241 | "Fn::ImportValue": { 242 | "Fn::Sub": "${lieStack}-lieValue", 243 | }, 244 | }, 245 | }, 246 | ] 247 | } 248 | 249 | actual = clean(source) 250 | 251 | assert expected == actual 252 | 253 | 254 | def test_gh_63_no_value(): 255 | """ 256 | Test that Joins with conditionals that can evaluate to AWS::NoValue 257 | are not converted to Fn::Sub 258 | """ 259 | 260 | source = { 261 | "Fn::Join": [ 262 | ",", 263 | [ 264 | { 265 | "Fn::If": [ 266 | "Condition1", 267 | "True1", 268 | "Ref: AWS::NoValue" 269 | ] 270 | }, 271 | { 272 | "Fn::If": [ 273 | "Condition2", 274 | "True2", 275 | "False2" 276 | ] 277 | } 278 | ] 279 | ] 280 | } 281 | 282 | assert source == clean(source) 283 | 284 | 285 | def test_gh_63_value(): 286 | """ 287 | Test that Joins with conditionals that cannot evaluate to AWS::NoValue 288 | are converted to Fn::Sub 289 | """ 290 | 291 | source = { 292 | "Fn::Join": [ 293 | ",", 294 | [ 295 | { 296 | "Fn::If": [ 297 | "Condition1", 298 | "True1", 299 | "False1" 300 | ] 301 | }, 302 | { 303 | "Fn::If": [ 304 | "Condition2", 305 | "True2", 306 | "False2" 307 | ] 308 | } 309 | ] 310 | ] 311 | } 312 | 313 | expected = ODict(( 314 | ("Fn::Sub", [ 315 | "${Param1},${Param2}", 316 | ODict(( 317 | ("Param1", ODict(( 318 | ("Fn::If", ["Condition1", "True1", "False1"]), 319 | ))), 320 | ("Param2", ODict(( 321 | ("Fn::If", ["Condition2", "True2", "False2"]), 322 | ))), 323 | )), 324 | ]), 325 | )) 326 | 327 | actual = clean(source) 328 | 329 | assert actual == expected 330 | 331 | 332 | def test_misused_join(): 333 | """ 334 | Test that we don't break in the case that there is 335 | a Fn::Join with a single element instead of a list. 336 | We'll just return the Join as it was unless it's clearly just a string. 337 | """ 338 | 339 | cases = ( 340 | { 341 | "Fn::Join": [ 342 | " ", 343 | "foo", 344 | ], 345 | }, 346 | { 347 | "Fn::Join": "Just a string", 348 | }, 349 | { 350 | "Fn::Join": [ 351 | " ", 352 | { 353 | "Ref": "foo", 354 | }, 355 | ], 356 | }, 357 | { 358 | "Fn::Join": [ 359 | "-", 360 | [ 361 | {"Ref": "This is fine"}, 362 | ["But this is unexpected"], 363 | ] 364 | ], 365 | } 366 | ) 367 | 368 | expecteds = ( 369 | "foo", 370 | "Just a string", 371 | { 372 | "Fn::Join": [ 373 | " ", 374 | { 375 | "Ref": "foo", 376 | }, 377 | ], 378 | }, 379 | { 380 | "Fn::Join": [ 381 | "-", 382 | [ 383 | {"Ref": "This is fine"}, 384 | ["But this is unexpected"], 385 | ] 386 | ], 387 | } 388 | ) 389 | 390 | for i, case in enumerate(cases): 391 | expected = expecteds[i] 392 | 393 | actual = clean(case) 394 | assert expected == actual 395 | 396 | 397 | def test_yaml_dumper(): 398 | """ 399 | The clean dumper should use | format for multi-line strings 400 | """ 401 | 402 | source = { 403 | "start": "This is\na multi-line\nstring", 404 | } 405 | 406 | actual = yaml.dump(source, Dumper=CleanCfnYamlDumper) 407 | 408 | assert actual == """start: |- 409 | This is 410 | a multi-line 411 | string 412 | """ 413 | 414 | 415 | def test_reused_sub_params(): 416 | """ 417 | Test that params in Joins converted to Subs get reused when possible 418 | """ 419 | 420 | source = { 421 | "Fn::Join": [ 422 | " ", [ 423 | "The", 424 | { 425 | "Fn::Join": ["-", [{ 426 | "Ref": "Cake" 427 | }, "Lie"]], 428 | }, 429 | "is", 430 | { 431 | "Fn::Join": ["-", [{ 432 | "Ref": "Cake" 433 | }, "Lie"]], 434 | }, 435 | "and isn't", 436 | { 437 | "Fn::Join": ["-", [{ 438 | "Ref": "Pizza" 439 | }, "Truth"]], 440 | }, 441 | ], 442 | ], 443 | } 444 | 445 | expected = ODict(( 446 | ("Fn::Sub", [ 447 | "The ${Param1} is ${Param1} and isn't ${Param2}", 448 | ODict(( 449 | ("Param1", ODict(( 450 | ("Fn::Sub", "${Cake}-Lie"), 451 | ))), 452 | ("Param2", ODict(( 453 | ("Fn::Sub", "${Pizza}-Truth"), 454 | ))), 455 | )), 456 | ]), 457 | )) 458 | 459 | assert clean(source) == expected 460 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # tests/cli.py 4 | # 5 | # Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | from click.testing import CliRunner 20 | from cfn_flip import main 21 | 22 | 23 | def test_cli_with_version(): 24 | runner = CliRunner() 25 | result = runner.invoke(main.main, ['--version']) 26 | assert not result.exception 27 | assert result.exit_code == 0 28 | assert 'AWS Cloudformation Template Flip, Version ' in result.output 29 | 30 | 31 | def test_cli_with_input_json(tmpdir): 32 | # file_standard is the reference document that we expect to generate 33 | file_standard = open('examples/test.yaml', 'r').read() 34 | # file_output is the temporary file that we will generate to compare with file_standard 35 | file_output = tmpdir.join('unit_test.yaml') 36 | runner = CliRunner() 37 | result = runner.invoke(main.main, ['--yaml', 'examples/test.json', file_output.strpath]) 38 | assert not result.exception 39 | assert result.exit_code == 0 40 | assert file_output.read() == file_standard 41 | 42 | 43 | def test_cli_with_input_yaml(tmpdir): 44 | # file_standard is the reference document that we expect to generate 45 | file_standard = open('examples/test.json', 'r').read() 46 | # file_output is the temporary file that we will generate to compare with file_standard 47 | file_output = tmpdir.join('unit_test.json') 48 | runner = CliRunner() 49 | result = runner.invoke(main.main, ['--json', 'examples/test.yaml', file_output.strpath]) 50 | assert not result.exception 51 | assert result.exit_code == 0 52 | file_result = file_output.read() 53 | assert file_result == file_standard 54 | 55 | 56 | def test_cli_with_invalid_input(): 57 | runner = CliRunner() 58 | result = runner.invoke(main.main, ['--yaml', 'examples/invalid']) 59 | assert result.exception 60 | assert result.exit_code == 1 61 | assert result.output.startswith("Error: Expecting property name") 62 | 63 | 64 | def test_format_detection_with_invalid_input(): 65 | runner = CliRunner() 66 | result = runner.invoke(main.main, ['examples/invalid']) 67 | assert result.exception 68 | assert result.exit_code == 1 69 | assert result.output.startswith("Error: Expecting property name") 70 | 71 | 72 | def test_specified_json_input_with_guessed_output(tmpdir): 73 | file_standard = open('examples/test.yaml', 'r').read() 74 | file_output = tmpdir.join('unit_test.yaml') 75 | 76 | runner = CliRunner() 77 | result = runner.invoke(main.main, ['-i', 'json', 'examples/test.json', file_output.strpath]) 78 | assert not result.exception 79 | assert result.exit_code == 0 80 | assert file_output.read() == file_standard 81 | 82 | 83 | def test_specified_yaml_input_with_guessed_output(tmpdir): 84 | file_standard = open('examples/test.json', 'r').read() 85 | file_output = tmpdir.join('unit_test.yaml') 86 | 87 | runner = CliRunner() 88 | result = runner.invoke(main.main, ['-i', 'yaml', 'examples/test.yaml', file_output.strpath]) 89 | assert not result.exception 90 | assert result.exit_code == 0 91 | assert file_output.read() == file_standard 92 | 93 | 94 | def test_specified_json_input_with_no_flip(tmpdir): 95 | file_standard = open('examples/test.json', 'r').read() 96 | file_output = tmpdir.join('unit_test.json') 97 | 98 | runner = CliRunner() 99 | result = runner.invoke(main.main, ['-i', 'json', '-n', 'examples/test.json', file_output.strpath]) 100 | assert not result.exception 101 | assert result.exit_code == 0 102 | assert file_output.read() == file_standard 103 | 104 | 105 | def test_specified_yaml_input_with_no_flip(tmpdir): 106 | file_standard = open('examples/test.yaml', 'r').read() 107 | file_output = tmpdir.join('unit_test.yaml') 108 | 109 | runner = CliRunner() 110 | result = runner.invoke(main.main, ['-i', 'yaml', '-n', 'examples/test.yaml', file_output.strpath]) 111 | assert not result.exception 112 | assert result.exit_code == 0 113 | assert file_output.read() == file_standard 114 | 115 | 116 | def test_no_flip_is_overriden_by_specified_json_output(tmpdir): 117 | file_standard = open('examples/test.json', 'r').read() 118 | file_output = tmpdir.join('unit_test.json') 119 | 120 | runner = CliRunner() 121 | result = runner.invoke(main.main, ['-o', 'json', '-n', 'examples/test.yaml', file_output.strpath]) 122 | assert not result.exception 123 | assert result.exit_code == 0 124 | assert file_output.read() == file_standard 125 | 126 | 127 | def test_no_flip_is_overriden_by_specified_yaml_output(tmpdir): 128 | file_standard = open('examples/test.yaml', 'r').read() 129 | file_output = tmpdir.join('unit_test.yaml') 130 | 131 | runner = CliRunner() 132 | result = runner.invoke(main.main, ['-o', 'yaml', '-n', 'examples/test.json', file_output.strpath]) 133 | assert not result.exception 134 | assert result.exit_code == 0 135 | assert file_output.read() == file_standard 136 | 137 | 138 | def test_specified_json_output_overrides_j_flag(tmpdir): 139 | file_standard = open('examples/test.json', 'r').read() 140 | file_output = tmpdir.join('unit_test.json') 141 | 142 | runner = CliRunner() 143 | result = runner.invoke(main.main, ['-o', 'json', '-y', 'examples/test.json', file_output.strpath]) 144 | assert not result.exception 145 | assert result.exit_code == 0 146 | assert file_output.read() == file_standard 147 | 148 | 149 | def test_specified_yaml_output_overrides_j_flag(tmpdir): 150 | file_standard = open('examples/test.yaml', 'r').read() 151 | file_output = tmpdir.join('unit_test.yaml') 152 | 153 | runner = CliRunner() 154 | result = runner.invoke(main.main, ['-o', 'yaml', '-j', 'examples/test.yaml', file_output.strpath]) 155 | assert not result.exception 156 | assert result.exit_code == 0 157 | assert file_output.read() == file_standard 158 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # tests/cli.py 4 | # 5 | # Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | import os 20 | import pytest 21 | from cfn_tools._config import config, apply_configs, _CONFIG_DEFAULTS, _ConfigArg, _Config 22 | 23 | 24 | def test_config_rest(): 25 | assert config.reset() is None 26 | 27 | 28 | def test_config_rest_with_item(): 29 | assert config.reset("max_col_width") is None 30 | 31 | 32 | def test_config_with_env_var(): 33 | os.environ["CFN_MAX_COL_WIDTH"] = "200" 34 | assert config.reset() is None 35 | del os.environ["CFN_MAX_COL_WIDTH"] 36 | 37 | 38 | def test_config_get_item(): 39 | assert config['max_col_width'] == 200 40 | 41 | 42 | def test_invalid_config_set_attr(): 43 | with pytest.raises(TypeError): 44 | config.invalid_entry = "200" 45 | 46 | 47 | def test_config_apply_configs(): 48 | @apply_configs 49 | def temp_my_func(max_col_width): 50 | return max_col_width 51 | assert temp_my_func() == 200 52 | 53 | 54 | def test_config_apply_type_null(): 55 | _CONFIG_DEFAULTS['test_nullable'] = _ConfigArg(dtype=bool, nullable=True, has_default=False) 56 | test_config = _Config() 57 | test_config.test_nullable = None 58 | assert test_config.test_nullable is None 59 | 60 | 61 | def test_config_apply_type_null_error(): 62 | _CONFIG_DEFAULTS['test_nullable'] = _ConfigArg(dtype=int, nullable=False, has_default=True, default="nil") 63 | with pytest.raises(ValueError): 64 | _ = _Config() 65 | -------------------------------------------------------------------------------- /tests/test_flip.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. A copy of the License is located at 6 | 7 | http://aws.amazon.com/apache2.0/ 8 | 9 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and limitations under the License. 12 | """ 13 | 14 | from cfn_tools import dump_json, load_json, load_yaml 15 | from cfn_tools.odict import ODict 16 | import cfn_flip 17 | import pytest 18 | import yaml 19 | 20 | try: 21 | from json import JSONDecodeError 22 | except ImportError: 23 | # Python < 3.5 24 | JSONDecodeError = ValueError 25 | 26 | 27 | @pytest.fixture 28 | def input_json(): 29 | with open("examples/test.json", "r") as f: 30 | return f.read().strip() 31 | 32 | 33 | @pytest.fixture 34 | def input_long_json(): 35 | with open("examples/test_long.json", "r") as f: 36 | return f.read().strip() 37 | 38 | 39 | @pytest.fixture 40 | def input_json_with_literal(): 41 | with open("examples/test_json_data.json", "r") as f: 42 | return f.read().strip() 43 | 44 | 45 | @pytest.fixture 46 | def input_json_with_def_string_with_sub(): 47 | with open("examples/test_json_def_string_with_sub.json", "r") as f: 48 | return f.read().strip() 49 | 50 | 51 | @pytest.fixture 52 | def input_json_with_long_line(): 53 | with open("examples/test_json_data_long_line.json", "r") as f: 54 | return f.read().strip() 55 | 56 | 57 | @pytest.fixture 58 | def input_yaml_with_def_string_with_sub(): 59 | with open("examples/test_yaml_def_string_with_sub.yaml", "r") as f: 60 | return f.read() 61 | 62 | 63 | @pytest.fixture 64 | def input_yaml(): 65 | with open("examples/test.yaml", "r") as f: 66 | return f.read().strip() 67 | 68 | 69 | @pytest.fixture 70 | def clean_json(): 71 | with open("examples/clean.json", "r") as f: 72 | return f.read().strip() 73 | 74 | 75 | @pytest.fixture 76 | def clean_yaml(): 77 | with open("examples/clean.yaml", "r") as f: 78 | return f.read().strip() 79 | 80 | 81 | @pytest.fixture 82 | def multibyte_json(): 83 | with open("examples/test_multibyte.json", "r") as f: 84 | return f.read().strip() 85 | 86 | 87 | @pytest.fixture 88 | def multibyte_yaml(): 89 | with open("examples/test_multibyte.yaml", "r") as f: 90 | return f.read().strip() 91 | 92 | 93 | @pytest.fixture 94 | def parsed_yaml_with_json_literal(): 95 | with open("examples/test_json_data.yaml") as f: 96 | return load_yaml(f.read().strip()) 97 | 98 | 99 | @pytest.fixture 100 | def parsed_yaml_with_long_line(): 101 | with open("examples/test_yaml_long_line.yaml") as f: 102 | return load_yaml(f.read().strip()) 103 | 104 | 105 | @pytest.fixture 106 | def parsed_json(input_json): 107 | return load_json(input_json) 108 | 109 | 110 | @pytest.fixture 111 | def parsed_yaml(input_yaml): 112 | return load_yaml(input_yaml) 113 | 114 | 115 | @pytest.fixture 116 | def parsed_clean_json(clean_json): 117 | return load_json(clean_json) 118 | 119 | 120 | @pytest.fixture 121 | def parsed_clean_yaml(clean_yaml): 122 | return load_yaml(clean_yaml) 123 | 124 | 125 | @pytest.fixture 126 | def parsed_multibyte_json(multibyte_json): 127 | return load_json(multibyte_json) 128 | 129 | 130 | @pytest.fixture 131 | def parsed_multibyte_yaml(multibyte_yaml): 132 | return load_yaml(multibyte_yaml) 133 | 134 | 135 | @pytest.fixture 136 | def bad_data(): 137 | return "\n\n\n\tThis isn't right!\n" 138 | 139 | 140 | @pytest.fixture 141 | def fail_message(): 142 | import six 143 | 144 | if six.PY2: 145 | return "No JSON object could be decoded" 146 | return "Expecting value: line 1 column 1" 147 | 148 | 149 | def test_to_json_with_yaml(input_yaml, parsed_json): 150 | """ 151 | Test that to_json performs correctly 152 | """ 153 | 154 | actual = cfn_flip.to_json(input_yaml) 155 | assert load_json(actual) == parsed_json 156 | 157 | 158 | def test_to_json_with_json(input_json, parsed_json): 159 | """ 160 | Test that to_json still works when passed json 161 | (All json is valid yaml) 162 | """ 163 | 164 | actual = cfn_flip.to_json(input_json) 165 | 166 | assert load_json(actual) == parsed_json 167 | 168 | 169 | def test_to_yaml_with_long_json(input_long_json): 170 | """ 171 | Test that to_yaml performs correctly 172 | """ 173 | 174 | actual = cfn_flip.to_yaml(input_long_json) 175 | 176 | # The result should not parse as json 177 | with pytest.raises(ValueError): 178 | load_json(actual) 179 | 180 | parsed_actual = load_yaml(actual) 181 | 182 | assert parsed_actual['TooShort'] == "foo\nbar\nbaz\nquuux" 183 | assert 'WideText: >-' in actual 184 | assert 'TooShort: "foo' in actual 185 | 186 | 187 | def test_to_yaml_with_json(input_json, parsed_yaml): 188 | """ 189 | Test that to_yaml performs correctly 190 | """ 191 | 192 | actual = cfn_flip.to_yaml(input_json) 193 | 194 | # The result should not parse as json 195 | with pytest.raises(ValueError): 196 | load_json(actual) 197 | 198 | parsed_actual = load_yaml(actual) 199 | 200 | assert parsed_actual == parsed_yaml 201 | 202 | 203 | def test_to_yaml_with_yaml(input_yaml, parsed_yaml): 204 | """ 205 | Test that to_yaml still works when passed yaml 206 | """ 207 | 208 | actual = cfn_flip.to_yaml(input_yaml) 209 | 210 | assert load_yaml(actual) == parsed_yaml 211 | 212 | 213 | def test_flip_to_json(input_yaml, input_json, parsed_json): 214 | """ 215 | Test that flip performs correctly transforming from yaml to json 216 | """ 217 | 218 | actual = cfn_flip.flip(input_yaml) 219 | 220 | assert load_json(actual) == parsed_json 221 | 222 | 223 | def test_flip_to_yaml(input_json, input_yaml, parsed_yaml): 224 | """ 225 | Test that flip performs correctly transforming from json to yaml 226 | """ 227 | 228 | actual = cfn_flip.flip(input_json) 229 | assert actual == input_yaml + "\n" 230 | 231 | # The result should not parse as json 232 | with pytest.raises(ValueError): 233 | load_json(actual) 234 | 235 | parsed_actual = load_yaml(actual) 236 | assert parsed_actual == parsed_yaml 237 | 238 | 239 | def test_flip_to_clean_json(input_yaml, clean_json, parsed_clean_json): 240 | """ 241 | Test that flip performs correctly transforming from yaml to json 242 | and the `clean_up` flag is active 243 | """ 244 | 245 | actual = cfn_flip.flip(input_yaml, clean_up=True) 246 | 247 | assert load_json(actual) == parsed_clean_json 248 | 249 | 250 | def test_flip_to_clean_yaml(input_json, clean_yaml, parsed_clean_yaml): 251 | """ 252 | Test that flip performs correctly transforming from json to yaml 253 | and the `clean_up` flag is active 254 | """ 255 | 256 | actual = cfn_flip.flip(input_json, clean_up=True) 257 | assert actual == clean_yaml + "\n" 258 | 259 | # The result should not parse as json 260 | with pytest.raises(ValueError): 261 | load_json(actual) 262 | 263 | parsed_actual = load_yaml(actual) 264 | assert parsed_actual == parsed_clean_yaml 265 | 266 | 267 | def test_flip_to_multibyte_json(multibyte_json, parsed_multibyte_yaml): 268 | """ 269 | Test that load multibyte file performs correctly 270 | """ 271 | 272 | actual = cfn_flip.to_yaml(multibyte_json) 273 | assert load_yaml(actual) == parsed_multibyte_yaml 274 | 275 | 276 | def test_flip_to_multibyte_yaml(multibyte_yaml, parsed_multibyte_json): 277 | """ 278 | Test that load multibyte file performs correctly 279 | """ 280 | 281 | actual = cfn_flip.to_json(multibyte_yaml) 282 | assert load_json(actual) == parsed_multibyte_json 283 | 284 | 285 | def test_flip_with_bad_data(fail_message, bad_data): 286 | """ 287 | Test that flip fails with an error message when passed bad data 288 | """ 289 | 290 | with pytest.raises(JSONDecodeError, match=fail_message): 291 | cfn_flip.flip(bad_data) 292 | 293 | 294 | def test_flip_to_json_with_datetimes(): 295 | """ 296 | Test that the json encoder correctly handles dates and datetimes 297 | """ 298 | 299 | tricky_data = """ 300 | a date: 2017-03-02 301 | a datetime: 2017-03-02 19:52:00 302 | """ 303 | 304 | actual = cfn_flip.to_json(tricky_data) 305 | 306 | parsed_actual = load_json(actual) 307 | 308 | assert parsed_actual == { 309 | "a date": "2017-03-02", 310 | "a datetime": "2017-03-02T19:52:00", 311 | } 312 | 313 | 314 | def test_flip_to_yaml_with_clean_getatt(): 315 | """ 316 | The clean flag should convert Fn::GetAtt to its short form 317 | """ 318 | 319 | data = """ 320 | { 321 | "Fn::GetAtt": ["Left", "Right"] 322 | } 323 | """ 324 | 325 | expected = "!GetAtt 'Left.Right'\n" 326 | 327 | assert cfn_flip.to_yaml(data, clean_up=False) == expected 328 | assert cfn_flip.to_yaml(data, clean_up=True) == expected 329 | 330 | 331 | def test_flip_to_yaml_with_multi_level_getatt(): 332 | """ 333 | Test that we correctly convert multi-level Fn::GetAtt 334 | from JSON to YAML format 335 | """ 336 | 337 | data = """ 338 | { 339 | "Fn::GetAtt": ["First", "Second", "Third"] 340 | } 341 | """ 342 | 343 | expected = "!GetAtt 'First.Second.Third'\n" 344 | 345 | assert cfn_flip.to_yaml(data) == expected 346 | 347 | 348 | def test_flip_to_yaml_with_dotted_getatt(): 349 | """ 350 | Even though documentation does not suggest Resource.Value is valid 351 | we should support it anyway as cloudformation allows it :) 352 | """ 353 | 354 | data = """ 355 | [ 356 | { 357 | "Fn::GetAtt": "One.Two" 358 | }, 359 | { 360 | "Fn::GetAtt": "Three.Four.Five" 361 | } 362 | ] 363 | """ 364 | 365 | expected = "- !GetAtt 'One.Two'\n- !GetAtt 'Three.Four.Five'\n" 366 | 367 | assert cfn_flip.to_yaml(data) == expected 368 | 369 | 370 | def test_flip_to_json_with_multi_level_getatt(): 371 | """ 372 | Test that we correctly convert multi-level Fn::GetAtt 373 | from YAML to JSON format 374 | """ 375 | 376 | data = "!GetAtt 'First.Second.Third'\n" 377 | 378 | expected = { 379 | "Fn::GetAtt": ["First", "Second.Third"] 380 | } 381 | 382 | actual = cfn_flip.to_json(data, clean_up=True) 383 | 384 | assert load_json(actual) == expected 385 | 386 | 387 | def test_getatt_from_yaml(): 388 | """ 389 | Test that we correctly convert the short form of GetAtt 390 | into the correct JSON format from YAML 391 | """ 392 | 393 | source = """ 394 | - !GetAtt foo.bar 395 | - Fn::GetAtt: [foo, bar] 396 | """ 397 | 398 | expected = [ 399 | {"Fn::GetAtt": ["foo", "bar"]}, 400 | {"Fn::GetAtt": ["foo", "bar"]}, 401 | ] 402 | 403 | # No clean 404 | actual = cfn_flip.to_json(source, clean_up=False) 405 | assert load_json(actual) == expected 406 | 407 | # With clean 408 | actual = cfn_flip.to_json(source, clean_up=True) 409 | assert load_json(actual) == expected 410 | 411 | 412 | def test_flip_to_json_with_condition(): 413 | """ 414 | Test that the Condition key is correctly converted 415 | """ 416 | 417 | source = """ 418 | MyAndCondition: !And 419 | - !Equals ["sg-mysggroup", !Ref "ASecurityGroup"] 420 | - !Condition SomeOtherCondition 421 | """ 422 | 423 | expected = { 424 | "MyAndCondition": { 425 | "Fn::And": [ 426 | {"Fn::Equals": ["sg-mysggroup", {"Ref": "ASecurityGroup"}]}, 427 | {"Condition": "SomeOtherCondition"} 428 | ] 429 | } 430 | } 431 | 432 | actual = cfn_flip.to_json(source, clean_up=True) 433 | assert load_json(actual) == expected 434 | 435 | 436 | def test_flip_to_yaml_with_newlines(): 437 | """ 438 | Test that strings containing newlines are quoted 439 | """ 440 | 441 | source = r'["a", "b\n", "c\r\n", "d\r"]' 442 | 443 | expected = "".join([ 444 | '- a\n', 445 | '- "b\\n"\n', 446 | '- "c\\r\\n"\n', 447 | '- "d\\r"\n', 448 | ]) 449 | 450 | assert cfn_flip.to_yaml(source) == expected 451 | 452 | 453 | def test_clean_flip_to_yaml_with_newlines(): 454 | """ 455 | Test that strings containing newlines use blockquotes when using "clean" 456 | """ 457 | 458 | source = dump_json(ODict(( 459 | ("outer", ODict(( 460 | ("inner", "#!/bin/bash\nyum -y update\nyum install python"), 461 | ("subbed", ODict(( 462 | ("Fn::Sub", "The cake\nis\n${CakeType}"), 463 | ))), 464 | ))), 465 | ))) 466 | 467 | expected = """outer: 468 | inner: |- 469 | #!/bin/bash 470 | yum -y update 471 | yum install python 472 | subbed: !Sub |- 473 | The cake 474 | is 475 | ${CakeType} 476 | """ 477 | 478 | assert cfn_flip.to_yaml(source, clean_up=True) == expected 479 | 480 | 481 | def test_flip_with_json_output(input_yaml, parsed_json): 482 | """ 483 | We should be able to specify that the output is JSON 484 | """ 485 | 486 | actual = cfn_flip.flip(input_yaml, out_format="json") 487 | 488 | assert load_json(actual) == parsed_json 489 | 490 | 491 | def test_flip_with_yaml_output(input_json, parsed_yaml): 492 | """ 493 | We should be able to specify that the output is YAML 494 | """ 495 | 496 | actual = cfn_flip.flip(input_json, out_format="yaml") 497 | 498 | parsed_actual = load_yaml(actual) 499 | 500 | assert parsed_actual == parsed_yaml 501 | 502 | 503 | def test_no_flip_with_json(input_json, parsed_json): 504 | """ 505 | We should be able to submit JSON and get JSON back 506 | """ 507 | 508 | actual = cfn_flip.flip(input_json, no_flip=True) 509 | 510 | assert load_json(actual) == parsed_json 511 | 512 | 513 | def test_no_flip_with_yaml(input_yaml, parsed_yaml): 514 | """ 515 | We should be able to submit YAML and get YAML back 516 | """ 517 | 518 | actual = cfn_flip.flip(input_yaml, no_flip=True) 519 | 520 | parsed_actual = load_yaml(actual) 521 | 522 | assert parsed_actual == parsed_yaml 523 | 524 | 525 | def test_no_flip_with_explicit_json(input_json, parsed_json): 526 | """ 527 | We should be able to submit JSON and get JSON back 528 | and specify the output format explicity 529 | """ 530 | 531 | actual = cfn_flip.flip(input_json, out_format="json", no_flip=True) 532 | 533 | assert load_json(actual) == parsed_json 534 | 535 | 536 | def test_no_flip_with_explicit_yaml(input_yaml, parsed_yaml): 537 | """ 538 | We should be able to submit YAML and get YAML back 539 | and specify the output format explicity 540 | """ 541 | 542 | actual = cfn_flip.flip(input_yaml, out_format="yaml", no_flip=True) 543 | 544 | parsed_actual = load_yaml(actual) 545 | 546 | assert parsed_actual == parsed_yaml 547 | 548 | 549 | def test_explicit_json_rejects_yaml(fail_message, input_yaml): 550 | """ 551 | Given an output format of YAML 552 | The input format should be assumed to be JSON 553 | and YAML input should be rejected 554 | """ 555 | 556 | with pytest.raises(JSONDecodeError, match=fail_message): 557 | cfn_flip.flip(input_yaml, out_format="yaml") 558 | 559 | 560 | def test_explicit_yaml_rejects_bad_yaml(bad_data): 561 | """ 562 | Given an output format of YAML 563 | The input format should be assumed to be JSON 564 | and YAML input should be rejected 565 | """ 566 | 567 | with pytest.raises( 568 | yaml.scanner.ScannerError, 569 | match="while scanning for the next token\nfound character \'\\\\t\' that cannot start any token", 570 | ): 571 | cfn_flip.flip(bad_data, out_format="json") 572 | 573 | 574 | def test_flip_to_yaml_with_longhand_functions(input_json, parsed_json): 575 | """ 576 | When converting to yaml, sometimes we'll want to keep the long form 577 | """ 578 | 579 | actual1 = cfn_flip.flip(input_json, long_form=True) 580 | actual2 = cfn_flip.to_yaml(input_json, long_form=True) 581 | 582 | # No custom loader as there should be no custom tags 583 | parsed_actual1 = yaml.load(actual1, Loader=yaml.SafeLoader) 584 | parsed_actual2 = yaml.load(actual2, Loader=yaml.SafeLoader) 585 | 586 | # We use the parsed JSON as it contains long form function calls 587 | assert parsed_actual1 == parsed_json 588 | assert parsed_actual2 == parsed_json 589 | 590 | 591 | def test_unconverted_types(): 592 | """ 593 | When converting to yaml, we need to make sure all short-form types are tagged 594 | """ 595 | 596 | fns = { 597 | "Fn::GetAtt": "!GetAtt", 598 | "Fn::Sub": "!Sub", 599 | "Ref": "!Ref", 600 | "Condition": "!Condition", 601 | } 602 | 603 | for fn, tag in fns.items(): 604 | value = dump_json({ 605 | fn: "something" 606 | }) 607 | 608 | expected = "{} 'something'\n".format(tag) 609 | 610 | assert cfn_flip.to_yaml(value) == expected 611 | 612 | 613 | def test_get_dumper(): 614 | """ 615 | When invoking get_dumper use clean_up & long_form 616 | :return: LongCleanDumper 617 | """ 618 | 619 | resp = cfn_flip.get_dumper(clean_up=True, long_form=True) 620 | assert resp == cfn_flip.yaml_dumper.LongCleanDumper 621 | 622 | 623 | def test_quoted_digits(): 624 | """ 625 | Any value that is composed entirely of digits 626 | should be quoted for safety. 627 | CloudFormation is happy for numbers to appear as strings. 628 | But the opposite (e.g. account numbers as numbers) can cause issues 629 | See https://github.com/awslabs/aws-cfn-template-flip/issues/41 630 | """ 631 | 632 | value = dump_json(ODict(( 633 | ("int", 123456), 634 | ("float", 123.456), 635 | ("oct", "0123456"), 636 | ("bad-oct", "012345678"), 637 | ("safe-oct", "0o123456"), 638 | ("string", "abcdef"), 639 | ))) 640 | 641 | expected = "\n".join(( 642 | "int: 123456", 643 | "float: 123.456", 644 | "oct: '0123456'", 645 | "bad-oct: '012345678'", 646 | "safe-oct: '0o123456'", 647 | "string: abcdef", 648 | "" 649 | )) 650 | 651 | actual = cfn_flip.to_yaml(value) 652 | 653 | assert actual == expected 654 | 655 | 656 | def test_flip_to_yaml_with_json_literal(input_json_with_literal, parsed_yaml_with_json_literal): 657 | """ 658 | Test that load json with json payload that must stay json when converted to yaml 659 | """ 660 | 661 | actual = cfn_flip.to_yaml(input_json_with_literal) 662 | assert load_yaml(actual) == parsed_yaml_with_json_literal 663 | 664 | 665 | def test_flip_to_yaml_with_json_literal_with_sub(input_json_with_def_string_with_sub, 666 | input_yaml_with_def_string_with_sub): 667 | actual = cfn_flip.to_yaml(input_json_with_def_string_with_sub) 668 | with open('output.yaml', 'w') as output: 669 | output.write(actual) 670 | assert actual == input_yaml_with_def_string_with_sub 671 | 672 | 673 | def test_flip_to_yaml_with_json_long_line(input_json_with_long_line, parsed_yaml_with_long_line): 674 | """ 675 | Test that load json with long line will translate to yaml without break line. 676 | The configuration settings is 120 columns 677 | """ 678 | actual = cfn_flip.to_yaml(input_json_with_long_line) 679 | assert load_yaml(actual) == parsed_yaml_with_long_line 680 | -------------------------------------------------------------------------------- /tests/test_odict.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at 5 | 6 | http://aws.amazon.com/apache2.0/ 7 | 8 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 9 | """ 10 | 11 | from cfn_tools.odict import ODict 12 | from copy import deepcopy 13 | import pickle 14 | import pytest 15 | 16 | 17 | def test_get_set(): 18 | """ 19 | It should at least work the same as a dict 20 | """ 21 | 22 | case = ODict() 23 | 24 | case["one"] = 1 25 | case["two"] = 2 26 | 27 | assert len(case.keys()) == 2 28 | assert case["one"] == 1 29 | 30 | 31 | def test_list_constructor(): 32 | """ 33 | We should be able to construct one from a tuple of pairs 34 | """ 35 | 36 | case = ODict(( 37 | ("one", 1), 38 | ("two", 2), 39 | )) 40 | 41 | assert len(case.keys()) == 2 42 | assert case["one"] == 1 43 | assert case["two"] == 2 44 | assert case["two"] == 2 45 | 46 | 47 | def test_ordering(): 48 | """ 49 | Ordering should be left intact 50 | """ 51 | 52 | case = ODict() 53 | 54 | case["z"] = 1 55 | case["a"] = 2 56 | 57 | assert list(case.keys()) == ["z", "a"] 58 | 59 | 60 | def test_ordering_from_constructor(): 61 | """ 62 | Ordering should be left intact 63 | """ 64 | 65 | case = ODict([ 66 | ("z", 1), 67 | ("a", 2), 68 | ]) 69 | 70 | assert list(case.keys()) == ["z", "a"] 71 | 72 | 73 | def test_constructor_disallows_dict(): 74 | """ 75 | For the sake of python<3.6, don't accept dicts 76 | as ordering will be lost 77 | """ 78 | 79 | with pytest.raises(Exception, match="ODict does not allow construction from a dict"): 80 | ODict({ 81 | "z": 1, 82 | "a": 2, 83 | }) 84 | 85 | 86 | def test_explicit_sorting(): 87 | """ 88 | Even an explicit sort should result in no change 89 | """ 90 | 91 | case = ODict(( 92 | ("z", 1), 93 | ("a", 2), 94 | )).items() 95 | 96 | actual = sorted(case) 97 | 98 | assert actual == case 99 | 100 | 101 | def test_post_deepcopy_repr(): 102 | """ 103 | Repr should behave normally after deepcopy 104 | """ 105 | 106 | dct = ODict([("a", 1)]) 107 | dct2 = deepcopy(dct) 108 | assert repr(dct) == repr(dct2) 109 | dct2["b"] = 2 110 | assert repr(dct) != repr(dct2) 111 | 112 | 113 | def test_pickle(): 114 | """ 115 | Should be able to pickle and unpickle 116 | """ 117 | 118 | dct = ODict([ 119 | ("c", 3), 120 | ("d", 4), 121 | ]) 122 | data = pickle.dumps(dct) 123 | dct2 = pickle.loads(data) 124 | assert dct == dct2 125 | -------------------------------------------------------------------------------- /tests/test_step_functions_template.py: -------------------------------------------------------------------------------- 1 | import cfn_flip 2 | import pytest 3 | 4 | 5 | @pytest.fixture 6 | def input_json_state_machine(): 7 | with open("examples/test_json_state_machine.json", "r") as f: 8 | return f.read() 9 | 10 | 11 | @pytest.fixture 12 | def output_yaml_state_machine(): 13 | with open("examples/test_yaml_state_machine.yaml", "r") as f: 14 | return f.read() 15 | 16 | 17 | def test_state_machine_with_str(input_json_state_machine, output_yaml_state_machine): 18 | resp = cfn_flip.to_yaml(input_json_state_machine) 19 | assert resp == output_yaml_state_machine 20 | -------------------------------------------------------------------------------- /tests/test_tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. A copy of the License is located at 6 | 7 | http://aws.amazon.com/apache2.0/ 8 | 9 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and limitations under the License. 12 | """ 13 | 14 | from cfn_tools import load_json, load_yaml, dump_json, dump_yaml, CfnYamlDumper 15 | from cfn_tools.odict import ODict, OdictItems 16 | from cfn_tools.yaml_loader import multi_constructor, construct_getatt 17 | from yaml import ScalarNode 18 | import datetime 19 | import pytest 20 | import six 21 | 22 | 23 | class MockNode(object): 24 | def __init__(self, value=None): 25 | self.value = value 26 | 27 | 28 | def test_load_json(): 29 | """ 30 | Should map to an ordered dict 31 | """ 32 | 33 | source = """ 34 | { 35 | "z": "first", 36 | "m": "middle", 37 | "a": "last" 38 | } 39 | """ 40 | 41 | actual = load_json(source) 42 | 43 | assert type(actual) == ODict 44 | assert list(actual.keys()) == ["z", "m", "a"] 45 | assert actual["z"] == "first" 46 | assert actual["m"] == "middle" 47 | assert actual["a"] == "last" 48 | 49 | 50 | def test_load_yaml(): 51 | """ 52 | Should map to an ordered dict 53 | """ 54 | 55 | source = """z: first 56 | m: !Sub 57 | - The cake is a ${CakeType} 58 | - CakeType: lie 59 | a: !Ref last 60 | """ 61 | 62 | actual = load_yaml(source) 63 | 64 | assert type(actual) == ODict 65 | assert list(actual.keys()) == ["z", "m", "a"] 66 | assert actual["z"] == "first" 67 | assert actual["m"] == { 68 | "Fn::Sub": [ 69 | "The cake is a ${CakeType}", 70 | { 71 | "CakeType": "lie", 72 | }, 73 | ], 74 | } 75 | assert actual["a"] == {"Ref": "last"} 76 | 77 | 78 | def test_dump_json(): 79 | """ 80 | JSON dumping just needs to know about datetimes, 81 | provide a nice indent, and preserve order 82 | """ 83 | import sys 84 | 85 | source = ODict(( 86 | ("z", datetime.time(3, 45)), 87 | ("m", datetime.date(2012, 5, 2)), 88 | ("a", datetime.datetime(2012, 5, 2, 3, 45)), 89 | )) 90 | 91 | actual = dump_json(source) 92 | 93 | assert load_json(actual) == { 94 | "z": "03:45:00", 95 | "m": "2012-05-02", 96 | "a": "2012-05-02T03:45:00", 97 | } 98 | 99 | if sys.version_info < (3, 6): 100 | fail_message = r"\(1\+1j\) is not JSON serializable" 101 | elif sys.version_info < (3, 7): 102 | fail_message = "Object of type 'complex' is not JSON serializable" 103 | else: 104 | fail_message = "Object of type complex is not JSON serializable" 105 | 106 | with pytest.raises(TypeError, match=fail_message): 107 | dump_json({ 108 | "c": 1 + 1j, 109 | }) 110 | 111 | 112 | def test_dump_yaml(): 113 | """ 114 | YAML dumping needs to use quoted style for strings with newlines, 115 | use a standard indenting style, and preserve order 116 | """ 117 | 118 | source = ODict(( 119 | ("z", "short string",), 120 | ("m", {"Ref": "embedded string"},), 121 | ("a", "A\nmulti-line\nstring",), 122 | )) 123 | 124 | actual = dump_yaml(source) 125 | 126 | assert actual == """z: short string 127 | m: 128 | Ref: embedded string 129 | a: "A\\nmulti-line\\nstring" 130 | """ 131 | 132 | 133 | def test_odict_items_sort(): 134 | """ 135 | Simple test to validate sort method. 136 | TODO: implement sort method 137 | :return: None 138 | """ 139 | items = ['B', 'A', 'C'] 140 | odict = OdictItems(items) 141 | assert not odict.sort() 142 | 143 | 144 | def test_odict_fail_with_dict(): 145 | """ 146 | Raise exception if we pass dict when initializing the class with dict 147 | :return: Exception 148 | """ 149 | items = {'key1': 'value1'} 150 | with pytest.raises(Exception) as e: 151 | ODict(items) 152 | assert 'ODict does not allow construction from a dict' == str(e.value) 153 | 154 | 155 | def test_represent_scalar(): 156 | """ 157 | When invoking represent_scalar apply style if value has \n 158 | :return: ScalarNode 159 | """ 160 | source = """z: first 161 | m: !Sub 162 | - The cake is a ${CakeType} 163 | - CakeType: lie 164 | a: !Ref last 165 | """ 166 | yaml_dumper = CfnYamlDumper(six.StringIO(source)) 167 | resp = yaml_dumper.represent_scalar('Key', 'value\n') 168 | print(resp) 169 | assert isinstance(resp, ScalarNode) 170 | 171 | 172 | def test_multi_constructor_with_invalid_node_type(): 173 | """ 174 | When invoking multi_constructor with invalid node type must raise Exception 175 | :return: 176 | """ 177 | with pytest.raises(Exception) as e: 178 | multi_constructor(None, None, None) 179 | assert 'Bad tag: !Fn::None' in str(e.value) 180 | 181 | 182 | def test_construct_getattr_with_invalid_node_type(): 183 | """ 184 | When invoking multi_constructor with invalid node type must raise Exception 185 | :return: 186 | """ 187 | node = MockNode() 188 | with pytest.raises(ValueError) as e: 189 | construct_getatt(node) 190 | assert 'Unexpected node type:' in str(e.value) 191 | 192 | 193 | def test_construct_getattr_with_list(): 194 | """ 195 | When invoking multi_constructor with invalid node type must raise Exception 196 | :return: 197 | """ 198 | node = MockNode() 199 | node.value = [MockNode('A'), MockNode('B'), MockNode('C')] 200 | resp = construct_getatt(node) 201 | assert resp == ['A', 'B', 'C'] 202 | -------------------------------------------------------------------------------- /tests/test_yaml_patching.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2016-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. A copy of the License is located at 6 | 7 | http://aws.amazon.com/apache2.0/ 8 | 9 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and limitations under the License. 12 | """ 13 | 14 | from cfn_tools import load_yaml 15 | from cfn_tools.odict import ODict 16 | import yaml 17 | 18 | 19 | def test_yaml_no_ordered_dict(): 20 | """ 21 | cfn-flip patches yaml to use ODict by default 22 | Check that we don't do this for folks who import cfn_flip and yaml 23 | """ 24 | 25 | yaml_string = "key: value" 26 | data = yaml.load(yaml_string, Loader=yaml.SafeLoader) 27 | 28 | assert type(data) == dict 29 | 30 | 31 | def test_yaml_no_ordered_dict_with_custom_loader(): 32 | """ 33 | cfn-flip patches yaml to use ODict by default 34 | Check that we do this for normal cfn_flip use cases 35 | """ 36 | 37 | yaml_string = "key: value" 38 | data = load_yaml(yaml_string) 39 | 40 | assert type(data) == ODict 41 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # tox (https://tox.readthedocs.io/) is a tool for running tests 2 | # in multiple virtualenvs. This configuration file will run the 3 | # test suite on all supported python versions. To use it, "pip install tox" 4 | # and then run "tox" from this directory. 5 | 6 | [tox] 7 | envlist = py36, py37, py38, py39, py310, flake8 8 | 9 | [gh-actions] 10 | python = 11 | 3.6: py36 12 | 3.7: py37 13 | 3.8: py38 14 | 3.9: py39 15 | 3.10: py310 16 | 17 | [testenv] 18 | deps = 19 | pytest>=4.3.0 20 | pytest-cov 21 | pytest-sugar 22 | 23 | commands = 24 | py.test \ 25 | --cov=cfn_clean \ 26 | --cov=cfn_flip \ 27 | --cov=cfn_tools \ 28 | --cov-report term-missing \ 29 | --cov-report html \ 30 | --cov-report xml \ 31 | {posargs} 32 | 33 | [testenv:flake8] 34 | deps = 35 | flake8 36 | 37 | commands = flake8 {posargs} cfn_clean cfn_flip cfn_tools tests 38 | 39 | [flake8] 40 | ignore = E501 41 | 42 | [pytest] 43 | addopts = --cov-report term-missing 44 | 45 | [coverage:run] 46 | omit = 47 | # Skip this file as this is just a bootstrap for python when running the zip_file 48 | cfn_flip/__main__.py 49 | --------------------------------------------------------------------------------