From 144b4c61c407b7c52d8912e51b258fd3ab8ca4dd Mon Sep 17 00:00:00 2001 From: beelit94 Date: Tue, 3 Jan 2017 23:09:23 +0800 Subject: [PATCH 1/9] python-terraform-3 Refactor readme and how default value being passed reorder readme, refactor name --- README.md | 26 +++++++++++++++----------- python_terraform/__init__.py | 7 +++---- test/test_terraform.py | 16 +++++++++------- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 31fbd21..ec3a8bc 100644 --- a/README.md +++ b/README.md @@ -8,16 +8,7 @@ python-terraform is a python module provide a wrapper of `terraform` command lin ## Installation pip install python-terraform - -## Implementation -IMHO, how terraform design boolean options is confusing. -Take `input=True` and `-no-color` option of `apply` command for example, -they're all boolean value but with different option type. -This make api caller don't have a general rule to follow but to do -a exhaustive method implementation which I don't prefer to. -Therefore I end-up with using `IsFlagged` or `IsNotFlagged` as value of option -like `-no-color` and `True/False` value reserved for option like - + ## Usage For any terraform command @@ -35,7 +26,7 @@ For any options if it's a flag could be used multiple times, assign list to it's value if it's a "var" variable flag, assign dictionary to it if a value is None, will skip this option - + ## Examples ### Have a test.tf file under folder "/home/test" #### 1. apply with variables a=b, c=d, refresh=false, no color in the output @@ -67,6 +58,19 @@ In python-terraform: from python_terraform import Terraform tf = terraform(working_dir='/home/test') tf.fmt(diff=True) + +## Implementation +IMHO, how terraform design boolean options is confusing. +Take `input=True` and `-no-color` option of `apply` command for example, +they're all boolean value but with different option type. +This make api caller don't have a general rule to follow but to do +a exhaustive method implementation which I don't prefer to. +Therefore I end-up with using `IsFlagged` or `IsNotFlagged` as value of option +like `-no-color` and `True/False` value reserved for option like + + + + diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index e61ff00..5f5395c 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -73,11 +73,10 @@ class Terraform(object): :returns return_code, stdout, stderr """ default = dict() - args, option_dict = self._create_cmd_args(dir_or_plan, default, kwargs) - + args, option_dict = self._generate_default_args(dir_or_plan, default, kwargs) return self.cmd('apply', *args, **option_dict) - def _create_cmd_args(self, dir_or_plan, default_dict, kwargs): + def _generate_default_args(self, dir_or_plan, default_dict, kwargs): option_dict = default_dict option_dict['state'] = self.state option_dict['target'] = self.targets @@ -97,7 +96,7 @@ class Terraform(object): :return: ret_code, stdout, stderr """ default = {'force': IsFlagged} - args, option_dict = self._create_cmd_args(dir_or_plan, default, kwargs) + args, option_dict = self._generate_default_args(dir_or_plan, default, kwargs) return self.cmd('destroy', *args, **option_dict) def generate_cmd_string(self, cmd, *args, **kwargs): diff --git a/test/test_terraform.py b/test/test_terraform.py index a190b15..f55323c 100644 --- a/test/test_terraform.py +++ b/test/test_terraform.py @@ -79,16 +79,18 @@ class TestTerraform(object): assert ret == 0 @pytest.mark.parametrize( - ("folder", "variables", "var_files", "expected_output"), + ("folder", "variables", "var_files", "expected_output", "options"), [ - ("var_to_output", {'test_var': 'test'}, None, "test_output=test"), - ("var_to_output", {'test_list_var': ['c', 'd']}, None, "test_list_output=[c,d]"), - ("var_to_output", {'test_map_var': {"c": "c", "d": "d"}}, None, "test_map_output={a=ab=bc=cd=d}"), - ("var_to_output", {'test_map_var': {"c": "c", "d": "d"}}, 'var_to_output/test_map_var.json', "test_map_output={a=ab=bc=cd=de=ef=f}") + ("var_to_output", + {'test_var': 'test'}, None, "test_output=test", {}), + ("var_to_output", {'test_list_var': ['c', 'd']}, None, "test_list_output=[c,d]", {}), + ("var_to_output", {'test_map_var': {"c": "c", "d": "d"}}, None, "test_map_output={a=ab=bc=cd=d}", {}), + ("var_to_output", {'test_map_var': {"c": "c", "d": "d"}}, 'var_to_output/test_map_var.json', "test_map_output={a=ab=bc=cd=de=ef=f}", {}), + ("var_to_output", {}, None, "\x1b[0m\x1b[1m\x1b[32mApplycomplete!", {"no_color": IsNotFlagged}) ]) - def test_apply(self, folder, variables, var_files, expected_output): + def test_apply(self, folder, variables, var_files, expected_output, options): tf = Terraform(working_dir=current_path, variables=variables, var_file=var_files) - ret, out, err = tf.apply(folder) + ret, out, err = tf.apply(folder, **options) assert ret == 0 assert expected_output in out.replace('\n', '').replace(' ', '') assert err == '' From c6ab9087b8f2e55269c930501962ec09ac303344 Mon Sep 17 00:00:00 2001 From: beelit94 Date: Tue, 3 Jan 2017 23:53:31 +0800 Subject: [PATCH 2/9] python-terraform-3 Refactor readme and how default value being passed refactor options usage illustration --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a5d47d3..e039c56 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,12 @@ python-terraform is a python module provide a wrapper of `terraform` command lin return_code, stdout, stderr = t.(*arguments, **options) ####For any parameter -simply pass as argument in order of method, for example, +simply pass the string to arguments of the method, for example, - terraform apply target_dir --> .apply('target_dir') + terraform apply target_dir + --> .apply('target_dir') + terraform import aws_instance.foo i-abcd1234 + --> .import('aws_instance.foo', 'i-abcd1234') ####For any options From dbd2f13384ad8278858211093e5be6de345b0c0c Mon Sep 17 00:00:00 2001 From: beelit94 Date: Wed, 4 Jan 2017 01:02:11 +0800 Subject: [PATCH 3/9] python-terraform-3 Refactor readme and how default value being passed refactor options usage illustration --- README.md | 16 ++++++++++++++-- python_terraform/__init__.py | 25 +++++++++++-------------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index e039c56..4987754 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ python-terraform is a python module provide a wrapper of `terraform` command lin t = Terraform() return_code, stdout, stderr = t.(*arguments, **options) -####For any parameter +####For any argument simply pass the string to arguments of the method, for example, terraform apply target_dir @@ -79,6 +79,12 @@ or from python_terraform import Terraform tf = Terraform() tf.apply('/home/test', no_color=IsFlagged, refresh=False, var={'a':'b', 'c':'d'}) + +or + + from python_terraform import Terraform + tf = Terraform(working_dir='/home/test', variables={'a':'b', 'c':'d'}) + tf.apply(no_color=IsFlagged, refresh=False) #### 2. fmt command, diff=true In shell: @@ -91,6 +97,12 @@ In python-terraform: from python_terraform import Terraform tf = terraform(working_dir='/home/test') tf.fmt(diff=True) + +## default values +for apply/plan/destroy command, assign with following default value to make +caller easier in python +1. ```input=False```, in this case process won't hang because you missing a variable +1. ```no_color=IsFlagged```, in this case, stdout of result is easier for parsing ## Implementation IMHO, how terraform design boolean options is confusing. @@ -99,7 +111,7 @@ they're all boolean value but with different option type. This make api caller don't have a general rule to follow but to do a exhaustive method implementation which I don't prefer to. Therefore I end-up with using `IsFlagged` or `IsNotFlagged` as value of option -like `-no-color` and `True/False` value reserved for option like +like `-no-color` and `True/False` value reserved for option like `refresh=true` diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index 5f5395c..86bcb7d 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -16,10 +16,6 @@ class IsFlagged: pass -class IsNotFlagged: - pass - - class Terraform(object): """ Wrapper of terraform command line tool @@ -34,13 +30,17 @@ class Terraform(object): var_file=None, terraform_bin_path=None): """ - :param working_dir: the folder of the working folder, if not given, will be where python + :param working_dir: the folder of the working folder, if not given, + will be current working folder :param targets: list of target - :param state: path of state file relative to working folder - :param variables: variables for apply/destroy/plan command - :param parallelism: parallelism for apply/destroy command - :param var_file: passed as value of -var-file option, could be string or list - list stands for multiple -var-file option + as default value of apply/destroy/plan command + :param state: path of state file relative to working folder, + as a default value of apply/destroy/plan command + :param variables: default variables for apply/destroy/plan command, + will be override by variable passing by apply/destroy/plan method + :param parallelism: default parallelism value for apply/destroy command + :param var_file: passed as value of -var-file option, + could be string or list, list stands for multiple -var-file option :param terraform_bin_path: binary path of terraform """ self.working_dir = working_dir @@ -148,15 +148,12 @@ class Terraform(object): cmds += ['-{k}'.format(k=k)] continue - if v is IsNotFlagged: + if v is None: continue if type(v) is bool: v = 'true' if v else 'false' - if not v: - continue - cmds += ['-{k}={v}'.format(k=k, v=v)] cmds += args From 29d391664ae719c42b8d9c54e79e5da1f249728a Mon Sep 17 00:00:00 2001 From: beelit94 Date: Wed, 4 Jan 2017 01:34:37 +0800 Subject: [PATCH 4/9] python-terraform-3 Refactor readme and how default value being passed refactor default values --- python_terraform/__init__.py | 37 ++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index 86bcb7d..8de4c0b 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -16,6 +16,10 @@ class IsFlagged: pass +class IsNotFlagged: + pass + + class Terraform(object): """ Wrapper of terraform command line tool @@ -64,20 +68,28 @@ class Terraform(object): return wrapper - def apply(self, dir_or_plan=None, **kwargs): + def apply(self, dir_or_plan=None, input=False, no_color=IsFlagged, **kwargs): """ refer to https://terraform.io/docs/commands/apply.html no-color is flagged by default + :param no_color: disable color of stdout + :param input: disable prompt for a missing variable :param dir_or_plan: folder relative to working folder :param kwargs: same as kwags in method 'cmd' :returns return_code, stdout, stderr """ - default = dict() - args, option_dict = self._generate_default_args(dir_or_plan, default, kwargs) + default = kwargs + default['input'] = input + default['no_color'] = no_color + option_dict = self._generate_default_options(default) + args = self._generate_default_args(dir_or_plan) return self.cmd('apply', *args, **option_dict) - def _generate_default_args(self, dir_or_plan, default_dict, kwargs): - option_dict = default_dict + def _generate_default_args(self, dir_or_plan): + return [dir_or_plan] if dir_or_plan else [] + + def _generate_default_options(self, input_options): + option_dict = dict() option_dict['state'] = self.state option_dict['target'] = self.targets option_dict['var'] = self.variables @@ -85,18 +97,19 @@ class Terraform(object): option_dict['parallelism'] = self.parallelism option_dict['no_color'] = IsFlagged option_dict['input'] = False - option_dict.update(kwargs) - args = [dir_or_plan] if dir_or_plan else [] - return args, option_dict + option_dict.update(input_options) + return option_dict - def destroy(self, dir_or_plan=None, **kwargs): + def destroy(self, dir_or_plan=None, force=IsFlagged, **kwargs): """ refer to https://www.terraform.io/docs/commands/destroy.html force/no-color option is flagged by default :return: ret_code, stdout, stderr """ - default = {'force': IsFlagged} - args, option_dict = self._generate_default_args(dir_or_plan, default, kwargs) + default = kwargs + default['force'] = force + option_dict = self._generate_default_options(default) + args = self._generate_default_args(dir_or_plan) return self.cmd('destroy', *args, **option_dict) def generate_cmd_string(self, cmd, *args, **kwargs): @@ -148,7 +161,7 @@ class Terraform(object): cmds += ['-{k}'.format(k=k)] continue - if v is None: + if v is None or v is IsNotFlagged: continue if type(v) is bool: From 6fc11b313f0a374a71d5e60d463cd4e3a4118b88 Mon Sep 17 00:00:00 2001 From: beelit94 Date: Wed, 4 Jan 2017 13:21:30 +0800 Subject: [PATCH 5/9] python-terraform-3 Refactor readme and how default value being passed 1. refactor default values 2. add plan method --- python_terraform/__init__.py | 18 ++++++++++++++++-- test/test_terraform.py | 18 ++++++++++++++++-- test/vars_require_input/main.tf | 20 ++++++++++++++++++++ 3 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 test/vars_require_input/main.tf diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index 8de4c0b..6ed9362 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -108,9 +108,23 @@ class Terraform(object): """ default = kwargs default['force'] = force - option_dict = self._generate_default_options(default) + options = self._generate_default_options(default) args = self._generate_default_args(dir_or_plan) - return self.cmd('destroy', *args, **option_dict) + return self.cmd('destroy', *args, **options) + + def plan(self, dir_or_plan=None, detailed_exitcode=IsFlagged, **kwargs): + """ + refert to https://www.terraform.io/docs/commands/plan.html + :param detailed_exitcode: Return a detailed exit code when the command exits. + :param dir_or_plan: relative path to plan/folder + :param kwargs: options + :return: ret_code, stdout, stderr + """ + options = kwargs + options['detailed_exitcode'] = detailed_exitcode + options = self._generate_default_options(options) + args = self._generate_default_args(dir_or_plan) + return self.cmd('plan', *args, **options) def generate_cmd_string(self, cmd, *args, **kwargs): """ diff --git a/test/test_terraform.py b/test/test_terraform.py index f55323c..eacb781 100644 --- a/test/test_terraform.py +++ b/test/test_terraform.py @@ -32,7 +32,8 @@ CMD_CASES = [ ] ] -@pytest.fixture() + +@pytest.fixture(scope='function') def fmt_test_file(request): target = os.path.join(current_path, 'bad_fmt', 'test.backup') orgin = os.path.join(current_path, 'bad_fmt', 'test.tf') @@ -131,6 +132,8 @@ class TestTerraform(object): no_color=IsNotFlagged) out = out.replace('\n', '') assert '\x1b[0m\x1b[1m\x1b[32mApply' in out + out = tf.output('test_output') + assert 'test2' in out def test_get_output(self): tf = Terraform(working_dir=current_path, variables={'test_var': 'test'}) @@ -143,7 +146,18 @@ class TestTerraform(object): assert ret == 0 assert 'Destroy complete! Resources: 0 destroyed.' in out - def test_fmt(self): + @pytest.mark.parametrize( + ("plan", "variables", "expected_ret"), + [ + ('vars_require_input', {}, 1) + ] + ) + def test_plan(self, plan, variables, expected_ret): + tf = Terraform(working_dir=current_path, variables=variables) + ret, out, err = tf.plan(plan) + assert ret == expected_ret + + def test_fmt(self, fmt_test_file): tf = Terraform(working_dir=current_path, variables={'test_var': 'test'}) ret, out, err = tf.fmt(diff=True) assert ret == 0 diff --git a/test/vars_require_input/main.tf b/test/vars_require_input/main.tf new file mode 100644 index 0000000..43e8e65 --- /dev/null +++ b/test/vars_require_input/main.tf @@ -0,0 +1,20 @@ +variable "ami" { + default = "foo" + type = "string" +} + +variable "list" { + default = [] + type = "list" +} + +variable "map" { + default = {} + type = "map" +} + +resource "aws_instance" "bar" { + foo = "${var.ami}" + bar = "${join(",", var.list)}" + baz = "${join(",", keys(var.map))}" +} \ No newline at end of file From 706fb2692bccb16a998a1ce0bcb589f3a10ff229 Mon Sep 17 00:00:00 2001 From: beelit94 Date: Wed, 4 Jan 2017 14:13:29 +0800 Subject: [PATCH 6/9] python-terraform-3 Refactor readme and how default value being passed remove old release script --- release.py | 104 ----------------------------------------------------- 1 file changed, 104 deletions(-) delete mode 100644 release.py diff --git a/release.py b/release.py deleted file mode 100644 index cc703cf..0000000 --- a/release.py +++ /dev/null @@ -1,104 +0,0 @@ -import subprocess -import click -import os -from distutils.version import StrictVersion -import shutil -import re - - -def get_version(): - p = get_version_file_path() - with open(p) as f: - version = f.read() - version = version.strip() - if not version: - raise ValueError("could not read version") - return version - - -def write_version(version_tuple): - p = get_version_file_path() - with open(p, 'w+') as f: - f.write('.'.join([str(i) for i in version_tuple])) - - -def get_version_file_path(): - p = os.path.join(os.path.dirname( - os.path.abspath(__file__)), 'VERSION') - return p - - -def release_patch(version_tuple): - patch_version = version_tuple[2] + 1 - new_version = version_tuple[:2] + (patch_version,) - click.echo('new version: %s' % str(new_version)) - write_version(new_version) - return new_version - - -def release_minor(version_tuple): - minor = version_tuple[1] + 1 - new_version = version_tuple[:1] + (minor, 0) - click.echo('new version: %s' % str(new_version)) - write_version(new_version) - return new_version - - -def release_major(version_tuple): - major = version_tuple[0] + 1 - new = (major, 0, 0) - click.echo('new version: %s' % str(new)) - write_version(new) - return new - - -@click.command() -@click.option('--release', '-r', type=click.Choice(['major', 'minor', 'patch']), default='patch') -@click.option('--url', prompt=True, default=lambda: os.environ.get('FURY_URL', '')) -def main(release, url): - version_tuple = StrictVersion(get_version()).version - click.echo('old version: %s' % str(version_tuple)) - - if release == 'major': - new_v = release_major(version_tuple) - elif release == 'minor': - new_v = release_minor(version_tuple) - else: - new_v = release_patch(version_tuple) - - new_v_s = '.'.join([str(i) for i in new_v]) - - os.chdir(os.path.dirname(os.path.abspath(__file__))) - subprocess.call("python setup.py sdist", shell=True) - - pkg_file = '' - for f in os.listdir('dist'): - r = re.compile(r'.*(%s\.tar\.gz)' % re.escape(new_v_s)) - result = r.match(f) - if result: - click.echo(f + ' is ready') - pkg_file = f - - break - - if not pkg_file: - raise ValueError - - this_folder = os.path.dirname(os.path.abspath(__file__)) - dist_folder = os.path.join(this_folder, 'dist') - pkg_file_name = pkg_file - pkg_file = os.path.join(dist_folder, pkg_file) - - shutil.move(pkg_file, this_folder) - shutil.rmtree(dist_folder) - os.remove('MANIFEST') - is_release = click.confirm('ready to release?') - - if is_release: - subprocess.call('curl -F package=@%s %s' % (pkg_file_name, url), - shell=True) - os.remove(pkg_file_name) - - -if __name__ == '__main__': - main() From 1f99b2e0bf59b732b3f8222f095a9d41ed1028df Mon Sep 17 00:00:00 2001 From: beelit94 Date: Wed, 4 Jan 2017 14:16:54 +0800 Subject: [PATCH 7/9] python-terraform-3 Refactor readme and how default value being passed clean up files --- MANIFEST.in | 1 - VERSION | 1 - setup.py | 13 +------------ 3 files changed, 1 insertion(+), 14 deletions(-) delete mode 100644 MANIFEST.in delete mode 100644 VERSION diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index b1fc69e..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include VERSION \ No newline at end of file diff --git a/VERSION b/VERSION deleted file mode 100644 index c18d72b..0000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.8.1 \ No newline at end of file diff --git a/setup.py b/setup.py index 7648478..68a5a92 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,6 @@ This is a python module provide a wrapper of terraform command line tool """ from setuptools import setup -import os dependencies = [] module_name = 'python-terraform' @@ -16,19 +15,9 @@ except IOError: long_description = short_description -def get_version(): - p = os.path.join(os.path.dirname( - os.path.abspath(__file__)), "VERSION") - with open(p) as f: - version = f.read() - version = version.strip() - if not version: - raise ValueError("could not read version") - return version - setup( name=module_name, - version=get_version(), + version='0.8.1', url='https://github.com/beelit94/python-terraform', license='MIT', author='Freddy Tan', From ae270927aeeef3f7822cb84ad844baafe59b92aa Mon Sep 17 00:00:00 2001 From: beelit94 Date: Wed, 4 Jan 2017 14:20:06 +0800 Subject: [PATCH 8/9] python-terraform-3 Refactor readme and how default value being passed only build on develop and master or release --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.travis.yml b/.travis.yml index ca64073..c66bc90 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,6 +30,11 @@ script: - "export PATH=$PATH:$PWD/tf_bin" - tox +branches: + only: + - master + - develop + deploy: # test pypi - provider: pypi From dbbd87bf5d4ccd49481bd931f168cd7c9f44eb7f Mon Sep 17 00:00:00 2001 From: beelit94 Date: Wed, 4 Jan 2017 14:20:24 +0800 Subject: [PATCH 9/9] python-terraform-3 Refactor readme and how default value being passed only build on develop and master or release --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index c66bc90..88dc659 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,6 +34,7 @@ branches: only: - master - develop + - release/** deploy: # test pypi