diff --git a/.travis.yml b/.travis.yml index ca64073..88dc659 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,6 +30,12 @@ script: - "export PATH=$PATH:$PWD/tf_bin" - tox +branches: + only: + - master + - develop + - release/** + deploy: # test pypi - provider: pypi 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/README.md b/README.md index 3f639af..4987754 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 @@ -25,10 +16,13 @@ like `-no-color` and `True/False` value reserved for option like t = Terraform() return_code, stdout, stderr = t.(*arguments, **options) -####For any parameter -simply pass as argument in order of method, for example, +####For any argument +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 @@ -85,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: @@ -98,6 +98,25 @@ In python-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. +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 `refresh=true` + + + + + \ 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/python_terraform/__init__.py b/python_terraform/__init__.py index e61ff00..6ed9362 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -34,13 +34,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 @@ -64,21 +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._create_cmd_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 _create_cmd_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 @@ -86,19 +97,34 @@ 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._create_cmd_args(dir_or_plan, default, kwargs) - return self.cmd('destroy', *args, **option_dict) + default = kwargs + default['force'] = force + options = self._generate_default_options(default) + args = self._generate_default_args(dir_or_plan) + 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): """ @@ -149,15 +175,12 @@ class Terraform(object): cmds += ['-{k}'.format(k=k)] continue - if v is IsNotFlagged: + if v is None or v is IsNotFlagged: 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 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() 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', diff --git a/test/test_terraform.py b/test/test_terraform.py index a190b15..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') @@ -79,16 +80,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 == '' @@ -129,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'}) @@ -141,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