From 4861a86ff0cfbe1c0bfa66514fa30b47f2ab1c86 Mon Sep 17 00:00:00 2001 From: hawry Date: Sun, 13 Oct 2019 13:04:38 +0200 Subject: [PATCH] add support for CloudFormation templates --- README.md | 16 ++++++- deforest/cfcleaner.py | 97 +++++++++++++++++++++++++++++++++++++++++++ deforest/cleaner.py | 11 ++++- deforest/constant.py | 2 +- deforest/deforest.py | 27 +++++++----- deforest/solution.py | 41 ++++++++++++++++++ 6 files changed, 181 insertions(+), 13 deletions(-) create mode 100644 deforest/cfcleaner.py create mode 100644 deforest/solution.py diff --git a/README.md b/README.md index c586353..94d3ac8 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ Remove all `x-amazon`-tags from your Open API 3 or Swagger 2 specification. Useful if you are using Cloudformation to specify your API Gateways, and want to provide your consumers with the same specification but not wanting to disclose your internal Amazon integrations. # Installation + `pip install --user deforest` ## Features @@ -10,19 +11,32 @@ Remove all `x-amazon`-tags from your Open API 3 or Swagger 2 specification. Usef - Clean keys starting with the string `x-amazon` - Handles JSON and YAML input - Handles JSON and YAML output (defaults to YAML) +- Support for AWS CloudFormation templates # Usage + ``` Usage: deforest [OPTIONS] INFILE Options: -o, --outfile TEXT specify output file, default is - ./-<version>.<format> + ./<title>-<version>.<format>, ignored if input is + a CloudFormation template and the template + contains more than one ApiGateway resource) -f, --format [yaml|json] output format [default: yaml] -i, --indent INTEGER if output format is json, specify indentation --version Show the version and exit. --help Show this message and exit. ``` +## CloudFormation templates + +Version 0.1.1 and later supports CloudFormation templates as input. If more than one API Gateway is part of the template, the `--outfile` flag will be ignored. + # Limitations + The output file looses its order of the keys in the file, which shouldn't affect you if you're using a converter to create a graphical documentation/specification - but can be confusing if you have a specific internal order you wish to keep. + +# Contribute + +If you wish to see a specific feature, please create an issue in the issue tracker - and if you want to help develop deforest, you're free to create a pull request as well. All submitted code will be subject to the licensing specified in the LICENSE file. diff --git a/deforest/cfcleaner.py b/deforest/cfcleaner.py new file mode 100644 index 0000000..f40d0b2 --- /dev/null +++ b/deforest/cfcleaner.py @@ -0,0 +1,97 @@ +from tags import GetAttTag, SubTag, RefTag +import yaml +import re + +class CFCleaner(): + keys = ["x-amazon"] + filedata = None + raw = {} + processed = None + KEY_TYPE = "Type" + KEY_RESOURCES = "Resources" + KEY_PROPERTIES = "Properties" + KEY_BODY = "Body" + TYPE_APIGW = "AWS::ApiGateway::RestApi" + + multi_raw = [] + multi_processed = [] + + def __init__(self,data): + self.filedata = data + + def convert(self): + self._enable_custom_tags() + self._load() + self._clean_all_keys() + self._dump_all() + return self.multi_processed + + def number_results(self): + return len(self.multi_processed) + + def _clean_all_keys(self): + self._identify_apigw(self.raw) + + def _identify_apigw(self,v): + resources = v[CFCleaner.KEY_RESOURCES] + for k in resources: + if isinstance(resources[k], dict): + if self._is_apigw(resources[k]) and self._has_properties_and_body(resources[k]): + self.multi_raw.append(resources[k][CFCleaner.KEY_PROPERTIES][CFCleaner.KEY_BODY]) + + for raw in self.multi_raw: + self._cleanup_keys(raw) + + def _cleanup_keys(self,v): + for k in v.keys(): + if any(m in k for m in self.keys): + del v[k] + else: + if isinstance(v[k], dict): + self._cleanup_keys(v[k]) + + def _is_apigw(self,node): + if CFCleaner.KEY_TYPE in node: + if node[CFCleaner.KEY_TYPE] == CFCleaner.TYPE_APIGW: + return True + return False + + def _has_properties_and_body(self,node): + return CFCleaner.KEY_PROPERTIES in node and CFCleaner.KEY_BODY in node[CFCleaner.KEY_PROPERTIES] + + def _namify(self, title, version): + s = "{}-{}".format(title.lower(),version.lower()) + s = re.sub(r"\s+", '-', s) + return s + + def get_title_and_version_all(self,index): + title = self.multi_raw[index]["info"]["title"] or "no-title" + version = self.multi_raw[index]["info"]["version"] or "no-version" + return self._namify(title,version) + + def get_title_and_version(self): + return self.get_title_and_version(0) + + def _enable_custom_tags(self): + yaml.SafeLoader.add_constructor('!GetAtt', GetAttTag.from_yaml) + yaml.SafeLoader.add_constructor('!Sub', SubTag.from_yaml) + yaml.SafeLoader.add_constructor('!Ref', RefTag.from_yaml) + yaml.SafeDumper.add_multi_representer(GetAttTag, GetAttTag.to_yaml) + yaml.SafeDumper.add_multi_representer(SubTag, SubTag.to_yaml) + yaml.SafeDumper.add_multi_representer(RefTag, RefTag.to_yaml) + + def _load(self): + self.raw = yaml.safe_load(self.filedata) + + def _dump(self): + self.processed = yaml.safe_dump(self.multi_raw[0]) + + def _dump_all(self): + for r in self.multi_raw: + self.multi_processed.append(yaml.safe_dump(r)) + + def get_raw(self,index): + return self.multi_raw[0] + + def get_raw_all(self,index): + return self.multi_raw[index] \ No newline at end of file diff --git a/deforest/cleaner.py b/deforest/cleaner.py index 8d17094..92bb743 100644 --- a/deforest/cleaner.py +++ b/deforest/cleaner.py @@ -21,15 +21,21 @@ def get_title_and_version(self): version = self.raw["info"]["version"] or "no-version" return self._namify(title,version) + def get_title_and_version_all(self,index): + return self.get_title_and_version() + def get_raw(self): return self.raw + def get_raw_all(self,index): + return self.raw + def convert(self): self._enable_custom_tags() self._load() self._clean_all_keys() self._dump() - return self.processed + return [self.processed] def _clean_all_keys(self): self._cleanup_keys(self.raw) @@ -55,3 +61,6 @@ def _load(self): def _dump(self): self.processed = yaml.safe_dump(self.raw) + + def number_results(self): + return 1 \ No newline at end of file diff --git a/deforest/constant.py b/deforest/constant.py index 15a791c..57c2dd0 100644 --- a/deforest/constant.py +++ b/deforest/constant.py @@ -1,2 +1,2 @@ -VERSION = "0.1.0" +VERSION = "0.1.1" LOGGER = "deforest" diff --git a/deforest/deforest.py b/deforest/deforest.py index ad34390..bdb02fe 100644 --- a/deforest/deforest.py +++ b/deforest/deforest.py @@ -3,27 +3,34 @@ from constant import VERSION, LOGGER from cleaner import DeforestCleaner +from solution import Solution @click.command() @click.argument("infile") -@click.option("--outfile","-o", help="specify output file, default is ./<title>-<version>.<format>") +@click.option("--outfile","-o", help="specify output file, default is ./<title>-<version>.<format>, ignored if input is a CloudFormation template and the template contains more than one ApiGateway resource)") @click.option("--format","-f", default="yaml", show_default=True, type=click.Choice(["yaml","json"]), help="output format") @click.option("--indent", "-i", default=4,type=int, help="if output format is json, specify indentation") -@click.version_option(None) +@click.version_option(VERSION) def main(infile,outfile,format,indent): with open(infile, "r") as fh: d = fh.read() - cleaner = DeforestCleaner(d) + cleaner = Solution(d).cleaner() result = cleaner.convert() - if outfile: + filename = None + if outfile and len(result) < 2: filename = outfile else: - filename = "{}.{}".format(cleaner.get_title_and_version(),"json" if format == "json" else "yaml") + print("output will be in {} files, ignoring --outfile flag setting".format(len(result))) - with open(filename,"w+") as fh: - if format == "json": - fh.write(json.dumps(cleaner.get_raw(), indent=indent)) - else: - fh.write(result) + for i,r in enumerate(result): + tfile = filename + if filename is None: + tfile = "{}.{}".format(cleaner.get_title_and_version_all(i),"json" if format == "json" else "yaml") + + with open(tfile,"w+") as fh: + if format == "json": + fh.write(json.dumps(cleaner.get_raw_all(i), indent=indent)) + else: + fh.write(r) diff --git a/deforest/solution.py b/deforest/solution.py new file mode 100644 index 0000000..08e045b --- /dev/null +++ b/deforest/solution.py @@ -0,0 +1,41 @@ +from tags import GetAttTag, SubTag, RefTag +import yaml +import re +from cleaner import DeforestCleaner +from cfcleaner import CFCleaner + +class Solution(): + raw = {} + filedata = None + processed = None + + def __init__(self, data): + self.filedata = data + + def _enable_custom_tags(self): + yaml.SafeLoader.add_constructor('!GetAtt', GetAttTag.from_yaml) + yaml.SafeLoader.add_constructor('!Sub', SubTag.from_yaml) + yaml.SafeLoader.add_constructor('!Ref', RefTag.from_yaml) + yaml.SafeDumper.add_multi_representer(GetAttTag, GetAttTag.to_yaml) + yaml.SafeDumper.add_multi_representer(SubTag, SubTag.to_yaml) + yaml.SafeDumper.add_multi_representer(RefTag, RefTag.to_yaml) + + def _load(self): + self.raw = yaml.safe_load(self.filedata) + + def _dump(self): + self.processed = yaml.safe_dump(self.raw) + + def cleaner(self): + self._enable_custom_tags() + self._load() + cleaner = self._identify_cleaner() + if cleaner is "default": + return DeforestCleaner(self.filedata) + if cleaner is "cloudformation": + return CFCleaner(self.filedata) + + def _identify_cleaner(self): + if "AWSTemplateFormatVersion" in self.raw: + return "cloudformation" + return "default" \ No newline at end of file