Repository: seek-oss/aec

Issue:
Inadvertant termination of ALL ec2 instances in region
I am using aec via a bash script, which because of a configuration error executed `aec ec2 terminate "$instanceId"` with an empty `$instanceId`.

This had the rather unfortunate effect of terminating *all* of the ec2 instances visible to my role in the selected region. Looking at `ec2.py` it looks like the empty `name` argument was passed through to describe which happily returned all of the instances which where then deleted.

I think that terminate (and probably start and stop) should probably refuse to operate on more than one instance at a time, or alternatively guard against an empty name filter.

Discussion:
tekumara: It wasn't obvious to me that no name parameter meant ALL instances would be terminated... rather than just doing nothing... so I thought it worth capturing that in the comment
huynguyent: ah ok, that seems fair to me 👍 

Patch:
diff --git a/src/aec/__init__.py b/src/aec/__init__.py
index a7c8d66..71ed17d 100644
--- a/src/aec/__init__.py
+++ b/src/aec/__init__.py
@@ -1,1 +1,1 @@
-__version__ = "2.5.5"
+__version__ = "2.5.6"
diff --git a/src/aec/command/ec2.py b/src/aec/command/ec2.py
index d76bbdc..9adc37c 100755
--- a/src/aec/command/ec2.py
+++ b/src/aec/command/ec2.py
@@ -250,6 +250,10 @@ def tag(
         parts = t.split("=")
         tagdefs.append({"Key": parts[0], "Value": parts[1]})
 
+    if not name and not name_match:
+        # avoid tagging all instances when there's no name
+        raise NoInstancesError(name=name, name_match=name_match)
+
     instances = describe(config, name, name_match)
 
     ids = [i["InstanceId"] for i in instances]
@@ -317,6 +321,10 @@ def start(config: Config, name: str) -> List[Instance]:
 
     print(f"Starting instances with the name {name} ... ")
 
+    if not name:
+        # avoid starting all instances when there's no name
+        raise NoInstancesError(name=name)
+
     instances = describe(config, name)
 
     if not instances:
@@ -336,6 +344,10 @@ def stop(config: Config, name: str) -> List[Dict[str, Any]]:
 
     ec2_client = boto3.client("ec2", region_name=config.get("region", None))
 
+    if not name:
+        # avoid stopping all instances when there's no name
+        raise NoInstancesError(name=name)
+
     instances = describe(config, name)
 
     if not instances:
@@ -351,6 +363,10 @@ def terminate(config: Config, name: str) -> List[Dict[str, Any]]:
 
     ec2_client = boto3.client("ec2", region_name=config.get("region", None))
 
+    if not name:
+        # avoid terminating all instances when there's no name
+        raise NoInstancesError(name=name)
+
     instances = describe(config, name)
 
     if not instances:
@@ -368,6 +384,10 @@ def modify(config: Config, name: str, type: str) -> List[Instance]:
 
     ec2_client = boto3.client("ec2", region_name=config.get("region", None))
 
+    if not name:
+        # avoid modifying all instances when there's no name
+        raise NoInstancesError(name=name)
+
     instances = describe(config, name)
 
     if not instances:
@@ -399,6 +419,10 @@ def logs(config: Config, name: str) -> str:
 
     ec2_client = boto3.client("ec2", region_name=config.get("region", None))
 
+    if not name:
+        # avoid describing all instances when there's no name
+        raise NoInstancesError(name=name)
+
     instances = describe(config, name)
 
     if not instances:


Test patch:
diff --git a/tests/test_ec2.py b/tests/test_ec2.py
index b6ce0ea..0632ec1 100644
--- a/tests/test_ec2.py
+++ b/tests/test_ec2.py
@@ -357,6 +357,18 @@ def test_terminate(mock_aws_config):
     terminate(config=mock_aws_config, name="alice")
 
 
+def test_terminate_empty_name_does_not_delete_all_instances(mock_aws_config):
+    launch(mock_aws_config, "alice", ami_id)
+
+    with pytest.raises(ValueError) as exc_info:
+        terminate(config=mock_aws_config, name="")
+    print(exc_info.value.args[0])
+    assert exc_info.value.args[0] == """Missing name or name_match"""
+
+    instances = describe(config=mock_aws_config)
+    assert len(instances) == 1
+
+
 def test_logs(mock_aws_config):
     launch(mock_aws_config, "alice", ami_id)