diff --git a/.gitignore b/.gitignore index bf4ea55..0aa11ee 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,11 @@ .dropbox Icon /pytestdebug.log +.DS_Store + +# virtualenv +.virtualenv/ + + +# Intellij +.idea/ diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index 709103b..4724346 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -21,6 +21,7 @@ except ImportError: log = logging.getLogger(__name__) log.addHandler(NullHandler()) +COMMAND_WITH_SUBCOMMANDS = ['workspace'] class IsFlagged: pass @@ -36,6 +37,7 @@ class TerraformCommandError(subprocess.CalledProcessError): self.out = out self.err = err + class Terraform(object): """ Wrapper of terraform command line tool @@ -205,6 +207,10 @@ class Terraform(object): """ cmds = cmd.split() cmds = [self.terraform_bin_path] + cmds + if cmd in COMMAND_WITH_SUBCOMMANDS: + args = list(args) + subcommand = args.pop(0) + cmds.append(subcommand) for option, value in kwargs.items(): if '_' in option: @@ -223,9 +229,13 @@ class Terraform(object): # since map type sent in string won't work, create temp var file for # variables, and clean it up later - else: - filename = self.temp_var_files.create(value) - cmds += ['-var-file={0}'.format(filename)] + elif option == 'var': + # We do not create empty var-files if there is no var passed. + # An empty var-file would result in an error: An argument or block definition is required here + if value: + filename = self.temp_var_files.create(value) + cmds += ['-var-file={0}'.format(filename)] + continue # simple flag, @@ -319,7 +329,6 @@ class Terraform(object): return ret_code, out, err - def output(self, *args, **kwargs): """ https://www.terraform.io/docs/commands/output.html @@ -388,40 +397,40 @@ class Terraform(object): self.tfstate = Tfstate.load_file(file_path) - def set_workspace(self, workspace): + def set_workspace(self, workspace, *args, **kwargs): """ set workspace :param workspace: the desired workspace. :return: status """ - return self.cmd('workspace' ,'select', workspace) + return self.cmd('workspace', 'select', workspace, *args, **kwargs) - def create_workspace(self, workspace): + def create_workspace(self, workspace, *args, **kwargs): """ create workspace :param workspace: the desired workspace. :return: status """ - return self.cmd('workspace', 'new', workspace) + return self.cmd('workspace', 'new', workspace, *args, **kwargs) - def delete_workspace(self, workspace): + def delete_workspace(self, workspace, *args, **kwargs): """ delete workspace :param workspace: the desired workspace. :return: status """ - return self.cmd('workspace', 'delete', workspace) + return self.cmd('workspace', 'delete', workspace, *args, **kwargs) - def show_workspace(self): + def show_workspace(self, **kwargs): """ - show workspace + show workspace, this command does not need the [DIR] part :return: workspace """ - return self.cmd('workspace', 'show') + return self.cmd('workspace', 'show', **kwargs) def __exit__(self, exc_type, exc_value, traceback): self.temp_var_files.clean_up() diff --git a/test/test_terraform.py b/test/test_terraform.py index 3b76e32..b8a79a5 100644 --- a/test/test_terraform.py +++ b/test/test_terraform.py @@ -3,6 +3,7 @@ try: except ImportError: from io import StringIO from python_terraform import * +from contextlib import contextmanager import pytest import os import logging @@ -34,7 +35,7 @@ CMD_CASES = [ ['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'}) , + lambda x: x.cmd('plan', 'var_to_output', no_color=IsFlagged, var={'test_var': 'test'}), # 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 @@ -69,7 +70,16 @@ CMD_CASES = [ False, '', 'var_to_output' - ] + ], + # test workspace command (commands with subcommand) + [ + lambda x: x.cmd('workspace', 'show', no_color=IsFlagged), + '', + 0, + False, + 'command: terraform workspace show -no-color', + '' + ], ] ] @@ -102,6 +112,28 @@ def string_logger(request): return lambda: str(log_stream.getvalue()) +@pytest.fixture() +def workspace_setup_teardown(): + """ + Fixture used in workspace related tests + + Create and tear down a workspace + *Use as a contextmanager* + """ + @contextmanager + def wrapper(workspace_name, create=True, delete=True, *args, **kwargs): + tf = Terraform(working_dir=current_path) + tf.init() + if create: + tf.create_workspace(workspace_name, *args, **kwargs) + yield tf + if delete: + tf.set_workspace('default') + tf.delete_workspace(workspace_name) + + yield wrapper + + class TestTerraform(object): def teardown_method(self, method): """ teardown any state that was previously setup with a setup_method @@ -184,6 +216,17 @@ class TestTerraform(object): assert expected_output in out.replace('\n', '').replace(' ', '') assert err == '' + def test_apply_with_var_file(self, string_logger): + tf = Terraform(working_dir=current_path) + + tf.init() + tf.apply(var_file=os.path.join(current_path, 'tfvar_file', 'test.tfvars')) + logs = string_logger() + logs = logs.split('\n') + for log in logs: + if log.startswith('command: terraform apply'): + assert log.count('-var-file=') == 1 + @pytest.mark.parametrize( ['cmd', 'args', 'options'], [ @@ -321,41 +364,94 @@ class TestTerraform(object): tf.import_cmd('aws_instance.foo', 'i-abc1234', no_color=IsFlagged) assert 'command: terraform import -no-color aws_instance.foo i-abc1234' in string_logger() - def test_create_workspace(self): - tf = Terraform(working_dir=current_path) - tf.init() - ret, out, err = tf.create_workspace('test') - tf.set_workspace('default') - tf.delete_workspace('test') + def test_create_workspace(self, workspace_setup_teardown): + workspace_name = 'test' + with workspace_setup_teardown(workspace_name, create=False) as tf: + ret, out, err = tf.create_workspace('test') assert ret == 0 assert err == '' - def test_set_workspace(self): - tf = Terraform(working_dir=current_path) - tf.init() - tf.create_workspace('test') - tf.set_workspace('test') - tf.set_workspace('default') - ret, out, err = tf.delete_workspace('test') + def test_create_workspace_with_args( + self, workspace_setup_teardown, string_logger + ): + workspace_name = 'test' + state_file_path = os.path.join(current_path, 'test_tfstate_file2', 'terraform.tfstate') + with workspace_setup_teardown(workspace_name, create=False) as tf: + ret, out, err = tf.create_workspace('test', current_path, no_color=IsFlagged) + assert ret == 0 assert err == '' - def test_show_workspace(self): - tf = Terraform(working_dir=current_path) - tf.init() - tf.create_workspace('test') - ret, out, err = tf.show_workspace() - tf.set_workspace('default') - tf.delete_workspace('test') + logs = string_logger() + logs = logs.replace('\n', '') + expected_log = 'command: terraform workspace new -no-color test {}'.format(current_path) + assert expected_log in logs + + def test_set_workspace(self, workspace_setup_teardown): + workspace_name = 'test' + with workspace_setup_teardown(workspace_name) as tf: + ret, out, err = tf.set_workspace(workspace_name) assert ret == 0 assert err == '' - def test_delete_workspace(self): - tf = Terraform(working_dir=current_path) - tf.init() - tf.create_workspace('test') - tf.set_workspace('default') - ret, out, err = tf.delete_workspace('test') - tf.show_workspace() + def test_set_workspace_with_args( + self, workspace_setup_teardown, string_logger): + workspace_name = 'test' + with workspace_setup_teardown(workspace_name) as tf: + ret, out, err = tf.set_workspace(workspace_name, current_path, no_color=IsFlagged) + assert ret == 0 assert err == '' + + logs = string_logger() + logs = logs.replace('\n', '') + expected_log = 'command: terraform workspace select -no-color test {}'.format(current_path) + assert expected_log in logs + + def test_show_workspace(self, workspace_setup_teardown): + workspace_name = 'test' + with workspace_setup_teardown(workspace_name) as tf: + ret, out, err = tf.show_workspace() + assert ret == 0 + assert err == '' + + def test_show_workspace_with_no_color( + self, workspace_setup_teardown, string_logger + ): + workspace_name = 'test' + with workspace_setup_teardown(workspace_name) as tf: + ret, out, err = tf.show_workspace(no_color=IsFlagged) + + assert ret == 0 + assert err == '' + + logs = string_logger() + logs = logs.replace('\n', '') + expected_log = 'command: terraform workspace show -no-color' + assert expected_log in logs + + def test_delete_workspace(self, workspace_setup_teardown): + workspace_name = 'test' + with workspace_setup_teardown(workspace_name, delete=False) as tf: + tf.set_workspace('default') + ret, out, err = tf.delete_workspace(workspace_name) + assert ret == 0 + assert err == '' + + def test_delete_workspace_with_args( + self, workspace_setup_teardown, string_logger + ): + workspace_name = 'test' + with workspace_setup_teardown(workspace_name, delete=False) as tf: + tf.set_workspace('default') + ret, out, err = tf.delete_workspace( + workspace_name, current_path, force=IsFlagged, + ) + + assert ret == 0 + assert err == '' + + logs = string_logger() + logs = logs.replace('\n', '') + expected_log = 'command: terraform workspace delete -force test {}'.format(current_path) + assert expected_log in logs diff --git a/test/tfvar_files/test.tfvars b/test/tfvar_files/test.tfvars new file mode 100644 index 0000000..0c78f75 --- /dev/null +++ b/test/tfvar_files/test.tfvars @@ -0,0 +1 @@ +test_var = "True"