Repository: Sceptre/sceptre

Issue:
It'd be great if `drift show` had an option to filter out IN_SYNC resources. 
Right now, when `drift show` is run, it shows the drift status of all the resources in all stacks. However, this is a bit overload if you're really just trying to find drifted resources. It would be a great addition if we added a `--filter` flag that would only show resources that are not IN_SYNC. 

Comments:

Patch:
diff --git a/sceptre/cli/drift.py b/sceptre/cli/drift.py
index 9aab47d..282d9b8 100644
--- a/sceptre/cli/drift.py
+++ b/sceptre/cli/drift.py
@@ -68,9 +68,12 @@ def drift_detect(ctx: Context, path: str):
 
 @drift_group.command(name="show", short_help="Shows stack drift on running stacks.")
 @click.argument("path")
+@click.option(
+    "-D", "--drifted", is_flag=True, default=False, help="Filter out in sync resources."
+)
 @click.pass_context
 @catch_exceptions
-def drift_show(ctx, path):
+def drift_show(ctx, path, drifted):
     """
     Show stack drift on deployed stacks.
 
@@ -92,7 +95,7 @@ def drift_show(ctx, path):
     )
 
     plan = SceptrePlan(context)
-    responses = plan.drift_show()
+    responses = plan.drift_show(drifted)
 
     output_format = "json" if context.output_format == "json" else "yaml"
 
@@ -100,7 +103,6 @@ def drift_show(ctx, path):
     for stack, (status, response) in responses.items():
         if status in BAD_STATUSES:
             exit_status += 1
-        response.pop("ResponseMetadata", None)
         write({stack.external_name: deserialize_json_properties(response)}, output_format)
 
     exit(exit_status)
diff --git a/sceptre/plan/actions.py b/sceptre/plan/actions.py
index 8711f0f..992a6e9 100644
--- a/sceptre/plan/actions.py
+++ b/sceptre/plan/actions.py
@@ -1039,10 +1039,11 @@ class StackActions(object):
         return response
 
     @add_stack_hooks
-    def drift_show(self) -> Tuple[str, dict]:
+    def drift_show(self, drifted: bool = False) -> Tuple[str, dict]:
         """
         Detect drift status on stacks.
 
+        :param drifted: Filter out IN_SYNC resources.
         :returns: The detection status and resource drifts.
         """
         response = self.drift_detect()
@@ -1055,6 +1056,7 @@ class StackActions(object):
         else:
             raise Exception("Not expected to be reachable")
 
+        response = self._filter_drifts(response, drifted)
         return (detection_status, response)
 
     def _wait_for_drift_status(self, detection_id: str) -> dict:
@@ -1144,6 +1146,24 @@ class StackActions(object):
             }
         )
 
+    def _filter_drifts(self, response: dict, drifted: bool) -> dict:
+        """
+        The filtered response after filtering out StackResourceDriftStatus.
+        :param drifted: Filter out IN_SYNC resources from CLI --drifted.
+        """
+        if "StackResourceDrifts" not in response:
+            return response
+
+        result = {"StackResourceDrifts": []}
+        include_all_drift_statuses = not drifted
+
+        for drift in response["StackResourceDrifts"]:
+            is_drifted = drift["StackResourceDriftStatus"] != "IN_SYNC"
+            if include_all_drift_statuses or is_drifted:
+                result["StackResourceDrifts"].append(drift)
+
+        return result
+
     @add_stack_hooks
     def dump_config(self, config_reader: ConfigReader):
         """


Test patch:
diff --git a/tests/test_actions.py b/tests/test_actions.py
index 672fe4a..62d76fd 100644
--- a/tests/test_actions.py
+++ b/tests/test_actions.py
@@ -1347,14 +1347,66 @@ class TestStackActions(object):
         mock_describe_stack_resource_drifts.return_value = expected_drifts
         expected_response = (detection_status, expected_drifts)
 
-        response = self.actions.drift_show()
+        response = self.actions.drift_show(drifted=False)
+
+        assert response == expected_response
+
+    @patch("sceptre.plan.actions.StackActions._describe_stack_resource_drifts")
+    @patch("sceptre.plan.actions.StackActions._describe_stack_drift_detection_status")
+    @patch("sceptre.plan.actions.StackActions._detect_stack_drift")
+    @patch("time.sleep")
+    def test_drift_show_drift_only(
+        self,
+        mock_sleep,
+        mock_detect_stack_drift,
+        mock_describe_stack_drift_detection_status,
+        mock_describe_stack_resource_drifts
+    ):
+        mock_sleep.return_value = None
+
+        mock_detect_stack_drift.return_value = {
+            "StackDriftDetectionId": "3fb76910-f660-11eb-80ac-0246f7a6da62"
+        }
+        mock_describe_stack_drift_detection_status.return_value = {
+            "StackId": "fake-stack-id",
+            "StackDriftDetectionId": "3fb76910-f660-11eb-80ac-0246f7a6da62",
+            "StackDriftStatus": "DRIFTED",
+            "DetectionStatus": "DETECTION_COMPLETE",
+            "DriftedStackResourceCount": 0
+        }
+
+        input_drifts = {
+            "StackResourceDrifts": [
+                {
+                    "LogicalResourceId": "ServerLoadBalancer",
+                    "PhysicalResourceId": "bi-tablea-ServerLo-1E133TWLWYLON",
+                    "ResourceType": "AWS::ElasticLoadBalancing::LoadBalancer",
+                    "StackId": "fake-stack-id",
+                    "StackResourceDriftStatus": "IN_SYNC",
+                },
+                {
+                    "LogicalResourceId": "TableauServer",
+                    "PhysicalResourceId": "i-08c16bc1c5e2cd185",
+                    "ResourceType": "AWS::EC2::Instance",
+                    "StackId": "fake-stack-id",
+                    "StackResourceDriftStatus": "DELETED",
+                }
+            ]
+        }
+        mock_describe_stack_resource_drifts.return_value = input_drifts
+
+        expected_response = ("DETECTION_COMPLETE", {
+            "StackResourceDrifts": [input_drifts["StackResourceDrifts"][1]]
+        })
+
+        response = self.actions.drift_show(drifted=True)
 
         assert response == expected_response
 
     @patch("sceptre.plan.actions.StackActions._get_status")
     def test_drift_show_with_stack_that_does_not_exist(self, mock_get_status):
         mock_get_status.side_effect = StackDoesNotExistError()
-        response = self.actions.drift_show()
+        response = self.actions.drift_show(drifted=False)
         assert response == (
             'STACK_DOES_NOT_EXIST', {
                 'StackResourceDriftStatus': 'STACK_DOES_NOT_EXIST'
@@ -1408,6 +1460,6 @@ class TestStackActions(object):
         mock_describe_stack_resource_drifts.return_value = expected_drifts
         expected_response = ("TIMED_OUT", {"StackResourceDriftStatus": "TIMED_OUT"})
 
-        response = self.actions.drift_show()
+        response = self.actions.drift_show(drifted=False)
 
         assert response == expected_response