Skip to content

Commit 813d23d

Browse files
authored
Merge pull request #37 from beelit94/develop
release 0.10.0
2 parents 9afa4e1 + 99950cb commit 813d23d

File tree

6 files changed

+154
-21
lines changed

6 files changed

+154
-21
lines changed

.bumpversion.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 0.9.1
2+
current_version = 0.10.0
33
commit = True
44
tag = False
55

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
/.pypirc
88
/.tox/
99
.dropbox
10-
Icon
10+
Icon
11+
/pytestdebug.log

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,10 @@
77
### Fixed
88
1. [#12] Output function doesn't accept parameter 'module'
99
1. [#16] Handle empty space/special characters when passing string to command line options
10-
1. Tested with terraform 0.10.0
10+
1. Tested with terraform 0.10.0
11+
12+
## [0.10.0]
13+
### Fixed
14+
1. [#27] No interaction for apply function
15+
1. [#18] Return access to the subprocess so output can be handled as desired
16+
1. [#24] Full support for output(); support for raise_on_error

python_terraform/__init__.py

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from python_terraform.tfstate import Tfstate
1212

13+
1314
try: # Python 2.7+
1415
from logging import NullHandler
1516
except ImportError:
@@ -29,6 +30,12 @@ class IsNotFlagged:
2930
pass
3031

3132

33+
class TerraformCommandError(subprocess.CalledProcessError):
34+
def __init__(self, ret_code, cmd, out, err):
35+
super(TerraformCommandError, self).__init__(ret_code, cmd)
36+
self.out = out
37+
self.err = err
38+
3239
class Terraform(object):
3340
"""
3441
Wrapper of terraform command line tool
@@ -84,20 +91,22 @@ def wrapper(*args, **kwargs):
8491

8592
return wrapper
8693

87-
def apply(self, dir_or_plan=None, input=False, no_color=IsFlagged,
94+
def apply(self, dir_or_plan=None, input=False, skip_plan=False, no_color=IsFlagged,
8895
**kwargs):
8996
"""
9097
refer to https://terraform.io/docs/commands/apply.html
9198
no-color is flagged by default
9299
:param no_color: disable color of stdout
93100
:param input: disable prompt for a missing variable
94101
:param dir_or_plan: folder relative to working folder
102+
:param skip_plan: force apply without plan (default: false)
95103
:param kwargs: same as kwags in method 'cmd'
96104
:returns return_code, stdout, stderr
97105
"""
98106
default = kwargs
99107
default['input'] = input
100108
default['no_color'] = no_color
109+
default['auto-approve'] = (skip_plan == True)
101110
option_dict = self._generate_default_options(default)
102111
args = self._generate_default_args(dir_or_plan)
103112
return self.cmd('apply', *args, **option_dict)
@@ -252,10 +261,17 @@ def cmd(self, cmd, *args, **kwargs):
252261
if the option 'capture_output' is passed (with any value other than
253262
True), terraform output will be printed to stdout/stderr and
254263
"None" will be returned as out and err.
264+
if the option 'raise_on_error' is passed (with any value that evaluates to True),
265+
and the terraform command returns a nonzerop return code, then
266+
a TerraformCommandError exception will be raised. The exception object will
267+
have the following properties:
268+
returncode: The command's return code
269+
out: The captured stdout, or None if not captured
270+
err: The captured stderr, or None if not captured
255271
:return: ret_code, out, err
256272
"""
257-
258273
capture_output = kwargs.pop('capture_output', True)
274+
raise_on_error = kwargs.pop('raise_on_error', False)
259275
if capture_output is True:
260276
stderr = subprocess.PIPE
261277
stdout = subprocess.PIPE
@@ -274,6 +290,11 @@ def cmd(self, cmd, *args, **kwargs):
274290

275291
p = subprocess.Popen(cmds, stdout=stdout, stderr=stderr,
276292
cwd=working_folder, env=environ_vars)
293+
294+
synchronous = kwargs.pop('synchronous', True)
295+
if not synchronous:
296+
return p, None, None
297+
277298
out, err = p.communicate()
278299
ret_code = p.returncode
279300
log.debug('output: {o}'.format(o=out))
@@ -285,27 +306,62 @@ def cmd(self, cmd, *args, **kwargs):
285306

286307
self.temp_var_files.clean_up()
287308
if capture_output is True:
288-
return ret_code, out.decode('utf-8'), err.decode('utf-8')
309+
out = out.decode('utf-8')
310+
err = err.decode('utf-8')
289311
else:
290-
return ret_code, None, None
312+
out = None
313+
err = None
314+
315+
if ret_code != 0 and raise_on_error:
316+
raise TerraformCommandError(
317+
ret_code, ' '.join(cmds), out=out, err=err)
291318

292-
def output(self, name, *args, **kwargs):
319+
return ret_code, out, err
320+
321+
322+
def output(self, *args, **kwargs):
293323
"""
294324
https://www.terraform.io/docs/commands/output.html
295-
:param name: name of output
296-
:return: output value
325+
326+
Note that this method does not conform to the (ret_code, out, err) return convention. To use
327+
the "output" command with the standard convention, call "output_cmd" instead of
328+
"output".
329+
330+
:param args: Positional arguments. There is one optional positional
331+
argument NAME; if supplied, the returned output text
332+
will be the json for a single named output value.
333+
:param kwargs: Named options, passed to the command. In addition,
334+
'full_value': If True, and NAME is provided, then
335+
the return value will be a dict with
336+
"value', 'type', and 'sensitive'
337+
properties.
338+
:return: None, if an error occured
339+
Output value as a string, if NAME is provided and full_value
340+
is False or not provided
341+
Output value as a dict with 'value', 'sensitive', and 'type' if
342+
NAME is provided and full_value is True.
343+
dict of named dicts each with 'value', 'sensitive', and 'type',
344+
if NAME is not provided
297345
"""
346+
full_value = kwargs.pop('full_value', False)
347+
name_provided = (len(args) > 0)
348+
kwargs['json'] = IsFlagged
349+
if not kwargs.get('capture_output', True) is True:
350+
raise ValueError('capture_output is required for this method')
298351

299-
ret, out, err = self.cmd(
300-
'output', name, json=IsFlagged, *args, **kwargs)
352+
ret, out, err = self.output_cmd(*args, **kwargs)
301353

302-
log.debug('output raw string: {0}'.format(out))
303354
if ret != 0:
304355
return None
356+
305357
out = out.lstrip()
306358

307-
output_dict = json.loads(out)
308-
return output_dict['value']
359+
value = json.loads(out)
360+
361+
if name_provided and not full_value:
362+
value = value['value']
363+
364+
return value
309365

310366
def read_state_file(self, file_path=None):
311367
"""

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
setup(
2222
name=module_name,
23-
version='0.9.1',
23+
version='0.10.0',
2424
url='https://github.com/beelit94/python-terraform',
2525
license='MIT',
2626
author='Freddy Tan',

test/test_terraform.py

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
logging.basicConfig(level=logging.DEBUG)
1414
root_logger = logging.getLogger()
15+
1516
current_path = os.path.dirname(os.path.realpath(__file__))
1617

1718
FILE_PATH_WITH_SPACE_AND_SPACIAL_CHARS = "test 'test.out!"
@@ -30,12 +31,15 @@
3031
]
3132

3233
CMD_CASES = [
33-
['method', 'expected_output', 'expected_ret_code', 'expected_logs', 'folder'],
34+
['method', 'expected_output', 'expected_ret_code', 'expected_exception', 'expected_logs', 'folder'],
3435
[
3536
[
3637
lambda x: x.cmd('plan', 'var_to_output', no_color=IsFlagged, var={'test_var': 'test'}) ,
37-
"doesn't need to do anything",
38+
# Expected output varies by terraform version
39+
["doesn't need to do anything", # Terraform < 0.10.7 (used in travis env)
40+
"no\nactions need to be performed"], # Terraform >= 0.10.7
3841
0,
42+
False,
3943
'',
4044
'var_to_output'
4145
],
@@ -44,6 +48,16 @@
4448
lambda x: x.cmd('import', 'aws_instance.foo', 'i-abcd1234', no_color=IsFlagged),
4549
'',
4650
1,
51+
False,
52+
'command: terraform import -no-color aws_instance.foo i-abcd1234',
53+
''
54+
],
55+
# try import aws instance with raise_on_error
56+
[
57+
lambda x: x.cmd('import', 'aws_instance.foo', 'i-abcd1234', no_color=IsFlagged, raise_on_error=True),
58+
'',
59+
1,
60+
True,
4761
'command: terraform import -no-color aws_instance.foo i-abcd1234',
4862
''
4963
],
@@ -52,6 +66,7 @@
5266
lambda x: x.cmd('plan', 'var_to_output', out=FILE_PATH_WITH_SPACE_AND_SPACIAL_CHARS),
5367
'',
5468
0,
69+
False,
5570
'',
5671
'var_to_output'
5772
]
@@ -123,13 +138,30 @@ def test_generate_cmd_string(self, method, expected):
123138
assert s in result
124139

125140
@pytest.mark.parametrize(*CMD_CASES)
126-
def test_cmd(self, method, expected_output, expected_ret_code, expected_logs, string_logger, folder):
141+
def test_cmd(self, method, expected_output, expected_ret_code, expected_exception, expected_logs, string_logger, folder):
127142
tf = Terraform(working_dir=current_path)
128143
tf.init(folder)
129-
ret, out, err = method(tf)
144+
try:
145+
ret, out, err = method(tf)
146+
assert not expected_exception
147+
except TerraformCommandError as e:
148+
assert expected_exception
149+
ret = e.returncode
150+
out = e.out
151+
err = e.err
152+
130153
logs = string_logger()
131154
logs = logs.replace('\n', '')
132-
assert expected_output in out
155+
if isinstance(expected_output, list):
156+
ok = False
157+
for xo in expected_output:
158+
if xo in out:
159+
ok = True
160+
break
161+
if not ok:
162+
assert expected_output[0] in out
163+
else:
164+
assert expected_output in out
133165
assert expected_ret_code == ret
134166
assert expected_logs in logs
135167

@@ -223,6 +255,44 @@ def test_output(self, param, string_logger):
223255
else:
224256
assert result == 'test'
225257

258+
@pytest.mark.parametrize(
259+
("param"),
260+
[
261+
({}),
262+
({'module': 'test2'}),
263+
]
264+
)
265+
def test_output_full_value(self, param, string_logger):
266+
tf = Terraform(working_dir=current_path, variables={'test_var': 'test'})
267+
tf.init('var_to_output')
268+
tf.apply('var_to_output')
269+
result = tf.output('test_output', **dict(param, full_value=True))
270+
regex = re.compile("terraform output (-module=test2 -json|-json -module=test2) test_output")
271+
log_str = string_logger()
272+
if param:
273+
assert re.search(regex, log_str), log_str
274+
else:
275+
assert result['value'] == 'test'
276+
277+
@pytest.mark.parametrize(
278+
("param"),
279+
[
280+
({}),
281+
({'module': 'test2'}),
282+
]
283+
)
284+
def test_output_all(self, param, string_logger):
285+
tf = Terraform(working_dir=current_path, variables={'test_var': 'test'})
286+
tf.init('var_to_output')
287+
tf.apply('var_to_output')
288+
result = tf.output(**param)
289+
regex = re.compile("terraform output (-module=test2 -json|-json -module=test2)")
290+
log_str = string_logger()
291+
if param:
292+
assert re.search(regex, log_str), log_str
293+
else:
294+
assert result['test_output']['value'] == 'test'
295+
226296
def test_destroy(self):
227297
tf = Terraform(working_dir=current_path, variables={'test_var': 'test'})
228298
tf.init('var_to_output')

0 commit comments

Comments
 (0)