From d3d98873918cfabb69ac3b46ea25d1d5cee7c71d Mon Sep 17 00:00:00 2001 From: abikouo Date: Fri, 26 Jan 2024 09:45:59 +0100 Subject: [PATCH 1/3] add support for backend_config_files --- plugins/inventory/terraform_state.py | 124 ++++++--- .../aliases | 0 .../runme.sh | 2 + .../setup.yml | 0 .../teardown.yml | 0 .../templates/aws_credentials.sh.j2 | 0 .../templates/backend.hcl.j2 | 3 + .../templates/inventory.yml.j2 | 7 + .../inventory_with_backend_files.yml.j2 | 4 + .../templates/inventory_with_compose.yml.j2 | 9 + .../inventory_with_constructed.yml.j2 | 12 + .../templates/inventory_with_hostname.yml.j2 | 11 + .../templates/main.tf.j2 | 0 .../test.yml | 38 ++- .../vars/main.yml | 0 .../templates/inventory.yml.j2 | 8 - .../templates/inventory_with_compose.yml.j2 | 10 - .../inventory_with_constructed.yml.j2 | 13 - .../templates/inventory_with_hostname.yml.j2 | 12 - .../plugins/inventory/test_terraform_state.py | 248 +++++++++++++----- tox.ini | 2 +- 21 files changed, 352 insertions(+), 151 deletions(-) rename tests/integration/targets/{terraform_state => inventory_terraform_state_aws}/aliases (100%) rename tests/integration/targets/{terraform_state => inventory_terraform_state_aws}/runme.sh (97%) rename tests/integration/targets/{terraform_state => inventory_terraform_state_aws}/setup.yml (100%) rename tests/integration/targets/{terraform_state => inventory_terraform_state_aws}/teardown.yml (100%) rename tests/integration/targets/{terraform_state => inventory_terraform_state_aws}/templates/aws_credentials.sh.j2 (100%) create mode 100644 tests/integration/targets/inventory_terraform_state_aws/templates/backend.hcl.j2 create mode 100644 tests/integration/targets/inventory_terraform_state_aws/templates/inventory.yml.j2 create mode 100644 tests/integration/targets/inventory_terraform_state_aws/templates/inventory_with_backend_files.yml.j2 create mode 100644 tests/integration/targets/inventory_terraform_state_aws/templates/inventory_with_compose.yml.j2 create mode 100644 tests/integration/targets/inventory_terraform_state_aws/templates/inventory_with_constructed.yml.j2 create mode 100644 tests/integration/targets/inventory_terraform_state_aws/templates/inventory_with_hostname.yml.j2 rename tests/integration/targets/{terraform_state => inventory_terraform_state_aws}/templates/main.tf.j2 (100%) rename tests/integration/targets/{terraform_state => inventory_terraform_state_aws}/test.yml (70%) rename tests/integration/targets/{terraform_state => inventory_terraform_state_aws}/vars/main.yml (100%) delete mode 100644 tests/integration/targets/terraform_state/templates/inventory.yml.j2 delete mode 100644 tests/integration/targets/terraform_state/templates/inventory_with_compose.yml.j2 delete mode 100644 tests/integration/targets/terraform_state/templates/inventory_with_constructed.yml.j2 delete mode 100644 tests/integration/targets/terraform_state/templates/inventory_with_hostname.yml.j2 diff --git a/plugins/inventory/terraform_state.py b/plugins/inventory/terraform_state.py index 23d4b0ff..5eef3e76 100644 --- a/plugins/inventory/terraform_state.py +++ b/plugins/inventory/terraform_state.py @@ -26,11 +26,22 @@ required: true type: str choices: [ cloud.terraform.terraform_state ] - backend_config: + backend_type: description: - - A Terraform backend configuration to an existing state file. + - The Terraform backend type from which the state file will be retrieved. + - Possible values include C(s3), C(remote), C(azurerm), C(local), C(consul), C(cos), C(gcs), C(http) type: str required: true + backend_config: + description: + - A group of key-values used to configure the backend. + - These values will be provided at init stage to the -backend-config parameter. + type: dict + backend_config_files: + description: + - The path to a configuration file to provide at init state to the -backend-config parameter. + This can accept a list of paths to multiple configuration files. + type: raw search_child_modules: description: - Whether to include resources from Terraform child modules. @@ -76,13 +87,12 @@ # Inventory with state file stored into http backend - name: Create an inventory from state file stored into http backend plugin: cloud.terraform.terraform_state - backend_config: | - backend "http" { - address = "https://localhost:8043/api/v2/state/3/" - skip_cert_verification = true - username = "ansible" - password = "test123!" - } + backend_type: http + backend_config: + address: https://localhost:8043/api/v2/state/3/ + skip_cert_verification: true + username: ansible + password: test123! # Running command `ansible-inventory -i basic_terraform_state.yaml --graph --vars` would then produce the inventory: # @all: @@ -152,13 +162,11 @@ # Example using constructed features to set ansible_host - name: Using compose feature to set the ansible_host plugin: cloud.terraform.terraform_state - backend_config: | - backend "http" { - address = "https://localhost:8043/api/v2/state/3/" - skip_cert_verification = true - username = "ansible" - password = "test123!" - } + backend_type: s3 + backend_config: + region: us-east-1 + key: terraform/state + bucket: my-sample-bucket compose: ansible_host: public_ip @@ -175,13 +183,11 @@ # Example using constructed features to create inventory groups - name: Using keyed_groups feature to add host into group plugin: cloud.terraform.terraform_state - backend_config: | - backend "http" { - address = "https://localhost:8043/api/v2/state/3/" - skip_cert_verification = true - username = "ansible" - password = "test123!" - } + backend_type: s3 + backend_config: + region: us-east-1 + key: terraform/state + bucket: my-sample-bucket keyed_groups: - key: instance_state prefix: state @@ -195,13 +201,11 @@ # Example using hostnames feature to define inventory hostname - name: Using hostnames feature to define inventory hostname plugin: cloud.terraform.terraform_state - backend_config: | - backend "http" { - address = "https://localhost:8043/api/v2/state/3/" - skip_cert_verification = true - username = "ansible" - password = "test123!" - } + backend_type: s3 + backend_config: + region: us-east-1 + key: terraform/state + bucket: my-sample-bucket hostnames: - name: 'tag:Phase' separator: "-" @@ -211,6 +215,27 @@ # @all: # |--@ungrouped: # | |--running-integration + +# Example using backend_config_files option to configure the backend +- name: Using backend_config_files to configure the backend + plugin: cloud.terraform.terraform_state + backend_type: s3 + backend_config: + region: us-east-1 + backend_config_files: + - /path/to/config1 + - /path/to/config2 + + # With the following content for config1 + # + # key = terraform/tfstate + # bucket = my-tf-backend-bucket + # + # and the following content for config2 + # + # access_key = xxxxxxxxxxxxxx + # secret_key = xxxxxxxxxxxxxx + # token = xxxxxxxxxxxxx """ @@ -277,8 +302,10 @@ def get_preferred_hostname(instance: TerraformModuleResource, hostnames: Optiona return hostname -def write_terraform_config(backend_config: str, path: str) -> None: - tf_config = "terraform {\n" + backend_config + "\n}" +def write_terraform_config(backend_type: str, path: str) -> None: + tf_config = "terraform {\n" + tf_config += 'backend "%s" {}' % backend_type + tf_config += "\n}" with open(path, "w") as temp_file: temp_file.write(tf_config) @@ -300,16 +327,18 @@ def verify_file(self, path): # type: ignore # mypy ignore def _query( self, terraform_binary: str, - backend_config: str, + backend_type: str, + backend_config: Optional[Dict[str, str]], + backend_config_files: Optional[List[str]], search_child_modules: bool, resources_types: List[str], provider_name: str, ) -> List[TerraformModuleResource]: with TemporaryDirectory() as temp_dir: - write_terraform_config(backend_config, os.path.join(temp_dir, "main.tf")) + write_terraform_config(backend_type, os.path.join(temp_dir, "main.tf")) terraform = TerraformCommands(module_run_command, temp_dir, terraform_binary, False) try: - terraform.init() + terraform.init(backend_config=backend_config, backend_config_files=backend_config_files) result = terraform.show() instances: List[TerraformModuleResource] = [] if result: @@ -357,20 +386,39 @@ def parse(self, inventory, loader, path, cache=False): # type: ignore # mypy i cfg = self.read_config_data(path) # type: ignore # mypy ignore backend_config = cfg.get("backend_config") + backend_config_files = cfg.get("backend_config_files") + backend_type = cfg.get("backend_type") terraform_binary = cfg.get("binary_path") search_child_modules = cfg.get("search_child_modules", False) - if not backend_config: - raise TerraformError("'backend_config' option is required to read existing state file.") + if not backend_type: + raise TerraformError("The parameter 'backend_type' is required to use this inventory plugin.") + + if not backend_config and not backend_config_files: + raise TerraformError( + "At least one of 'backend_config' or 'backend_config_files' option is required to configure the Terraform backend." + ) if terraform_binary is not None: validate_bin_path(terraform_binary) else: terraform_binary = process.get_bin_path("terraform") + # Transform the backend_config_files from Str to List[Str] + if backend_config_files and not isinstance(backend_config_files, list): + backend_config_files = [backend_config_files] + provider_name = "registry.terraform.io/hashicorp/aws" resources_types = ["aws_instance"] - instances = self._query(terraform_binary, backend_config, search_child_modules, resources_types, provider_name) + instances = self._query( + terraform_binary, + backend_type, + backend_config, + backend_config_files, + search_child_modules, + resources_types, + provider_name, + ) self.create_inventory( instances, cfg.get("hostnames"), cfg.get("compose"), cfg.get("keyed_groups"), cfg.get("strict") ) diff --git a/tests/integration/targets/terraform_state/aliases b/tests/integration/targets/inventory_terraform_state_aws/aliases similarity index 100% rename from tests/integration/targets/terraform_state/aliases rename to tests/integration/targets/inventory_terraform_state_aws/aliases diff --git a/tests/integration/targets/terraform_state/runme.sh b/tests/integration/targets/inventory_terraform_state_aws/runme.sh similarity index 97% rename from tests/integration/targets/terraform_state/runme.sh rename to tests/integration/targets/inventory_terraform_state_aws/runme.sh index f9e7c252..1acbdf94 100755 --- a/tests/integration/targets/terraform_state/runme.sh +++ b/tests/integration/targets/inventory_terraform_state_aws/runme.sh @@ -17,7 +17,9 @@ export ANSIBLE_INVENTORY_ENABLED="cloud.terraform.terraform_state" export ANSIBLE_INVENTORY=test.terraform_state.yml +set +x source aws_credentials.sh +set -x ansible-playbook test.yml "$@" diff --git a/tests/integration/targets/terraform_state/setup.yml b/tests/integration/targets/inventory_terraform_state_aws/setup.yml similarity index 100% rename from tests/integration/targets/terraform_state/setup.yml rename to tests/integration/targets/inventory_terraform_state_aws/setup.yml diff --git a/tests/integration/targets/terraform_state/teardown.yml b/tests/integration/targets/inventory_terraform_state_aws/teardown.yml similarity index 100% rename from tests/integration/targets/terraform_state/teardown.yml rename to tests/integration/targets/inventory_terraform_state_aws/teardown.yml diff --git a/tests/integration/targets/terraform_state/templates/aws_credentials.sh.j2 b/tests/integration/targets/inventory_terraform_state_aws/templates/aws_credentials.sh.j2 similarity index 100% rename from tests/integration/targets/terraform_state/templates/aws_credentials.sh.j2 rename to tests/integration/targets/inventory_terraform_state_aws/templates/aws_credentials.sh.j2 diff --git a/tests/integration/targets/inventory_terraform_state_aws/templates/backend.hcl.j2 b/tests/integration/targets/inventory_terraform_state_aws/templates/backend.hcl.j2 new file mode 100644 index 00000000..559fd63c --- /dev/null +++ b/tests/integration/targets/inventory_terraform_state_aws/templates/backend.hcl.j2 @@ -0,0 +1,3 @@ +bucket = "{{ bucket_name }}" +key = "ansible/terraform.tfstate" +region = "{{ aws_region }}" diff --git a/tests/integration/targets/inventory_terraform_state_aws/templates/inventory.yml.j2 b/tests/integration/targets/inventory_terraform_state_aws/templates/inventory.yml.j2 new file mode 100644 index 00000000..c888f73b --- /dev/null +++ b/tests/integration/targets/inventory_terraform_state_aws/templates/inventory.yml.j2 @@ -0,0 +1,7 @@ +--- +plugin: cloud.terraform.terraform_state +backend_type: s3 +backend_config: + bucket: {{ bucket_name }} + key: ansible/terraform.tfstate + region: {{ aws_region }} \ No newline at end of file diff --git a/tests/integration/targets/inventory_terraform_state_aws/templates/inventory_with_backend_files.yml.j2 b/tests/integration/targets/inventory_terraform_state_aws/templates/inventory_with_backend_files.yml.j2 new file mode 100644 index 00000000..8c15530a --- /dev/null +++ b/tests/integration/targets/inventory_terraform_state_aws/templates/inventory_with_backend_files.yml.j2 @@ -0,0 +1,4 @@ +--- +plugin: cloud.terraform.terraform_state +backend_type: s3 +backend_config_files: {{ backend_config_files }} diff --git a/tests/integration/targets/inventory_terraform_state_aws/templates/inventory_with_compose.yml.j2 b/tests/integration/targets/inventory_terraform_state_aws/templates/inventory_with_compose.yml.j2 new file mode 100644 index 00000000..9148e901 --- /dev/null +++ b/tests/integration/targets/inventory_terraform_state_aws/templates/inventory_with_compose.yml.j2 @@ -0,0 +1,9 @@ +--- +plugin: cloud.terraform.terraform_state +backend_type: s3 +backend_config: + bucket: {{ bucket_name }} + key: ansible/terraform.tfstate + region: {{ aws_region }} +compose: + ansible_host: 'private_ip' \ No newline at end of file diff --git a/tests/integration/targets/inventory_terraform_state_aws/templates/inventory_with_constructed.yml.j2 b/tests/integration/targets/inventory_terraform_state_aws/templates/inventory_with_constructed.yml.j2 new file mode 100644 index 00000000..9b4b4163 --- /dev/null +++ b/tests/integration/targets/inventory_terraform_state_aws/templates/inventory_with_constructed.yml.j2 @@ -0,0 +1,12 @@ +--- +plugin: cloud.terraform.terraform_state +backend_type: s3 +backend_config: + bucket: {{ bucket_name }} + key: ansible/terraform.tfstate + region: {{ aws_region }} +keyed_groups: +- key: instance_state + prefix: state +- prefix: tag + key: tags diff --git a/tests/integration/targets/inventory_terraform_state_aws/templates/inventory_with_hostname.yml.j2 b/tests/integration/targets/inventory_terraform_state_aws/templates/inventory_with_hostname.yml.j2 new file mode 100644 index 00000000..a192c5bd --- /dev/null +++ b/tests/integration/targets/inventory_terraform_state_aws/templates/inventory_with_hostname.yml.j2 @@ -0,0 +1,11 @@ +--- +plugin: cloud.terraform.terraform_state +backend_type: s3 +backend_config: + bucket: {{ bucket_name }} + key: ansible/terraform.tfstate + region: {{ aws_region }} +hostnames: + - name: 'tag:Name' + separator: "-" + prefix: 'instance_type' \ No newline at end of file diff --git a/tests/integration/targets/terraform_state/templates/main.tf.j2 b/tests/integration/targets/inventory_terraform_state_aws/templates/main.tf.j2 similarity index 100% rename from tests/integration/targets/terraform_state/templates/main.tf.j2 rename to tests/integration/targets/inventory_terraform_state_aws/templates/main.tf.j2 diff --git a/tests/integration/targets/terraform_state/test.yml b/tests/integration/targets/inventory_terraform_state_aws/test.yml similarity index 70% rename from tests/integration/targets/terraform_state/test.yml rename to tests/integration/targets/inventory_terraform_state_aws/test.yml index 709b6e94..8ca54827 100644 --- a/tests/integration/targets/terraform_state/test.yml +++ b/tests/integration/targets/inventory_terraform_state_aws/test.yml @@ -16,14 +16,45 @@ vars: inventory_path: "{{ lookup('env', 'ANSIBLE_INVENTORY') }}" block: + - name: Create temporary file to store backend configuration + tempfile: + suffix: ".hcl" + register: tmpfile + + - name: Generate backend configuration file + template: + src: backend.hcl.j2 + dest: "{{ tmpfile.path }}" + # Simple inventory configuration - - name: Generate inventory file + - name: Generate inventory file with backend_config template: src: "inventory.yml.j2" dest: "{{ inventory_path }}" - meta: refresh_inventory + - name: 'assert that host {{ default_hostname }} is defined' + assert: + that: + - default_hostname in hostvars + + - name: Assert that '{{ default_hostname }}' host has required variables + assert: + that: + - item in hostvars[default_hostname] + with_items: "{{ host_variables }}" + + # Inventory with backend_config_files + - name: Generate inventory file with backend_config_files + template: + src: "inventory_with_backend_files.yml.j2" + dest: "{{ inventory_path }}" + vars: + backend_config_files: "{{ tmpfile.path }}" + + - meta: refresh_inventory + - name: 'assert that host {{ default_hostname }} is defined' assert: that: @@ -89,6 +120,11 @@ - default_hostname in groups.state_running always: + - name: Delete temporary file + file: + state: absent + path: "{{ tmpfile.path }}" + - name: Delete inventory file file: state: absent diff --git a/tests/integration/targets/terraform_state/vars/main.yml b/tests/integration/targets/inventory_terraform_state_aws/vars/main.yml similarity index 100% rename from tests/integration/targets/terraform_state/vars/main.yml rename to tests/integration/targets/inventory_terraform_state_aws/vars/main.yml diff --git a/tests/integration/targets/terraform_state/templates/inventory.yml.j2 b/tests/integration/targets/terraform_state/templates/inventory.yml.j2 deleted file mode 100644 index 385be29e..00000000 --- a/tests/integration/targets/terraform_state/templates/inventory.yml.j2 +++ /dev/null @@ -1,8 +0,0 @@ ---- -plugin: cloud.terraform.terraform_state -backend_config: | - backend "s3" { - bucket = "{{ bucket_name }}" - key = "ansible/terraform.tfstate" - region = "{{ aws_region }}" - } diff --git a/tests/integration/targets/terraform_state/templates/inventory_with_compose.yml.j2 b/tests/integration/targets/terraform_state/templates/inventory_with_compose.yml.j2 deleted file mode 100644 index 13ca9898..00000000 --- a/tests/integration/targets/terraform_state/templates/inventory_with_compose.yml.j2 +++ /dev/null @@ -1,10 +0,0 @@ ---- -plugin: cloud.terraform.terraform_state -backend_config: | - backend "s3" { - bucket = "{{ bucket_name }}" - key = "ansible/terraform.tfstate" - region = "{{ aws_region }}" - } -compose: - ansible_host: 'private_ip' \ No newline at end of file diff --git a/tests/integration/targets/terraform_state/templates/inventory_with_constructed.yml.j2 b/tests/integration/targets/terraform_state/templates/inventory_with_constructed.yml.j2 deleted file mode 100644 index 8bc4c939..00000000 --- a/tests/integration/targets/terraform_state/templates/inventory_with_constructed.yml.j2 +++ /dev/null @@ -1,13 +0,0 @@ ---- -plugin: cloud.terraform.terraform_state -backend_config: | - backend "s3" { - bucket = "{{ bucket_name }}" - key = "ansible/terraform.tfstate" - region = "{{ aws_region }}" - } -keyed_groups: -- key: instance_state - prefix: state -- prefix: tag - key: tags diff --git a/tests/integration/targets/terraform_state/templates/inventory_with_hostname.yml.j2 b/tests/integration/targets/terraform_state/templates/inventory_with_hostname.yml.j2 deleted file mode 100644 index cdc870ef..00000000 --- a/tests/integration/targets/terraform_state/templates/inventory_with_hostname.yml.j2 +++ /dev/null @@ -1,12 +0,0 @@ ---- -plugin: cloud.terraform.terraform_state -backend_config: | - backend "s3" { - bucket = "{{ bucket_name }}" - key = "ansible/terraform.tfstate" - region = "{{ aws_region }}" - } -hostnames: - - name: 'tag:Name' - separator: "-" - prefix: 'instance_type' \ No newline at end of file diff --git a/tests/unit/plugins/inventory/test_terraform_state.py b/tests/unit/plugins/inventory/test_terraform_state.py index 46b80ad4..ee7bfd94 100644 --- a/tests/unit/plugins/inventory/test_terraform_state.py +++ b/tests/unit/plugins/inventory/test_terraform_state.py @@ -14,6 +14,7 @@ from ansible.template import Templar from ansible_collections.cloud.terraform.plugins.inventory.terraform_state import ( InventoryModule, + TerraformError, filter_instances, get_preferred_hostname, get_tag_hostname, @@ -27,7 +28,7 @@ TerraformShowValues, ) -from plugins.module_utils.errors import TerraformError +# from plugins.module_utils.errors import TerraformError @pytest.fixture @@ -283,11 +284,15 @@ def test_create_inventory(self, inventory_plugin, mocker): class TestWriteTerraformConfig: - def test_write_terraform_config(self, terraform_backend_config, tmp_path): + @pytest.mark.parametrize( + "backend_type", + ["s3", "remote", "azurerm", "local", "consul", "cos", "gcs", "http"], + ) + def test_write_terraform_config(self, backend_type, tmp_path): main_tf = tmp_path / "main.tf" - write_terraform_config(terraform_backend_config, str(main_tf)) + write_terraform_config(backend_type, str(main_tf)) - assert main_tf.read_text() == "terraform {\n" + terraform_backend_config + "\n}" + assert main_tf.read_text() == "terraform {\n" + 'backend "%s" {}' % backend_type + "\n}" class TestInventoryModuleQuery: @@ -295,7 +300,7 @@ class TestInventoryModuleQuery: "search_child_modules", [True, False], ) - def test__query(self, inventory_plugin, terraform_backend_config, mocker, search_child_modules): + def test__query(self, inventory_plugin, mocker, search_child_modules): write_terraform_config_patch = mocker.patch( "ansible_collections.cloud.terraform.plugins.inventory.terraform_state.write_terraform_config" ) @@ -326,96 +331,203 @@ def test__query(self, inventory_plugin, terraform_backend_config, mocker, search terraform_binary = MagicMock() resources_types = MagicMock() provider_name = MagicMock() + tf_backend_type = MagicMock() + tf_backend_config = MagicMock() + tf_backend_config_files = MagicMock() result = inventory_plugin._query( - terraform_binary, terraform_backend_config, search_child_modules, resources_types, provider_name + terraform_binary, + tf_backend_type, + tf_backend_config, + tf_backend_config_files, + search_child_modules, + resources_types, + provider_name, ) assert instances == result - write_terraform_config_patch.assert_called_once_with(terraform_backend_config, ANY) - terraform_commands.init.assert_called_once() + write_terraform_config_patch.assert_called_once_with(tf_backend_type, ANY) + terraform_commands.init.assert_called_once_with( + backend_config=tf_backend_config, backend_config_files=tf_backend_config_files + ) terraform_commands.show.assert_called_once() class TestInventoryModuleParse: - @pytest.mark.parametrize( - "has_backend_config", - [True, False], - ) - @pytest.mark.parametrize( - "has_binary_path", - [True, False], - ) - def test_parse(self, inventory_plugin, mocker, has_backend_config, has_binary_path): - loader = MagicMock() - path = MagicMock() - inventory = MagicMock() - cache = MagicMock() + mockers = {} + def get_mock(self, name): + if name not in self.mockers: + self.mockers[name] = MagicMock(name=name) + return self.mockers.get(name) + + def test_parse_missing_backend_type(self, inventory_plugin, mocker): config = { - "backend_config": MagicMock(), - "binary_path": MagicMock(), - "search_child_modules": MagicMock(), + "backend_config": {"some": "configuration"}, + "backend_config_files": ["config1", "config2"], + "binary_path": "path_to_my_binary", } - if not has_backend_config: - del config["backend_config"] + read_config_data_patch = mocker.patch( + "ansible_collections.cloud.terraform.plugins.inventory.terraform_state.InventoryModule.read_config_data" + ) + read_config_data_patch.side_effect = lambda _: config - if not has_binary_path: - del config["binary_path"] + super_parse_patch = mocker.patch( + "ansible_collections.cloud.terraform.plugins.inventory.terraform_state.TerraformInventoryPluginBase.parse" + ) + super_parse_patch.return_value = True + with pytest.raises(TerraformError) as exc: + inventory_plugin.parse( + self.get_mock("inventory"), self.get_mock("loader"), self.get_mock("path"), cache=True + ) + + assert "The parameter 'backend_type' is required to use this inventory plugin." == str(exc.value) + + def test_parse_missing_backend_configure(self, inventory_plugin, mocker): + config = { + "backend_type": "s3", + "binary_path": "path_to_my_binary", + } read_config_data_patch = mocker.patch( "ansible_collections.cloud.terraform.plugins.inventory.terraform_state.InventoryModule.read_config_data" ) read_config_data_patch.side_effect = lambda _: config + + super_parse_patch = mocker.patch( + "ansible_collections.cloud.terraform.plugins.inventory.terraform_state.TerraformInventoryPluginBase.parse" + ) + super_parse_patch.return_value = True + with pytest.raises(TerraformError) as exc: + inventory_plugin.parse( + self.get_mock("inventory"), self.get_mock("loader"), self.get_mock("path"), cache=True + ) + err = "At least one of 'backend_config' or 'backend_config_files' option is required to configure the Terraform backend." + assert err == str(exc.value) + + def assert_calls(self, config, super_parse_patch, read_config_data_patch): + self.get_mock("inventory_query").assert_called_once_with( + self.get_mock("terraform"), + config.get("backend_type"), + config.get("backend_config"), + config.get("backend_config_files"), + config.get("search_child_modules", False), + ["aws_instance"], + "registry.terraform.io/hashicorp/aws", + ) + self.get_mock("create_inventory").assert_called_once_with( + self.get_mock("_query_instances"), + config.get("hostnames"), + config.get("compose"), + config.get("keyed_groups"), + config.get("strict"), + ) + + super_parse_patch.assert_called_once_with( + self.get_mock("inventory"), self.get_mock("loader"), self.get_mock("path"), cache=True + ) + read_config_data_patch.assert_called_once_with(self.get_mock("path")) + + def test_parse_missing_terraform_binary(self, inventory_plugin, mocker): + config = { + "backend_type": "http", + "backend_config": {"some": "key"}, + } + + read_config_data_patch = mocker.patch( + "ansible_collections.cloud.terraform.plugins.inventory.terraform_state.InventoryModule.read_config_data" + ) + read_config_data_patch.side_effect = lambda _: config + + super_parse_patch = mocker.patch( + "ansible_collections.cloud.terraform.plugins.inventory.terraform_state.TerraformInventoryPluginBase.parse" + ) + super_parse_patch.return_value = True + process_patch = mocker.patch("ansible_collections.cloud.terraform.plugins.inventory.terraform_state.process") + process_patch.get_bin_path = self.get_mock("get_bin_path") + process_patch.get_bin_path.return_value = self.get_mock("terraform") + validate_bin_patch = mocker.patch( "ansible_collections.cloud.terraform.plugins.inventory.terraform_state.validate_bin_path" ) validate_bin_patch.return_value = True + inventory_plugin._query = self.get_mock("inventory_query") + inventory_plugin._query.return_value = self.get_mock("_query_instances") + inventory_plugin.create_inventory = self.get_mock("create_inventory") + inventory_plugin.parse(self.get_mock("inventory"), self.get_mock("loader"), self.get_mock("path"), cache=True) + + process_patch.get_bin_path.assert_called_once_with("terraform") + validate_bin_patch.assert_not_called() + + self.assert_calls(config, super_parse_patch, read_config_data_patch) + + def test_parse_with_backend_config_files_as_string(self, inventory_plugin, mocker): + config = { + "backend_type": "remote", + "backend_config": {"some": "key"}, + "backend_config_files": "backend.hcl", + } + + read_config_data_patch = mocker.patch( + "ansible_collections.cloud.terraform.plugins.inventory.terraform_state.InventoryModule.read_config_data" + ) + read_config_data_patch.side_effect = lambda _: config + super_parse_patch = mocker.patch( "ansible_collections.cloud.terraform.plugins.inventory.terraform_state.TerraformInventoryPluginBase.parse" ) super_parse_patch.return_value = True + process_patch = mocker.patch("ansible_collections.cloud.terraform.plugins.inventory.terraform_state.process") + process_patch.get_bin_path = self.get_mock("get_bin_path") + process_patch.get_bin_path.return_value = self.get_mock("terraform") + + validate_bin_patch = mocker.patch( + "ansible_collections.cloud.terraform.plugins.inventory.terraform_state.validate_bin_path" + ) + validate_bin_patch.return_value = True + + inventory_plugin._query = self.get_mock("inventory_query") + inventory_plugin._query.return_value = self.get_mock("_query_instances") + inventory_plugin.create_inventory = self.get_mock("create_inventory") + inventory_plugin.parse(self.get_mock("inventory"), self.get_mock("loader"), self.get_mock("path"), cache=True) + + process_patch.get_bin_path.assert_called_once_with("terraform") + validate_bin_patch.assert_not_called() + config.update({"backend_config_files": ["backend.hcl"]}) + self.assert_calls(config, super_parse_patch, read_config_data_patch) + + def test_parse_with_backend_config_files_as_list(self, inventory_plugin, mocker): + config = { + "backend_type": "gcs", + "backend_config": {"some": "key"}, + "backend_config_files": ["backend.hcl"], + } + + read_config_data_patch = mocker.patch( + "ansible_collections.cloud.terraform.plugins.inventory.terraform_state.InventoryModule.read_config_data" + ) + read_config_data_patch.side_effect = lambda _: config + + super_parse_patch = mocker.patch( + "ansible_collections.cloud.terraform.plugins.inventory.terraform_state.TerraformInventoryPluginBase.parse" + ) + super_parse_patch.return_value = True process_patch = mocker.patch("ansible_collections.cloud.terraform.plugins.inventory.terraform_state.process") - process_patch.get_bin_path = MagicMock() - terraform_bin_mock = MagicMock() - process_patch.get_bin_path.return_value = terraform_bin_mock - - if not has_backend_config: - with pytest.raises(Exception): - try: - inventory_plugin.parse(inventory, loader, path, cache=cache) - except TerraformError as e: - assert "'backend_config' option is required to read existing state file." in str(e.value) - raise - else: - instances = MagicMock() - inventory_plugin._query = MagicMock() - inventory_plugin._query.return_value = instances - inventory_plugin.create_inventory = MagicMock() - inventory_plugin.parse(inventory, loader, path, cache=cache) - - inventory_plugin._query.assert_called_once_with( - config.get("binary_path") or terraform_bin_mock, - config.get("backend_config"), - config.get("search_child_modules"), - ["aws_instance"], - "registry.terraform.io/hashicorp/aws", - ) - inventory_plugin.create_inventory.assert_called_once_with( - instances, - config.get("hostnames"), - config.get("compose"), - config.get("keyed_groups"), - config.get("strict"), - ) + process_patch.get_bin_path = self.get_mock("get_bin_path") + process_patch.get_bin_path.return_value = self.get_mock("terraform") + + validate_bin_patch = mocker.patch( + "ansible_collections.cloud.terraform.plugins.inventory.terraform_state.validate_bin_path" + ) + validate_bin_patch.return_value = True + + inventory_plugin._query = self.get_mock("inventory_query") + inventory_plugin._query.return_value = self.get_mock("_query_instances") + inventory_plugin.create_inventory = self.get_mock("create_inventory") + inventory_plugin.parse(self.get_mock("inventory"), self.get_mock("loader"), self.get_mock("path"), cache=True) - if has_binary_path: - validate_bin_patch.assert_called_once_with(config.get("binary_path")) - process_patch.get_bin_path.assert_not_called() - else: - process_patch.get_bin_path.assert_called_once_with("terraform") - validate_bin_patch.assert_not_called() + process_patch.get_bin_path.assert_called_once_with("terraform") + validate_bin_patch.assert_not_called() - super_parse_patch.assert_called_once_with(inventory, loader, path, cache=cache) - read_config_data_patch.assert_called_once_with(path) + self.assert_calls(config, super_parse_patch, read_config_data_patch) diff --git a/tox.ini b/tox.ini index 46f1d572..2406bc2a 100644 --- a/tox.ini +++ b/tox.ini @@ -29,7 +29,7 @@ commands = [testenv:black] deps = - black + black==23.12.1 commands = black --check --diff {[common]format_dirs} From 5cc7acc093751e520050f6af8ddcdb50aca10785 Mon Sep 17 00:00:00 2001 From: abikouo Date: Wed, 31 Jan 2024 09:05:37 +0100 Subject: [PATCH 2/3] minor code review updates --- plugins/inventory/terraform_state.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/plugins/inventory/terraform_state.py b/plugins/inventory/terraform_state.py index 5eef3e76..a86ea237 100644 --- a/plugins/inventory/terraform_state.py +++ b/plugins/inventory/terraform_state.py @@ -41,7 +41,8 @@ description: - The path to a configuration file to provide at init state to the -backend-config parameter. This can accept a list of paths to multiple configuration files. - type: raw + type: list + elements: path search_child_modules: description: - Whether to include resources from Terraform child modules. @@ -228,14 +229,14 @@ # With the following content for config1 # - # key = terraform/tfstate - # bucket = my-tf-backend-bucket + # key = "terraform/tfstate" + # bucket = "my-tf-backend-bucket" # # and the following content for config2 # - # access_key = xxxxxxxxxxxxxx - # secret_key = xxxxxxxxxxxxxx - # token = xxxxxxxxxxxxx + # access_key = "xxxxxxxxxxxxxx" + # secret_key = "xxxxxxxxxxxxxx" + # token = "xxxxxxxxxxxxx" """ From bb900ef4558d2d02ff9ba5b29f32f8e09b258a01 Mon Sep 17 00:00:00 2001 From: Bikouo Aubin <79859644+abikouo@users.noreply.github.com> Date: Wed, 31 Jan 2024 15:44:28 +0100 Subject: [PATCH 3/3] Remove possible values for backend_type --- plugins/inventory/terraform_state.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/inventory/terraform_state.py b/plugins/inventory/terraform_state.py index a86ea237..3b2d7637 100644 --- a/plugins/inventory/terraform_state.py +++ b/plugins/inventory/terraform_state.py @@ -29,7 +29,6 @@ backend_type: description: - The Terraform backend type from which the state file will be retrieved. - - Possible values include C(s3), C(remote), C(azurerm), C(local), C(consul), C(cos), C(gcs), C(http) type: str required: true backend_config: