Skip to content

Module filtering #204

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 74 additions & 2 deletions docs/cloud.terraform.terraform_provider_inventory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,52 @@ Parameters
<div>The path of a terraform binary to use.</div>
</td>
</tr>
<tr>
<td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
<b>exclude_modules</b>
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
<div style="font-size: small">
<span style="color: purple">list</span>
/ <span style="color: purple">elements=string</span>
</div>
<div style="font-style: italic; font-size: small; color: darkgreen">added in 4.0.0</div>
</td>
<td>
</td>
<td>
</td>
<td>
<div>List of child module names to exclude from the inventory.</div>
<div>If not specified, no modules are excluded.</div>
<div>Filters based on module name extracted from resource address.</div>
<div>Cannot be used together with <em>include_modules</em>.</div>
<div>Only applies when <em>search_child_modules</em> is true.</div>
</td>
</tr>
<tr>
<td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
<b>include_modules</b>
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
<div style="font-size: small">
<span style="color: purple">list</span>
/ <span style="color: purple">elements=string</span>
</div>
<div style="font-style: italic; font-size: small; color: darkgreen">added in 4.0.0</div>
</td>
<td>
</td>
<td>
</td>
<td>
<div>List of child module names to include in the inventory.</div>
<div>If not specified, all modules are included.</div>
<div>Filters based on module name extracted from resource address.</div>
<div>Cannot be used together with <em>exclude_modules</em>.</div>
<div>Only applies when <em>search_child_modules</em> is true.</div>
</td>
</tr>
<tr>
<td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
Expand Down Expand Up @@ -110,8 +156,8 @@ Parameters
</td>
<td>
<ul style="margin: 0; padding: 0"><b>Choices:</b>
<li><div style="color: blue"><b>no</b>&nbsp;&larr;</div></li>
<li>yes</li>
<li>no</li>
<li><div style="color: blue"><b>yes</b>&nbsp;&larr;</div></li>
</ul>
</td>
<td>
Expand Down Expand Up @@ -200,6 +246,32 @@ Examples
project_path: some/project/path
state_file: mycustomstate.tfstate

- name: Create an inventory including only specific modules
plugin: cloud.terraform.terraform_provider
project_path: some/project/path
include_modules:
- web_servers
- database

- name: Create an inventory excluding specific modules
plugin: cloud.terraform.terraform_provider
project_path: some/project/path
exclude_modules:
- development
- testing

- name: Create an inventory with nested module filtering
plugin: cloud.terraform.terraform_provider
project_path: some/project/path
include_modules:
- production.frontend
- production.backend

- name: Create an inventory with child modules disabled
plugin: cloud.terraform.terraform_provider
project_path: some/project/path
search_child_modules: false




Expand Down
132 changes: 129 additions & 3 deletions plugins/inventory/terraform_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,35 @@
description:
- Whether to include ansible_host and ansible_group resources from Terraform child modules.
type: bool
default: false
default: true
version_added: 1.2.0
binary_path:
description:
- The path of a terraform binary to use.
type: path
version_added: 1.1.0
include_modules:
description:
- List of child module names to include in the inventory.
- If not specified, all modules are included.
- Filters based on module name extracted from resource address.
- Cannot be used together with I(exclude_modules).
- Only applies when I(search_child_modules) is true.
type: list
elements: str
required: false
version_added: 4.0.0
exclude_modules:
description:
- List of child module names to exclude from the inventory.
- If not specified, no modules are excluded.
- Filters based on module name extracted from resource address.
- Cannot be used together with I(include_modules).
- Only applies when I(search_child_modules) is true.
type: list
elements: str
required: false
version_added: 4.0.0
"""

EXAMPLES = r"""
Expand Down Expand Up @@ -101,6 +123,32 @@
plugin: cloud.terraform.terraform_provider
project_path: some/project/path
state_file: mycustomstate.tfstate

- name: Create an inventory including only specific modules
plugin: cloud.terraform.terraform_provider
project_path: some/project/path
include_modules:
- web_servers
- database

- name: Create an inventory excluding specific modules
plugin: cloud.terraform.terraform_provider
project_path: some/project/path
exclude_modules:
- development
- testing

- name: Create an inventory with nested module filtering
plugin: cloud.terraform.terraform_provider
project_path: some/project/path
include_modules:
- production.frontend
- production.backend

- name: Create an inventory with child modules disabled
plugin: cloud.terraform.terraform_provider
project_path: some/project/path
search_child_modules: false
"""


Expand Down Expand Up @@ -162,8 +210,72 @@ def _add_host(self, inventory: Any, resource: TerraformModuleResource) -> None:
for key, value in attributes.variables.items():
inventory.set_variable(attributes.name, key, value)

def _extract_module_name(self, resource: TerraformModuleResource) -> Optional[str]:
address = getattr(resource, "address", "")
resource_type = getattr(resource, "type", "")

if not address or not resource_type:
return None

parts = address.split(".")

# Child resources needs to be at least module.name.type.resource_name
if len(parts) >= 4:
# Checks if resource.address starts with data.module or module
if "module" in parts[:2]:
# Find the resource type in the address to determine module boundary
try:
resource_type_index = parts.index(resource_type)
except ValueError:
# Resource type not found in address, should not happen in normal cases
return None

# Everything before the resource type could be module path
module_parts = parts[:resource_type_index]

# Remove 'data' prefix if present
if module_parts and module_parts[0] == "data":
module_parts = module_parts[1:]

# Remove 'module' prefix if present and extract module name
if module_parts:
module_parts = module_parts[1:]
return ".".join(module_parts)

# Not in a module (root module)
return None

def _should_include_resource(
self,
resource: TerraformModuleResource,
include_modules: Optional[List[str]],
exclude_modules: Optional[List[str]],
) -> bool:
module_name = self._extract_module_name(resource)

# If resource is in root module (module_name is None)
if module_name is None and not include_modules:
# Root module resources are always included unless include_modules is used
return True

# Handle exclude_modules first (takes precedence)
if exclude_modules is not None and module_name in exclude_modules:
return False

# Handle include_modules
if include_modules is not None:
return module_name in include_modules

# Default: include everything
return True

def create_inventory(
self, inventory: Any, state_content: List[Optional[TerraformShow]], search_child_modules: bool
self,
inventory: Any,
state_content: List[Optional[TerraformShow]],
search_child_modules: bool,
include_modules: Optional[List[str]] = None,
exclude_modules: Optional[List[str]] = None,
) -> None:
for state in state_content:
if state is None:
Expand All @@ -173,7 +285,14 @@ def create_inventory(
if not search_child_modules
else state.values.root_module.flatten_resources()
)

for resource in root_resources:
# Apply module filtering only if search_child_modules is enabled
if search_child_modules and not self._should_include_resource(
resource, include_modules, exclude_modules
):
continue

if resource.type == "ansible_group":
self._add_group(inventory, resource)
elif resource.type == "ansible_host":
Expand All @@ -188,6 +307,13 @@ def parse(self, inventory, loader, path, cache=False): # type: ignore # mypy i
state_file = cfg.get("state_file", "")
search_child_modules = cfg.get("search_child_modules", True)
terraform_binary = cfg.get("binary_path", None)
include_modules = cfg.get("include_modules", None)
exclude_modules = cfg.get("exclude_modules", None)

# Validate mutually exclusive options
if include_modules and exclude_modules:
raise TerraformError("Options 'include_modules' and 'exclude_modules' are mutually exclusive")

if terraform_binary is not None:
validate_bin_path(terraform_binary)
else:
Expand All @@ -205,4 +331,4 @@ def parse(self, inventory, loader, path, cache=False): # type: ignore # mypy i
raise TerraformError(e.message)

if state_content: # to avoid mypy error: Item "None" of "Optional[TerraformShow]" has no attribute "values"
self.create_inventory(inventory, state_content, search_child_modules)
self.create_inventory(inventory, state_content, search_child_modules, include_modules, exclude_modules)
Loading
Loading