From 825fa0e54f4ee74e4534bf03b458472cca605093 Mon Sep 17 00:00:00 2001 From: Austin Page Date: Tue, 22 Aug 2017 15:12:13 -0500 Subject: [PATCH 1/5] Adding init command and support for backend terraform state files --- python_terraform/__init__.py | 63 +++++++++++++++---- test/test_terraform.py | 17 +++++ test/test_tfstate_file2/terraform.tfstate | 62 ++++++++++++++++++ .../.terraform/terraform.tfstate | 62 ++++++++++++++++++ 4 files changed, 192 insertions(+), 12 deletions(-) create mode 100644 test/test_tfstate_file2/terraform.tfstate create mode 100644 test/test_tfstate_file3/.terraform/terraform.tfstate diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index 19ca603..4da547a 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -122,7 +122,7 @@ class Terraform(object): def plan(self, dir_or_plan=None, detailed_exitcode=IsFlagged, **kwargs): """ - refert 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 @@ -134,6 +134,35 @@ class Terraform(object): 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, force_copy=IsNotFlagged, 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 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 force_copy: whether or not to migrate from the previous backend + settings to the new backend settings + :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['force_copy'] = force_copy + 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 @@ -170,13 +199,18 @@ class Terraform(object): cmds += ['-{k}={v}'.format(k=k, v=sub_v)] continue - # right now we assume only variables will be passed as dict - # since map type sent in string won't work, create temp var file for - # variables, and clean it up later if type(v) is dict: - filename = self.temp_var_files.create(v) - cmds += ['-var-file={0}'.format(filename)] - continue + if 'backend-config' in k: + for bk, bv in v.items(): + cmds += ['-backend-config={k}={v}'.format(k=bk, v=bv)] + continue + + # 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(v) + cmds += ['-var-file={0}'.format(filename)] + continue # simple flag, if v is IsFlagged: @@ -274,14 +308,19 @@ class Terraform(object): :return: states file in dict type """ - if not file_path: - file_path = self.state + working_dir = self.working_dir or '' + + file_path = file_path or self.state or '' if not file_path: - file_path = 'terraform.tfstate' + backend_path = os.path.join(file_path, '.terraform', 'terraform.tfstate') - if self.working_dir: - file_path = os.path.join(self.working_dir, file_path) + 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) diff --git a/test/test_terraform.py b/test/test_terraform.py index 84ae447..e2f725f 100644 --- a/test/test_terraform.py +++ b/test/test_terraform.py @@ -92,9 +92,13 @@ class TestTerraform(object): """ 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): 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): f = os.path.join(root, filename) os.remove(f) @@ -103,6 +107,7 @@ class TestTerraform(object): shutil.rmtree(d) purge('.', '*.tfstate') + purge('.', '*.tfstate.backup') purge('.', '*.terraform') purge('.', FILE_PATH_WITH_SPACE_AND_SPACIAL_CHARS) @@ -166,6 +171,18 @@ class TestTerraform(object): tf.read_state_file() assert tf.tfstate.modules[0]['path'] == ['root'] + def test_state_default(self): + 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'] + + def test_state_default_backend(self): + 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'] + def test_pre_load_state_data(self): cwd = os.path.join(current_path, 'test_tfstate_file') tf = Terraform(working_dir=cwd, state='tfstate.test') diff --git a/test/test_tfstate_file2/terraform.tfstate b/test/test_tfstate_file2/terraform.tfstate new file mode 100644 index 0000000..d9cd0a2 --- /dev/null +++ b/test/test_tfstate_file2/terraform.tfstate @@ -0,0 +1,62 @@ +{ + "version": 3, + "terraform_version": "0.7.10", + "serial": 0, + "lineage": "d03ecdf7-8be0-4593-a952-1d8127875119", + "modules": [ + { + "path": [ + "default" + ], + "outputs": {}, + "resources": { + "aws_instance.ubuntu-1404": { + "type": "aws_instance", + "depends_on": [], + "primary": { + "id": "i-84d10edb", + "attributes": { + "ami": "ami-9abea4fb", + "associate_public_ip_address": "true", + "availability_zone": "us-west-2b", + "disable_api_termination": "false", + "ebs_block_device.#": "0", + "ebs_optimized": "false", + "ephemeral_block_device.#": "0", + "iam_instance_profile": "", + "id": "i-84d10edb", + "instance_state": "running", + "instance_type": "t2.micro", + "key_name": "", + "monitoring": "false", + "network_interface_id": "eni-46544f07", + "private_dns": "ip-172-31-25-244.us-west-2.compute.internal", + "private_ip": "172.31.25.244", + "public_dns": "ec2-35-162-30-219.us-west-2.compute.amazonaws.com", + "public_ip": "35.162.30.219", + "root_block_device.#": "1", + "root_block_device.0.delete_on_termination": "true", + "root_block_device.0.iops": "100", + "root_block_device.0.volume_size": "8", + "root_block_device.0.volume_type": "gp2", + "security_groups.#": "0", + "source_dest_check": "true", + "subnet_id": "subnet-d2c0f0a6", + "tags.%": "0", + "tenancy": "default", + "vpc_security_group_ids.#": "1", + "vpc_security_group_ids.619359045": "sg-9fc7dcfd" + }, + "meta": { + "schema_version": "1" + }, + "tainted": false + }, + "deposed": [], + "provider": "" + } + }, + "depends_on": [] + } + ] +} diff --git a/test/test_tfstate_file3/.terraform/terraform.tfstate b/test/test_tfstate_file3/.terraform/terraform.tfstate new file mode 100644 index 0000000..197ba71 --- /dev/null +++ b/test/test_tfstate_file3/.terraform/terraform.tfstate @@ -0,0 +1,62 @@ +{ + "version": 3, + "terraform_version": "0.7.10", + "serial": 0, + "lineage": "d03ecdf7-8be0-4593-a952-1d8127875119", + "modules": [ + { + "path": [ + "default_backend" + ], + "outputs": {}, + "resources": { + "aws_instance.ubuntu-1404": { + "type": "aws_instance", + "depends_on": [], + "primary": { + "id": "i-84d10edb", + "attributes": { + "ami": "ami-9abea4fb", + "associate_public_ip_address": "true", + "availability_zone": "us-west-2b", + "disable_api_termination": "false", + "ebs_block_device.#": "0", + "ebs_optimized": "false", + "ephemeral_block_device.#": "0", + "iam_instance_profile": "", + "id": "i-84d10edb", + "instance_state": "running", + "instance_type": "t2.micro", + "key_name": "", + "monitoring": "false", + "network_interface_id": "eni-46544f07", + "private_dns": "ip-172-31-25-244.us-west-2.compute.internal", + "private_ip": "172.31.25.244", + "public_dns": "ec2-35-162-30-219.us-west-2.compute.amazonaws.com", + "public_ip": "35.162.30.219", + "root_block_device.#": "1", + "root_block_device.0.delete_on_termination": "true", + "root_block_device.0.iops": "100", + "root_block_device.0.volume_size": "8", + "root_block_device.0.volume_type": "gp2", + "security_groups.#": "0", + "source_dest_check": "true", + "subnet_id": "subnet-d2c0f0a6", + "tags.%": "0", + "tenancy": "default", + "vpc_security_group_ids.#": "1", + "vpc_security_group_ids.619359045": "sg-9fc7dcfd" + }, + "meta": { + "schema_version": "1" + }, + "tainted": false + }, + "deposed": [], + "provider": "" + } + }, + "depends_on": [] + } + ] +} From 86182a604fa99cba54968818329a038354aa2863 Mon Sep 17 00:00:00 2001 From: beelit94 Date: Mon, 28 Aug 2017 11:17:33 -0700 Subject: [PATCH 2/5] add changelog --- python_terraform/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index 4da547a..3983d61 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -10,7 +10,15 @@ import tempfile 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 + log = logging.getLogger(__name__) +log.addHandler(NullHandler()) class IsFlagged: From b098b5a1d8e6286653c29c9318443ef8453ae978 Mon Sep 17 00:00:00 2001 From: beelit94 Date: Wed, 30 Aug 2017 11:00:15 -0700 Subject: [PATCH 3/5] minor refactor --- python_terraform/__init__.py | 48 ++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/python_terraform/__init__.py b/python_terraform/__init__.py index 3983d61..3d33a28 100644 --- a/python_terraform/__init__.py +++ b/python_terraform/__init__.py @@ -84,7 +84,8 @@ class Terraform(object): return wrapper - def apply(self, dir_or_plan=None, input=False, no_color=IsFlagged, **kwargs): + def apply(self, dir_or_plan=None, input=False, no_color=IsFlagged, + **kwargs): """ refer to https://terraform.io/docs/commands/apply.html no-color is flagged by default @@ -143,21 +144,19 @@ class Terraform(object): return self.cmd('plan', *args, **options) def init(self, dir_or_plan=None, backend_config=None, - reconfigure=IsFlagged, force_copy=IsNotFlagged, backend=True, - **kwargs): + 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 force_copy: whether or not to migrate from the previous backend - settings to the new backend settings :param backend: whether or not to use backend settings for init :param kwargs: options :return: ret_code, stdout, stderr @@ -165,7 +164,6 @@ class Terraform(object): options = kwargs options['backend_config'] = backend_config options['reconfigure'] = reconfigure - options['force_copy'] = force_copy options['backend'] = backend options = self._generate_default_options(options) args = self._generate_default_args(dir_or_plan) @@ -198,40 +196,40 @@ class Terraform(object): cmds = cmd.split() cmds = [self.terraform_bin_path] + cmds - for k, v in kwargs.items(): - if '_' in k: - k = k.replace('_', '-') + for option, value in kwargs.items(): + if '_' in option: + option = option.replace('_', '-') - if type(v) is list: - for sub_v in v: - cmds += ['-{k}={v}'.format(k=k, v=sub_v)] + if type(value) is list: + for sub_v in value: + cmds += ['-{k}={v}'.format(k=option, v=sub_v)] continue - if type(v) is dict: - if 'backend-config' in k: - for bk, bv in v.items(): + if type(value) is dict: + if 'backend-config' in option: + for bk, bv in value.items(): cmds += ['-backend-config={k}={v}'.format(k=bk, v=bv)] continue # 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(v) + filename = self.temp_var_files.create(value) cmds += ['-var-file={0}'.format(filename)] continue # simple flag, - if v is IsFlagged: - cmds += ['-{k}'.format(k=k)] + if value is IsFlagged: + cmds += ['-{k}'.format(k=option)] continue - if v is None or v is IsNotFlagged: + if value is None or value is IsNotFlagged: continue - if type(v) is bool: - v = 'true' if v else 'false' + if type(value) is bool: + value = 'true' if value else 'false' - cmds += ['-{k}={v}'.format(k=k, v=v)] + cmds += ['-{k}={v}'.format(k=option, v=value)] cmds += args return cmds @@ -321,7 +319,8 @@ class Terraform(object): 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 @@ -344,7 +343,8 @@ class VariableFiles(object): with tempfile.NamedTemporaryFile('w+t', delete=False) as temp: log.debug('{0} is created'.format(temp.name)) self.files.append(temp) - log.debug('variables wrote to tempfile: {0}'.format(str(variables))) + log.debug( + 'variables wrote to tempfile: {0}'.format(str(variables))) temp.write(json.dumps(variables)) file_name = temp.name From 45860aa39cf355c5779060523d7520bd64a9100e Mon Sep 17 00:00:00 2001 From: beelit94 Date: Fri, 1 Sep 2017 09:51:37 -0700 Subject: [PATCH 4/5] add release note --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18de51f..b3e68f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ # Changelog +## [0.9.1] +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' From 3c9ea8b526c39092e4be66155af7fdf14d312b2f Mon Sep 17 00:00:00 2001 From: beelit94 Date: Fri, 1 Sep 2017 09:51:54 -0700 Subject: [PATCH 5/5] =?UTF-8?q?Bump=20version:=200.9.0=20=E2=86=92=200.9.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 35141c7..91784e0 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.9.0 +current_version = 0.9.1 commit = True tag = False diff --git a/setup.py b/setup.py index b4fb40e..9ec25cd 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ except IOError: setup( name=module_name, - version='0.9.0', + version='0.9.1', url='https://github.com/beelit94/python-terraform', license='MIT', author='Freddy Tan',