Skip to content

Commit

Permalink
Merge pull request #1 from hawry/add-support-for-cloudformation
Browse files Browse the repository at this point in the history
add support for CloudFormation templates
  • Loading branch information
hawry authored Oct 13, 2019
2 parents 9299599 + 4861a86 commit 5d97cbb
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 13 deletions.
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,40 @@
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

- 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
./<title>-<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.
97 changes: 97 additions & 0 deletions deforest/cfcleaner.py
Original file line number Diff line number Diff line change
@@ -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]
11 changes: 10 additions & 1 deletion deforest/cleaner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -55,3 +61,6 @@ def _load(self):

def _dump(self):
self.processed = yaml.safe_dump(self.raw)

def number_results(self):
return 1
2 changes: 1 addition & 1 deletion deforest/constant.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
VERSION = "0.1.0"
VERSION = "0.1.1"
LOGGER = "deforest"
27 changes: 17 additions & 10 deletions deforest/deforest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
41 changes: 41 additions & 0 deletions deforest/solution.py
Original file line number Diff line number Diff line change
@@ -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"

0 comments on commit 5d97cbb

Please sign in to comment.