From 576197d768fdb000cbebbbd3b8c06dccac3f4517 Mon Sep 17 00:00:00 2001 From: John Schoewe Date: Tue, 9 Apr 2019 22:54:15 -0400 Subject: [PATCH 01/56] Added suffix variable --- 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 4e0ff7d..f8d769d 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -396,7 +396,7 @@ def __init__(self): self.files = [] def create(self, variables): - with tempfile.NamedTemporaryFile('w+t', delete=False) as temp: + with tempfile.NamedTemporaryFile('w+t', suffix='.tfvars.json', delete=False) as temp: log.debug('{0} is created'.format(temp.name)) self.files.append(temp) log.debug( From 05c44c2cdcf48d8041f618a825dd083e812d74e4 Mon Sep 17 00:00:00 2001 From: DJDavisson Date: Fri, 3 May 2019 13:54:28 -0700 Subject: [PATCH 02/56] Adding workspace support. --- python_terraform/__init__.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index 4e0ff7d..7017af2 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -49,7 +49,8 @@ def __init__(self, working_dir=None, parallelism=None, var_file=None, terraform_bin_path=None, - is_env_vars_included=True): + is_env_vars_included=True, + workspace=None): """ :param working_dir: the folder of the working folder, if not given, will be current working folder @@ -65,6 +66,7 @@ def __init__(self, working_dir=None, :param terraform_bin_path: binary path of terraform :type is_env_vars_included: bool :param is_env_vars_included: included env variables when calling terraform cmd + :param workspace: current workspace. """ self.is_env_vars_included = is_env_vars_included self.working_dir = working_dir @@ -76,6 +78,7 @@ def __init__(self, working_dir=None, if terraform_bin_path else 'terraform' self.var_file = var_file self.temp_var_files = VariableFiles() + self.workspace = workspace # store the tfstate data self.tfstate = None @@ -387,6 +390,21 @@ def read_state_file(self, file_path=None): self.tfstate = Tfstate.load_file(file_path) + def set_workspace(self, workspace=None, dir_or_plan=None, **kwargs): + """ + set workspace + :param workspace: the desired workspace. + :param dir: The dir we want to swap workspace in. + :return: nothing + """ + + working_dir = self.working_dir if self.working_dir else None + options = kwargs + options['workspace'] = workspace + options = self._generate_default_options(options) + args = self._generate_default_args(dir_or_plan) + return self.cmd('workspace', *args, **options) + def __exit__(self, exc_type, exc_value, traceback): self.temp_var_files.clean_up() From 57a80d7e71be17a5409ef9a8971283d17071326d Mon Sep 17 00:00:00 2001 From: DJDavisson Date: Fri, 3 May 2019 13:55:38 -0700 Subject: [PATCH 03/56] Removing working dir. --- python_terraform/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index 7017af2..e5d31db 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -398,7 +398,6 @@ def set_workspace(self, workspace=None, dir_or_plan=None, **kwargs): :return: nothing """ - working_dir = self.working_dir if self.working_dir else None options = kwargs options['workspace'] = workspace options = self._generate_default_options(options) From 749bec79e43cf4bf210d01299f89f16174eb2c9e Mon Sep 17 00:00:00 2001 From: DJDavisson Date: Fri, 3 May 2019 13:59:48 -0700 Subject: [PATCH 04/56] Adding additional workspace items. --- python_terraform/__init__.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index e5d31db..d554019 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -399,10 +399,35 @@ def set_workspace(self, workspace=None, dir_or_plan=None, **kwargs): """ options = kwargs - options['workspace'] = workspace options = self._generate_default_options(options) args = self._generate_default_args(dir_or_plan) - return self.cmd('workspace', *args, **options) + return self.cmd('workspace select ' + workspace, *args, **options) + + def create_workspace(self, workspace=None, dir_or_plan=None, **kwargs): + """ + set workspace + :param workspace: the desired workspace. + :param dir: The dir we want to swap workspace in. + :return: nothing + """ + + options = kwargs + options = self._generate_default_options(options) + args = self._generate_default_args(dir_or_plan) + return self.cmd('workspace new ' + workspace, *args, **options) + + def show_workspace(self, workspace=None, dir_or_plan=None, **kwargs): + """ + set workspace + :param workspace: the desired workspace. + :param dir: The dir we want to swap workspace in. + :return: nothing + """ + + options = kwargs + options = self._generate_default_options(options) + args = self._generate_default_args(dir_or_plan) + return self.cmd('workspace show', *args, **options) def __exit__(self, exc_type, exc_value, traceback): self.temp_var_files.clean_up() From 0228de1a60e7da69b18e210f5f29fa0c74cadfed Mon Sep 17 00:00:00 2001 From: DJDavisson Date: Fri, 3 May 2019 14:17:46 -0700 Subject: [PATCH 05/56] Cleanup. --- python_terraform/__init__.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index d554019..4e0ccb1 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -394,7 +394,6 @@ def set_workspace(self, workspace=None, dir_or_plan=None, **kwargs): """ set workspace :param workspace: the desired workspace. - :param dir: The dir we want to swap workspace in. :return: nothing """ @@ -407,7 +406,6 @@ def create_workspace(self, workspace=None, dir_or_plan=None, **kwargs): """ set workspace :param workspace: the desired workspace. - :param dir: The dir we want to swap workspace in. :return: nothing """ @@ -416,11 +414,9 @@ def create_workspace(self, workspace=None, dir_or_plan=None, **kwargs): args = self._generate_default_args(dir_or_plan) return self.cmd('workspace new ' + workspace, *args, **options) - def show_workspace(self, workspace=None, dir_or_plan=None, **kwargs): + def show_workspace(self, dir_or_plan=None, **kwargs): """ set workspace - :param workspace: the desired workspace. - :param dir: The dir we want to swap workspace in. :return: nothing """ From 64e804e7a01c903d4c551bdc80421eeca7167c99 Mon Sep 17 00:00:00 2001 From: DJDavisson Date: Tue, 7 May 2019 09:55:13 -0700 Subject: [PATCH 06/56] Removing uncessary workspace from __init__ Will add back with some checking in the future. --- python_terraform/__init__.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index 4e0ccb1..dd0e0d3 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -50,7 +50,7 @@ def __init__(self, working_dir=None, var_file=None, terraform_bin_path=None, is_env_vars_included=True, - workspace=None): + ): """ :param working_dir: the folder of the working folder, if not given, will be current working folder @@ -78,7 +78,6 @@ def __init__(self, working_dir=None, if terraform_bin_path else 'terraform' self.var_file = var_file self.temp_var_files = VariableFiles() - self.workspace = workspace # store the tfstate data self.tfstate = None @@ -394,7 +393,7 @@ def set_workspace(self, workspace=None, dir_or_plan=None, **kwargs): """ set workspace :param workspace: the desired workspace. - :return: nothing + :return: status """ options = kwargs @@ -404,9 +403,9 @@ def set_workspace(self, workspace=None, dir_or_plan=None, **kwargs): def create_workspace(self, workspace=None, dir_or_plan=None, **kwargs): """ - set workspace + create workspace :param workspace: the desired workspace. - :return: nothing + :return: status """ options = kwargs @@ -416,8 +415,8 @@ def create_workspace(self, workspace=None, dir_or_plan=None, **kwargs): def show_workspace(self, dir_or_plan=None, **kwargs): """ - set workspace - :return: nothing + show workspace + :return: workspace """ options = kwargs From 3ddab331f02c3eff4a50234dd304a5531d6add0a Mon Sep 17 00:00:00 2001 From: DJDavisson Date: Tue, 7 May 2019 11:39:07 -0700 Subject: [PATCH 07/56] Adding tests, 2 are failing, looking into how to handle. --- test/test_terraform.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/test_terraform.py b/test/test_terraform.py index d787ae6..1a7d04e 100644 --- a/test/test_terraform.py +++ b/test/test_terraform.py @@ -320,3 +320,23 @@ def test_import(self, string_logger): tf = Terraform(working_dir=current_path) 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) + ret, out, err = tf.create_workspace('test') + assert ret == 0 + assert err == '' + + def test_set_workspace(self): + tf = Terraform(working_dir=current_path) + tf.create_workspace('test') + ret, out, err = tf.set_workspace('test') + assert ret == 0 + assert err == '' + + def test_show_workspace(self): + tf = Terraform(working_dir=current_path) + tf.create_workspace('test') + ret, out, err = tf.show_workspace('test') + assert ret == 0 + assert err == '' \ No newline at end of file From 01e3dbb05a0e9ba427a62c5b945bf16096d94738 Mon Sep 17 00:00:00 2001 From: DJDavisson Date: Tue, 7 May 2019 12:11:28 -0700 Subject: [PATCH 08/56] Removing optiosn and args pass from state stuff, state stuff doesn't take options or args. --- python_terraform/__init__.py | 21 ++++++--------------- test/test_terraform.py | 7 +++++-- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index dd0e0d3..468a114 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -389,40 +389,31 @@ def read_state_file(self, file_path=None): self.tfstate = Tfstate.load_file(file_path) - def set_workspace(self, workspace=None, dir_or_plan=None, **kwargs): + def set_workspace(self, workspace): """ set workspace :param workspace: the desired workspace. :return: status """ - options = kwargs - options = self._generate_default_options(options) - args = self._generate_default_args(dir_or_plan) - return self.cmd('workspace select ' + workspace, *args, **options) + return self.cmd('workspace select ' + workspace) - def create_workspace(self, workspace=None, dir_or_plan=None, **kwargs): + def create_workspace(self, workspace): """ create workspace :param workspace: the desired workspace. :return: status """ - options = kwargs - options = self._generate_default_options(options) - args = self._generate_default_args(dir_or_plan) - return self.cmd('workspace new ' + workspace, *args, **options) + return self.cmd('workspace new ' + workspace) - def show_workspace(self, dir_or_plan=None, **kwargs): + def show_workspace(self): """ show workspace :return: workspace """ - options = kwargs - options = self._generate_default_options(options) - args = self._generate_default_args(dir_or_plan) - return self.cmd('workspace show', *args, **options) + return self.cmd('workspace show') 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 1a7d04e..f49be62 100644 --- a/test/test_terraform.py +++ b/test/test_terraform.py @@ -323,12 +323,14 @@ def test_import(self, string_logger): def test_create_workspace(self): tf = Terraform(working_dir=current_path) + tf.init() 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') ret, out, err = tf.set_workspace('test') assert ret == 0 @@ -336,7 +338,8 @@ def test_set_workspace(self): def test_show_workspace(self): tf = Terraform(working_dir=current_path) + tf.init() tf.create_workspace('test') - ret, out, err = tf.show_workspace('test') + ret, out, err = tf.show_workspace() assert ret == 0 - assert err == '' \ No newline at end of file + assert err == '' \ No newline at end of file From 70920d828c3e199c2d356f07d76746ecb552d00f Mon Sep 17 00:00:00 2001 From: DJDavisson Date: Wed, 8 May 2019 10:55:53 -0700 Subject: [PATCH 09/56] Adding workspace deletion. --- python_terraform/__init__.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index 468a114..64be9e7 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -405,7 +405,16 @@ def create_workspace(self, workspace): :return: status """ - return self.cmd('workspace new ' + workspace) + return self.cmd('workspace new ' + workspace) + + def delete_workspace(self, workspace): + """ + delete workspace + :param workspace: the desired workspace. + :return: status + """ + + return self.cmd('workspace delete ' + workspace) def show_workspace(self): """ From c000d00504a6ec2c91afa073fcb52c47e2bc88f4 Mon Sep 17 00:00:00 2001 From: DJDavisson Date: Mon, 20 May 2019 11:34:42 -0700 Subject: [PATCH 10/56] Fixing tests. --- test/test_terraform.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/test/test_terraform.py b/test/test_terraform.py index f49be62..3b76e32 100644 --- a/test/test_terraform.py +++ b/test/test_terraform.py @@ -325,6 +325,8 @@ 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') assert ret == 0 assert err == '' @@ -332,7 +334,9 @@ def test_set_workspace(self): tf = Terraform(working_dir=current_path) tf.init() tf.create_workspace('test') - ret, out, err = tf.set_workspace('test') + tf.set_workspace('test') + tf.set_workspace('default') + ret, out, err = tf.delete_workspace('test') assert ret == 0 assert err == '' @@ -341,5 +345,17 @@ def test_show_workspace(self): tf.init() tf.create_workspace('test') ret, out, err = tf.show_workspace() + tf.set_workspace('default') + tf.delete_workspace('test') assert ret == 0 - assert err == '' \ No newline at end of file + 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() + assert ret == 0 + assert err == '' From 29daa9c40a597c055a9a800aa48e72d39f0543ad Mon Sep 17 00:00:00 2001 From: DJDavisson Date: Fri, 31 May 2019 11:31:30 -0700 Subject: [PATCH 11/56] Modifying returns to use proper format. --- python_terraform/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index 64be9e7..a41e123 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -396,7 +396,7 @@ def set_workspace(self, workspace): :return: status """ - return self.cmd('workspace select ' + workspace) + return self.cmd('workspace' ,'select', workspace) def create_workspace(self, workspace): """ @@ -405,7 +405,7 @@ def create_workspace(self, workspace): :return: status """ - return self.cmd('workspace new ' + workspace) + return self.cmd('workspace', 'new', workspace) def delete_workspace(self, workspace): """ @@ -414,7 +414,7 @@ def delete_workspace(self, workspace): :return: status """ - return self.cmd('workspace delete ' + workspace) + return self.cmd('workspace', 'delete', workspace) def show_workspace(self): """ @@ -422,7 +422,7 @@ def show_workspace(self): :return: workspace """ - return self.cmd('workspace show') + return self.cmd('workspace' 'show') def __exit__(self, exc_type, exc_value, traceback): self.temp_var_files.clean_up() From 35db7917b09531cd0ef9adbd7c060f6cbbcb5741 Mon Sep 17 00:00:00 2001 From: DJDavisson Date: Fri, 31 May 2019 11:36:53 -0700 Subject: [PATCH 12/56] Fixing missing , --- 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 a41e123..9c6d24f 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -422,7 +422,7 @@ def show_workspace(self): :return: workspace """ - return self.cmd('workspace' 'show') + return self.cmd('workspace', 'show') def __exit__(self, exc_type, exc_value, traceback): self.temp_var_files.clean_up() From c6d37cbaeb2482583f9de2371531f789ec20a992 Mon Sep 17 00:00:00 2001 From: DJDavisson Date: Fri, 31 May 2019 11:49:32 -0700 Subject: [PATCH 13/56] Removing uncessary comment in __init__. --- python_terraform/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index 9c6d24f..ba98610 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -66,7 +66,6 @@ def __init__(self, working_dir=None, :param terraform_bin_path: binary path of terraform :type is_env_vars_included: bool :param is_env_vars_included: included env variables when calling terraform cmd - :param workspace: current workspace. """ self.is_env_vars_included = is_env_vars_included self.working_dir = working_dir From 3a32241cf1c83f3ebd7106a44ab1bd197e94ad78 Mon Sep 17 00:00:00 2001 From: beelit94 Date: Wed, 5 Jun 2019 00:07:24 -0700 Subject: [PATCH 14/56] Updating test env and Contribution doc --- .travis.yml | 8 ++++---- CONTRIBUTING.md | 0 2 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/.travis.yml b/.travis.yml index bb62e9c..c0420c7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,11 @@ language: python python: - '2.7' -- '3.5' - '3.6' +- '3.7' before_install: sudo apt-get install unzip before_script: - - export TFVER=0.10.0 + - export TFVER=0.12.1 - export TFURL=https://releases.hashicorp.com/terraform/ - TFURL+=$TFVER - TFURL+="/terraform_" @@ -36,7 +36,7 @@ deploy: on: branch: release/** tags: false - condition: $TRAVIS_PYTHON_VERSION = "3.5" + condition: $TRAVIS_PYTHON_VERSION = "3.7" - provider: pypi distributions: sdist user: beelit94 @@ -45,7 +45,7 @@ deploy: on: branch: master tags: false - condition: $TRAVIS_PYTHON_VERSION = "3.5" + condition: $TRAVIS_PYTHON_VERSION = "3.7" notifications: email: recipients: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e69de29 From c79e8070282769eb6ac9a7a3cec5d266ba1b7307 Mon Sep 17 00:00:00 2001 From: beelit94 Date: Wed, 5 Jun 2019 00:14:23 -0700 Subject: [PATCH 15/56] adding change log for 0.10.1 --- CHANGELOG.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4a7b5e..36bf9c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,14 +3,17 @@ 1. [#10] log handler error on Linux environment 1. [#11] Fix reading state file for remote state and support backend config for init command + ## [0.9.0] -### 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 ## [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 +1. [#24] Full support for output(); support for raise_on_error + +## [0.10.1] +1. [#48] adding extension for temp file to addapt change in terraform 0.12.0 +1. [#49] add workspace support \ No newline at end of file From 7056a853da0121e5d1e3001e53aef1c787250755 Mon Sep 17 00:00:00 2001 From: Stephan Zaria Date: Mon, 10 Jun 2019 09:59:40 -0700 Subject: [PATCH 16/56] Move synchrounous pop uphill, so it doesn't end up as a flag in the actual terraform command --- python_terraform/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index 4e0ff7d..2a9a094 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -272,6 +272,8 @@ def cmd(self, cmd, *args, **kwargs): """ capture_output = kwargs.pop('capture_output', True) raise_on_error = kwargs.pop('raise_on_error', False) + synchronous = kwargs.pop('synchronous', True) + if capture_output is True: stderr = subprocess.PIPE stdout = subprocess.PIPE @@ -291,7 +293,6 @@ def cmd(self, cmd, *args, **kwargs): p = subprocess.Popen(cmds, stdout=stdout, stderr=stderr, cwd=working_folder, env=environ_vars) - synchronous = kwargs.pop('synchronous', True) if not synchronous: return p, None, None From 172ab6b509fc62a1c8d15422e58a26e0f89db24b Mon Sep 17 00:00:00 2001 From: beelit94 Date: Thu, 20 Jun 2019 23:04:30 -0700 Subject: [PATCH 17/56] roll back python version --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index c0420c7..212ef0d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: python python: - '2.7' +- '3.5' - '3.6' -- '3.7' before_install: sudo apt-get install unzip before_script: - export TFVER=0.12.1 @@ -36,7 +36,7 @@ deploy: on: branch: release/** tags: false - condition: $TRAVIS_PYTHON_VERSION = "3.7" + condition: $TRAVIS_PYTHON_VERSION = "3.5" - provider: pypi distributions: sdist user: beelit94 @@ -45,7 +45,7 @@ deploy: on: branch: master tags: false - condition: $TRAVIS_PYTHON_VERSION = "3.7" + condition: $TRAVIS_PYTHON_VERSION = "3.5" notifications: email: recipients: From 0a7162a5ceb539544aa7f90676998a7a7fbae93a Mon Sep 17 00:00:00 2001 From: beelit94 Date: Thu, 20 Jun 2019 23:22:28 -0700 Subject: [PATCH 18/56] roll back terraform version --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 212ef0d..bb62e9c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ python: - '3.6' before_install: sudo apt-get install unzip before_script: - - export TFVER=0.12.1 + - export TFVER=0.10.0 - export TFURL=https://releases.hashicorp.com/terraform/ - TFURL+=$TFVER - TFURL+="/terraform_" From 6d8dde7991149687c221b0981cdc2e56deb8c690 Mon Sep 17 00:00:00 2001 From: beelit94 Date: Wed, 5 Jun 2019 00:07:24 -0700 Subject: [PATCH 19/56] 1. Updating test env and Contribution doc 2. adding change log for 0.10.1 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36bf9c6..f7fa6ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,5 +15,5 @@ 1. [#24] Full support for output(); support for raise_on_error ## [0.10.1] -1. [#48] adding extension for temp file to addapt change in terraform 0.12.0 +1. [#48] adding extension for temp file to adopt the change in terraform 0.12.0 1. [#49] add workspace support \ No newline at end of file From ce788acc3f3267a98c4b1ccacbbe66e9252b258e Mon Sep 17 00:00:00 2001 From: beelit94 Date: Thu, 20 Jun 2019 23:42:16 -0700 Subject: [PATCH 20/56] =?UTF-8?q?Bump=20version:=200.10.0=20=E2=86=92=200.?= =?UTF-8?q?10.1?= 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 07dac08..1c1f0f9 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.10.0 +current_version = 0.10.1 commit = True tag = False diff --git a/setup.py b/setup.py index 81f7609..1d8c5d1 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ setup( name=module_name, - version='0.10.0', + version='0.10.1', url='https://github.com/beelit94/python-terraform', license='MIT', author='Freddy Tan', From f39b7e237e74dc38f69fef725a3d0963c08c7304 Mon Sep 17 00:00:00 2001 From: BNMetrics Date: Tue, 25 Jun 2019 19:53:39 +0100 Subject: [PATCH 21/56] fixed a bug with var-file argument, allow workspace commands to pass flags and options --- .gitignore | 8 ++ python_terraform/__init__.py | 35 +++++--- test/test_terraform.py | 153 ++++++++++++++++++++++++++++------- test/tfvar_files/test.tfvars | 1 + 4 files changed, 155 insertions(+), 42 deletions(-) create mode 100644 test/tfvar_files/test.tfvars diff --git a/.gitignore b/.gitignore index bf4ea55..a2b13ad 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,11 @@ .dropbox Icon /pytestdebug.log +.DS_Store + +# virtualenv +.virtualenv/ + + +# Intellij +.idea/ \ No newline at end of file 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 @@ def emit(self, record): log = logging.getLogger(__name__) log.addHandler(NullHandler()) +COMMAND_WITH_SUBCOMMANDS = ['workspace'] class IsFlagged: pass @@ -36,6 +37,7 @@ def __init__(self, ret_code, cmd, out, err): self.out = out self.err = err + class Terraform(object): """ Wrapper of terraform command line tool @@ -205,6 +207,10 @@ def generate_cmd_string(self, cmd, *args, **kwargs): """ 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 @@ def generate_cmd_string(self, cmd, *args, **kwargs): # 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 @@ def cmd(self, cmd, *args, **kwargs): return ret_code, out, err - def output(self, *args, **kwargs): """ https://www.terraform.io/docs/commands/output.html @@ -388,40 +397,40 @@ def read_state_file(self, file_path=None): 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..9c45991 100644 --- a/test/test_terraform.py +++ b/test/test_terraform.py @@ -3,6 +3,7 @@ except ImportError: from io import StringIO from python_terraform import * +from contextlib import contextmanager import pytest import os import logging @@ -34,7 +35,7 @@ ['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 @@ 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 td(): 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 @@ def test_apply(self, folder, variables, var_files, expected_output, options): 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) + var_file = os.path.join(current_path, 'tfvar_file', 'test.tfvars') + + tf.init() + tf.apply(var_file=os.path.join(current_path, 'tfvar_file', 'test.tfvars')) + logs = string_logger() + logs = logs.replace('\n', '') + expected_log = 'command: terraform apply -var-file={} -no-color -input=false -auto-approve=false'.format(var_file) + assert expected_log in logs + @pytest.mark.parametrize( ['cmd', 'args', 'options'], [ @@ -321,41 +364,93 @@ def test_import(self, string_logger): 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' + with workspace_setup_teardown(workspace_name, create=False) as tf: + ret, out, err = tf.create_workspace('test', current_path, lock=True, 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 -lock=true -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, no_color=IsFlagged, lock=True, + ) + + assert ret == 0 + assert err == '' + + logs = string_logger() + logs = logs.replace('\n', '') + expected_log = 'command: terraform workspace delete -force -no-color -lock=true 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" From 4a76a44c914599644060359fb4c465c72a6de4fd Mon Sep 17 00:00:00 2001 From: BNMetrics Date: Tue, 25 Jun 2019 19:57:03 +0100 Subject: [PATCH 22/56] formatting on .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a2b13ad..0aa11ee 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,4 @@ Icon # Intellij -.idea/ \ No newline at end of file +.idea/ From c0ff5bf65a1b6a3c35a2e55014523313808ba0a2 Mon Sep 17 00:00:00 2001 From: BNMetrics Date: Tue, 25 Jun 2019 21:33:38 +0100 Subject: [PATCH 23/56] fixed terraform version and python version compat issue in test --- test/test_terraform.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/test/test_terraform.py b/test/test_terraform.py index 9c45991..183c765 100644 --- a/test/test_terraform.py +++ b/test/test_terraform.py @@ -218,14 +218,14 @@ def test_apply(self, folder, variables, var_files, expected_output, options): def test_apply_with_var_file(self, string_logger): tf = Terraform(working_dir=current_path) - var_file = os.path.join(current_path, 'tfvar_file', 'test.tfvars') tf.init() tf.apply(var_file=os.path.join(current_path, 'tfvar_file', 'test.tfvars')) logs = string_logger() - logs = logs.replace('\n', '') - expected_log = 'command: terraform apply -var-file={} -no-color -input=false -auto-approve=false'.format(var_file) - assert expected_log in logs + 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'], @@ -375,15 +375,18 @@ 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, lock=True, no_color=IsFlagged) + ret, out, err = tf.create_workspace('test', current_path, no_color=IsFlagged, state=state_file_path) assert ret == 0 assert err == '' logs = string_logger() logs = logs.replace('\n', '') - expected_log = 'command: terraform workspace new -lock=true -no-color test {}'.format(current_path) + expected_log = 'command: terraform workspace new -no-color -state={} test {}'.format( + state_file_path, current_path + ) assert expected_log in logs def test_set_workspace(self, workspace_setup_teardown): @@ -444,7 +447,7 @@ def test_delete_workspace_with_args( 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, no_color=IsFlagged, lock=True, + workspace_name, current_path, no_color=IsFlagged, force=IsFlagged, ) assert ret == 0 @@ -452,5 +455,5 @@ def test_delete_workspace_with_args( logs = string_logger() logs = logs.replace('\n', '') - expected_log = 'command: terraform workspace delete -force -no-color -lock=true test {}'.format(current_path) + expected_log = 'command: terraform workspace delete -no-color -force test {}'.format(current_path) assert expected_log in logs From 09fdcf2f116c394d94b99080de83b1405263103c Mon Sep 17 00:00:00 2001 From: BNMetrics Date: Tue, 25 Jun 2019 22:12:41 +0100 Subject: [PATCH 24/56] fixed flaky tests --- test/test_terraform.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/test/test_terraform.py b/test/test_terraform.py index 183c765..b8a79a5 100644 --- a/test/test_terraform.py +++ b/test/test_terraform.py @@ -377,16 +377,14 @@ def test_create_workspace_with_args( 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, state=state_file_path) + ret, out, err = tf.create_workspace('test', current_path, no_color=IsFlagged) assert ret == 0 assert err == '' logs = string_logger() logs = logs.replace('\n', '') - expected_log = 'command: terraform workspace new -no-color -state={} test {}'.format( - state_file_path, current_path - ) + 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): @@ -447,7 +445,7 @@ def test_delete_workspace_with_args( with workspace_setup_teardown(workspace_name, delete=False) as tf: tf.set_workspace('default') ret, out, err = tf.delete_workspace( - workspace_name, current_path, no_color=IsFlagged, force=IsFlagged, + workspace_name, current_path, force=IsFlagged, ) assert ret == 0 @@ -455,5 +453,5 @@ def test_delete_workspace_with_args( logs = string_logger() logs = logs.replace('\n', '') - expected_log = 'command: terraform workspace delete -no-color -force test {}'.format(current_path) + expected_log = 'command: terraform workspace delete -force test {}'.format(current_path) assert expected_log in logs From 89914bbbd8e0cb83e9c3f13f8b793d699bb87256 Mon Sep 17 00:00:00 2001 From: Spike Date: Fri, 16 Aug 2019 17:52:07 -0700 Subject: [PATCH 25/56] Bumping version, adding vscode to .gitignore. Prepping for release of 0.10.2 --- .gitignore | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0aa11ee..f936565 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ Icon # Intellij .idea/ + +# VSCode +.vscode/ diff --git a/setup.py b/setup.py index 1d8c5d1..2659f1e 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ setup( name=module_name, - version='0.10.1', + version='0.10.2', url='https://github.com/beelit94/python-terraform', license='MIT', author='Freddy Tan', From 7c4ac21fb3f0c42bb52b4b11eb1ef824df8ec08e Mon Sep 17 00:00:00 2001 From: noname Date: Thu, 10 Oct 2019 14:00:18 +0300 Subject: [PATCH 26/56] Replace deprecated warn with warning --- 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 4bb83d7..3bf45ea 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -314,7 +314,7 @@ def cmd(self, cmd, *args, **kwargs): if ret_code == 0: self.read_state_file() else: - log.warn('error: {e}'.format(e=err)) + log.warning('error: {e}'.format(e=err)) self.temp_var_files.clean_up() if capture_output is True: From 3efa8b2f65393fdccdb1d0af74c58eb3cf97b820 Mon Sep 17 00:00:00 2001 From: Dustin Davisson Date: Wed, 16 Oct 2019 13:25:54 -0700 Subject: [PATCH 27/56] updating gitignore for env dir from venv. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f936565..a99ba67 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ /.pypirc /.tox/ .dropbox +env/ Icon /pytestdebug.log .DS_Store From 6da2d853fc6cec2896a6de6d8a39a4c2276ac50d Mon Sep 17 00:00:00 2001 From: caixisheng Date: Sat, 26 Oct 2019 15:00:53 +0800 Subject: [PATCH 28/56] Update __init__.py --- 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 3bf45ea..94fe2cf 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -89,7 +89,7 @@ def wrapper(*args, **kwargs): cmd_name = str(item) if cmd_name.endswith('_cmd'): cmd_name = cmd_name[:-4] - logging.debug('called with %r and %r' % (args, kwargs)) + log.debug('called with %r and %r' % (args, kwargs)) return self.cmd(cmd_name, *args, **kwargs) return wrapper From 632b653b2b009ca7501423f4fc74af44aabf75d9 Mon Sep 17 00:00:00 2001 From: Michal Widera Date: Thu, 7 Nov 2019 11:00:59 +0100 Subject: [PATCH 29/56] Fix for subprocess call for Robot Framework execution This change modifies behavior of capture_output flag to work with robot framework. When set to given parameter it'll set stderr and stdout to None for subprocess command call. This fixes problem with error: "UnsupportedOperation: fileno" Signed-off-by: Michal Widera --- python_terraform/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index 94fe2cf..33ab114 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -288,6 +288,9 @@ def cmd(self, cmd, *args, **kwargs): if capture_output is True: stderr = subprocess.PIPE stdout = subprocess.PIPE + elif capture_output == "framework": + stderr = None + stdout = None else: stderr = sys.stderr stdout = sys.stdout From fc584c2a480816bda8c8c65f61f0803fc3c4bb68 Mon Sep 17 00:00:00 2001 From: aubustou Date: Mon, 19 Oct 2020 23:57:22 +0200 Subject: [PATCH 30/56] Migrate to Python 3 only --- .travis.yml | 29 +--- python_terraform/__init__.py | 262 +++++++++++++++++------------------ python_terraform/tfstate.py | 18 ++- 3 files changed, 141 insertions(+), 168 deletions(-) diff --git a/.travis.yml b/.travis.yml index bb62e9c..8e95f16 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,9 @@ language: python python: -- '2.7' -- '3.5' - '3.6' +- '3.7' +- '3.8' +- '3.9' before_install: sudo apt-get install unzip before_script: - export TFVER=0.10.0 @@ -26,27 +27,3 @@ branches: - master - develop - release/** -deploy: -- provider: pypi - distributions: sdist - server: https://testpypi.python.org/pypi - user: beelit94 - password: - secure: sWxc+p/gdq3k2WbUGNG2F4TukFNkTkvq6OPaiOvyfgWThYNk6/juRkMd8flmTbh0VGhcjFbpDLeSApb2kFhfiokYJSH1hOOcmXf8xzYH8/+R4DDEiGa5Y/pR9TBvYu4S8eJEfFUFfb1BBpapykj7o43hcaqMExBJIdVJU7aeoEAC1jQeTJh8wWwdJKHy2dNSM+6RVhk3e5+b0LfK7Bk5sU5P+YdEMj79MJU450J4OmZXWzJgvBN5/2QfVa5LrUD00nYuGuiBniz2lVevIHWjUYawUzpPsTa7F0s2WemG9YcV7U8u06xNjY9Ce3CTbxNhc7OIKq+TCkOgR3qZFXVJ8A87G+AT2iQ01VslQ4DJCxnJNTnpqojWnwf6MFL9O8ONioWYO32bhQFKOQ806ASHP4lNMRDKqx8hXtP5In7/r0SARKscv6Bas83rp+FESkKD5vWgkZJG+yx96LlwRLUhSVnVyb/nOJ++zt5RR3BvY2O4p9YAZY3Qt8TQihOdBQKnY3UXsMyNaE25+yvyNWpmyJiePRbTUd+cpLnycnqG9Ll8v6TpFXb6ahFMjlAFfJNQYlREfseClTHSRjZNxfsXGQCsJh6TZAq7jOB5hCk3q41eOUFWARxbyj8j59NBV8fSQrrGJJ9/VZKQeYiQlBB9KpK4PrnH84oeQ8i+VSbVr5w= - on: - branch: release/** - tags: false - condition: $TRAVIS_PYTHON_VERSION = "3.5" -- provider: pypi - distributions: sdist - user: beelit94 - password: - secure: QhCiTLrBvw/Uzt3eiLEmvMP3uHnayVCETqEDA+2+Q9vFavqj0CHA76zqYonBFqnh0a3HFCRIVVt+6ynpZ10kpQ3tAObIw+pY39ZPnpAhOjSpFzzMdpIF9Bhv9A93ng2iSESAZPAOwktHzUwjFx0Zvl0lSYD9rutHgttGgdU2CajiUtwTUhCTjOAVdR2Gm+15H808vzKWnMaKflXxZt+fkt279mQTYAtz6eBWtZwIKry/uAJCSrPSWtbi50O0HsWRMXLXWH5Jn/BVjWSDSM92DssUDq0D+tQyp4M5nQXJ9EyAvEdsKNLx3cvNruznh2ohI2jmcoIjwFiS6+wrEmUiXkP86iyzCSqL/EbcOG0xUh3vbfYtMBp7jENgD405+3SEhPY4PlqUmc+HDtB7FUcHz4y7wGWJRGyQzNnjJ6Tv0Ajdz5mfJubWVIvHjcRqkxTVtUKt50o00xZ62M0ZzQkDTIHQEsZly0XeHAgSvNzWkmjt9BiBrZ9OkoWVkRpSrCBy/EcpDNPCTSfSzOQ0Nq1ePFjkkW1n8QWDW9Pdb+/7/P2y9E2S8CT+nXBkRQeQiO86Qf1Ireg7k9TA5VYisVZ6bEXEc9UV0mAojpSsC7zWhVlbAoltN6ZbjKmqy/wqn2QIcJemcSie0JigzKpdw7l8FPT2lCRyTKlYLpRyKXzSkNI= - on: - branch: master - tags: false - condition: $TRAVIS_PYTHON_VERSION = "3.5" -notifications: - email: - recipients: - - beelit94@gmail.com diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index 33ab114..a0533a7 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -# above is for compatibility of python2.7.11 - import subprocess import os import sys @@ -11,17 +8,10 @@ from python_terraform.tfstate import Tfstate -try: # Python 2.7+ - from logging import NullHandler -except ImportError: - class NullHandler(logging.Handler): - def emit(self, record): - pass +logger = logging.getLogger(__name__) -log = logging.getLogger(__name__) -log.addHandler(NullHandler()) +COMMAND_WITH_SUBCOMMANDS = {"workspace"} -COMMAND_WITH_SUBCOMMANDS = ['workspace'] class IsFlagged: pass @@ -32,27 +22,29 @@ 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 + 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 + """Wrapper of terraform command line tool. + https://www.terraform.io/ """ - def __init__(self, working_dir=None, - targets=None, - state=None, - variables=None, - parallelism=None, - var_file=None, - terraform_bin_path=None, - is_env_vars_included=True, - ): + def __init__( + self, + working_dir=None, + targets=None, + state=None, + variables=None, + parallelism=None, + var_file=None, + terraform_bin_path=None, + is_env_vars_included=True, + ): """ :param working_dir: the folder of the working folder, if not given, will be current working folder @@ -75,8 +67,9 @@ def __init__(self, working_dir=None, self.targets = [] if targets is None else targets self.variables = dict() if variables is None else variables self.parallelism = parallelism - self.terraform_bin_path = terraform_bin_path \ - if terraform_bin_path else 'terraform' + self.terraform_bin_path = ( + terraform_bin_path if terraform_bin_path else "terraform" + ) self.var_file = var_file self.temp_var_files = VariableFiles() @@ -87,17 +80,23 @@ def __init__(self, working_dir=None, def __getattr__(self, item): def wrapper(*args, **kwargs): cmd_name = str(item) - if cmd_name.endswith('_cmd'): + if cmd_name.endswith("_cmd"): cmd_name = cmd_name[:-4] - log.debug('called with %r and %r' % (args, kwargs)) + logger.debug("called with %r and %r", args, kwargs) return self.cmd(cmd_name, *args, **kwargs) return wrapper - 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 + 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 + no-color is flagged by default :param no_color: disable color of stdout :param input: disable prompt for a missing variable @@ -107,58 +106,63 @@ def apply(self, dir_or_plan=None, input=False, skip_plan=False, no_color=IsFlagg :returns return_code, stdout, stderr """ default = kwargs - default['input'] = input - default['no_color'] = no_color - default['auto-approve'] = (skip_plan == True) + default["input"] = input + default["no_color"] = no_color + default["auto-approve"] = skip_plan is True option_dict = self._generate_default_options(default) args = self._generate_default_args(dir_or_plan) - return self.cmd('apply', *args, **option_dict) + return self.cmd("apply", *args, **option_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 - option_dict['var_file'] = self.var_file - option_dict['parallelism'] = self.parallelism - option_dict['no_color'] = IsFlagged - option_dict['input'] = False + option_dict["state"] = self.state + option_dict["target"] = self.targets + option_dict["var"] = self.variables + option_dict["var_file"] = self.var_file + option_dict["parallelism"] = self.parallelism + option_dict["no_color"] = IsFlagged + option_dict["input"] = False option_dict.update(input_options) return option_dict def destroy(self, dir_or_plan=None, force=IsFlagged, **kwargs): - """ - refer to https://www.terraform.io/docs/commands/destroy.html + """Refer to https://www.terraform.io/docs/commands/destroy.html + force/no-color option is flagged by default :return: ret_code, stdout, stderr """ default = kwargs - default['force'] = force + default["force"] = force options = self._generate_default_options(default) args = self._generate_default_args(dir_or_plan) - return self.cmd('destroy', *args, **options) + return self.cmd("destroy", *args, **options) def plan(self, dir_or_plan=None, detailed_exitcode=IsFlagged, **kwargs): - """ - refer to https://www.terraform.io/docs/commands/plan.html + """Refer 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["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 init(self, dir_or_plan=None, backend_config=None, - reconfigure=IsFlagged, backend=True, **kwargs): - """ - refer to https://www.terraform.io/docs/commands/init.html + return self.cmd("plan", *args, **options) + + def init( + self, + dir_or_plan=None, + backend_config=None, + reconfigure=IsFlagged, + backend=True, + **kwargs, + ): + """Refer to https://www.terraform.io/docs/commands/init.html By default, this assumes you want to use backend config, and tries to init fresh. The flags -reconfigure and -backend=true are default. @@ -166,7 +170,7 @@ def init(self, dir_or_plan=None, backend_config=None, :param dir_or_plan: relative path to the folder want to init :param backend_config: a dictionary of backend config options. eg. t = Terraform() - t.init(backend_config={'access_key': 'myaccesskey', + t.init(backend_config={'access_key': 'myaccesskey', 'secret_key': 'mysecretkey', 'bucket': 'mybucketname'}) :param reconfigure: whether or not to force reconfiguration of backend :param backend: whether or not to use backend settings for init @@ -174,16 +178,15 @@ def init(self, dir_or_plan=None, backend_config=None, :return: ret_code, stdout, stderr """ options = kwargs - options['backend_config'] = backend_config - options['reconfigure'] = reconfigure - options['backend'] = backend + options["backend_config"] = backend_config + options["reconfigure"] = reconfigure + options["backend"] = backend options = self._generate_default_options(options) args = self._generate_default_args(dir_or_plan) - return self.cmd('init', *args, **options) + return self.cmd("init", *args, **options) def generate_cmd_string(self, cmd, *args, **kwargs): - """ - for any generate_cmd_string doesn't written as public method of terraform + """For any generate_cmd_string doesn't written as public method of Terraform examples: 1. call import command, @@ -213,50 +216,50 @@ def generate_cmd_string(self, cmd, *args, **kwargs): cmds.append(subcommand) for option, value in kwargs.items(): - if '_' in option: - option = option.replace('_', '-') + if "_" in option: + option = option.replace("_", "-") if type(value) is list: for sub_v in value: - cmds += ['-{k}={v}'.format(k=option, v=sub_v)] + cmds += [f"-{option}={sub_v}"] continue if type(value) is dict: - if 'backend-config' in option: + if "backend-config" in option: for bk, bv in value.items(): - cmds += ['-backend-config={k}={v}'.format(k=bk, v=bv)] + cmds += [f"-backend-config={bk}={bv}"] continue # since map type sent in string won't work, create temp var file for # variables, and clean it up later - elif option == 'var': + 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)] + cmds += [f"-var-file={filename}"] continue # simple flag, if value is IsFlagged: - cmds += ['-{k}'.format(k=option)] + cmds += ["-{k}".format(k=option)] continue if value is None or value is IsNotFlagged: continue if type(value) is bool: - value = 'true' if value else 'false' + value = "true" if value else "false" - cmds += ['-{k}={v}'.format(k=option, v=value)] + cmds += [f"-{option}={value}"] cmds += args return cmds def cmd(self, cmd, *args, **kwargs): - """ - run a terraform command, if success, will try to read state file + """Run a terraform command, if success, will try to read state file + :param cmd: command and sub-command of terraform, seperated with space refer to https://www.terraform.io/docs/commands/index.html :param args: arguments of a command @@ -281,10 +284,10 @@ def cmd(self, cmd, *args, **kwargs): 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) - synchronous = kwargs.pop('synchronous', True) - + capture_output = kwargs.pop("capture_output", True) + raise_on_error = kwargs.pop("raise_on_error", False) + synchronous = kwargs.pop("synchronous", True) + if capture_output is True: stderr = subprocess.PIPE stdout = subprocess.PIPE @@ -296,7 +299,7 @@ def cmd(self, cmd, *args, **kwargs): stdout = sys.stdout cmds = self.generate_cmd_string(cmd, *args, **kwargs) - log.debug('command: {c}'.format(c=' '.join(cmds))) + logger.debug("Command: %s", " ".join(cmds)) working_folder = self.working_dir if self.working_dir else None @@ -304,47 +307,46 @@ def cmd(self, cmd, *args, **kwargs): if self.is_env_vars_included: environ_vars = os.environ.copy() - p = subprocess.Popen(cmds, stdout=stdout, stderr=stderr, - cwd=working_folder, env=environ_vars) + p = subprocess.Popen( + cmds, stdout=stdout, stderr=stderr, cwd=working_folder, env=environ_vars + ) if not synchronous: return p, None, None out, err = p.communicate() ret_code = p.returncode - log.debug('output: {o}'.format(o=out)) + logger.debug("output: %s", out) if ret_code == 0: self.read_state_file() else: - log.warning('error: {e}'.format(e=err)) + logger.warning("error: %s", err) self.temp_var_files.clean_up() if capture_output is True: - out = out.decode('utf-8') - err = err.decode('utf-8') + out = out.decode() + err = err.decode() else: out = None err = None - if ret_code != 0 and raise_on_error: - raise TerraformCommandError( - ret_code, ' '.join(cmds), out=out, err=err) + if ret_code 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 + """Refer https://www.terraform.io/docs/commands/output.html - 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". + 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, + :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' @@ -357,15 +359,15 @@ def output(self, *args, **kwargs): 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 - if not kwargs.get('capture_output', True) is True: - raise ValueError('capture_output is required for this method') + full_value = kwargs.pop("full_value", False) + name_provided = bool(len(args)) + kwargs["json"] = IsFlagged + 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) - if ret != 0: + if ret: return None out = out.lstrip() @@ -373,68 +375,63 @@ def output(self, *args, **kwargs): value = json.loads(out) if name_provided and not full_value: - value = value['value'] + value = value["value"] return value def read_state_file(self, file_path=None): - """ - read .tfstate file + """Read .tfstate file + :param file_path: relative path to working dir :return: states file in dict type """ - working_dir = self.working_dir or '' + working_dir = self.working_dir or "" - file_path = file_path or self.state or '' + file_path = file_path or self.state or "" if not file_path: - backend_path = os.path.join(file_path, '.terraform', - 'terraform.tfstate') + backend_path = os.path.join(file_path, ".terraform", "terraform.tfstate") if os.path.exists(os.path.join(working_dir, backend_path)): file_path = backend_path else: - file_path = os.path.join(file_path, 'terraform.tfstate') + file_path = os.path.join(file_path, "terraform.tfstate") file_path = os.path.join(working_dir, file_path) self.tfstate = Tfstate.load_file(file_path) def set_workspace(self, workspace, *args, **kwargs): - """ - set workspace + """Set workspace + :param workspace: the desired workspace. :return: status """ - - return self.cmd('workspace', 'select', workspace, *args, **kwargs) + return self.cmd("workspace", "select", workspace, *args, **kwargs) def create_workspace(self, workspace, *args, **kwargs): - """ - create workspace + """Create workspace + :param workspace: the desired workspace. :return: status """ - - return self.cmd('workspace', 'new', workspace, *args, **kwargs) + return self.cmd("workspace", "new", workspace, *args, **kwargs) def delete_workspace(self, workspace, *args, **kwargs): - """ - delete workspace + """Delete workspace + :param workspace: the desired workspace. :return: status """ - - return self.cmd('workspace', 'delete', workspace, *args, **kwargs) + return self.cmd("workspace", "delete", workspace, *args, **kwargs) def show_workspace(self, **kwargs): - """ - show workspace, this command does not need the [DIR] part + """Show workspace, this command does not need the [DIR] part + :return: workspace """ - - return self.cmd('workspace', 'show', **kwargs) + return self.cmd("workspace", "show", **kwargs) def __exit__(self, exc_type, exc_value, traceback): self.temp_var_files.clean_up() @@ -445,11 +442,12 @@ def __init__(self): self.files = [] def create(self, variables): - with tempfile.NamedTemporaryFile('w+t', suffix='.tfvars.json', delete=False) as temp: - log.debug('{0} is created'.format(temp.name)) + with tempfile.NamedTemporaryFile( + "w+t", suffix=".tfvars.json", delete=False + ) as temp: + logger.debug("%s is created", temp.name) self.files.append(temp) - log.debug( - 'variables wrote to tempfile: {0}'.format(str(variables))) + logger.debug("variables wrote to tempfile: %s", variables) temp.write(json.dumps(variables)) file_name = temp.name diff --git a/python_terraform/tfstate.py b/python_terraform/tfstate.py index 497e757..2ecd64b 100644 --- a/python_terraform/tfstate.py +++ b/python_terraform/tfstate.py @@ -1,14 +1,11 @@ -# -*- coding: utf-8 -*- -# above is for compatibility of python2.7.11 - import json import os import logging -log = logging.getLogger(__name__) +logger = logging.getLogger(__name__) -class Tfstate(object): +class Tfstate: def __init__(self, data=None): self.tfstate_file = None self.native_data = data @@ -17,10 +14,11 @@ def __init__(self, data=None): @staticmethod def load_file(file_path): + """Read the tfstate file and load its contents. + + Parses then as JSON and put the result into the object. """ - Read the tfstate file and load its contents, parses then as JSON and put the result into the object - """ - log.debug('read data from {0}'.format(file_path)) + logger.debug('read data from %s', file_path) if os.path.exists(file_path): with open(file_path) as f: json_data = json.load(f) @@ -29,6 +27,6 @@ def load_file(file_path): tf_state.tfstate_file = file_path return tf_state - log.debug('{0} is not exist'.format(file_path)) + logger.debug('%s is not exist', file_path) - return Tfstate() \ No newline at end of file + return Tfstate() From 47cec4d211cb74de8ede86ddd937acf08f015de2 Mon Sep 17 00:00:00 2001 From: aubustou Date: Tue, 20 Oct 2020 00:24:16 +0200 Subject: [PATCH 31/56] Bump Terraform version in travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8e95f16..5ab820d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ python: - '3.9' before_install: sudo apt-get install unzip before_script: - - export TFVER=0.10.0 + - export TFVER=0.13.4 - export TFURL=https://releases.hashicorp.com/terraform/ - TFURL+=$TFVER - TFURL+="/terraform_" From ffd9e1de2a9ed60036be5edf3e52bf8954277528 Mon Sep 17 00:00:00 2001 From: aubustou Date: Tue, 20 Oct 2020 00:24:33 +0200 Subject: [PATCH 32/56] Fix tests --- test/test_terraform.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/test_terraform.py b/test/test_terraform.py index b8a79a5..96e8dd6 100644 --- a/test/test_terraform.py +++ b/test/test_terraform.py @@ -50,7 +50,7 @@ '', 1, False, - 'command: terraform import -no-color aws_instance.foo i-abcd1234', + 'Command: terraform import -no-color aws_instance.foo i-abcd1234', '' ], # try import aws instance with raise_on_error @@ -59,7 +59,7 @@ '', 1, True, - 'command: terraform import -no-color aws_instance.foo i-abcd1234', + 'Command: terraform import -no-color aws_instance.foo i-abcd1234', '' ], # test with space and special character in file path @@ -77,7 +77,7 @@ '', 0, False, - 'command: terraform workspace show -no-color', + 'Command: terraform workspace show -no-color', '' ], ] @@ -181,7 +181,7 @@ def test_cmd(self, method, expected_output, expected_ret_code, expected_exceptio ret = e.returncode out = e.out err = e.err - + logs = string_logger() logs = logs.replace('\n', '') if isinstance(expected_output, list): @@ -362,8 +362,8 @@ def test_fmt(self, fmt_test_file): def test_import(self, string_logger): tf = Terraform(working_dir=current_path) 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() - + assert 'Command: terraform import -no-color aws_instance.foo i-abc1234' in string_logger() + def test_create_workspace(self, workspace_setup_teardown): workspace_name = 'test' with workspace_setup_teardown(workspace_name, create=False) as tf: @@ -384,7 +384,7 @@ def test_create_workspace_with_args( logs = string_logger() logs = logs.replace('\n', '') - expected_log = 'command: terraform workspace new -no-color test {}'.format(current_path) + 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): @@ -405,7 +405,7 @@ def test_set_workspace_with_args( logs = string_logger() logs = logs.replace('\n', '') - expected_log = 'command: terraform workspace select -no-color test {}'.format(current_path) + 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): @@ -427,7 +427,7 @@ def test_show_workspace_with_no_color( logs = string_logger() logs = logs.replace('\n', '') - expected_log = 'command: terraform workspace show -no-color' + expected_log = 'Command: terraform workspace show -no-color' assert expected_log in logs def test_delete_workspace(self, workspace_setup_teardown): @@ -453,5 +453,5 @@ def test_delete_workspace_with_args( logs = string_logger() logs = logs.replace('\n', '') - expected_log = 'command: terraform workspace delete -force test {}'.format(current_path) + expected_log = 'Command: terraform workspace delete -force test {}'.format(current_path) assert expected_log in logs From 61a76803d4c4f3671f3c924d79e86e7f01dabbd0 Mon Sep 17 00:00:00 2001 From: aubustou Date: Tue, 20 Oct 2020 00:24:47 +0200 Subject: [PATCH 33/56] Bump python version in travis --- tox.ini | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index 1f9a9cd..a3ecede 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,13 @@ # content of: tox.ini , put in same dir as setup.py [tox] -envlist = py27, py35, py36 +envlist = py36, py37, py38, py39 [testenv] deps=pytest commands=py.test test [travis] python = - 2.7: py27 - 3.5: py35 - 3.6: py36 \ No newline at end of file + 3.6: py36 + 3.7: py37 + 3.8: py38 + 3.9: py39 From 151c5dc92ae6c61afaa80e01e49a9235bce74eb7 Mon Sep 17 00:00:00 2001 From: aubustou Date: Tue, 20 Oct 2020 00:30:02 +0200 Subject: [PATCH 34/56] Add pre-commit and apply Black format --- .bumpversion.cfg | 1 - .pre-commit-config.yaml | 28 ++ CHANGELOG.md | 2 +- DESCRIPTION.rst | 2 +- LICENSE.txt | 2 +- README.md | 68 ++--- python_terraform/__init__.py | 7 +- python_terraform/tfstate.py | 6 +- requirements.txt | 4 +- setup.py | 47 +-- test/test_terraform.py | 413 +++++++++++++++------------ test/var_to_output/test_map_var.json | 2 +- 12 files changed, 322 insertions(+), 260 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 1c1f0f9..a582bb3 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -4,4 +4,3 @@ commit = True tag = False [bumpversion:file:setup.py] - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..c2348b9 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +default_language_version: + python: python3.6 +fail_fast: true +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.1.0 # v2.1.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-docstring-first + - id: check-json + - id: check-merge-conflict + - id: check-toml + - id: check-yaml + - id: debug-statements + - id: requirements-txt-fixer +- repo: https://github.com/pre-commit/mirrors-isort + rev: v4.3.21 + hooks: + - id: isort +- repo: https://github.com/lovesegfault/beautysh + rev: 6.0.1 + hooks: + - id: beautysh +- repo: https://github.com/psf/black + rev: 19.10b0 + hooks: + - id: black diff --git a/CHANGELOG.md b/CHANGELOG.md index f7fa6ec..9020c8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,4 +16,4 @@ ## [0.10.1] 1. [#48] adding extension for temp file to adopt the change in terraform 0.12.0 -1. [#49] add workspace support \ No newline at end of file +1. [#49] add workspace support diff --git a/DESCRIPTION.rst b/DESCRIPTION.rst index f9deb59..f94e562 100644 --- a/DESCRIPTION.rst +++ b/DESCRIPTION.rst @@ -1,3 +1,3 @@ Please see README at github_ -.. _github: https://github.com/beelit94/python-terraform/blob/master/README.md \ No newline at end of file +.. _github: https://github.com/beelit94/python-terraform/blob/master/README.md diff --git a/LICENSE.txt b/LICENSE.txt index 70afd37..5cb3228 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -4,4 +4,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 1540092..1ec9e47 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ## Introduction -python-terraform is a python module provide a wrapper of `terraform` command line tool. +python-terraform is a python module provide a wrapper of `terraform` command line tool. `terraform` is a tool made by Hashicorp, please refer to https://terraform.io/ ### Status @@ -8,7 +8,7 @@ python-terraform is a python module provide a wrapper of `terraform` command lin ## Installation pip install python-terraform - + ## Usage #### For any terraform command @@ -29,44 +29,44 @@ or just call cmd method directly from python_terraform import * t = Terraform() return_code, stdout, stderr = t.cmd(, *arguments, **options) - + #### For any argument simply pass the string to arguments of the method, for example, - terraform apply target_dir + terraform apply target_dir --> .apply('target_dir') - terraform import aws_instance.foo i-abcd1234 + terraform import aws_instance.foo i-abcd1234 --> .import('aws_instance.foo', 'i-abcd1234') #### For any options - + * dash to underscore remove first dash, and then use underscore to replace dash symbol as option name - + ex. -no-color --> no_color * for a simple flag option - use ```IsFlagged/None``` as value for raising/not raising flag, for example, - - terraform taint -allow-missing + use ```IsFlagged/None``` as value for raising/not raising flag, for example, + + terraform taint -allow-missing --> .taint(allow_missing=IsFlagged) - terraform taint + terraform taint --> .taint(allow_missing=None) or .taint() terraform apply -no-color --> .apply(no_color=IsFlagged) - + * for a boolean value option - + assign True or False, for example, - + terraform apply -refresh=true --> .apply(refresh=True) - + * if a flag could be used multiple times, assign a list to it's value - + terraform apply -target=aws_instance.foo[1] -target=aws_instance.foo[2] - ---> + ---> .apply(target=['aws_instance.foo[1]', 'aws_instance.foo[2]']) * for the "var" flag, assign dictionary to it @@ -84,19 +84,19 @@ By default, stdout and stderr are captured and returned. This causes the applica return_code, stdout, stderr = t.(capture_output=False) ## Examples -### Have a test.tf file under folder "/home/test" +### Have a test.tf file under folder "/home/test" #### 1. apply with variables a=b, c=d, refresh=false, no color in the output In shell: cd /home/test terraform apply -var='a=b' -var='c=d' -refresh=false -no-color - + In python-terraform: from python_terraform import * tf = Terraform(working_dir='/home/test') tf.apply(no_color=IsFlagged, refresh=False, var={'a':'b', 'c':'d'}) - + or from python_terraform import * @@ -108,40 +108,32 @@ or from python_terraform import * 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: cd /home/test - terraform fmt -diff=true - + terraform fmt -diff=true + In python-terraform: - + from python_terraform import * tf = terraform(working_dir='/home/test') tf.fmt(diff=True) - + ## default values -for apply/plan/destroy command, assign with following default value to make +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. +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 +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 +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` - - - - - - - - diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index a0533a7..6cf05b0 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -1,13 +1,12 @@ -import subprocess -import os -import sys import json import logging +import os +import subprocess +import sys import tempfile from python_terraform.tfstate import Tfstate - logger = logging.getLogger(__name__) COMMAND_WITH_SUBCOMMANDS = {"workspace"} diff --git a/python_terraform/tfstate.py b/python_terraform/tfstate.py index 2ecd64b..e16cf36 100644 --- a/python_terraform/tfstate.py +++ b/python_terraform/tfstate.py @@ -1,6 +1,6 @@ import json -import os import logging +import os logger = logging.getLogger(__name__) @@ -18,7 +18,7 @@ def load_file(file_path): Parses then as JSON and put the result into the object. """ - logger.debug('read data from %s', file_path) + logger.debug("read data from %s", file_path) if os.path.exists(file_path): with open(file_path) as f: json_data = json.load(f) @@ -27,6 +27,6 @@ def load_file(file_path): tf_state.tfstate_file = file_path return tf_state - logger.debug('%s is not exist', file_path) + logger.debug("%s is not exist", file_path) return Tfstate() diff --git a/requirements.txt b/requirements.txt index 0740785..85323d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -tox-pyenv pytest -tox \ No newline at end of file +tox +tox-pyenv diff --git a/setup.py b/setup.py index 2659f1e..65125c4 100644 --- a/setup.py +++ b/setup.py @@ -7,12 +7,13 @@ from distutils.core import setup dependencies = [] -module_name = 'python-terraform' -short_description = 'This is a python module provide a wrapper ' \ - 'of terraform command line tool' +module_name = "python-terraform" +short_description = ( + "This is a python module provide a wrapper " "of terraform command line tool" +) try: - with open('DESCRIPTION.rst') as f: + with open("DESCRIPTION.rst") as f: long_description = f.read() except IOError: long_description = short_description @@ -20,36 +21,36 @@ setup( name=module_name, - version='0.10.2', - url='https://github.com/beelit94/python-terraform', - license='MIT', - author='Freddy Tan', - author_email='beelit94@gmail.com', + version="0.10.2", + url="https://github.com/beelit94/python-terraform", + license="MIT", + author="Freddy Tan", + author_email="beelit94@gmail.com", description=short_description, long_description=long_description, - packages=['python_terraform'], + packages=["python_terraform"], package_data={}, - platforms='any', + platforms="any", install_requires=dependencies, classifiers=[ # As from http://pypi.python.org/pypi?%3Aaction=list_classifiers # 'Development Status :: 1 - Planning', # 'Development Status :: 2 - Pre-Alpha', # 'Development Status :: 3 - Alpha', - 'Development Status :: 4 - Beta', + "Development Status :: 4 - Beta", # 'Development Status :: 5 - Production/Stable', # 'Development Status :: 6 - Mature', # 'Development Status :: 7 - Inactive', - 'Environment :: Console', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: POSIX', - 'Operating System :: MacOS', - 'Operating System :: Unix', + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: POSIX", + "Operating System :: MacOS", + "Operating System :: Unix", # 'Operating System :: Windows', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 3', - 'Topic :: Software Development :: Libraries :: Python Modules', - ] + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 3", + "Topic :: Software Development :: Libraries :: Python Modules", + ], ) diff --git a/test/test_terraform.py b/test/test_terraform.py index 96e8dd6..4d4a3ab 100644 --- a/test/test_terraform.py +++ b/test/test_terraform.py @@ -1,15 +1,16 @@ try: - from cStringIO import StringIO # Python 2 + from cStringIO import StringIO # Python 2 except ImportError: from io import StringIO -from python_terraform import * -from contextlib import contextmanager -import pytest -import os +import fnmatch import logging +import os import re import shutil -import fnmatch +from contextlib import contextmanager + +import pytest +from python_terraform import * logging.basicConfig(level=logging.DEBUG) root_logger = logging.getLogger() @@ -18,78 +19,97 @@ FILE_PATH_WITH_SPACE_AND_SPACIAL_CHARS = "test 'test.out!" STRING_CASES = [ - [ - lambda x: x.generate_cmd_string('apply', 'the_folder', - no_color=IsFlagged), - "terraform apply -no-color the_folder" - ], - [ - lambda x: x.generate_cmd_string('push', 'path', vcs=True, - token='token', - atlas_address='url'), - "terraform push -vcs=true -token=token -atlas-address=url path" - ], - ] + [ + lambda x: x.generate_cmd_string("apply", "the_folder", no_color=IsFlagged), + "terraform apply -no-color the_folder", + ], + [ + lambda x: x.generate_cmd_string( + "push", "path", vcs=True, token="token", atlas_address="url" + ), + "terraform push -vcs=true -token=token -atlas-address=url path", + ], +] CMD_CASES = [ - ['method', 'expected_output', 'expected_ret_code', 'expected_exception', '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'}), + 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 + [ + "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, - '', - 'var_to_output' + "", + "var_to_output", ], # try import aws instance [ - lambda x: x.cmd('import', 'aws_instance.foo', 'i-abcd1234', no_color=IsFlagged), - '', + 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', - '' + "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), - '', + 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', - '' + "Command: terraform import -no-color aws_instance.foo i-abcd1234", + "", ], # test with space and special character in file path [ - lambda x: x.cmd('plan', 'var_to_output', out=FILE_PATH_WITH_SPACE_AND_SPACIAL_CHARS), - '', + lambda x: x.cmd( + "plan", "var_to_output", out=FILE_PATH_WITH_SPACE_AND_SPACIAL_CHARS + ), + "", 0, False, - '', - 'var_to_output' + "", + "var_to_output", ], # test workspace command (commands with subcommand) [ - lambda x: x.cmd('workspace', 'show', no_color=IsFlagged), - '', + lambda x: x.cmd("workspace", "show", no_color=IsFlagged), + "", 0, False, - 'Command: terraform workspace show -no-color', - '' + "Command: terraform workspace show -no-color", + "", ], - ] + ], ] -@pytest.fixture(scope='function') +@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') - shutil.copy(orgin, - target) + target = os.path.join(current_path, "bad_fmt", "test.backup") + orgin = os.path.join(current_path, "bad_fmt", "test.tf") + shutil.copy(orgin, target) def td(): shutil.move(target, orgin) @@ -120,6 +140,7 @@ def workspace_setup_teardown(): 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) @@ -128,7 +149,7 @@ def wrapper(workspace_name, create=True, delete=True, *args, **kwargs): tf.create_workspace(workspace_name, *args, **kwargs) yield tf if delete: - tf.set_workspace('default') + tf.set_workspace("default") tf.delete_workspace(workspace_name) yield wrapper @@ -139,9 +160,7 @@ def teardown_method(self, method): """ teardown any state that was previously setup with a setup_method call. """ - exclude = ['test_tfstate_file', - 'test_tfstate_file2', - 'test_tfstate_file3'] + exclude = ["test_tfstate_file", "test_tfstate_file2", "test_tfstate_file3"] def purge(dir, pattern): for root, dirnames, filenames in os.walk(dir): @@ -153,14 +172,12 @@ def purge(dir, pattern): d = os.path.join(root, dirname) shutil.rmtree(d) - purge('.', '*.tfstate') - purge('.', '*.tfstate.backup') - purge('.', '*.terraform') - purge('.', FILE_PATH_WITH_SPACE_AND_SPACIAL_CHARS) + purge(".", "*.tfstate") + purge(".", "*.tfstate.backup") + purge(".", "*.terraform") + purge(".", FILE_PATH_WITH_SPACE_AND_SPACIAL_CHARS) - @pytest.mark.parametrize([ - "method", "expected" - ], STRING_CASES) + @pytest.mark.parametrize(["method", "expected"], STRING_CASES) def test_generate_cmd_string(self, method, expected): tf = Terraform(working_dir=current_path) result = method(tf) @@ -170,7 +187,16 @@ def test_generate_cmd_string(self, method, expected): assert s in result @pytest.mark.parametrize(*CMD_CASES) - def test_cmd(self, method, expected_output, expected_ret_code, expected_exception, 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) try: @@ -183,7 +209,7 @@ def test_cmd(self, method, expected_output, expected_ret_code, expected_exceptio err = e.err logs = string_logger() - logs = logs.replace('\n', '') + logs = logs.replace("\n", "") if isinstance(expected_output, list): ok = False for xo in expected_output: @@ -200,154 +226,161 @@ def test_cmd(self, method, expected_output, expected_ret_code, expected_exceptio @pytest.mark.parametrize( ("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", {}, None, "\x1b[0m\x1b[1m\x1b[32mApplycomplete!", {"no_color": IsNotFlagged}) - ]) + ("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, options): - tf = Terraform(working_dir=current_path, variables=variables, var_file=var_files) + tf = Terraform( + working_dir=current_path, variables=variables, var_file=var_files + ) # after 0.10.0 we always need to init tf.init(folder) ret, out, err = tf.apply(folder, **options) assert ret == 0 - assert expected_output in out.replace('\n', '').replace(' ', '') - assert err == '' + 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')) + tf.apply(var_file=os.path.join(current_path, "tfvar_file", "test.tfvars")) logs = string_logger() - logs = logs.split('\n') + logs = logs.split("\n") for log in logs: - if log.startswith('command: terraform apply'): - assert log.count('-var-file=') == 1 + if log.startswith("command: terraform apply"): + assert log.count("-var-file=") == 1 @pytest.mark.parametrize( - ['cmd', 'args', 'options'], + ["cmd", "args", "options"], [ # bool value - ('fmt', ['bad_fmt'], {'list': False, 'diff': False}) - ] + ("fmt", ["bad_fmt"], {"list": False, "diff": False}) + ], ) def test_options(self, cmd, args, options, fmt_test_file): tf = Terraform(working_dir=current_path) ret, out, err = getattr(tf, cmd)(*args, **options) assert ret == 0 - assert out == '' + assert out == "" def test_state_data(self): - cwd = os.path.join(current_path, 'test_tfstate_file') - tf = Terraform(working_dir=cwd, state='tfstate.test') + cwd = os.path.join(current_path, "test_tfstate_file") + tf = Terraform(working_dir=cwd, state="tfstate.test") tf.read_state_file() - assert tf.tfstate.modules[0]['path'] == ['root'] + assert tf.tfstate.modules[0]["path"] == ["root"] def test_state_default(self): - cwd = os.path.join(current_path, 'test_tfstate_file2') + cwd = os.path.join(current_path, "test_tfstate_file2") tf = Terraform(working_dir=cwd) tf.read_state_file() - assert tf.tfstate.modules[0]['path'] == ['default'] + assert tf.tfstate.modules[0]["path"] == ["default"] def test_state_default_backend(self): - cwd = os.path.join(current_path, 'test_tfstate_file3') + cwd = os.path.join(current_path, "test_tfstate_file3") tf = Terraform(working_dir=cwd) tf.read_state_file() - assert tf.tfstate.modules[0]['path'] == ['default_backend'] + assert tf.tfstate.modules[0]["path"] == ["default_backend"] def test_pre_load_state_data(self): - cwd = os.path.join(current_path, 'test_tfstate_file') - tf = Terraform(working_dir=cwd, state='tfstate.test') - assert tf.tfstate.modules[0]['path'] == ['root'] + cwd = os.path.join(current_path, "test_tfstate_file") + tf = Terraform(working_dir=cwd, state="tfstate.test") + assert tf.tfstate.modules[0]["path"] == ["root"] @pytest.mark.parametrize( - ("folder", 'variables'), - [ - ("var_to_output", {'test_var': 'test'}) - ] + ("folder", "variables"), [("var_to_output", {"test_var": "test"})] ) def test_override_default(self, folder, variables): tf = Terraform(working_dir=current_path, variables=variables) tf.init(folder) - ret, out, err = tf.apply(folder, var={'test_var': 'test2'}, - 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 - - @pytest.mark.parametrize( - ("param"), - [ - ({}), - ({'module': 'test2'}), - ] - ) + ret, out, err = tf.apply( + folder, var={"test_var": "test2"}, 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 + + @pytest.mark.parametrize(("param"), [({}), ({"module": "test2"}),]) def test_output(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', **param) - regex = re.compile("terraform output (-module=test2 -json|-json -module=test2) test_output") + 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", **param) + 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 == 'test' + assert result == "test" - @pytest.mark.parametrize( - ("param"), - [ - ({}), - ({'module': 'test2'}), - ] - ) + @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") + 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' + assert result["value"] == "test" - @pytest.mark.parametrize( - ("param"), - [ - ({}), - ({'module': 'test2'}), - ] - ) + @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') + 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' + 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') - ret, out, err = tf.destroy('var_to_output') + tf = Terraform(working_dir=current_path, variables={"test_var": "test"}) + tf.init("var_to_output") + ret, out, err = tf.destroy("var_to_output") assert ret == 0 - assert 'Destroy complete! Resources: 0 destroyed.' in out + assert "Destroy complete! Resources: 0 destroyed." in out @pytest.mark.parametrize( - ("plan", "variables", "expected_ret"), - [ - ('vars_require_input', {}, 1) - ] + ("plan", "variables", "expected_ret"), [("vars_require_input", {}, 1)] ) def test_plan(self, plan, variables, expected_ret): tf = Terraform(working_dir=current_path, variables=variables) @@ -355,103 +388,113 @@ def test_plan(self, plan, variables, expected_ret): assert ret == expected_ret def test_fmt(self, fmt_test_file): - tf = Terraform(working_dir=current_path, variables={'test_var': 'test'}) + tf = Terraform(working_dir=current_path, variables={"test_var": "test"}) ret, out, err = tf.fmt(diff=True) assert ret == 0 def test_import(self, string_logger): tf = Terraform(working_dir=current_path) - 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() + 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, workspace_setup_teardown): - workspace_name = 'test' + workspace_name = "test" with workspace_setup_teardown(workspace_name, create=False) as tf: - ret, out, err = tf.create_workspace('test') + ret, out, err = tf.create_workspace("test") assert ret == 0 - assert err == '' + assert err == "" - 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') + 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) + ret, out, err = tf.create_workspace( + "test", current_path, no_color=IsFlagged + ) assert ret == 0 - assert err == '' + assert err == "" logs = string_logger() - logs = logs.replace('\n', '') - expected_log = 'Command: terraform workspace new -no-color test {}'.format(current_path) + 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' + workspace_name = "test" with workspace_setup_teardown(workspace_name) as tf: ret, out, err = tf.set_workspace(workspace_name) assert ret == 0 - assert err == '' + assert err == "" - def test_set_workspace_with_args( - self, workspace_setup_teardown, string_logger): - workspace_name = 'test' + 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) + ret, out, err = tf.set_workspace( + workspace_name, current_path, no_color=IsFlagged + ) assert ret == 0 - assert err == '' + assert err == "" logs = string_logger() - logs = logs.replace('\n', '') - expected_log = 'Command: terraform workspace select -no-color test {}'.format(current_path) + 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' + workspace_name = "test" with workspace_setup_teardown(workspace_name) as tf: ret, out, err = tf.show_workspace() assert ret == 0 - assert err == '' + assert err == "" def test_show_workspace_with_no_color( - self, workspace_setup_teardown, string_logger + self, workspace_setup_teardown, string_logger ): - workspace_name = 'test' + 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 == '' + assert err == "" logs = string_logger() - logs = logs.replace('\n', '') - expected_log = 'Command: terraform workspace show -no-color' + 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' + workspace_name = "test" with workspace_setup_teardown(workspace_name, delete=False) as tf: - tf.set_workspace('default') + tf.set_workspace("default") ret, out, err = tf.delete_workspace(workspace_name) assert ret == 0 - assert err == '' + assert err == "" - def test_delete_workspace_with_args( - self, workspace_setup_teardown, string_logger - ): - workspace_name = 'test' + 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') + tf.set_workspace("default") ret, out, err = tf.delete_workspace( workspace_name, current_path, force=IsFlagged, ) assert ret == 0 - assert err == '' + assert err == "" logs = string_logger() - logs = logs.replace('\n', '') - expected_log = 'Command: terraform workspace delete -force test {}'.format(current_path) + logs = logs.replace("\n", "") + expected_log = "Command: terraform workspace delete -force test {}".format( + current_path + ) assert expected_log in logs diff --git a/test/var_to_output/test_map_var.json b/test/var_to_output/test_map_var.json index 0673f77..78d35a0 100644 --- a/test/var_to_output/test_map_var.json +++ b/test/var_to_output/test_map_var.json @@ -4,4 +4,4 @@ "e": "e", "f": "f" } -} \ No newline at end of file +} From 6ea1320bef60b35f162daf981e10e5d37dc4a611 Mon Sep 17 00:00:00 2001 From: aubustou Date: Wed, 21 Oct 2020 00:08:58 +0200 Subject: [PATCH 35/56] Fix unit tests for Terraform 0.13 --- python_terraform/__init__.py | 17 +++++------------ setup.cfg | 8 ++++++++ test/bad_fmt/test.tf | 10 +++++----- test/test_terraform.py | 37 ++++++++---------------------------- test/var_to_output/test.tf | 10 +++++----- 5 files changed, 31 insertions(+), 51 deletions(-) diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index 6cf05b0..664ed8f 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -90,7 +90,7 @@ def apply( self, dir_or_plan=None, input=False, - skip_plan=False, + skip_plan=True, no_color=IsFlagged, **kwargs, ): @@ -104,10 +104,12 @@ def apply( :param kwargs: same as kwags in method 'cmd' :returns return_code, stdout, stderr """ + if not skip_plan: + return self.plan(dir_or_plan=dir_or_plan, **kwargs) default = kwargs default["input"] = input default["no_color"] = no_color - default["auto-approve"] = skip_plan is True + default["auto-approve"] = True option_dict = self._generate_default_options(default) args = self._generate_default_args(dir_or_plan) return self.cmd("apply", *args, **option_dict) @@ -358,8 +360,6 @@ def output(self, *args, **kwargs): dict of named dicts each with 'value', 'sensitive', and 'type', if NAME is not provided """ - full_value = kwargs.pop("full_value", False) - name_provided = bool(len(args)) kwargs["json"] = IsFlagged if not kwargs.get("capture_output", True) is True: raise ValueError("capture_output is required for this method") @@ -369,14 +369,7 @@ def output(self, *args, **kwargs): if ret: return None - out = out.lstrip() - - value = json.loads(out) - - if name_provided and not full_value: - value = value["value"] - - return value + return json.loads(out.lstrip()) def read_state_file(self, file_path=None): """Read .tfstate file diff --git a/setup.cfg b/setup.cfg index 5e40900..0710cda 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,10 @@ [wheel] universal = 1 + +[isort] +line_length=88 +known_third_party= +indent=' ' +multi_line_output=3 +sections=FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER +include_trailing_comma=true diff --git a/test/bad_fmt/test.tf b/test/bad_fmt/test.tf index b756909..09b9e4d 100644 --- a/test/bad_fmt/test.tf +++ b/test/bad_fmt/test.tf @@ -5,12 +5,12 @@ variable "test_var" { provider "archive" {} variable "test_list_var" { - type = "list" + type = list(string) default = ["a", "b"] } variable "test_map_var" { - type = "map" + type = map default = { "a" = "a" @@ -19,13 +19,13 @@ variable "test_map_var" { } output "test_output" { - value = "${var.test_var}" + value = var.test_var } output "test_list_output" { - value = "${var.test_list_var}" + value = var.test_list_var } output "test_map_output" { - value = "${var.test_map_var}" + value = var.test_map_var } diff --git a/test/test_terraform.py b/test/test_terraform.py index 4d4a3ab..3c45660 100644 --- a/test/test_terraform.py +++ b/test/test_terraform.py @@ -1,16 +1,13 @@ -try: - from cStringIO import StringIO # Python 2 -except ImportError: - from io import StringIO import fnmatch import logging import os import re import shutil from contextlib import contextmanager +from io import StringIO import pytest -from python_terraform import * +from python_terraform import IsFlagged, IsNotFlagged, Terraform, TerraformCommandError logging.basicConfig(level=logging.DEBUG) root_logger = logging.getLogger() @@ -46,10 +43,7 @@ "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 + "Plan: 0 to add, 0 to change, 0 to destroy.", 0, False, "", @@ -231,21 +225,22 @@ def test_cmd( "var_to_output", {"test_list_var": ["c", "d"]}, None, - "test_list_output=[c,d]", + 'test_list_output=["c","d",]', {}, ), ( "var_to_output", {"test_map_var": {"c": "c", "d": "d"}}, None, - "test_map_output={a=ab=bc=cd=d}", + 'test_map_output={"c"="c""d"="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}", + # Values are overriden + 'test_map_output={"e"="e""f"="f"}', {}, ), ( @@ -261,7 +256,6 @@ def test_apply(self, folder, variables, var_files, expected_output, options): tf = Terraform( working_dir=current_path, variables=variables, var_file=var_files ) - # after 0.10.0 we always need to init tf.init(folder) ret, out, err = tf.apply(folder, **options) assert ret == 0 @@ -322,7 +316,7 @@ def test_override_default(self, folder, variables): tf = Terraform(working_dir=current_path, variables=variables) tf.init(folder) ret, out, err = tf.apply( - folder, var={"test_var": "test2"}, no_color=IsNotFlagged + folder, var={"test_var": "test2"}, no_color=IsNotFlagged, ) out = out.replace("\n", "") assert "\x1b[0m\x1b[1m\x1b[32mApply" in out @@ -344,21 +338,6 @@ def test_output(self, param, string_logger): 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"}) diff --git a/test/var_to_output/test.tf b/test/var_to_output/test.tf index 71c2e8c..761d0bb 100644 --- a/test/var_to_output/test.tf +++ b/test/var_to_output/test.tf @@ -5,12 +5,12 @@ variable "test_var" { provider "archive" {} variable "test_list_var" { - type = "list" + type = list(string) default = ["a", "b"] } variable "test_map_var" { - type = "map" + type = map default = { "a" = "a" @@ -19,13 +19,13 @@ variable "test_map_var" { } output "test_output" { - value = "${var.test_var}" + value = var.test_var } output "test_list_output" { - value = "${var.test_list_var}" + value = var.test_list_var } output "test_map_output" { - value = "${var.test_map_var}" + value = var.test_map_var } From 134786ebf03f449c03eb2f8f41fda8c76068ddc6 Mon Sep 17 00:00:00 2001 From: aubustou Date: Wed, 21 Oct 2020 00:10:17 +0200 Subject: [PATCH 36/56] Update isort --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c2348b9..8dbbe15 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,8 +14,8 @@ repos: - id: check-yaml - id: debug-statements - id: requirements-txt-fixer -- repo: https://github.com/pre-commit/mirrors-isort - rev: v4.3.21 +- repo: https://github.com/pycqa/isort + rev: 5.5.2 hooks: - id: isort - repo: https://github.com/lovesegfault/beautysh From ba72a09409439e127ede7dbceaf2d70842d3862c Mon Sep 17 00:00:00 2001 From: aubustou Date: Wed, 21 Oct 2020 00:15:14 +0200 Subject: [PATCH 37/56] Update README --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1ec9e47..56dee7a 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,11 @@ python-terraform is a python module provide a wrapper of `terraform` command line tool. `terraform` is a tool made by Hashicorp, please refer to https://terraform.io/ +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) + ### Status -[![Build Status](https://travis-ci.org/beelit94/python-terraform.svg?branch=develop)](https://travis-ci.org/beelit94/python-terraform) +[![Build Status](https://travis-ci.org/aubustou/python-terraform.svg?branch=develop)](https://travis-ci.org/aubustou/python-terraform) ## Installation pip install python-terraform From 09120a943b2be08eebb06ecabcf40481bce87d60 Mon Sep 17 00:00:00 2001 From: aubustou Date: Wed, 21 Oct 2020 00:28:52 +0200 Subject: [PATCH 38/56] Fix bumpversion --- .bumpversion.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index a582bb3..641c6a3 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.10.1 +current_version = 0.10.2 commit = True tag = False From d83132b61a396cda00fa78c1d1793cf319aaac2d Mon Sep 17 00:00:00 2001 From: aubustou Date: Wed, 21 Oct 2020 00:28:55 +0200 Subject: [PATCH 39/56] =?UTF-8?q?Bump=20version:=200.10.2=20=E2=86=92=200.?= =?UTF-8?q?11.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 641c6a3..5b46bad 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.10.2 +current_version = 0.11.0 commit = True tag = False diff --git a/setup.py b/setup.py index 65125c4..f9c7cb9 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ setup( name=module_name, - version="0.10.2", + version="0.11.0", url="https://github.com/beelit94/python-terraform", license="MIT", author="Freddy Tan", From 053f956774db480eafd8a0682d9b12156151b5d1 Mon Sep 17 00:00:00 2001 From: aubustou Date: Wed, 21 Oct 2020 00:39:43 +0200 Subject: [PATCH 40/56] Remove tox --- .travis.yml | 2 +- requirements.txt | 4 +--- setup.py | 3 ++- tox.ini | 13 ------------- 4 files changed, 4 insertions(+), 18 deletions(-) delete mode 100644 tox.ini diff --git a/.travis.yml b/.travis.yml index 5ab820d..9661ffa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ install: - pip install . script: - export PATH=$PATH:$PWD/tf_bin - - tox + - pytest -v branches: only: - master diff --git a/requirements.txt b/requirements.txt index 85323d6..398f64e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1 @@ -pytest -tox -tox-pyenv +/bin/bash: q : commande introuvable diff --git a/setup.py b/setup.py index f9c7cb9..8629fee 100644 --- a/setup.py +++ b/setup.py @@ -32,6 +32,8 @@ package_data={}, platforms="any", install_requires=dependencies, + tests_require=["pytest"], + python_requires=">=3.6", classifiers=[ # As from http://pypi.python.org/pypi?%3Aaction=list_classifiers # 'Development Status :: 1 - Planning', @@ -49,7 +51,6 @@ "Operating System :: Unix", # 'Operating System :: Windows', "Programming Language :: Python", - "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries :: Python Modules", ], diff --git a/tox.ini b/tox.ini deleted file mode 100644 index a3ecede..0000000 --- a/tox.ini +++ /dev/null @@ -1,13 +0,0 @@ -# content of: tox.ini , put in same dir as setup.py -[tox] -envlist = py36, py37, py38, py39 -[testenv] -deps=pytest -commands=py.test test - -[travis] -python = - 3.6: py36 - 3.7: py37 - 3.8: py38 - 3.9: py39 From b373f85311b81a4aaf2104ac26f15f77bf2d42ba Mon Sep 17 00:00:00 2001 From: aubustou Date: Wed, 21 Oct 2020 00:48:31 +0200 Subject: [PATCH 41/56] Remove Python 3.9 as it is not yet available on Travis --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9661ffa..39c16d6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ python: - '3.6' - '3.7' - '3.8' -- '3.9' before_install: sudo apt-get install unzip before_script: - export TFVER=0.13.4 @@ -17,7 +16,6 @@ before_script: - unzip terraform_bin.zip -d tf_bin install: - curl https://bootstrap.pypa.io/ez_setup.py -o - | python - - pip install tox-travis - pip install . script: - export PATH=$PATH:$PWD/tf_bin From bcd523ff67385c7bb24d6273699993aa58f1b4de Mon Sep 17 00:00:00 2001 From: Francois Lebreau Date: Thu, 21 Jan 2021 01:01:34 +0000 Subject: [PATCH 42/56] Add typing --- .gitignore | 1 + .pre-commit-config.yaml | 1 - .travis.yml | 1 + python_terraform/__init__.py | 454 +------------------------------- python_terraform/terraform.py | 476 ++++++++++++++++++++++++++++++++++ python_terraform/tfstate.py | 7 +- 6 files changed, 484 insertions(+), 456 deletions(-) create mode 100644 python_terraform/terraform.py diff --git a/.gitignore b/.gitignore index a99ba67..b62d9be 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ Icon # virtualenv .virtualenv/ +venv/ # Intellij diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8dbbe15..4c7fa54 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,5 @@ default_language_version: python: python3.6 -fail_fast: true repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v3.1.0 # v2.1.0 diff --git a/.travis.yml b/.travis.yml index 39c16d6..7ab948e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ python: - '3.6' - '3.7' - '3.8' +- '3.9' before_install: sudo apt-get install unzip before_script: - export TFVER=0.13.4 diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index 664ed8f..c9852e5 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -1,452 +1,2 @@ -import json -import logging -import os -import subprocess -import sys -import tempfile - -from python_terraform.tfstate import Tfstate - -logger = logging.getLogger(__name__) - -COMMAND_WITH_SUBCOMMANDS = {"workspace"} - - -class IsFlagged: - pass - - -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. - - https://www.terraform.io/ - """ - - def __init__( - self, - working_dir=None, - targets=None, - state=None, - variables=None, - parallelism=None, - var_file=None, - terraform_bin_path=None, - is_env_vars_included=True, - ): - """ - :param working_dir: the folder of the working folder, if not given, - will be current working folder - :param targets: list of target - 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 - :type is_env_vars_included: bool - :param is_env_vars_included: included env variables when calling terraform cmd - """ - self.is_env_vars_included = is_env_vars_included - self.working_dir = working_dir - self.state = state - self.targets = [] if targets is None else targets - self.variables = dict() if variables is None else variables - self.parallelism = parallelism - self.terraform_bin_path = ( - terraform_bin_path if terraform_bin_path else "terraform" - ) - self.var_file = var_file - self.temp_var_files = VariableFiles() - - # store the tfstate data - self.tfstate = None - self.read_state_file(self.state) - - def __getattr__(self, item): - def wrapper(*args, **kwargs): - cmd_name = str(item) - if cmd_name.endswith("_cmd"): - cmd_name = cmd_name[:-4] - logger.debug("called with %r and %r", args, kwargs) - return self.cmd(cmd_name, *args, **kwargs) - - return wrapper - - def apply( - self, - dir_or_plan=None, - input=False, - skip_plan=True, - 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 skip_plan: force apply without plan (default: false) - :param kwargs: same as kwags in method 'cmd' - :returns return_code, stdout, stderr - """ - if not skip_plan: - return self.plan(dir_or_plan=dir_or_plan, **kwargs) - default = kwargs - default["input"] = input - default["no_color"] = no_color - default["auto-approve"] = True - option_dict = self._generate_default_options(default) - args = self._generate_default_args(dir_or_plan) - return self.cmd("apply", *args, **option_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 - option_dict["var_file"] = self.var_file - option_dict["parallelism"] = self.parallelism - option_dict["no_color"] = IsFlagged - option_dict["input"] = False - option_dict.update(input_options) - return option_dict - - 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 = 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): - """Refer 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 init( - self, - dir_or_plan=None, - backend_config=None, - reconfigure=IsFlagged, - backend=True, - **kwargs, - ): - """Refer to https://www.terraform.io/docs/commands/init.html - - By default, this assumes you want to use backend config, and tries to - init fresh. The flags -reconfigure and -backend=true are default. - - :param dir_or_plan: relative path to the folder want to init - :param backend_config: a dictionary of backend config options. eg. - t = Terraform() - t.init(backend_config={'access_key': 'myaccesskey', - 'secret_key': 'mysecretkey', 'bucket': 'mybucketname'}) - :param reconfigure: whether or not to force reconfiguration of backend - :param backend: whether or not to use backend settings for init - :param kwargs: options - :return: ret_code, stdout, stderr - """ - options = kwargs - options["backend_config"] = backend_config - options["reconfigure"] = reconfigure - options["backend"] = backend - options = self._generate_default_options(options) - args = self._generate_default_args(dir_or_plan) - return self.cmd("init", *args, **options) - - def generate_cmd_string(self, cmd, *args, **kwargs): - """For any generate_cmd_string doesn't written as public method of Terraform - - examples: - 1. call import command, - ref to https://www.terraform.io/docs/commands/import.html - --> generate_cmd_string call: - terraform import -input=true aws_instance.foo i-abcd1234 - --> python call: - tf.generate_cmd_string('import', 'aws_instance.foo', 'i-abcd1234', input=True) - - 2. call apply command, - --> generate_cmd_string call: - terraform apply -var='a=b' -var='c=d' -no-color the_folder - --> python call: - tf.generate_cmd_string('apply', the_folder, no_color=IsFlagged, var={'a':'b', 'c':'d'}) - - :param cmd: command and sub-command of terraform, seperated with space - refer to https://www.terraform.io/docs/commands/index.html - :param args: arguments of a command - :param kwargs: same as kwags in method 'cmd' - :return: string of valid terraform command - """ - 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: - option = option.replace("_", "-") - - if type(value) is list: - for sub_v in value: - cmds += [f"-{option}={sub_v}"] - continue - - if type(value) is dict: - if "backend-config" in option: - for bk, bv in value.items(): - cmds += [f"-backend-config={bk}={bv}"] - continue - - # since map type sent in string won't work, create temp var file for - # variables, and clean it up later - 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 += [f"-var-file={filename}"] - - continue - - # simple flag, - if value is IsFlagged: - cmds += ["-{k}".format(k=option)] - continue - - if value is None or value is IsNotFlagged: - continue - - if type(value) is bool: - value = "true" if value else "false" - - cmds += [f"-{option}={value}"] - - cmds += args - return cmds - - def cmd(self, cmd, *args, **kwargs): - """Run a terraform command, if success, will try to read state file - - :param cmd: command and sub-command of terraform, seperated with space - refer to https://www.terraform.io/docs/commands/index.html - :param args: arguments of a command - :param kwargs: any option flag with key value without prefixed dash character - if there's a dash in the option name, use under line instead of dash, - ex. -no-color --> no_color - if it's a simple flag with no value, value should be IsFlagged - ex. cmd('taint', allow_missing=IsFlagged) - if it's a boolean value flag, assign True or false - if it's a flag could be used multiple times, assign list to it's value - if it's a "var" variable flag, assign dictionary to it - if a value is None, will skip this option - 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) - synchronous = kwargs.pop("synchronous", True) - - if capture_output is True: - stderr = subprocess.PIPE - stdout = subprocess.PIPE - elif capture_output == "framework": - stderr = None - stdout = None - else: - stderr = sys.stderr - stdout = sys.stdout - - cmds = self.generate_cmd_string(cmd, *args, **kwargs) - logger.debug("Command: %s", " ".join(cmds)) - - working_folder = self.working_dir if self.working_dir else None - - environ_vars = {} - if self.is_env_vars_included: - environ_vars = os.environ.copy() - - p = subprocess.Popen( - cmds, stdout=stdout, stderr=stderr, cwd=working_folder, env=environ_vars - ) - - if not synchronous: - return p, None, None - - out, err = p.communicate() - ret_code = p.returncode - logger.debug("output: %s", out) - - if ret_code == 0: - self.read_state_file() - else: - logger.warning("error: %s", err) - - self.temp_var_files.clean_up() - if capture_output is True: - out = out.decode() - err = err.decode() - else: - out = None - err = None - - if ret_code and raise_on_error: - raise TerraformCommandError(ret_code, " ".join(cmds), out=out, err=err) - - return ret_code, out, err - - def output(self, *args, **kwargs): - """Refer https://www.terraform.io/docs/commands/output.html - - 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 - """ - kwargs["json"] = IsFlagged - 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) - - if ret: - return None - - return json.loads(out.lstrip()) - - def read_state_file(self, file_path=None): - """Read .tfstate file - - :param file_path: relative path to working dir - :return: states file in dict type - """ - - working_dir = self.working_dir or "" - - file_path = file_path or self.state or "" - - if not file_path: - backend_path = os.path.join(file_path, ".terraform", "terraform.tfstate") - - if os.path.exists(os.path.join(working_dir, backend_path)): - file_path = backend_path - else: - file_path = os.path.join(file_path, "terraform.tfstate") - - file_path = os.path.join(working_dir, file_path) - - self.tfstate = Tfstate.load_file(file_path) - - def set_workspace(self, workspace, *args, **kwargs): - """Set workspace - - :param workspace: the desired workspace. - :return: status - """ - return self.cmd("workspace", "select", workspace, *args, **kwargs) - - def create_workspace(self, workspace, *args, **kwargs): - """Create workspace - - :param workspace: the desired workspace. - :return: status - """ - return self.cmd("workspace", "new", workspace, *args, **kwargs) - - def delete_workspace(self, workspace, *args, **kwargs): - """Delete workspace - - :param workspace: the desired workspace. - :return: status - """ - return self.cmd("workspace", "delete", workspace, *args, **kwargs) - - def show_workspace(self, **kwargs): - """Show workspace, this command does not need the [DIR] part - - :return: workspace - """ - return self.cmd("workspace", "show", **kwargs) - - def __exit__(self, exc_type, exc_value, traceback): - self.temp_var_files.clean_up() - - -class VariableFiles(object): - def __init__(self): - self.files = [] - - def create(self, variables): - with tempfile.NamedTemporaryFile( - "w+t", suffix=".tfvars.json", delete=False - ) as temp: - logger.debug("%s is created", temp.name) - self.files.append(temp) - logger.debug("variables wrote to tempfile: %s", variables) - temp.write(json.dumps(variables)) - file_name = temp.name - - return file_name - - def clean_up(self): - for f in self.files: - os.unlink(f.name) - - self.files = [] +from .terraform import Terraform, VariableFiles +from .tfstate import Tfstate diff --git a/python_terraform/terraform.py b/python_terraform/terraform.py new file mode 100644 index 0000000..cd4b712 --- /dev/null +++ b/python_terraform/terraform.py @@ -0,0 +1,476 @@ +import json +import logging +import os +import subprocess +import sys +import tempfile +from typing import Callable, Dict, List, Optional, Sequence, Tuple, Type, TypeVar, Union + +from python_terraform.tfstate import Tfstate + +logger = logging.getLogger(__name__) + +COMMAND_WITH_SUBCOMMANDS = {"workspace"} + + +class TerraformFlag: + pass + + +class IsFlagged(TerraformFlag): + pass + + +class IsNotFlagged(TerraformFlag): + pass + + +CommandOutput = Tuple[Optional[int], Optional[str], Optional[str]] + + +class TerraformCommandError(subprocess.CalledProcessError): + def __init__(self, ret_code: int, cmd: str, out: Optional[str], err: Optional[str]): + super(TerraformCommandError, self).__init__(ret_code, cmd) + self.out = out + self.err = err + + +class Terraform: + """Wrapper of terraform command line tool. + + https://www.terraform.io/ + """ + + def __init__( + self, + working_dir: Optional[str] = None, + targets: Optional[Sequence[str]] = None, + state: Optional[str] = None, + variables: Optional[Sequence[str]] = None, + parallelism: Optional[str] = None, + var_file: Optional[str] = None, + terraform_bin_path: Optional[str] = None, + is_env_vars_included: bool = True, + ): + """ + :param working_dir: the folder of the working folder, if not given, + will be current working folder + :param targets: list of target + 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 + :type is_env_vars_included: bool + :param is_env_vars_included: included env variables when calling terraform cmd + """ + self.is_env_vars_included = is_env_vars_included + self.working_dir = working_dir + self.state = state + self.targets = [] if targets is None else targets + self.variables = dict() if variables is None else variables + self.parallelism = parallelism + self.terraform_bin_path = ( + terraform_bin_path if terraform_bin_path else "terraform" + ) + self.var_file = var_file + self.temp_var_files = VariableFiles() + + # store the tfstate data + self.tfstate = None + self.read_state_file(self.state) + + def __getattr__(self, item: str) -> Callable: + def wrapper(*args, **kwargs): + cmd_name = str(item) + if cmd_name.endswith("_cmd"): + cmd_name = cmd_name[:-4] + logger.debug("called with %r and %r", args, kwargs) + return self.cmd(cmd_name, *args, **kwargs) + + return wrapper + + def apply( + self, + dir_or_plan: Optional[str] = None, + input: bool = False, + skip_plan: bool = True, + no_color: Type[TerraformFlag] = IsFlagged, + **kwargs, + ) -> CommandOutput: + """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 skip_plan: force apply without plan (default: false) + :param kwargs: same as kwags in method 'cmd' + :returns return_code, stdout, stderr + """ + if not skip_plan: + return self.plan(dir_or_plan=dir_or_plan, **kwargs) + default = kwargs + default["input"] = input + default["no_color"] = no_color + default["auto-approve"] = True + option_dict = self._generate_default_options(default) + args = self._generate_default_args(dir_or_plan) + return self.cmd("apply", *args, **option_dict) + + def _generate_default_args(self, dir_or_plan) -> Sequence[str]: + 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 + option_dict["var_file"] = self.var_file + option_dict["parallelism"] = self.parallelism + option_dict["no_color"] = IsFlagged + option_dict["input"] = False + option_dict.update(input_options) + return option_dict + + def destroy( + self, + dir_or_plan: Optional[str] = None, + force: Type[TerraformFlag] = IsFlagged, + **kwargs, + ) -> CommandOutput: + """Refer to https://www.terraform.io/docs/commands/destroy.html + + force/no-color option is flagged by default + :return: ret_code, stdout, stderr + """ + 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: Optional[str] = None, + detailed_exitcode: Type[TerraformFlag] = IsFlagged, + **kwargs, + ) -> CommandOutput: + """Refer 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 init( + self, + dir_or_plan: Optional[str] = None, + backend_config: Optional[Dict[str, str]] = None, + reconfigure: Type[TerraformFlag] = IsFlagged, + backend: bool = True, + **kwargs, + ) -> CommandOutput: + """Refer to https://www.terraform.io/docs/commands/init.html + + By default, this assumes you want to use backend config, and tries to + init fresh. The flags -reconfigure and -backend=true are default. + + :param dir_or_plan: relative path to the folder want to init + :param backend_config: a dictionary of backend config options. eg. + t = Terraform() + t.init(backend_config={'access_key': 'myaccesskey', + 'secret_key': 'mysecretkey', 'bucket': 'mybucketname'}) + :param reconfigure: whether or not to force reconfiguration of backend + :param backend: whether or not to use backend settings for init + :param kwargs: options + :return: ret_code, stdout, stderr + """ + options = kwargs + options["backend_config"] = backend_config + options["reconfigure"] = reconfigure + options["backend"] = backend + options = self._generate_default_options(options) + args = self._generate_default_args(dir_or_plan) + return self.cmd("init", *args, **options) + + def generate_cmd_string(self, cmd: str, *args, **kwargs) -> List[str]: + """For any generate_cmd_string doesn't written as public method of Terraform + + examples: + 1. call import command, + ref to https://www.terraform.io/docs/commands/import.html + --> generate_cmd_string call: + terraform import -input=true aws_instance.foo i-abcd1234 + --> python call: + tf.generate_cmd_string('import', 'aws_instance.foo', 'i-abcd1234', input=True) + + 2. call apply command, + --> generate_cmd_string call: + terraform apply -var='a=b' -var='c=d' -no-color the_folder + --> python call: + tf.generate_cmd_string('apply', the_folder, no_color=IsFlagged, var={'a':'b', 'c':'d'}) + + :param cmd: command and sub-command of terraform, seperated with space + refer to https://www.terraform.io/docs/commands/index.html + :param args: arguments of a command + :param kwargs: same as kwags in method 'cmd' + :return: string of valid terraform command + """ + 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: + option = option.replace("_", "-") + + if isinstance(value, list): + for sub_v in value: + cmds += [f"-{option}={sub_v}"] + continue + + if isinstance(value, dict): + if "backend-config" in option: + for bk, bv in value.items(): + cmds += [f"-backend-config={bk}={bv}"] + continue + + # since map type sent in string won't work, create temp var file for + # variables, and clean it up later + 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 += [f"-var-file={filename}"] + + continue + + # simple flag, + if value is IsFlagged: + cmds += [f"-{option}"] + continue + + if value is None or value is IsNotFlagged: + continue + + if isinstance(value, bool): + value = "true" if value else "false" + + cmds += [f"-{option}={value}"] + + cmds += args + return cmds + + def cmd( + self, + cmd: str, + *args, + capture_output: Union[bool, str] = True, + raise_on_error: bool = False, + synchronous: bool = True, + **kwargs, + ) -> CommandOutput: + """Run a terraform command, if success, will try to read state file + + :param cmd: command and sub-command of terraform, seperated with space + refer to https://www.terraform.io/docs/commands/index.html + :param args: arguments of a command + :param kwargs: any option flag with key value without prefixed dash character + if there's a dash in the option name, use under line instead of dash, + ex. -no-color --> no_color + if it's a simple flag with no value, value should be IsFlagged + ex. cmd('taint', allow_missing=IsFlagged) + if it's a boolean value flag, assign True or false + if it's a flag could be used multiple times, assign list to it's value + if it's a "var" variable flag, assign dictionary to it + if a value is None, will skip this option + 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 + """ + if capture_output is True: + stderr = subprocess.PIPE + stdout = subprocess.PIPE + elif capture_output == "framework": + stderr = None + stdout = None + else: + stderr = sys.stderr + stdout = sys.stdout + + cmds = self.generate_cmd_string(cmd, *args, **kwargs) + logger.debug("Command: %s", " ".join(cmds)) + + working_folder = self.working_dir if self.working_dir else None + + environ_vars = {} + if self.is_env_vars_included: + environ_vars = os.environ.copy() + + p = subprocess.Popen( + cmds, stdout=stdout, stderr=stderr, cwd=working_folder, env=environ_vars + ) + + if not synchronous: + return None, None, None + + out, err = p.communicate() + ret_code = p.returncode + logger.debug("output: %s", out) + + if ret_code == 0: + self.read_state_file() + else: + logger.warning("error: %s", err) + + self.temp_var_files.clean_up() + if capture_output is True: + out = out.decode() + err = err.decode() + else: + out = None + err = None + + if ret_code and raise_on_error: + raise TerraformCommandError(ret_code, " ".join(cmds), out=out, err=err) + + return ret_code, out, err + + def output( + self, *args, capture_output: bool = True, **kwargs + ) -> Union[None, str, Dict[str, str], Dict[str, Dict[str, str]]]: + """Refer https://www.terraform.io/docs/commands/output.html + + 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 + """ + kwargs["json"] = IsFlagged + if capture_output is False: + raise ValueError("capture_output is required for this method") + + ret, out, _ = self.output_cmd(*args, **kwargs) + + if ret: + return None + + return json.loads(out.lstrip()) + + def read_state_file(self, file_path=None) -> None: + """Read .tfstate file + + :param file_path: relative path to working dir + :return: states file in dict type + """ + + working_dir = self.working_dir or "" + + file_path = file_path or self.state or "" + + if not file_path: + backend_path = os.path.join(file_path, ".terraform", "terraform.tfstate") + + if os.path.exists(os.path.join(working_dir, backend_path)): + file_path = backend_path + else: + file_path = os.path.join(file_path, "terraform.tfstate") + + file_path = os.path.join(working_dir, file_path) + + self.tfstate = Tfstate.load_file(file_path) + + def set_workspace(self, workspace, *args, **kwargs) -> CommandOutput: + """Set workspace + + :param workspace: the desired workspace. + :return: status + """ + return self.cmd("workspace", "select", workspace, *args, **kwargs) + + def create_workspace(self, workspace, *args, **kwargs) -> CommandOutput: + """Create workspace + + :param workspace: the desired workspace. + :return: status + """ + return self.cmd("workspace", "new", workspace, *args, **kwargs) + + def delete_workspace(self, workspace, *args, **kwargs) -> CommandOutput: + """Delete workspace + + :param workspace: the desired workspace. + :return: status + """ + return self.cmd("workspace", "delete", workspace, *args, **kwargs) + + def show_workspace(self, **kwargs) -> CommandOutput: + """Show workspace, this command does not need the [DIR] part + + :return: workspace + """ + return self.cmd("workspace", "show", **kwargs) + + def __exit__(self, exc_type, exc_value, traceback) -> None: + self.temp_var_files.clean_up() + + +class VariableFiles: + def __init__(self): + self.files = [] + + def create(self, variables: Dict[str, str]) -> str: + with tempfile.NamedTemporaryFile( + "w+t", suffix=".tfvars.json", delete=False + ) as temp: + logger.debug("%s is created", temp.name) + self.files.append(temp) + logger.debug("variables wrote to tempfile: %s", variables) + temp.write(json.dumps(variables)) + file_name = temp.name + + return file_name + + def clean_up(self): + for f in self.files: + os.unlink(f.name) + + self.files = [] diff --git a/python_terraform/tfstate.py b/python_terraform/tfstate.py index e16cf36..20061b9 100644 --- a/python_terraform/tfstate.py +++ b/python_terraform/tfstate.py @@ -1,19 +1,20 @@ import json import logging import os +from typing import Dict, Optional logger = logging.getLogger(__name__) class Tfstate: - def __init__(self, data=None): - self.tfstate_file = None + def __init__(self, data: Optional[Dict[str, str]] = None): + self.tfstate_file: Optional[str] = None self.native_data = data if data: self.__dict__ = data @staticmethod - def load_file(file_path): + def load_file(file_path: str) -> "Tfstate": """Read the tfstate file and load its contents. Parses then as JSON and put the result into the object. From 44675d38f56bc25c8b3f40b5e3f3550ed6b17771 Mon Sep 17 00:00:00 2001 From: Francois Lebreau Date: Thu, 21 Jan 2021 01:21:48 +0000 Subject: [PATCH 43/56] Release 0.12.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8629fee..aff5d00 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ setup( name=module_name, - version="0.11.0", + version="0.12.0", url="https://github.com/beelit94/python-terraform", license="MIT", author="Freddy Tan", From 785e9a3ce7ee679346cdeab6a100fbf7a197ffed Mon Sep 17 00:00:00 2001 From: Francois Lebreau Date: Thu, 21 Jan 2021 10:07:00 +0000 Subject: [PATCH 44/56] Fix tests --- .gitignore | 1 + python_terraform/__init__.py | 8 +++++++- test/test_terraform.py | 36 ++++++++++++++---------------------- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index b62d9be..d330498 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ venv/ # VSCode .vscode/ +pyrightconfig.json diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index c9852e5..c2e2c10 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -1,2 +1,8 @@ -from .terraform import Terraform, VariableFiles +from .terraform import ( + IsFlagged, + IsNotFlagged, + Terraform, + TerraformCommandError, + VariableFiles, +) from .tfstate import Tfstate diff --git a/test/test_terraform.py b/test/test_terraform.py index 3c45660..38b6199 100644 --- a/test/test_terraform.py +++ b/test/test_terraform.py @@ -5,8 +5,10 @@ import shutil from contextlib import contextmanager from io import StringIO +from typing import Callable import pytest + from python_terraform import IsFlagged, IsNotFlagged, Terraform, TerraformCommandError logging.basicConfig(level=logging.DEBUG) @@ -113,7 +115,7 @@ def td(): @pytest.fixture() -def string_logger(request): +def string_logger(request) -> Callable[..., str]: log_stream = StringIO() handler = logging.StreamHandler(log_stream) root_logger.addHandler(handler) @@ -150,13 +152,13 @@ def wrapper(workspace_name, create=True, delete=True, *args, **kwargs): class TestTerraform(object): - def teardown_method(self, method): + def teardown_method(self, _) -> None: """ teardown any state that was previously setup with a setup_method call. """ exclude = ["test_tfstate_file", "test_tfstate_file2", "test_tfstate_file3"] - def purge(dir, pattern): + def purge(dir: str, pattern: str) -> None: for root, dirnames, filenames in os.walk(dir): dirnames[:] = [d for d in dirnames if d not in exclude] for filename in fnmatch.filter(filenames, pattern): @@ -172,7 +174,7 @@ def purge(dir, pattern): purge(".", FILE_PATH_WITH_SPACE_AND_SPACIAL_CHARS) @pytest.mark.parametrize(["method", "expected"], STRING_CASES) - def test_generate_cmd_string(self, method, expected): + def test_generate_cmd_string(self, method: Callable[..., str], expected: str): tf = Terraform(working_dir=current_path) result = method(tf) @@ -183,37 +185,27 @@ def test_generate_cmd_string(self, method, expected): @pytest.mark.parametrize(*CMD_CASES) def test_cmd( self, - method, - expected_output, - expected_ret_code, - expected_exception, - expected_logs, - string_logger, + method: Callable[..., str], + expected_output: str, + expected_ret_code: int, + expected_exception: bool, + expected_logs: str, + string_logger: Callable[..., str], folder, ): tf = Terraform(working_dir=current_path) tf.init(folder) try: - ret, out, err = method(tf) + ret, out, _ = 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", "") - 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_output in out assert expected_ret_code == ret assert expected_logs in logs From 7080f38a939b2e397944691e0f4f6be5af1b0a60 Mon Sep 17 00:00:00 2001 From: Francois Lebreau Date: Sun, 28 Feb 2021 13:44:58 +0000 Subject: [PATCH 45/56] Support Terraform 0.14 --- python_terraform/terraform.py | 57 +++++---- python_terraform/tfstate.py | 2 +- test/test_terraform.py | 228 +++++++++++++++------------------- 3 files changed, 136 insertions(+), 151 deletions(-) diff --git a/python_terraform/terraform.py b/python_terraform/terraform.py index cd4b712..8117da1 100644 --- a/python_terraform/terraform.py +++ b/python_terraform/terraform.py @@ -4,7 +4,7 @@ import subprocess import sys import tempfile -from typing import Callable, Dict, List, Optional, Sequence, Tuple, Type, TypeVar, Union +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Type, Union from python_terraform.tfstate import Tfstate @@ -33,6 +33,7 @@ def __init__(self, ret_code: int, cmd: str, out: Optional[str], err: Optional[st super(TerraformCommandError, self).__init__(ret_code, cmd) self.out = out self.err = err + logger.error("Error with command %s. Reason: %s", self.cmd, self.err) class Terraform: @@ -46,7 +47,7 @@ def __init__( working_dir: Optional[str] = None, targets: Optional[Sequence[str]] = None, state: Optional[str] = None, - variables: Optional[Sequence[str]] = None, + variables: Optional[Dict[str, str]] = None, parallelism: Optional[str] = None, var_file: Optional[str] = None, terraform_bin_path: Optional[str] = None, @@ -114,28 +115,30 @@ def apply( """ if not skip_plan: return self.plan(dir_or_plan=dir_or_plan, **kwargs) - default = kwargs + default = kwargs.copy() default["input"] = input default["no_color"] = no_color - default["auto-approve"] = True + default["auto-approve"] = True # a False value will require an input option_dict = self._generate_default_options(default) args = self._generate_default_args(dir_or_plan) return self.cmd("apply", *args, **option_dict) - def _generate_default_args(self, dir_or_plan) -> Sequence[str]: + def _generate_default_args(self, dir_or_plan: Optional[str]) -> Sequence[str]: 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 - option_dict["var_file"] = self.var_file - option_dict["parallelism"] = self.parallelism - option_dict["no_color"] = IsFlagged - option_dict["input"] = False - option_dict.update(input_options) - return option_dict + def _generate_default_options( + self, input_options: Dict[str, Any] + ) -> Dict[str, Any]: + return { + "state": self.state, + "target": self.targets, + "var": self.variables, + "var_file": self.var_file, + "parallelism": self.parallelism, + "no_color": IsFlagged, + "input": False, + **input_options, + } def destroy( self, @@ -148,7 +151,7 @@ def destroy( force/no-color option is flagged by default :return: ret_code, stdout, stderr """ - default = kwargs + default = kwargs.copy() default["force"] = force options = self._generate_default_options(default) args = self._generate_default_args(dir_or_plan) @@ -167,7 +170,7 @@ def plan( :param kwargs: options :return: ret_code, stdout, stderr """ - options = kwargs + options = kwargs.copy() options["detailed_exitcode"] = detailed_exitcode options = self._generate_default_options(options) args = self._generate_default_args(dir_or_plan) @@ -196,10 +199,14 @@ def init( :param kwargs: options :return: ret_code, stdout, stderr """ - options = kwargs - options["backend_config"] = backend_config - options["reconfigure"] = reconfigure - options["backend"] = backend + options = kwargs.copy() + options.update( + { + "backend_config": backend_config, + "reconfigure": reconfigure, + "backend": backend, + } + ) options = self._generate_default_options(options) args = self._generate_default_args(dir_or_plan) return self.cmd("init", *args, **options) @@ -281,7 +288,7 @@ def cmd( cmd: str, *args, capture_output: Union[bool, str] = True, - raise_on_error: bool = False, + raise_on_error: bool = True, synchronous: bool = True, **kwargs, ) -> CommandOutput: @@ -322,7 +329,7 @@ def cmd( stdout = sys.stdout cmds = self.generate_cmd_string(cmd, *args, **kwargs) - logger.debug("Command: %s", " ".join(cmds)) + logger.info("Command: %s", " ".join(cmds)) working_folder = self.working_dir if self.working_dir else None @@ -339,7 +346,7 @@ def cmd( out, err = p.communicate() ret_code = p.returncode - logger.debug("output: %s", out) + logger.info("output: %s", out) if ret_code == 0: self.read_state_file() diff --git a/python_terraform/tfstate.py b/python_terraform/tfstate.py index 20061b9..f39af0f 100644 --- a/python_terraform/tfstate.py +++ b/python_terraform/tfstate.py @@ -28,6 +28,6 @@ def load_file(file_path: str) -> "Tfstate": tf_state.tfstate_file = file_path return tf_state - logger.debug("%s is not exist", file_path) + logger.debug("%s does not exist", file_path) return Tfstate() diff --git a/test/test_terraform.py b/test/test_terraform.py index 38b6199..221f6d3 100644 --- a/test/test_terraform.py +++ b/test/test_terraform.py @@ -8,6 +8,7 @@ from typing import Callable import pytest +from _pytest.logging import LogCaptureFixture, caplog from python_terraform import IsFlagged, IsNotFlagged, Terraform, TerraformCommandError @@ -42,7 +43,11 @@ [ [ lambda x: x.cmd( - "plan", "var_to_output", no_color=IsFlagged, var={"test_var": "test"} + "plan", + "var_to_output", + no_color=IsFlagged, + var={"test_var": "test"}, + raise_on_error=False, ), # Expected output varies by terraform version "Plan: 0 to add, 0 to change, 0 to destroy.", @@ -52,35 +57,27 @@ "var_to_output", ], # try import aws instance - [ - 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, + raise_on_error=False, ), "", 1, - True, - "Command: terraform import -no-color aws_instance.foo i-abcd1234", + False, + "Error: No Terraform configuration files", "", ], # test with space and special character in file path [ lambda x: x.cmd( - "plan", "var_to_output", out=FILE_PATH_WITH_SPACE_AND_SPACIAL_CHARS + "plan", + "var_to_output", + out=FILE_PATH_WITH_SPACE_AND_SPACIAL_CHARS, + raise_on_error=False, ), "", 0, @@ -90,7 +87,9 @@ ], # test workspace command (commands with subcommand) [ - lambda x: x.cmd("workspace", "show", no_color=IsFlagged), + lambda x: x.cmd( + "workspace", "show", no_color=IsFlagged, raise_on_error=False + ), "", 0, False, @@ -114,24 +113,23 @@ def td(): return -@pytest.fixture() -def string_logger(request) -> Callable[..., str]: - log_stream = StringIO() - handler = logging.StreamHandler(log_stream) - root_logger.addHandler(handler) +# @pytest.fixture() +# def string_logger(request) -> Callable[..., str]: +# log_stream = StringIO() +# handler = logging.StreamHandler(log_stream) +# root_logger.addHandler(handler) - def td(): - root_logger.removeHandler(handler) - log_stream.close() +# def td(): +# root_logger.removeHandler(handler) +# log_stream.close() - request.addfinalizer(td) - return lambda: str(log_stream.getvalue()) +# request.addfinalizer(td) +# return lambda: str(log_stream.getvalue()) @pytest.fixture() def workspace_setup_teardown(): - """ - Fixture used in workspace related tests + """Fixture used in workspace related tests. Create and tear down a workspace *Use as a contextmanager* @@ -151,11 +149,9 @@ def wrapper(workspace_name, create=True, delete=True, *args, **kwargs): yield wrapper -class TestTerraform(object): +class TestTerraform: def teardown_method(self, _) -> None: - """ teardown any state that was previously setup with a setup_method - call. - """ + """Teardown any state that was previously setup with a setup_method call.""" exclude = ["test_tfstate_file", "test_tfstate_file2", "test_tfstate_file3"] def purge(dir: str, pattern: str) -> None: @@ -190,24 +186,23 @@ def test_cmd( expected_ret_code: int, expected_exception: bool, expected_logs: str, - string_logger: Callable[..., str], - folder, + caplog: LogCaptureFixture, + folder: str, ): - tf = Terraform(working_dir=current_path) - tf.init(folder) - try: - ret, out, _ = method(tf) - assert not expected_exception - except TerraformCommandError as e: - assert expected_exception - ret = e.returncode - out = e.out - - logs = string_logger() - logs = logs.replace("\n", "") + with caplog.at_level(logging.INFO): + tf = Terraform(working_dir=current_path) + tf.init(folder) + try: + ret, out, _ = method(tf) + assert not expected_exception + except TerraformCommandError as e: + assert expected_exception + ret = e.returncode + out = e.out + assert expected_output in out assert expected_ret_code == ret - assert expected_logs in logs + assert expected_logs in caplog.text @pytest.mark.parametrize( ("folder", "variables", "var_files", "expected_output", "options"), @@ -254,15 +249,18 @@ def test_apply(self, folder, variables, var_files, expected_output, options): 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) + def test_apply_with_var_file(self, caplog: LogCaptureFixture): + with caplog.at_level(logging.INFO): + 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"): + folder = "var_to_output" + tf.init(folder) + tf.apply( + folder, + var_file=os.path.join(current_path, "tfvar_files", "test.tfvars"), + ) + for log in caplog.messages: + if log.startswith("Command: terraform apply"): assert log.count("-var-file=") == 1 @pytest.mark.parametrize( @@ -315,33 +313,23 @@ def test_override_default(self, folder, variables): out = tf.output("test_output") assert "test2" in out - @pytest.mark.parametrize(("param"), [({}), ({"module": "test2"}),]) - def test_output(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", **param) - 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 == "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 + @pytest.mark.parametrize("output_all", [True, False]) + def test_output(self, caplog: LogCaptureFixture, output_all: bool): + expected_value = "test" + required_output = "test_output" + with caplog.at_level(logging.INFO): + tf = Terraform( + working_dir=current_path, variables={"test_var": expected_value} + ) + tf.init("var_to_output") + tf.apply("var_to_output") + params = tuple() if output_all else (required_output,) + result = tf.output(*params) + if output_all: + assert result[required_output]["value"] == expected_value else: - assert result["test_output"]["value"] == "test" + assert result == expected_value + assert expected_value in caplog.messages[-1] def test_destroy(self): tf = Terraform(working_dir=current_path, variables={"test_var": "test"}) @@ -355,22 +343,19 @@ def test_destroy(self): ) 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 + tf.init(plan) + with pytest.raises(TerraformCommandError) as e: + tf.plan(plan) + assert ( + e.value.err + == """\nError: Missing required argument\n\nThe argument "region" is required, but was not set.\n\n""" + ) 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 - def test_import(self, string_logger): - tf = Terraform(working_dir=current_path) - 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, workspace_setup_teardown): workspace_name = "test" with workspace_setup_teardown(workspace_name, create=False) as tf: @@ -378,25 +363,24 @@ def test_create_workspace(self, workspace_setup_teardown): assert ret == 0 assert err == "" - def test_create_workspace_with_args(self, workspace_setup_teardown, string_logger): + def test_create_workspace_with_args(self, workspace_setup_teardown, caplog): 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: + with workspace_setup_teardown( + workspace_name, create=False + ) as tf, caplog.at_level(logging.INFO): ret, out, err = tf.create_workspace( "test", current_path, no_color=IsFlagged ) assert ret == 0 assert err == "" - - logs = string_logger() - logs = logs.replace("\n", "") - expected_log = "Command: terraform workspace new -no-color test {}".format( - current_path + assert ( + f"Command: terraform workspace new -no-color test {current_path}" + in caplog.messages ) - assert expected_log in logs def test_set_workspace(self, workspace_setup_teardown): workspace_name = "test" @@ -405,22 +389,21 @@ def test_set_workspace(self, workspace_setup_teardown): assert ret == 0 assert err == "" - def test_set_workspace_with_args(self, workspace_setup_teardown, string_logger): + def test_set_workspace_with_args(self, workspace_setup_teardown, caplog): workspace_name = "test" - with workspace_setup_teardown(workspace_name) as tf: + with workspace_setup_teardown(workspace_name) as tf, caplog.at_level( + logging.INFO + ): 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 ( + f"Command: terraform workspace select -no-color test {current_path}" + in caplog.messages ) - assert expected_log in logs def test_show_workspace(self, workspace_setup_teardown): workspace_name = "test" @@ -429,20 +412,16 @@ def test_show_workspace(self, workspace_setup_teardown): assert ret == 0 assert err == "" - def test_show_workspace_with_no_color( - self, workspace_setup_teardown, string_logger - ): + def test_show_workspace_with_no_color(self, workspace_setup_teardown, caplog): workspace_name = "test" - with workspace_setup_teardown(workspace_name) as tf: + with workspace_setup_teardown(workspace_name) as tf, caplog.at_level( + logging.INFO + ): 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 + assert "Command: terraform workspace show -no-color" in caplog.messages def test_delete_workspace(self, workspace_setup_teardown): workspace_name = "test" @@ -452,9 +431,11 @@ def test_delete_workspace(self, workspace_setup_teardown): assert ret == 0 assert err == "" - def test_delete_workspace_with_args(self, workspace_setup_teardown, string_logger): + def test_delete_workspace_with_args(self, workspace_setup_teardown, caplog): workspace_name = "test" - with workspace_setup_teardown(workspace_name, delete=False) as tf: + with workspace_setup_teardown( + workspace_name, delete=False + ) as tf, caplog.at_level(logging.INFO): tf.set_workspace("default") ret, out, err = tf.delete_workspace( workspace_name, current_path, force=IsFlagged, @@ -462,10 +443,7 @@ def test_delete_workspace_with_args(self, workspace_setup_teardown, string_logge assert ret == 0 assert err == "" - - logs = string_logger() - logs = logs.replace("\n", "") - expected_log = "Command: terraform workspace delete -force test {}".format( - current_path + assert ( + f"Command: terraform workspace delete -force test {current_path}" + in caplog.messages ) - assert expected_log in logs From edfc33b0bdaba409afc5a8d3e0130763590eca8b Mon Sep 17 00:00:00 2001 From: Francois Lebreau Date: Sun, 28 Feb 2021 14:02:43 +0000 Subject: [PATCH 46/56] Release 0.14.0 --- .bumpversion.cfg | 6 ------ setup.py | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) delete mode 100644 .bumpversion.cfg diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index 5b46bad..0000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,6 +0,0 @@ -[bumpversion] -current_version = 0.11.0 -commit = True -tag = False - -[bumpversion:file:setup.py] diff --git a/setup.py b/setup.py index aff5d00..f3bace4 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ setup( name=module_name, - version="0.12.0", + version="0.14.0", url="https://github.com/beelit94/python-terraform", license="MIT", author="Freddy Tan", From 93cb7877aa9c270ec931bfe46c8299b0de7b2637 Mon Sep 17 00:00:00 2001 From: "Md. Fasihul Kabir" Date: Sun, 4 Apr 2021 01:46:33 +0600 Subject: [PATCH 47/56] Added support for getting list of workspaces --- python_terraform/terraform.py | 7 +++++++ test/test_terraform.py | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/python_terraform/terraform.py b/python_terraform/terraform.py index 8117da1..19d7251 100644 --- a/python_terraform/terraform.py +++ b/python_terraform/terraform.py @@ -456,6 +456,13 @@ def show_workspace(self, **kwargs) -> CommandOutput: """ return self.cmd("workspace", "show", **kwargs) + def list_workspace(self) -> List[str]: + """List of workspaces + + :return: workspaces + """ + return list(filter(lambda x: len(x) > 0, map(lambda x: x.strip('*').strip(), self.cmd("workspace", "list")[1].split()))) + def __exit__(self, exc_type, exc_value, traceback) -> None: self.temp_var_files.clean_up() diff --git a/test/test_terraform.py b/test/test_terraform.py index 221f6d3..d6d966e 100644 --- a/test/test_terraform.py +++ b/test/test_terraform.py @@ -447,3 +447,9 @@ def test_delete_workspace_with_args(self, workspace_setup_teardown, caplog): f"Command: terraform workspace delete -force test {current_path}" in caplog.messages ) + + def test_list_workspace(self): + tf = Terraform(working_dir=current_path) + workspaces = tf.list_workspace() + assert len(workspaces) > 0 + assert 'default' in workspaces \ No newline at end of file From f6003d8e2fc331e20baa377dcf40e233c4481399 Mon Sep 17 00:00:00 2001 From: Kiran Kumar Kotari Date: Thu, 27 May 2021 11:39:22 +0200 Subject: [PATCH 48/56] Create python-publish.yml Auto publish python package to pypi --- .github/workflows/python-publish.yml | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/python-publish.yml diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 0000000..b826ba6 --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,31 @@ +name: Upload Python Package + +on: + release: + types: [created] + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build python package + run: | + python setup.py sdist bdist_wheel + - name: Publish package + uses: pypa/gh-action-pypi-publish@v1.4.2 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + skip_existing: true + verbose: true From ebfcce2a21d3cc485b916eaebb327a8d13031e9d Mon Sep 17 00:00:00 2001 From: "Md. Fasihul Kabir" Date: Mon, 31 May 2021 11:38:45 +0600 Subject: [PATCH 49/56] fixed all the PR comments --- python_terraform/terraform.py | 14 +++++++++++++- test/test_terraform.py | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/python_terraform/terraform.py b/python_terraform/terraform.py index 19d7251..dc723db 100644 --- a/python_terraform/terraform.py +++ b/python_terraform/terraform.py @@ -460,8 +460,20 @@ def list_workspace(self) -> List[str]: """List of workspaces :return: workspaces + :example: + >>> tf = Terraform() + >>> tf.list_workspace() + ['default', 'test'] """ - return list(filter(lambda x: len(x) > 0, map(lambda x: x.strip('*').strip(), self.cmd("workspace", "list")[1].split()))) + return list( + filter( + lambda x: len(x) > 0, + map( + lambda x: x.strip('*').strip(), + (self.cmd("workspace", "list")[1] or '').split() + ) + ) + ) def __exit__(self, exc_type, exc_value, traceback) -> None: self.temp_var_files.clean_up() diff --git a/test/test_terraform.py b/test/test_terraform.py index d6d966e..8bf4248 100644 --- a/test/test_terraform.py +++ b/test/test_terraform.py @@ -452,4 +452,4 @@ def test_list_workspace(self): tf = Terraform(working_dir=current_path) workspaces = tf.list_workspace() assert len(workspaces) > 0 - assert 'default' in workspaces \ No newline at end of file + assert 'default' in workspaces From 80ca26b26af6ab16df3941167dcaa89e3ee74465 Mon Sep 17 00:00:00 2001 From: Kiran Kumar Kotari Date: Tue, 1 Jun 2021 07:16:52 +0200 Subject: [PATCH 50/56] Create pytest.yml --- .github/workflows/pytest.yml | 44 ++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/pytest.yml diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 0000000..567da32 --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,44 @@ +name: "pytest:py3 " + +on: + push: + branches: + - '**' + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: [3.7, 3.8, 3.9] + os: [ubuntu-latest, macOS-latest] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install wheel flake8 pytest pytest-cov + pip install -r requirements.txt + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest + run: | + pytest -v --cov=python-terraform + - name: Upload coverage + uses: codecov/codecov-action@v1 + with: + name: ${{ matrix.os }} Python ${{ matrix.python-version }} + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true From 996f27be8885f2441692da32752d826639e79e19 Mon Sep 17 00:00:00 2001 From: "Md. Fasihul Kabir" Date: Fri, 4 Jun 2021 13:07:17 +0600 Subject: [PATCH 51/56] Used meaningful name instead of shortcut x --- python_terraform/terraform.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python_terraform/terraform.py b/python_terraform/terraform.py index dc723db..590dcec 100644 --- a/python_terraform/terraform.py +++ b/python_terraform/terraform.py @@ -467,9 +467,9 @@ def list_workspace(self) -> List[str]: """ return list( filter( - lambda x: len(x) > 0, + lambda workspace: len(workspace) > 0, map( - lambda x: x.strip('*').strip(), + lambda workspace: workspace.strip('*').strip(), (self.cmd("workspace", "list")[1] or '').split() ) ) From 2f816a8712d57f2b7a7d23e454e46170faaf0574 Mon Sep 17 00:00:00 2001 From: Kiran Kumar Kotari Date: Thu, 26 May 2022 04:12:47 -0400 Subject: [PATCH 52/56] Update pytest.yml Adding workflow_dispatch and removed code coverage, for initial test action setup. --- .github/workflows/pytest.yml | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 567da32..606c865 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -6,6 +6,9 @@ on: - '**' pull_request: branches: [ master ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: jobs: build: @@ -27,18 +30,6 @@ jobs: python -m pip install --upgrade pip python -m pip install wheel flake8 pytest pytest-cov pip install -r requirements.txt - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest run: | - pytest -v --cov=python-terraform - - name: Upload coverage - uses: codecov/codecov-action@v1 - with: - name: ${{ matrix.os }} Python ${{ matrix.python-version }} - token: ${{ secrets.CODECOV_TOKEN }} - verbose: true + PYTHONPATH=. python -m pytest -v -s --cache-clear --cov=python-terraform tests From a121ff5baed2bad1035673e1c4e4ecfba438bb12 Mon Sep 17 00:00:00 2001 From: Kiran Kumar Kotari Date: Thu, 26 May 2022 04:21:03 -0400 Subject: [PATCH 53/56] Create requirements_dev.txt Adding requirements for pytest to run --- requirements_dev.txt | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 requirements_dev.txt diff --git a/requirements_dev.txt b/requirements_dev.txt new file mode 100644 index 0000000..956e214 --- /dev/null +++ b/requirements_dev.txt @@ -0,0 +1,4 @@ +wheel +flake8 +pytest +pytest-cov From e08cf08c589e82b3d83cc4d8214796158aa3a9fb Mon Sep 17 00:00:00 2001 From: Kiran Kumar Kotari Date: Thu, 26 May 2022 04:21:46 -0400 Subject: [PATCH 54/56] Update pytest.yml updated workflow to install from requirements_dev.txt --- .github/workflows/pytest.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 606c865..f7ee60c 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -28,8 +28,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install wheel flake8 pytest pytest-cov - pip install -r requirements.txt + pip install -r requirements_dev.txt - name: Test with pytest run: | PYTHONPATH=. python -m pytest -v -s --cache-clear --cov=python-terraform tests From 6848798f454bc501444a422a35066c29d6b25eec Mon Sep 17 00:00:00 2001 From: Kiran Kumar Kotari Date: Thu, 26 May 2022 04:24:02 -0400 Subject: [PATCH 55/56] Update pytest.yml test folder name correction in workflow --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index f7ee60c..ffa6f47 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -31,4 +31,4 @@ jobs: pip install -r requirements_dev.txt - name: Test with pytest run: | - PYTHONPATH=. python -m pytest -v -s --cache-clear --cov=python-terraform tests + PYTHONPATH=. python -m pytest -v -s --cache-clear --cov=python-terraform test From f22dfae6c2ff1074333da0ee67acd1bd87875144 Mon Sep 17 00:00:00 2001 From: Kiran Kumar Kotari Date: Thu, 26 May 2022 04:31:48 -0400 Subject: [PATCH 56/56] Create codeql-analysis.yml Adding codeql for code analysis --- .github/workflows/codeql-analysis.yml | 72 +++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..43e7fd0 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,72 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ develop ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ develop ] + schedule: + - cron: '29 6 * * 6' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2