Repository: neovasili/tflens

Issue:
[resources] Add filter resources by name support
We need to be able to filter resources shown in a table by name field.

The filter must be able to use regular expressions as the filter pattern.

Discussion:

Patch:
diff --git a/README.md b/README.md
index 4c1fad7..3b089dd 100644
--- a/README.md
+++ b/README.md
@@ -76,6 +76,8 @@ optional arguments:
         according to the following pattern: bucket-name/tfstate-key
   -m FILTER_MODULE, --filter-module FILTER_MODULE
         Applies a regular expression to the module field in order to filter the resources list to output
+  -n FILTER_NAME, --filter-name FILTER_NAME
+        Applies a regular expression to the name field in order to filter the resources list to output
 ```
 
 ### Examples
@@ -93,6 +95,16 @@ View table of resources for a tfstate located in the file system in the director
 
 View filtered table of resources for a tfstate located in the file system in the directory:
 
+```bash
+➜ tflens --filter-name "current"
+
+|   provider   |        type         |   mode  |           name                | module |
+|--------------|---------------------|---------|-------------------------------|--------|
+| provider.aws | aws_caller_identity |   data  |        current_user           |  test  |
+```
+
+Or:
+
 ```bash
 ➜ tflens --filter-module "test"
 
@@ -101,6 +113,16 @@ View filtered table of resources for a tfstate located in the file system in the
 | provider.aws | aws_caller_identity |   data  |        current_user           |  test  |
 ```
 
+Or:
+
+```bash
+➜ tflens --filter-name "current" --filter-module "test"
+
+|   provider   |        type         |   mode  |           name                | module |
+|--------------|---------------------|---------|-------------------------------|--------|
+| provider.aws | aws_caller_identity |   data  |        current_user           |  test  |
+```
+
 View table of resources for a tfstate located in the file system in the `dev/terraform.tfstate.json` path:
 
 ```bash
diff --git a/tflens/__main__.py b/tflens/__main__.py
index 45b72cf..8991726 100644
--- a/tflens/__main__.py
+++ b/tflens/__main__.py
@@ -43,12 +43,21 @@ parser.add_argument('-m', '--filter-module',
     filter the resources list to output",
   default="")
 
+parser.add_argument('-n', '--filter-name',
+  type=str,
+  action="store",
+  dest="filter_name",
+  help="Applies a regular expression to the name field in order to \
+    filter the resources list to output",
+  default="")
+
 args = parser.parse_args()
 
 ARGS_REMOTE = args.remote
 ARGS_FILE_LOCATION = args.file_location
 ARGS_OUTPUT = args.output
 ARGS_FILTER_MODULE = args.filter_module
+ARGS_FILTER_NAME = args.filter_name
 
 if not ARGS_FILE_LOCATION:
   ARGS_FILE_LOCATION = "{}/terraform.tfstate".format(Path().absolute())
@@ -61,6 +70,7 @@ def main():
 
   tfstate_controller = remote_router[ARGS_REMOTE](
     file_location=ARGS_FILE_LOCATION,
+    name_filter_expression=ARGS_FILTER_NAME,
     module_filter_expression=ARGS_FILTER_MODULE
   )
 
diff --git a/tflens/controller/tfstate.py b/tflens/controller/tfstate.py
index f4a58ec..7f0fec6 100644
--- a/tflens/controller/tfstate.py
+++ b/tflens/controller/tfstate.py
@@ -11,11 +11,17 @@ from tflens.helper.filter import TfStateFilterHelper
 
 class TfStateController():
 
-  def __init__(self, tfstate_content: dict, module_filter_expression: str=None):
+  def __init__(
+    self,
+    tfstate_content: dict,
+    name_filter_expression: str=None,
+    module_filter_expression: str=None
+  ):
     self.__tfstate = TfState(
       content=tfstate_content
     )
     self.__resources = TfStateFilterHelper(
+      name_filter_expression=name_filter_expression,
       module_filter_expression=module_filter_expression,
       resources=self.__tfstate.get_resources()
     ).apply_filter()
@@ -43,24 +49,36 @@ class TfStateController():
 
 class LocalTfStateController(TfStateController):
 
-  def __init__(self, file_location: str, module_filter_expression: str=None):
+  def __init__(
+    self,
+    file_location: str,
+    module_filter_expression: str=None,
+    name_filter_expression: str=None
+  ):
     self.__local_tfstate_service = LocalTfStateService(
       file_location=file_location
     )
 
     super().__init__(
       tfstate_content=self.__local_tfstate_service.read_content(),
+      name_filter_expression=name_filter_expression,
       module_filter_expression=module_filter_expression
     )
 
 class RemoteS3TfStateController(TfStateController):
 
-  def __init__(self, file_location: str, module_filter_expression: str=None):
+  def __init__(
+    self,
+    file_location: str,
+    module_filter_expression: str=None,
+    name_filter_expression: str=None
+  ):
     self.__remote_s3_tfstate_service = RemoteS3TfStateService(
       file_location=file_location
     )
 
     super().__init__(
       tfstate_content=self.__remote_s3_tfstate_service.read_content(),
+      name_filter_expression=name_filter_expression,
       module_filter_expression=module_filter_expression
     )
diff --git a/tflens/helper/filter.py b/tflens/helper/filter.py
index 18343e2..d5cd16c 100644
--- a/tflens/helper/filter.py
+++ b/tflens/helper/filter.py
@@ -16,12 +16,26 @@ class ModuleFilterHelper(FilterHelper):
   def __init__(self, filter_expression: str, resource: TfStateResource):
     super().__init__(
       filter_expression=filter_expression,
-      object_attribute_value = resource.get_parent_module()
+      object_attribute_value=resource.get_parent_module()
+    )
+
+class NameFilterHelper(FilterHelper):
+
+  def __init__(self, filter_expression: str, resource: TfStateResource):
+    super().__init__(
+      filter_expression=filter_expression,
+      object_attribute_value=resource.get_name()
     )
 
 class TfStateFilterHelper():
 
-  def __init__(self, module_filter_expression: str=None, resources: list=None):
+  def __init__(
+    self,
+    name_filter_expression: str=None,
+    module_filter_expression: str=None,
+    resources: list=None
+  ):
+    self.__name_filter_expression = name_filter_expression
     self.__module_filter_expression = module_filter_expression
     self.__resources = resources
 
@@ -29,13 +43,18 @@ class TfStateFilterHelper():
     filtered_list = list()
 
     for resource in self.__resources or []:
+      pass_name_filter = True
       pass_module_filter = True
 
+      if self.__name_filter_expression:
+        filter_helper = NameFilterHelper(filter_expression=self.__name_filter_expression, resource=resource)
+        pass_name_filter = filter_helper.check_filter()
+
       if self.__module_filter_expression:
         filter_helper = ModuleFilterHelper(filter_expression=self.__module_filter_expression, resource=resource)
         pass_module_filter = filter_helper.check_filter()
 
-      if pass_module_filter:
+      if pass_module_filter and pass_name_filter:
         filtered_list.append(resource)
 
     return filtered_list
diff --git a/tflens/model/tfstate_resource.py b/tflens/model/tfstate_resource.py
index 09c839f..be6a07a 100644
--- a/tflens/model/tfstate_resource.py
+++ b/tflens/model/tfstate_resource.py
@@ -27,6 +27,9 @@ class TfStateResource():
       self.get_parent_module()
     ]
 
+  def get_name(self):
+    return self.__name
+
   def get_parent_module(self):
     return self.__module.split('.')[1] if self.__module else '-'
 


Test patch:
diff --git a/tests/controllers_test.py b/tests/controllers_test.py
index e6925f8..61504b4 100644
--- a/tests/controllers_test.py
+++ b/tests/controllers_test.py
@@ -92,6 +92,28 @@ class TestLocalTfStateController(unittest.TestCase):
 
     self.assertEqual(captured_output.getvalue().replace('\n', ''), '')
 
+  def test_local_show_resources_matching_name_filter(self):
+    local_tfstate_controller = LocalTfStateController(
+      file_location=self.existing_file,
+      name_filter_expression="current_user"
+    )
+    captured_output = io.StringIO()
+    sys.stdout = captured_output
+    local_tfstate_controller.show_resources()
+
+    self.assertEqual(captured_output.getvalue().replace('\n', ''), self.print_table_output)
+
+  def test_local_show_resources_not_matching_name_filter(self):
+    local_tfstate_controller = LocalTfStateController(
+      file_location=self.existing_file,
+      name_filter_expression="Current_user"
+    )
+    captured_output = io.StringIO()
+    sys.stdout = captured_output
+    local_tfstate_controller.show_resources()
+
+    self.assertEqual(captured_output.getvalue().replace('\n', ''), '')
+
   def test_local_create_markdown_file(self):
     local_tfstate_controller = LocalTfStateController(self.existing_file)
     local_tfstate_controller.create_markdown_file()