├── .gitignore ├── Pipfile ├── Pipfile.lock ├── README.md └── terrapyn ├── __init__.py ├── core.py ├── environment.py ├── exceptions.py ├── terraform.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | t.py 3 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | "delegator.py" = "*" 8 | semver = "*" 9 | 10 | [dev-packages] 11 | black = "*" 12 | "flake8" = "*" 13 | 14 | [requires] 15 | python_version = "3.7" 16 | 17 | [pipenv] 18 | allow_prereleases = true 19 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "ddb51aaa9e29c91e0a3a5328aa86a591e428bd035c358c32b78bda0ab5117293" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "delegator.py": { 20 | "hashes": [ 21 | "sha256:2d46966a7f484d271b09e2646eae1e9acadc4fdf2cb760c142f073e81c927d8d", 22 | "sha256:58f3ea6fe36680e1d828e2e66e52844b826f186409dfee4436e42351b0e699fe" 23 | ], 24 | "index": "pypi", 25 | "version": "==0.1.0" 26 | }, 27 | "pexpect": { 28 | "hashes": [ 29 | "sha256:2a8e88259839571d1251d278476f3eec5db26deb73a70be5ed5dc5435e418aba", 30 | "sha256:3fbd41d4caf27fa4a377bfd16fef87271099463e6fa73e92a52f92dfee5d425b" 31 | ], 32 | "version": "==4.6.0" 33 | }, 34 | "ptyprocess": { 35 | "hashes": [ 36 | "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0", 37 | "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f" 38 | ], 39 | "version": "==0.6.0" 40 | }, 41 | "semver": { 42 | "hashes": [ 43 | "sha256:41c9aa26c67dc16c54be13074c352ab666bce1fa219c7110e8f03374cd4206b0", 44 | "sha256:5b09010a66d9a3837211bb7ae5a20d10ba88f8cb49e92cb139a69ef90d5060d8" 45 | ], 46 | "index": "pypi", 47 | "version": "==2.8.1" 48 | } 49 | }, 50 | "develop": { 51 | "appdirs": { 52 | "hashes": [ 53 | "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", 54 | "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" 55 | ], 56 | "version": "==1.4.3" 57 | }, 58 | "attrs": { 59 | "hashes": [ 60 | "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", 61 | "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb" 62 | ], 63 | "version": "==18.2.0" 64 | }, 65 | "black": { 66 | "hashes": [ 67 | "sha256:22158b89c1a6b4eb333a1e65e791a3f8b998cf3b11ae094adb2570f31f769a44", 68 | "sha256:4b475bbd528acce094c503a3d2dbc2d05a4075f6d0ef7d9e7514518e14cc5191" 69 | ], 70 | "index": "pypi", 71 | "version": "==18.6b4" 72 | }, 73 | "click": { 74 | "hashes": [ 75 | "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", 76 | "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" 77 | ], 78 | "version": "==6.7" 79 | }, 80 | "flake8": { 81 | "hashes": [ 82 | "sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0", 83 | "sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37" 84 | ], 85 | "index": "pypi", 86 | "version": "==3.5.0" 87 | }, 88 | "mccabe": { 89 | "hashes": [ 90 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 91 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 92 | ], 93 | "version": "==0.6.1" 94 | }, 95 | "pycodestyle": { 96 | "hashes": [ 97 | "sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766", 98 | "sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9" 99 | ], 100 | "version": "==2.3.1" 101 | }, 102 | "pyflakes": { 103 | "hashes": [ 104 | "sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f", 105 | "sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805" 106 | ], 107 | "version": "==1.6.0" 108 | }, 109 | "toml": { 110 | "hashes": [ 111 | "sha256:380178cde50a6a79f9d2cf6f42a62a5174febe5eea4126fe4038785f1d888d42", 112 | "sha256:a7901919d3e4f92ffba7ff40a9d697e35bbbc8a8049fe8da742f34c83606d957" 113 | ], 114 | "version": "==0.9.6" 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terrapyn: Orchestrate Terraform with Python 2 | 3 | Work in progress. Passion project. 4 | 5 | 6 | ## Usage 7 | 8 | >>> from terrapyn import Terraform 9 | >>> tf = Terraform() 10 | 11 | >>> tf 12 | 13 | 14 | >>> tf.working_dir 15 | WindowsPath('C:/Users/me/AppData/Local/Temp/terrapyn-p2p9iy63-terraform-plan') 16 | 17 | The rest is a work in progress :) 18 | -------------------------------------------------------------------------------- /terrapyn/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import * 2 | -------------------------------------------------------------------------------- /terrapyn/core.py: -------------------------------------------------------------------------------- 1 | from .terraform import Terraform 2 | -------------------------------------------------------------------------------- /terrapyn/environment.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | TERRAFORM_EXE = "terraform" 4 | TERRAPYN_TIMEOUT = 60 * 5 5 | 6 | 7 | def evaluate(*, environ=os.environ, executable=TERRAFORM_EXE): 8 | """Evaluate the (potentially) isolated runtime environment variables.""" 9 | # If False was passed to environ... 10 | if not environ: 11 | environ = {} 12 | 13 | d = environ.copy() 14 | d.update( 15 | { 16 | "TERRAFORM_PATH": environ.get("TERRAFORM_PATH", executable), 17 | "TERRAPYN_TIMEOUT": environ.get("TERRAPYN_TIMEOUT", TERRAPYN_TIMEOUT), 18 | } 19 | ) 20 | return d 21 | -------------------------------------------------------------------------------- /terrapyn/exceptions.py: -------------------------------------------------------------------------------- 1 | class TerrapynException(BaseException): 2 | pass 3 | 4 | class DependencyNotFound(RuntimeError, TerrapynException): 5 | pass 6 | -------------------------------------------------------------------------------- /terrapyn/terraform.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | import pathlib 4 | 5 | import delegator 6 | import semver 7 | 8 | from . import environment 9 | from .exceptions import DependencyNotFound 10 | 11 | 12 | class BaseCommand: 13 | @classmethod 14 | def _which(command): 15 | """Emulates the system's which -a.""" 16 | 17 | _which = "where" if os.name == "nt" else "which -a" 18 | 19 | c = delegator.run(f"{_which} {command}") 20 | 21 | try: 22 | # Which Not found… 23 | if c.return_code == 127: 24 | raise DependencyNotFound( 25 | "Please install terraform (or provide its path)." 26 | ) 27 | else: 28 | assert c.return_code == 0 29 | except AssertionError: 30 | return [] 31 | 32 | return (c.out.strip() or c.err.strip()).split("\n") 33 | 34 | 35 | class Terraform(BaseCommand): 36 | def __init__(self, *, working_dir=None, environ=None): 37 | self.environ = environment.evaluate(environ=environ) 38 | self._version = None 39 | self._temp_dir = None 40 | if working_dir: 41 | self._working_dir = pathlib.Path(working_dir) 42 | else: 43 | self._working_dir = None 44 | 45 | self.setup() 46 | self._sanity_check() 47 | 48 | def __repr__(self): 49 | return f"" 50 | 51 | def __enter__(self): 52 | if not self.temp_dir: 53 | self.setup() 54 | 55 | def __exit__(self, type, value, tb): 56 | self.cleanup() 57 | 58 | def setup(self): 59 | if not self.temp_dir and self.temp_dir is not False: 60 | self.temp_dir 61 | 62 | def cleanup(self): 63 | if self.temp_dir: 64 | os.rmdir(self.temp_dir) 65 | self._temp_dir = False 66 | 67 | @property 68 | def version(self): 69 | return f"{self._version['major']}.{self._version['minor']}.{self._version['patch']}" 70 | 71 | @property 72 | def temp_dir(self): 73 | if not self._temp_dir: 74 | self._temp_dir = pathlib.Path( 75 | tempfile.mkdtemp(suffix="-terraform-plan", prefix="terrapyn-") 76 | ) 77 | 78 | return self._temp_dir 79 | 80 | @property 81 | def working_dir(self): 82 | # Make the working directory, if it doesn't already exist. 83 | if self._working_dir: 84 | os.makedirs(self._working_dir, exist_okay=True) 85 | return self._working_dir 86 | else: 87 | return self.temp_dir 88 | 89 | def tf(self, *args, output=True): 90 | # Change to working directory. 91 | os.chdir(self.temp_dir) 92 | 93 | cmd = [self.environ["TERRAFORM_PATH"]] + list(args) 94 | c = delegator.run( 95 | cmd, timeout=self.environ["TERRAPYN_TIMEOUT"], cwd=self.working_dir 96 | ) 97 | if not output: 98 | return c 99 | else: 100 | assert c.return_code == 0 101 | return c.out.strip() 102 | 103 | def _sanity_check(self): 104 | self._version = semver.parse(self.tf("--version").split()[1][1:]) 105 | -------------------------------------------------------------------------------- /terrapyn/utils.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kennethreitz-archive/terrapyn/94dd3199b886049b05b6b7e9d5c3f98a6a8e7a4d/terrapyn/utils.py --------------------------------------------------------------------------------