Repository: Sceptre/sceptre

Issue:
An empty var file causes Sceptre to blow up
### Subject of the issue
If an empty var file is passed to Sceptre, Sceptre raises an exception

### Your environment
* version of sceptre (sceptre --version) 3.0.0
* version of python (python --version) 3.8.10
* which OS/distro Ubuntu

### Steps to reproduce
```
> common-env.yaml
sceptre --var-file=common-env.yaml config/stack.yaml 
```
### Expected behaviour
It should validate stack.yaml.

### Actual behaviour
```
Traceback (most recent call last):
  File "/usr/local/bin/sceptre", line 8, in <module>
    sys.exit(cli())
  File "/usr/local/lib/python3.8/site-packages/click/core.py", line 829, in __call__
    return self.main(*args, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/click/core.py", line 782, in main
    rv = self.invoke(ctx)
  File "/usr/local/lib/python3.8/site-packages/click/core.py", line 1256, in invoke
    Command.invoke(self, ctx)
  File "/usr/local/lib/python3.8/site-packages/click/core.py", line 1066, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/usr/local/lib/python3.8/site-packages/click/core.py", line 610, in invoke
    return callback(*args, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/click/decorators.py", line 21, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/sceptre/cli/helpers.py", line 42, in decorated
    return func(*args, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/sceptre/cli/__init__.py", line 69, in cli
    "user_variables": setup_vars(var_file, var, merge_vars, debug, no_colour),
  File "/usr/local/lib/python3.8/site-packages/sceptre/cli/helpers.py", line 224, in setup_vars
    return_value.update(parsed)
TypeError: 'NoneType' object is not iterable
```


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/cli/helpers.py b/sceptre/cli/helpers.py
index 5d680d9..5f48da8 100644
--- a/sceptre/cli/helpers.py
+++ b/sceptre/cli/helpers.py
@@ -216,7 +216,7 @@ def setup_vars(var_file, var, merge_vars, debug, no_colour):
 
     if var_file:
         for fh in var_file:
-            parsed = yaml.safe_load(fh.read())
+            parsed = yaml.safe_load(fh.read()) or {}
 
             if merge_vars:
                 return_value = _deep_merge(parsed, return_value)
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