Repository: getmoto/moto
Pull request: Add keypair validation
proposed fix for https://github.com/spulec/moto/issues/5151

Problem statement:
No keypair validation on run instance
**Repro steps:**
1. Start moto server.
2. Prepare terraform file that will contain an instance with declared key_name that is not assigned to any existing aws_key_pair
3. Run terraform init; terraform apply

**Description:**
When attempting to create an instance with key_name that is not assigned to any existing aws_key_pair moto doesn't verify that and creates an instance.

**Observed results:**
It is possible to create an instance with nonexisting key_pair

**expected results:**
Informative error with status code 400 - matching true AWS behaviour when attempting to create Instance with key_name that is not assigned to any existing aws_key_pair

Hints from issue discussion:

Commit message: add-keypair validation on add instances

Patch to review:
diff --git a/moto/ec2/models/instances.py b/moto/ec2/models/instances.py
index dcc578d88767..5c9263be7e0d 100644
--- a/moto/ec2/models/instances.py
+++ b/moto/ec2/models/instances.py
@@ -550,6 +550,8 @@ def get_instance(self, instance_id):
     def add_instances(self, image_id, count, user_data, security_group_names, **kwargs):
         location_type = "availability-zone" if kwargs.get("placement") else "region"
         default_region = "us-east-1"
+        describe_key_pairs = self.describe_key_pairs
+        describe_key_pairs(key_names=[kwargs.get("key_name")])
         valid_instance_types = INSTANCE_TYPE_OFFERINGS[location_type]
         if "region_name" in kwargs and kwargs.get("placement"):
             valid_availability_zones = {
diff --git a/moto/ec2/models/key_pairs.py b/moto/ec2/models/key_pairs.py
index 7075f7e7f018..ac9b5f58daa0 100644
--- a/moto/ec2/models/key_pairs.py
+++ b/moto/ec2/models/key_pairs.py
@@ -46,7 +46,7 @@ def delete_key_pair(self, name):
 
     def describe_key_pairs(self, key_names=None, filters=None):
         results = []
-        if key_names:
+        if any(key_names):
             results = [
                 keypair
                 for keypair in self.keypairs.values()
diff --git a/tests/test_ec2/test_instances.py b/tests/test_ec2/test_instances.py
index e0c34cb140ce..17e3b0e1a3c1 100644
--- a/tests/test_ec2/test_instances.py
+++ b/tests/test_ec2/test_instances.py
@@ -1561,22 +1561,43 @@ def test_ec2_classic_has_public_ip_address():
 @mock_ec2
 def test_run_instance_with_keypair():
     ec2 = boto3.resource("ec2", region_name="us-east-1")
+    key_name = "keypair_name"
+    ec2.create_key_pair(KeyName=key_name)
+
     instance = ec2.create_instances(
-        ImageId=EXAMPLE_AMI_ID, MinCount=1, MaxCount=1, KeyName="keypair_name"
+        ImageId=EXAMPLE_AMI_ID, MinCount=1, MaxCount=1, KeyName=key_name
     )[0]
 
     instance.key_name.should.equal("keypair_name")
 
 
+@mock_ec2
+def test_run_instance_with_invalid_keypair():
+    ec2 = boto3.resource("ec2", region_name="us-east-1")
+    key_name = "keypair_name"
+    ec2.create_key_pair(KeyName=key_name)
+
+    with pytest.raises(ClientError) as ex:
+        ec2.create_instances(
+            ImageId=EXAMPLE_AMI_ID, MinCount=1, MaxCount=1, KeyName="not a key name"
+        )[0]
+
+    ex.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
+    ex.value.response["Error"]["Code"].should.equal("InvalidKeyPair.NotFound")
+
+
 @mock_ec2
 def test_run_instance_with_block_device_mappings():
     ec2_client = boto3.client("ec2", region_name="us-east-1")
+    ec2_resource = boto3.resource("ec2", region_name="us-east-1")
+    key_name = "keypair_name"
+    ec2_resource.create_key_pair(KeyName=key_name)
 
     kwargs = {
         "MinCount": 1,
         "MaxCount": 1,
         "ImageId": EXAMPLE_AMI_ID,
-        "KeyName": "the_key",
+        "KeyName": key_name,
         "InstanceType": "t1.micro",
         "BlockDeviceMappings": [{"DeviceName": "/dev/sda2", "Ebs": {"VolumeSize": 50}}],
     }
@@ -1617,12 +1638,14 @@ def test_run_instance_with_block_device_mappings_missing_ebs():
 @mock_ec2
 def test_run_instance_with_block_device_mappings_using_no_device():
     ec2_client = boto3.client("ec2", region_name="us-east-1")
+    key_name = "keypair_name"
+    ec2_client.create_key_pair(KeyName=key_name)
 
     kwargs = {
         "MinCount": 1,
         "MaxCount": 1,
         "ImageId": EXAMPLE_AMI_ID,
-        "KeyName": "the_key",
+        "KeyName": key_name,
         "InstanceType": "t1.micro",
         "BlockDeviceMappings": [{"DeviceName": "/dev/sda2", "NoDevice": ""}],
     }
@@ -1684,6 +1707,8 @@ def test_run_instance_with_block_device_mappings_missing_size():
 def test_run_instance_with_block_device_mappings_from_snapshot():
     ec2_client = boto3.client("ec2", region_name="us-east-1")
     ec2_resource = boto3.resource("ec2", region_name="us-east-1")
+    key_name = "the_key"
+    ec2_resource.create_key_pair(KeyName=key_name)
     volume_details = {
         "AvailabilityZone": "1a",
         "Size": 30,
@@ -1695,7 +1720,7 @@ def test_run_instance_with_block_device_mappings_from_snapshot():
         "MinCount": 1,
         "MaxCount": 1,
         "ImageId": EXAMPLE_AMI_ID,
-        "KeyName": "the_key",
+        "KeyName": key_name,
         "InstanceType": "t1.micro",
         "BlockDeviceMappings": [
             {"DeviceName": "/dev/sda2", "Ebs": {"SnapshotId": snapshot.snapshot_id}}


Code review comments:
diff_hunk: @@ -550,6 +550,8 @@ def get_instance(self, instance_id):
     def add_instances(self, image_id, count, user_data, security_group_names, **kwargs):
         location_type = "availability-zone" if kwargs.get("placement") else "region"
         default_region = "us-east-1"
+        describe_key_pairs = self.describe_key_pairs
+        describe_key_pairs(key_names=[kwargs.get("key_name")])
path: moto/ec2/models/instances.py
text: @user1:
```suggestion
        self.describe_key_pairs(key_names=[kwargs.get("key_name")])
```
Minor nitpick - I don't think reassigning the method does anything for readability