Repository: getmoto/moto (python)

Issue:
elbv2 register_targets returning 200 when registering a stopped instance
From boto3 docs: 
> If the target is an EC2 instance, it must be in the running state when you register it.

Reproduce:
- Create an ec2 instance in moto
- Stop that instance
- Create a target group and register that instance in the target group, moto returns 200
  - Expectation: ClientError (InvalidTargetError) is raised

```python
EXAMPLE_AMI_ID = "ami-12c6146b"
pytest.aws_region = "us-west-2"

def test_tg_registration(elbv2):
    from boto3 import client, resource
    import os

    ec2_client = client("ec2")
    ec2_resource = resource("ec2")
    ec2_client.stop_instances(InstanceIds=[elbv2[0]])

    print(elbv2[0])

    res = ec2_resource.instances.filter(Filters=[{"Name": "instance-id", "Values": [elbv2[0]]}])
    for instance in res:
        print(instance.state)
    elb_client = client("elbv2")
    tg_res = elb_client.create_target_group(
            Name="test",
            Protocol="HTTP",
            Port=80,
            VpcId=os.environ["VPC_ID"],
            HealthCheckEnabled=True,
            HealthCheckProtocol="HTTP",
            HealthCheckPort=str(80),
            TargetType="instance",
        )
    print(tg_res["TargetGroups"][0]["TargetGroupArn"])
    target_reg_res = elb_client.register_targets(TargetGroupArn=tg_arn, Targets=targets)
    print(target_reg_res)

@pytest.fixture(scope="session")
def aws_credentials():
    os.environ["AWS_ACCESS_KEY_ID"] = "testing"
    os.environ["AWS_SECRET_ACCESS_KEY"] = "testing"
    os.environ["AWS_SECURITY_TOKEN"] = "testing"
    os.environ["AWS_SESSION_TOKEN"] = "testing"
    os.environ["AWS_DEFAULT_REGION"] = "us-west-2"
    os.environ["LOG_LEVEL"] = "debug"

@pytest.fixture(scope="session", autouse=True)
def ec2(aws_credentials):
    with mock_aws():
        client = boto3.client("ec2", region_name=pytest.aws_region)
        reservation = client.run_instances(ImageId=EXAMPLE_AMI_ID, MinCount=1, MaxCount=1)
        instance = reservation["Instances"][0]
        instance_id = instance["InstanceId"]
        private_ip_address = instance["PrivateIpAddress"]
        vpc = client.create_vpc(CidrBlock="10.0.0.0/16")
        subnet = client.create_subnet(VpcId=vpc["Vpc"]["VpcId"], CidrBlock="10.0.0.0/18")
        subnet_id = subnet["Subnet"]["SubnetId"]
        os.environ["VPC_ID"] = vpc["Vpc"]["VpcId"]
        logger.info(">>> VPC ID: %s", vpc["Vpc"]["VpcId"])
        logger.info(">>> Subnet ID: %s", subnet["Subnet"]["SubnetId"])
        yield client, instance_id, subnet_id, private_ip_address

@pytest.fixture(scope="session", autouse=True)
def elbv2(ec2):
    with mock_aws():
        client = boto3.client("elbv2", region_name=pytest.aws_region)
        lb = client.create_load_balancer(Name="app-lb", Subnets=[ec2[2]])
        lb_arn = lb["LoadBalancers"][0]["LoadBalancerArn"]

        listener = client.create_listener(
            LoadBalancerArn=lb_arn,
            Protocol="HTTP",
            Port=80,
            DefaultActions=[
                {
                    "Type": "fixed-response",
                    "FixedResponseConfig": {
                        "MessageBody": "test",
                        "StatusCode": "200",
                        "ContentType": "text/plain",
                    },
                }
            ],
        )
        listener_arn = listener["Listeners"][0]["ListenerArn"]
        os.environ["LB_ARN"] = lb_arn
        os.environ["LISTENER_ARN"] = listener_arn

        yield ec2[1], lb_arn, listener_arn
```

Outputs:
```
LISTENER_ARN: arn:aws:elasticloadbalancing:us-west-2:123456789012:listener/app/app-lb/50dc6c495c0c9188/804761638992
{'Code': 80, 'Name': 'stopped'}
arn:aws:elasticloadbalancing:us-west-2:123456789012:targetgroup/test/50dc6c495c0c9188
Target Registration Result: {'ResponseMetadata': {'RequestId': 'dbtP5ZFm6zIoVvxNN8VJi38KivH0HbJYuWeVZpLlWLaZPzasvAIW', 'HTTPStatusCode': 200, 'HTTPHeaders': {'server': 'amazon.com', 'date': 'Sun, 03 Nov 2024 23:57:00 GMT', 'x-amzn-requestid': 'dbtP5ZFm6zIoVvxNN8VJi38KivH0HbJYuWeVZpLlWLaZPzasvAIW'}, 'RetryAttempts': 0}}
```

Pull request description:
ELBv2: register_targets() now validates that instances are running
Fixes #8286 

Patch:
diff --git a/moto/elbv2/exceptions.py b/moto/elbv2/exceptions.py
index cd4cc6e47..8ab6d7a77 100644
--- a/moto/elbv2/exceptions.py
+++ b/moto/elbv2/exceptions.py
@@ -94,6 +94,14 @@ class InvalidTargetError(ELBClientError):
         )
 
 
+class TargetNotRunning(ELBClientError):
+    def __init__(self, instance_id: str) -> None:
+        super().__init__(
+            "InvalidTarget",
+            f"The following targets are not in a running state and cannot be registered: '{instance_id}'",
+        )
+
+
 class EmptyListenersError(ELBClientError):
     def __init__(self) -> None:
         super().__init__("ValidationError", "Listeners cannot be empty")
diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py
index 4c691c8bc..10ae12cd9 100644
--- a/moto/elbv2/models.py
+++ b/moto/elbv2/models.py
@@ -9,7 +9,7 @@ from moto.core.base_backend import BackendDict, BaseBackend
 from moto.core.common_models import BaseModel, CloudFormationModel
 from moto.core.exceptions import RESTError
 from moto.core.utils import iso_8601_datetime_with_milliseconds
-from moto.ec2.models import ec2_backends
+from moto.ec2.models import EC2Backend, ec2_backends
 from moto.ec2.models.subnets import Subnet
 from moto.moto_api._internal import mock_random
 from moto.utilities.tagging_service import TaggingService
@@ -39,6 +39,7 @@ from .exceptions import (
     RuleNotFoundError,
     SubnetNotFoundError,
     TargetGroupNotFoundError,
+    TargetNotRunning,
     TooManyCertificatesError,
     TooManyTagsError,
     ValidationError,
@@ -78,7 +79,8 @@ class FakeTargetGroup(CloudFormationModel):
     def __init__(
         self,
         name: str,
-        arn: str,
+        account_id: str,
+        region_name: str,
         vpc_id: str,
         protocol: str,
         port: str,
@@ -97,7 +99,11 @@ class FakeTargetGroup(CloudFormationModel):
     ):
         # TODO: default values differs when you add Network Load balancer
         self.name = name
-        self.arn = arn
+        self.account_id = account_id
+        self.region_name = region_name
+        self.arn = make_arn_for_target_group(
+            account_id=self.account_id, name=name, region_name=self.region_name
+        )
         self.vpc_id = vpc_id
         if target_type == "lambda":
             self.protocol = None
@@ -156,6 +162,10 @@ class FakeTargetGroup(CloudFormationModel):
 
     def register(self, targets: List[Dict[str, Any]]) -> None:
         for target in targets:
+            if instance := self.ec2_backend.get_instance_by_id(target["id"]):
+                if instance.state != "running":
+                    raise TargetNotRunning(instance_id=target["id"])
+
             self.targets[target["id"]] = {
                 "id": target["id"],
                 "port": target.get("port", self.port),
@@ -175,7 +185,9 @@ class FakeTargetGroup(CloudFormationModel):
                 t = self.targets.pop(target_id)
                 self.terminated_targets[target_id] = t
 
-    def health_for(self, target: Dict[str, Any], ec2_backend: Any) -> FakeHealthStatus:
+    def health_for(
+        self, target: Dict[str, Any], ec2_backend: EC2Backend
+    ) -> FakeHealthStatus:
         t = self.targets.get(target["id"])
         if t is None:
             port = self.port
@@ -220,7 +232,7 @@ class FakeTargetGroup(CloudFormationModel):
             )
         if t["id"].startswith("i-"):  # EC2 instance ID
             instance = ec2_backend.get_instance_by_id(t["id"])
-            if instance.state == "stopped":
+            if instance and instance.state == "stopped":
                 return FakeHealthStatus(
                     t["id"],
                     t["port"],
@@ -283,6 +295,10 @@ class FakeTargetGroup(CloudFormationModel):
         )
         return target_group
 
+    @property
+    def ec2_backend(self) -> EC2Backend:
+        return ec2_backends[self.account_id][self.region_name]
+
 
 class FakeListener(CloudFormationModel):
     def __init__(
@@ -768,13 +784,7 @@ class ELBv2Backend(BaseBackend):
         self.tagging_service = TaggingService()
 
     @property
-    def ec2_backend(self) -> Any:  # type: ignore[misc]
-        """
-        EC2 backend
-
-        :return: EC2 Backend
-        :rtype: moto.ec2.models.EC2Backend
-        """
+    def ec2_backend(self) -> EC2Backend:
         return ec2_backends[self.account_id][self.region_name]
 
     def create_load_balancer(
@@ -798,8 +808,8 @@ class ELBv2Backend(BaseBackend):
             if subnet is None:
                 raise SubnetNotFoundError()
             subnets.append(subnet)
-        for subnet in subnet_mappings or []:
-            subnet_id = subnet["SubnetId"]
+        for subnet_mapping in subnet_mappings or []:
+            subnet_id = subnet_mapping["SubnetId"]
             subnet = self.ec2_backend.get_subnet(subnet_id)
             if subnet is None:
                 raise SubnetNotFoundError()
@@ -1166,7 +1176,7 @@ Member must satisfy regular expression pattern: {expression}"
             from moto.ec2.exceptions import InvalidVPCIdError
 
             try:
-                self.ec2_backend.get_vpc(kwargs.get("vpc_id"))
+                self.ec2_backend.get_vpc(kwargs.get("vpc_id") or "")
             except InvalidVPCIdError:
                 raise ValidationError(
                     f"The VPC ID '{kwargs.get('vpc_id')}' is not found"
@@ -1283,11 +1293,13 @@ Member must satisfy regular expression pattern: {expression}"
                     f"Health check timeout '{healthcheck_timeout_seconds}' must be smaller than the interval '{healthcheck_interval_seconds}'"
                 )
 
-        arn = make_arn_for_target_group(
-            account_id=self.account_id, name=name, region_name=self.region_name
-        )
         tags = kwargs.pop("tags", None)
-        target_group = FakeTargetGroup(name, arn, **kwargs)
+        target_group = FakeTargetGroup(
+            name=name,
+            account_id=self.account_id,
+            region_name=self.region_name,
+            **kwargs,
+        )
         self.target_groups[target_group.arn] = target_group
         if tags:
             self.add_tags(resource_arns=[target_group.arn], tags=tags)
diff --git a/moto/opensearch/models.py b/moto/opensearch/models.py
index 5032e0adc..31c91821d 100644
--- a/moto/opensearch/models.py
+++ b/moto/opensearch/models.py
@@ -1,7 +1,9 @@
+import datetime
 from typing import Any, Dict, List, Optional
 
 from moto.core.base_backend import BackendDict, BaseBackend
 from moto.core.common_models import BaseModel
+from moto.core.utils import unix_time
 from moto.utilities.tagging_service import TaggingService
 from moto.utilities.utils import get_partition
 
@@ -42,6 +44,25 @@ default_advanced_options = {
 }
 
 
+class EngineVersion(BaseModel):
+    def __init__(self, options: str, create_time: datetime.datetime) -> None:
+        self.options = options or "OpenSearch_2.5"
+        self.create_time = unix_time(create_time)
+        self.update_time = self.create_time
+
+    def to_dict(self) -> Dict[str, Any]:
+        return {
+            "Options": self.options,
+            "Status": {
+                "CreationDate": self.create_time,
+                "PendingDeletion": False,
+                "State": "Active",
+                "UpdateDate": self.update_time,
+                "UpdateVersion": 28,
+            },
+        }
+
+
 class OpenSearchDomain(BaseModel):
     def __init__(
         self,
@@ -70,7 +91,7 @@ class OpenSearchDomain(BaseModel):
         self.arn = (
             f"arn:{get_partition(region)}:es:{region}:{account_id}:domain/{domain_name}"
         )
-        self.engine_version = engine_version or "OpenSearch 2.5"
+        self.engine_version = EngineVersion(engine_version, datetime.datetime.now())
         self.cluster_config = cluster_config or {}
         self.ebs_options = ebs_options or {"EBSEnabled": False}
         self.access_policies = access_policies or ""
@@ -120,7 +141,7 @@ class OpenSearchDomain(BaseModel):
         return {
             "Endpoint": self.endpoint,
             "Endpoints": self.endpoints,
-            "EngineVersion": self.engine_version,
+            "EngineVersion": self.engine_version.to_dict(),
             "ClusterConfig": self.cluster_config,
             "EBSOptions": self.ebs_options,
             "AccessPolicies": self.access_policies,
@@ -207,6 +228,7 @@ class OpenSearchDomain(BaseModel):
         self.software_update_options = (
             software_update_options or self.software_update_options
         )
+        self.engine_version.update_time = unix_time(datetime.datetime.now())
 
 
 class OpenSearchServiceBackend(BaseBackend):
@@ -335,7 +357,7 @@ class OpenSearchServiceBackend(BaseBackend):
         domains = []
         for domain in self.domains.values():
             if engine_type:
-                if engine_type in domain.engine_version:
+                if engine_type in domain.engine_version.options:
                     domains.append(
                         {
                             "DomainName": domain.domain_name,
@@ -348,7 +370,7 @@ class OpenSearchServiceBackend(BaseBackend):
                 domains.append(
                     {
                         "DomainName": domain.domain_name,
-                        "EngineType": domain.engine_version.split("_")[0],
+                        "EngineType": domain.engine_version.options.split("_")[0],
                     }
                 )
         return domains
diff --git a/moto/s3/responses.py b/moto/s3/responses.py
index d0c82428a..c4427212c 100644
--- a/moto/s3/responses.py
+++ b/moto/s3/responses.py
@@ -124,7 +124,7 @@ ACTION_MAP = {
         },
     },
     "KEY": {
-        "HEAD": {"DEFAULT": "HeadObject"},
+        "HEAD": {"DEFAULT": "GetObject"},
         "GET": {
             "uploadId": "ListMultipartUploadParts",
             "acl": "GetObjectAcl",


Test patch:
diff --git a/tests/test_elbv2/test_elbv2_target_health.py b/tests/test_elbv2/test_elbv2_target_health.py
index 47d68b5e3..dea198acf 100644
--- a/tests/test_elbv2/test_elbv2_target_health.py
+++ b/tests/test_elbv2/test_elbv2_target_health.py
@@ -3,7 +3,7 @@ from uuid import uuid4
 
 import boto3
 import pytest
-from botocore.exceptions import WaiterError
+from botocore.exceptions import ClientError, WaiterError
 
 from . import elbv2_aws_verified
 
@@ -94,9 +94,6 @@ sudo systemctl enable httpd"""
             UserData=init_script,
         )[0].id
 
-        waiter = ec2_client.get_waiter("instance_running")
-        waiter.wait(InstanceIds=[instance_id1, instance_id2])
-
         load_balancer_arn = conn.create_load_balancer(
             Name=lb_name,
             Subnets=[subnet1.id, subnet2.id],
@@ -124,9 +121,7 @@ sudo systemctl enable httpd"""
         )["TargetGroups"][0]["TargetGroupArn"]
 
         # No targets registered yet
-        healths = conn.describe_target_health(TargetGroupArn=target_group_arn)[
-            "TargetHealthDescriptions"
-        ]
+        healths = describe_healths(target_group_arn)
         assert len(healths) == 0
 
         listener_arn = conn.create_listener(
@@ -141,6 +136,11 @@ sudo systemctl enable httpd"""
             ],
         )["Listeners"][0]["ListenerArn"]
 
+        # Instances were started a while ago
+        # Make sure they are ready before trying to register them
+        waiter = ec2_client.get_waiter("instance_running")
+        waiter.wait(InstanceIds=[instance_id1, instance_id2])
+
         # Register target
         # Verify that they are healthy
         conn.register_targets(
@@ -150,9 +150,8 @@ sudo systemctl enable httpd"""
         waiter = conn.get_waiter("target_in_service")
         waiter.wait(TargetGroupArn=target_group_arn)
 
-        healths = conn.describe_target_health(TargetGroupArn=target_group_arn)[
-            "TargetHealthDescriptions"
-        ]
+        healths = describe_healths(target_group_arn)
+
         assert {
             "Target": {"Id": instance_id1, "Port": 80},
             "HealthCheckPort": "80",
@@ -173,9 +172,7 @@ sudo systemctl enable httpd"""
         waiter = conn.get_waiter("target_deregistered")
         waiter.wait(TargetGroupArn=target_group_arn, Targets=[{"Id": instance_id1}])
 
-        healths = conn.describe_target_health(TargetGroupArn=target_group_arn)[
-            "TargetHealthDescriptions"
-        ]
+        healths = describe_healths(target_group_arn)
         assert len(healths) == 1
         assert {
             "Target": {"Id": instance_id2, "Port": 80},
@@ -187,6 +184,9 @@ sudo systemctl enable httpd"""
         healths = conn.describe_target_health(
             TargetGroupArn=target_group_arn, Targets=[{"Id": instance_id1, "Port": 80}]
         )["TargetHealthDescriptions"]
+        for h in healths:
+            # Skip attributes not implemented in Moto
+            h.pop("AdministrativeOverride", None)
         assert len(healths) == 1
         assert {
             "Target": {"Id": instance_id1, "Port": 80},
@@ -206,9 +206,7 @@ sudo systemctl enable httpd"""
         waiter = conn.get_waiter("target_in_service")
         waiter.wait(TargetGroupArn=target_group_arn)
 
-        healths = conn.describe_target_health(TargetGroupArn=target_group_arn)[
-            "TargetHealthDescriptions"
-        ]
+        healths = describe_healths(target_group_arn)
         assert {
             "Target": {"Id": instance_id1, "Port": 80},
             "HealthCheckPort": "80",
@@ -224,12 +222,24 @@ sudo systemctl enable httpd"""
         waiter = ec2_client.get_waiter("instance_stopped")
         waiter.wait(InstanceIds=[instance_id1])
 
+        # Verify we can't register an instance that has been stopped
+        with pytest.raises(ClientError) as exc:
+            conn.register_targets(
+                TargetGroupArn=target_group_arn,
+                Targets=[{"Id": instance_id1}],
+            )
+        err = exc.value.response
+        assert err["ResponseMetadata"]["HTTPStatusCode"] == 400
+        assert err["Error"]["Code"] == "InvalidTarget"
+        assert (
+            err["Error"]["Message"]
+            == f"The following targets are not in a running state and cannot be registered: '{instance_id1}'"
+        )
+
         # Instance is marked as 'unused' when stopped
         states = set()
         while states != {"healthy", "unused"}:
-            healths = conn.describe_target_health(TargetGroupArn=target_group_arn)[
-                "TargetHealthDescriptions"
-            ]
+            healths = describe_healths(target_group_arn)
             states = set([h["TargetHealth"]["State"] for h in healths])
 
             if target_is_aws:
@@ -254,13 +264,9 @@ sudo systemctl enable httpd"""
         waiter.wait(InstanceIds=[instance_id1])
 
         # Instance is removed when terminated
-        healths = conn.describe_target_health(TargetGroupArn=target_group_arn)[
-            "TargetHealthDescriptions"
-        ]
+        healths = describe_healths(target_group_arn)
         while len(healths) != 1:
-            healths = conn.describe_target_health(TargetGroupArn=target_group_arn)[
-                "TargetHealthDescriptions"
-            ]
+            healths = describe_healths(target_group_arn)
 
             if target_is_aws:
                 sleep(5)
@@ -275,6 +281,9 @@ sudo systemctl enable httpd"""
         healths = conn.describe_target_health(
             TargetGroupArn=target_group_arn, Targets=[{"Id": instance_id1, "Port": 80}]
         )["TargetHealthDescriptions"]
+        for h in healths:
+            # Skip attributes not implemented in Moto
+            h.pop("AdministrativeOverride", None)
         assert healths == [
             {
                 "Target": {"Id": instance_id1, "Port": 80},
@@ -336,6 +345,17 @@ sudo systemctl enable httpd"""
         ec2_client.delete_internet_gateway(InternetGatewayId=igw_id)
 
 
+def describe_healths(target_group_arn):
+    conn = boto3.client("elbv2", region_name="us-east-1")
+    healths = conn.describe_target_health(TargetGroupArn=target_group_arn)[
+        "TargetHealthDescriptions"
+    ]
+    for h in healths:
+        # Skip attributes not implemented in Moto
+        h.pop("AdministrativeOverride", None)
+    return healths
+
+
 @elbv2_aws_verified()
 @pytest.mark.aws_verified
 def test_describe_unknown_targets(target_is_aws=False):
diff --git a/tests/test_opensearch/test_opensearch.py b/tests/test_opensearch/test_opensearch.py
index 7c92475fb..1ce66fef1 100644
--- a/tests/test_opensearch/test_opensearch.py
+++ b/tests/test_opensearch/test_opensearch.py
@@ -1,8 +1,9 @@
 import boto3
 import pytest
 from botocore.exceptions import ClientError
+from freezegun import freeze_time
 
-from moto import mock_aws
+from moto import mock_aws, settings
 
 # See our Development Tips on writing tests for hints on how to write good tests:
 # http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html
@@ -47,7 +48,7 @@ def test_create_domain_with_some_options():
         EngineVersion="OpenSearch_1.1",
     )["DomainStatus"]
     assert status["Created"]
-    assert status["EngineVersion"] == "OpenSearch_1.1"
+    assert status["EngineVersion"]["Options"] == "OpenSearch_1.1"
     assert status["DomainEndpointOptions"] == {
         "EnforceHTTPS": True,
         "TLSSecurityPolicy": "Policy-Min-TLS-1-0-2019-07",
@@ -89,14 +90,27 @@ def test_describe_unknown_domain():
 
 
 @mock_aws
+@freeze_time("2015-01-01 12:00:00")
 def test_describe_domain():
+    # Setup
     client = boto3.client("opensearch", region_name="eu-west-1")
     client.create_domain(DomainName="testdn")
 
+    # Execute
     status = client.describe_domain(DomainName="testdn")["DomainStatus"]
+    ev_status = status["EngineVersion"]["Status"]
+
+    # Verify
     assert "DomainId" in status
     assert "DomainName" in status
     assert status["DomainName"] == "testdn"
+    assert status["EngineVersion"]["Options"] == "OpenSearch_2.5"
+    assert ev_status["State"] == "Active"
+    assert ev_status["PendingDeletion"] is False
+
+    if not settings.TEST_SERVER_MODE:
+        assert ev_status["CreationDate"] == 1420113600.0
+        assert ev_status["UpdateDate"] == 1420113600.0
 
 
 @mock_aws