Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
.idea/
.DS_Store

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
.python-version

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/
28 changes: 28 additions & 0 deletions .pylintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[MASTER]

ignore=CVS
jobs=1
persistent=yes

[MESSAGES CONTROL]

disable=
missing-docstring, # not everything needs a docstring
fixme, # work in progress
bad-continuation, # clashes with black
duplicate-code, # finds dupes between tests and plugins
too-few-public-methods, # triggers when inheriting
ungrouped-imports, # clashes with isort

[BASIC]

good-names=e,ex,f,fp,i,j,k,n,_

[FORMAT]

indent-string=' '
max-line-length=120

[DESIGN]
max-attributes=12
max-args=7
7 changes: 7 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
include README.md
include LICENSE

graft python/rpdk/python

# last rule wins, put excludes last
global-exclude __pycache__ *.py[co] .DS_Store
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
## AWS Cloudformation Rpdk Python Plugin
## AWS CloudFormation Resource Provider Python Plugin

The CloudFormation Provider Development Toolkit Python Plugin allows you to autogenerate Python code based on an input schema.

## License
The CloudFormation Resource Provider Development Kit (RPDK) allows you to author your own resource providers that can be used by CloudFormation.

This library is licensed under the Apache 2.0 License.
This plugin library helps to provide runtime bindings for the execution of your providers by CloudFormation.

License
-------

This library is licensed under the Apache 2.0 License.
18 changes: 16 additions & 2 deletions buildspec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,25 @@ phases:
# patch boto3/awscli with internal SDK (must be done after RPDK is installed, since awscli is a dep currently)
- aws configure add-model --service-model "file://cloudformation-2010-05-15.normal.json" --service-name cloudformation
# install aws-cloudformation-rpdk-python-plugin (Python)
- cd "$CODEBUILD_SRC_DIR"
- ls -la
- pip install . # -r requirements.txt (currently no testing/linting dependencies)
# work around https://github.com/boto/botocore/issues/1733
- pip install 'urllib3<1.25'
# run unit tests/linting here
# end-to-end test
- pip install pytest pylint
- pytest
- pylint python/rpdk/python/ src/
# end-to-end test - need privileged codebuild job and docker installed in container
# - pip install aws-sam-cli
# - ./tests/functional-tests.sh ${CODEBUILD_SRC_DIR}/src/
- DIR=$(mktemp -d)
- cd "$DIR"
- ls -la
#- echo "AWS::Foo::Bar" | uluru-cli init -vv # enable me when plugin lands
- |
echo "AWS::Foo::Bar
1" | uluru-cli init -vv # python3.6
- |
echo "AWS::Foo::Bar
2" | uluru-cli init -vv # python3.7
- ls -la
5 changes: 5 additions & 0 deletions python/rpdk/python/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import logging

__version__ = "0.1"

logging.getLogger(__name__).addHandler(logging.NullHandler())
173 changes: 173 additions & 0 deletions python/rpdk/python/codegen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import logging
import os
import shutil

import docker

from rpdk.core.plugin_base import LanguagePlugin


LOG = logging.getLogger(__name__)

EXECUTABLE = "uluru-cli"
OLD_VIRTUAL_ENV = ''
OLD_PATH = []


class Python36LanguagePlugin(LanguagePlugin):
MODULE_NAME = __name__
NAME = "python37"
RUNTIME = "python3.7"
ENTRY_POINT = "cfn_resource.handler_wrapper._handler_wrapper"
CODE_URI = "./target/{}.zip"

def __init__(self):
self.env = self._setup_jinja_env(
trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True
)
self.package_name = None
self.schema_filename = None
self.namespace = None
self.cfn_resource_version = 'cfn_resource==0.0.1'

def _package_from_project(self, project):
self.namespace = tuple(s.lower() for s in project.type_info)
self.package_name = "_".join(self.namespace)

def _schema_from_project(self, project):
self.namespace = tuple(s.lower() for s in project.type_info)
self.schema_filename = "{}.json".format("-".join(self.namespace))

def init(self, project):
LOG.debug("Init started")

self._package_from_project(project)

project.runtime = self.RUNTIME
project.entrypoint = self.ENTRY_POINT

folders = [project.root / self.package_name, project.root / "tests"]

# venv_dir = project.root / ".venv"
# venv.create(venv_dir, system_site_packages=False, with_pip=True)

for f in folders:
LOG.debug("Making folder: %s", f)
f.mkdir(parents=True, exist_ok=True)

templates = [
[
project.root / "Handler.yaml",
self.env.get_template("Handler.yaml"),
{
'resource_type': project.type_name,
'handler_params': {
"Handler": project.entrypoint,
"Runtime": project.runtime,
"CodeUri": self.CODE_URI.format(self.package_name),
}
}
],
[
project.root / self.package_name / "handlers.py",
self.env.get_template("handlers.py"),
{}
],
[
project.root / self.package_name / "__init__.py",
self.env.get_template("__init__.py.jinja2"),
{}
],
[
project.root / "README.md",
self.env.get_template("README.md"),
{
'type_name': project.type_name,
'schema_path': project.schema_path,
'project_path': self.package_name,
'executable': EXECUTABLE
}
],
[
project.root / "requirements.txt",
self.env.get_template("requirements.txt.jinja2"),
# until cfn_resource has it's own pypi package, this will need to be updated to point to the absolute
# path for the src folder in your working copy
{'cfn_resource_version': self.cfn_resource_version}
]
]

for path, template, kwargs in templates:
LOG.debug("Writing file: %s", path)
contents = template.render(**kwargs)
project.safewrite(path, contents)

LOG.debug("Init complete")

def generate(self, project):
LOG.debug("Generate started")

self._package_from_project(project)
self._schema_from_project(project)

shutil.rmtree(project.root / "resource_model", ignore_errors=True)
os.mkdir(project.root / "resource_model")

resource_model_path = project.root / "resource_model" / "__init__.py"

templates = [
[
resource_model_path,
self.env.get_template("resource_model.py.jinja2"),
{'properties': project.schema["properties"]}
]
]

for path, template, kwargs in templates:
LOG.debug("Writing file: %s", path)
contents = template.render(**kwargs)
project.safewrite(path, contents)

LOG.debug("Generate complete")

def package(self, project, zip_file):
LOG.debug("Package started")

self._package_from_project(project)

def write_with_relative_path(path, base=project.root):
relative = path.relative_to(base)
zip_file.write(path.resolve(), str(relative))

resource_model_path = project.root / "resource_model"
handlers_path = project.root / self.package_name
deps_path = project.root / 'build'

self._docker_build(project)
write_with_relative_path(resource_model_path)
write_with_relative_path(handlers_path)
write_with_relative_path(deps_path, deps_path)
LOG.debug("Package complete")

@classmethod
def _docker_build(cls, project):
LOG.debug("Dependencies build started")
docker_client = docker.from_env()
volumes = {str(project.root): {'bind': '/project', 'mode': 'rw'}}
with open(project.root / 'requirements.txt', 'r') as f:
for line in f.readlines():
if line.startswith("/"):
line = line.rstrip('\n')
volumes[line] = {'bind': line, 'mode': 'ro'}
logs = docker_client.containers.run(
image='lambci/lambda:build-{}'.format(cls.RUNTIME),
command='pip install --upgrade -r /project/requirements.txt -t /project/build/',
auto_remove=True,
volumes=volumes
)
LOG.debug("pip install logs: \n%s", logs.decode('utf-8'))


class Python37LanguagePlugin(Python36LanguagePlugin):
NAME = "python37"
RUNTIME = "python3.7"
Loading