From 0b2eb3b1be8153ce4ea115ab7596df736c824b95 Mon Sep 17 00:00:00 2001 From: Raquel Alegre Date: Fri, 11 Aug 2017 19:00:06 +0100 Subject: [PATCH 1/9] Return a reference to the subprocess so output can be handled from elsewhere. --- python_terraform/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index 35c6e3b..144a735 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -235,6 +235,11 @@ class Terraform(object): p = subprocess.Popen(cmd_string, stdout=stdout, stderr=stderr, shell=True, cwd=working_folder, env=environ_vars) + + synchronous = kwargs.pop('synchronous', True) + if not synchronous: + return p, None, None + out, err = p.communicate() ret_code = p.returncode log.debug('output: {o}'.format(o=out)) From da5e648e3f1a5c023ace20c1af5ceb6e904cd88e Mon Sep 17 00:00:00 2001 From: Raquel Alegre Date: Fri, 11 Aug 2017 19:00:22 +0100 Subject: [PATCH 2/9] Move version up. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 988cc76..a09f83b 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ except IOError: setup( name=module_name, - version='0.8.6', + version='0.8.7', url='https://github.com/beelit94/python-terraform', license='MIT', author='Freddy Tan', From ec826887f56c55db00eb51ba06a24334beb309c4 Mon Sep 17 00:00:00 2001 From: Sam McKelvie Date: Fri, 13 Oct 2017 13:36:25 -0700 Subject: [PATCH 3/9] Add full support for 'output' command, and enable raise_on_error option Add a general "raise_on_error" option to all terraform commands. If provided and set to anything that evaluates to True, then TerraformCommandError (a subclass of subprocess.CalledProcessError) will be raised if the returncode is not 0. The exception object will have the following special proerties: returncode: The returncode from the command, as in subprocess.CalledProcessError. out: The contents of stdout if available, otherwise None err: The contents of stderr if available, otherwise None Terraform.output() no longer requires an argument for the output name; if omitted, it returns a dict of all outputs, exactly as expected from 'terraform output -json'. Terraform.output() now accepts an optional "full_value" option. If provided and True, and an output name was provided, then the return value will be a dict with "value", "type", and "sensitive" fields, exactly as expected from 'terraform output -json ' Added tests for all of this new functionality... --- .gitignore | 3 +- python_terraform/__init__.py | 72 ++++++++++++++++++++++++++++++------ test/test_terraform.py | 68 ++++++++++++++++++++++++++++++++-- 3 files changed, 126 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index 7b47fe6..bf4ea55 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ /.pypirc /.tox/ .dropbox -Icon \ No newline at end of file +Icon +/pytestdebug.log diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index 3d33a28..4fa394e 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -10,6 +10,7 @@ import tempfile from python_terraform.tfstate import Tfstate + try: # Python 2.7+ from logging import NullHandler except ImportError: @@ -29,6 +30,12 @@ class IsNotFlagged: pass +class TerraformCommandError(subprocess.CalledProcessError): + def __init__(self, ret_code, cmd, out, err): + super(TerraformCommandError, self).__init__(ret_code, cmd) + self.out = out + self.err = err + class Terraform(object): """ Wrapper of terraform command line tool @@ -252,10 +259,17 @@ class Terraform(object): if the option 'capture_output' is passed (with any value other than True), terraform output will be printed to stdout/stderr and "None" will be returned as out and err. + if the option 'raise_on_error' is passed (with any value that evaluates to True), + and the terraform command returns a nonzerop return code, then + a TerraformCommandError exception will be raised. The exception object will + have the following properties: + returncode: The command's return code + out: The captured stdout, or None if not captured + err: The captured stderr, or None if not captured :return: ret_code, out, err """ - capture_output = kwargs.pop('capture_output', True) + raise_on_error = kwargs.pop('raise_on_error', False) if capture_output is True: stderr = subprocess.PIPE stdout = subprocess.PIPE @@ -285,27 +299,61 @@ class Terraform(object): self.temp_var_files.clean_up() if capture_output is True: - return ret_code, out.decode('utf-8'), err.decode('utf-8') + out = out.decode('utf-8') + err = err.decode('utf-8') else: - return ret_code, None, None + out = None + err = None - def output(self, name, *args, **kwargs): + if ret_code != 0 and raise_on_error: + raise TerraformCommandError( + ret_code, ' '.join(cmds), out=out, err=err) + + return ret_code, out, err + + + def output(self, *args, **kwargs): """ https://www.terraform.io/docs/commands/output.html - :param name: name of output - :return: output value + + Note that this method does not conform to the (ret_code, out, err) return convention. To use + the "output" command with the standard convention, call "output_cmd" instead of + "output". + + :param args: Positional arguments. There is one optional positional + argument NAME; if supplied, the returned output text + will be the json for a single named output value. + :param kwargs: Named options, passed to the command. In addition, + 'full_value': If True, and NAME is provided, then + the return value will be a dict with + "value', 'type', and 'sensitive' + properties. + :return: None, if an error occured + Output value as a string, if NAME is provided and full_value + is False or not provided + Output value as a dict with 'value', 'sensitive', and 'type' if + NAME is provided and full_value is True. + dict of named dicts each with 'value', 'sensitive', and 'type', + if NAME is not provided """ + full_value = kwargs.pop('full_value', False) + name_provided = (len(args) > 0) + kwargs['json'] = IsFlagged + kwargs['capture_output'] = True - ret, out, err = self.cmd( - 'output', name, json=IsFlagged, *args, **kwargs) + ret, out, err = self.output_cmd(*args, **kwargs) - log.debug('output raw string: {0}'.format(out)) if ret != 0: - return None + return None + out = out.lstrip() - output_dict = json.loads(out) - return output_dict['value'] + value = json.loads(out) + + if name_provided and not full_value: + value = value['value'] + + return value def read_state_file(self, file_path=None): """ diff --git a/test/test_terraform.py b/test/test_terraform.py index e2f725f..c0111d2 100644 --- a/test/test_terraform.py +++ b/test/test_terraform.py @@ -12,6 +12,7 @@ import fnmatch logging.basicConfig(level=logging.DEBUG) root_logger = logging.getLogger() + current_path = os.path.dirname(os.path.realpath(__file__)) FILE_PATH_WITH_SPACE_AND_SPACIAL_CHARS = "test 'test.out!" @@ -30,12 +31,14 @@ STRING_CASES = [ ] CMD_CASES = [ - ['method', 'expected_output', 'expected_ret_code', 'expected_logs', 'folder'], + ['method', 'expected_output', 'expected_ret_code', 'expected_exception', 'expected_logs', 'folder'], [ [ lambda x: x.cmd('plan', 'var_to_output', no_color=IsFlagged, var={'test_var': 'test'}) , - "doesn't need to do anything", + #"doesn't need to do anything", + "no\nactions need to be performed", 0, + False, '', 'var_to_output' ], @@ -44,6 +47,16 @@ CMD_CASES = [ lambda x: x.cmd('import', 'aws_instance.foo', 'i-abcd1234', no_color=IsFlagged), '', 1, + False, + 'command: terraform import -no-color aws_instance.foo i-abcd1234', + '' + ], + # try import aws instance with raise_on_error + [ + lambda x: x.cmd('import', 'aws_instance.foo', 'i-abcd1234', no_color=IsFlagged, raise_on_error=True), + '', + 1, + True, 'command: terraform import -no-color aws_instance.foo i-abcd1234', '' ], @@ -52,6 +65,7 @@ CMD_CASES = [ lambda x: x.cmd('plan', 'var_to_output', out=FILE_PATH_WITH_SPACE_AND_SPACIAL_CHARS), '', 0, + False, '', 'var_to_output' ] @@ -123,10 +137,18 @@ class TestTerraform(object): assert s in result @pytest.mark.parametrize(*CMD_CASES) - def test_cmd(self, method, expected_output, expected_ret_code, expected_logs, string_logger, folder): + def test_cmd(self, method, expected_output, expected_ret_code, expected_exception, expected_logs, string_logger, folder): tf = Terraform(working_dir=current_path) tf.init(folder) - ret, out, err = method(tf) + try: + ret, out, err = method(tf) + assert not expected_exception + except TerraformCommandError as e: + assert expected_exception + ret = e.returncode + out = e.out + err = e.err + logs = string_logger() logs = logs.replace('\n', '') assert expected_output in out @@ -223,6 +245,44 @@ class TestTerraform(object): else: assert result == 'test' + @pytest.mark.parametrize( + ("param"), + [ + ({}), + ({'module': 'test2'}), + ] + ) + def test_output_full_value(self, param, string_logger): + tf = Terraform(working_dir=current_path, variables={'test_var': 'test'}) + tf.init('var_to_output') + tf.apply('var_to_output') + result = tf.output('test_output', **dict(param, full_value=True)) + regex = re.compile("terraform output (-module=test2 -json|-json -module=test2) test_output") + log_str = string_logger() + if param: + assert re.search(regex, log_str), log_str + else: + assert result['value'] == 'test' + + @pytest.mark.parametrize( + ("param"), + [ + ({}), + ({'module': 'test2'}), + ] + ) + def test_output_all(self, param, string_logger): + tf = Terraform(working_dir=current_path, variables={'test_var': 'test'}) + tf.init('var_to_output') + tf.apply('var_to_output') + result = tf.output(**param) + regex = re.compile("terraform output (-module=test2 -json|-json -module=test2)") + log_str = string_logger() + if param: + assert re.search(regex, log_str), log_str + else: + assert result['test_output']['value'] == 'test' + def test_destroy(self): tf = Terraform(working_dir=current_path, variables={'test_var': 'test'}) tf.init('var_to_output') From 4945f4591db6d8bb83f9ef607ba48aac3118d6c0 Mon Sep 17 00:00:00 2001 From: Sam McKelvie Date: Fri, 13 Oct 2017 14:20:36 -0700 Subject: [PATCH 4/9] Make tests pass with old and new terraform --- test/test_terraform.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/test/test_terraform.py b/test/test_terraform.py index c0111d2..e8c9891 100644 --- a/test/test_terraform.py +++ b/test/test_terraform.py @@ -35,8 +35,8 @@ CMD_CASES = [ [ [ lambda x: x.cmd('plan', 'var_to_output', no_color=IsFlagged, var={'test_var': 'test'}) , - #"doesn't need to do anything", - "no\nactions need to be performed", + ["doesn't need to do anything", + "no\nactions need to be performed"], 0, False, '', @@ -151,7 +151,16 @@ class TestTerraform(object): logs = string_logger() logs = logs.replace('\n', '') - assert expected_output in out + if isinstance(expected_output, list): + ok = False + for xo in expected_output: + if xo in out: + ok = True + break + if not ok: + assert expected_output[0] in out + else: + assert expected_output in out assert expected_ret_code == ret assert expected_logs in logs From 99c67e5fe585855742c559573943c8d968b7fc72 Mon Sep 17 00:00:00 2001 From: Sam McKelvie Date: Mon, 16 Oct 2017 09:03:45 -0700 Subject: [PATCH 5/9] Address pull request feedback --- python_terraform/__init__.py | 9 +++++---- test/test_terraform.py | 21 +++++++++++---------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index 4fa394e..1f22546 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -32,9 +32,9 @@ class IsNotFlagged: class TerraformCommandError(subprocess.CalledProcessError): def __init__(self, ret_code, cmd, out, err): - super(TerraformCommandError, self).__init__(ret_code, cmd) - self.out = out - self.err = err + super(TerraformCommandError, self).__init__(ret_code, cmd) + self.out = out + self.err = err class Terraform(object): """ @@ -339,7 +339,8 @@ class Terraform(object): full_value = kwargs.pop('full_value', False) name_provided = (len(args) > 0) kwargs['json'] = IsFlagged - kwargs['capture_output'] = True + if not kwargs.get('capture_output', True) is True: + raise ValueError('capture_output is required for this method') ret, out, err = self.output_cmd(*args, **kwargs) diff --git a/test/test_terraform.py b/test/test_terraform.py index e8c9891..d787ae6 100644 --- a/test/test_terraform.py +++ b/test/test_terraform.py @@ -35,8 +35,9 @@ CMD_CASES = [ [ [ lambda x: x.cmd('plan', 'var_to_output', no_color=IsFlagged, var={'test_var': 'test'}) , - ["doesn't need to do anything", - "no\nactions need to be performed"], + # Expected output varies by terraform version + ["doesn't need to do anything", # Terraform < 0.10.7 (used in travis env) + "no\nactions need to be performed"], # Terraform >= 0.10.7 0, False, '', @@ -141,13 +142,13 @@ class TestTerraform(object): tf = Terraform(working_dir=current_path) tf.init(folder) try: - ret, out, err = method(tf) - assert not expected_exception + ret, out, err = method(tf) + assert not expected_exception except TerraformCommandError as e: - assert expected_exception - ret = e.returncode - out = e.out - err = e.err + assert expected_exception + ret = e.returncode + out = e.out + err = e.err logs = string_logger() logs = logs.replace('\n', '') @@ -158,9 +159,9 @@ class TestTerraform(object): ok = True break if not ok: - assert expected_output[0] in out + assert expected_output[0] in out else: - assert expected_output in out + assert expected_output in out assert expected_ret_code == ret assert expected_logs in logs From 98d221c77933eae7cc2150137d5215587d7db1a6 Mon Sep 17 00:00:00 2001 From: Sam McKelvie Date: Mon, 16 Oct 2017 09:12:34 -0700 Subject: [PATCH 6/9] address pull request feedback --- python_terraform/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index 1f22546..7cb729d 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -345,7 +345,7 @@ class Terraform(object): ret, out, err = self.output_cmd(*args, **kwargs) if ret != 0: - return None + return None out = out.lstrip() From e9255b118c76c84ba2c7bf1a940c119f397ab2a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marwan=20Rabb=C3=A2a?= Date: Fri, 24 Nov 2017 22:03:56 +0100 Subject: [PATCH 7/9] add force param to terraform apply --- python_terraform/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index 3d33a28..c3ef955 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -84,7 +84,7 @@ class Terraform(object): return wrapper - def apply(self, dir_or_plan=None, input=False, no_color=IsFlagged, + def apply(self, dir_or_plan=None, input=False, skip_plan=False, no_color=IsFlagged, **kwargs): """ refer to https://terraform.io/docs/commands/apply.html @@ -92,12 +92,14 @@ class Terraform(object): :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 skip_plan: force apply without plan (default: false) :param kwargs: same as kwags in method 'cmd' :returns return_code, stdout, stderr """ default = kwargs default['input'] = input default['no_color'] = no_color + default['auto-approve'] = (skip_plan == True) option_dict = self._generate_default_options(default) args = self._generate_default_args(dir_or_plan) return self.cmd('apply', *args, **option_dict) From a27362a145a256f557d9f7050e10b5481c25ff0d Mon Sep 17 00:00:00 2001 From: beelit94 Date: Mon, 19 Mar 2018 18:30:49 -0700 Subject: [PATCH 8/9] =?UTF-8?q?Bump=20version:=200.9.1=20=E2=86=92=200.10.?= =?UTF-8?q?0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 91784e0..07dac08 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.9.1 +current_version = 0.10.0 commit = True tag = False diff --git a/setup.py b/setup.py index 9ec25cd..81f7609 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ except IOError: setup( name=module_name, - version='0.9.1', + version='0.10.0', url='https://github.com/beelit94/python-terraform', license='MIT', author='Freddy Tan', From 99950cb03c37abadb0d7e136452e43f4f17dd4e1 Mon Sep 17 00:00:00 2001 From: beelit94 Date: Mon, 19 Mar 2018 18:34:08 -0700 Subject: [PATCH 9/9] add release doc --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3e68f0..e4a7b5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,4 +7,10 @@ ### Fixed 1. [#12] Output function doesn't accept parameter 'module' 1. [#16] Handle empty space/special characters when passing string to command line options -1. Tested with terraform 0.10.0 \ No newline at end of file +1. Tested with terraform 0.10.0 + +## [0.10.0] +### Fixed +1. [#27] No interaction for apply function +1. [#18] Return access to the subprocess so output can be handled as desired +1. [#24] Full support for output(); support for raise_on_error \ No newline at end of file