10
10
11
11
from python_terraform .tfstate import Tfstate
12
12
13
+
13
14
try : # Python 2.7+
14
15
from logging import NullHandler
15
16
except ImportError :
@@ -29,6 +30,12 @@ class IsNotFlagged:
29
30
pass
30
31
31
32
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
+
32
39
class Terraform (object ):
33
40
"""
34
41
Wrapper of terraform command line tool
@@ -84,20 +91,22 @@ def wrapper(*args, **kwargs):
84
91
85
92
return wrapper
86
93
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 ,
88
95
** kwargs ):
89
96
"""
90
97
refer to https://terraform.io/docs/commands/apply.html
91
98
no-color is flagged by default
92
99
:param no_color: disable color of stdout
93
100
:param input: disable prompt for a missing variable
94
101
:param dir_or_plan: folder relative to working folder
102
+ :param skip_plan: force apply without plan (default: false)
95
103
:param kwargs: same as kwags in method 'cmd'
96
104
:returns return_code, stdout, stderr
97
105
"""
98
106
default = kwargs
99
107
default ['input' ] = input
100
108
default ['no_color' ] = no_color
109
+ default ['auto-approve' ] = (skip_plan == True )
101
110
option_dict = self ._generate_default_options (default )
102
111
args = self ._generate_default_args (dir_or_plan )
103
112
return self .cmd ('apply' , * args , ** option_dict )
@@ -252,10 +261,17 @@ def cmd(self, cmd, *args, **kwargs):
252
261
if the option 'capture_output' is passed (with any value other than
253
262
True), terraform output will be printed to stdout/stderr and
254
263
"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
255
271
:return: ret_code, out, err
256
272
"""
257
-
258
273
capture_output = kwargs .pop ('capture_output' , True )
274
+ raise_on_error = kwargs .pop ('raise_on_error' , False )
259
275
if capture_output is True :
260
276
stderr = subprocess .PIPE
261
277
stdout = subprocess .PIPE
@@ -274,6 +290,11 @@ def cmd(self, cmd, *args, **kwargs):
274
290
275
291
p = subprocess .Popen (cmds , stdout = stdout , stderr = stderr ,
276
292
cwd = working_folder , env = environ_vars )
293
+
294
+ synchronous = kwargs .pop ('synchronous' , True )
295
+ if not synchronous :
296
+ return p , None , None
297
+
277
298
out , err = p .communicate ()
278
299
ret_code = p .returncode
279
300
log .debug ('output: {o}' .format (o = out ))
@@ -285,27 +306,62 @@ def cmd(self, cmd, *args, **kwargs):
285
306
286
307
self .temp_var_files .clean_up ()
287
308
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' )
289
311
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 )
291
318
292
- def output (self , name , * args , ** kwargs ):
319
+ return ret_code , out , err
320
+
321
+
322
+ def output (self , * args , ** kwargs ):
293
323
"""
294
324
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
297
345
"""
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' )
298
351
299
- ret , out , err = self .cmd (
300
- 'output' , name , json = IsFlagged , * args , ** kwargs )
352
+ ret , out , err = self .output_cmd (* args , ** kwargs )
301
353
302
- log .debug ('output raw string: {0}' .format (out ))
303
354
if ret != 0 :
304
355
return None
356
+
305
357
out = out .lstrip ()
306
358
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
309
365
310
366
def read_state_file (self , file_path = None ):
311
367
"""
0 commit comments