Repository: getmoto/moto
Pull request #7326 (2024-02-09 12:02:31)

feat: use AWS Lambda entry point if ImageConfig.EntryPoint is set
With the proposed change it is possible to use `public.ecr.aws/lambda/python:3.12` image in a hacky way as
```
@mock.patch.dict(
    os.environ,
    {
        "MOTO_DOCKER_LAMBDA_IMAGE": "public.ecr.aws/lambda/python",
    },
)
def test_send_message():
    ...

    func = lambda_.create_function(
        FunctionName=function_name,

        Runtime=f"{sys.version_info.major}.{sys.version_info.minor}",
        ImageConfig={
            "EntryPoint": [
                "/var/lang/bin/python3",
                "-c",
                f"import sys, json; from {handler_package} import {handler_callback} as cb; cb(json.loads(sys.argv[2]), None)",
            ],
        },
   )
...
```

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/7326?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
Attention: `1 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`9aef694`)](https://app.codecov.io/gh/getmoto/moto/commit/9aef6944da6a6f1b84be99cefb34f1724e6ca58f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) 95.88% compared to head [(`de79876`)](https://app.codecov.io/gh/getmoto/moto/pull/7326?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) 95.88%.
> Report is 5 commits behind head on master.

| [Files](https://app.codecov.io/gh/getmoto/moto/pull/7326?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Patch % | Lines |
|---|---|---|
| [moto/awslambda/models.py](https://app.codecov.io/gh/getmoto/moto/pull/7326?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#diff-bW90by9hd3NsYW1iZGEvbW9kZWxzLnB5) | 50.00% | [1 Missing :warning: ](https://app.codecov.io/gh/getmoto/moto/pull/7326?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) |

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master    #7326   +/-   ##
=======================================
  Coverage   95.88%   95.88%           
=======================================
  Files         843      843           
  Lines       82578    82593   +15     
=======================================
+ Hits        79178    79195   +17     
+ Misses       3400     3398    -2     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/7326/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/7326/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `32.51% <14.75%> (-0.01%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/7326/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `95.85% <98.36%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/getmoto/moto/pull/7326?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).


Patch:
diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py
index bb402f493..8b6ca83a9 100644
--- a/moto/awslambda/models.py
+++ b/moto/awslambda/models.py
@@ -1001,6 +1001,10 @@ class LambdaFunction(CloudFormationModel, DockerModel):
                             "host.docker.internal": "host-gateway"
                         }
 
+                    # Change entry point if requested
+                    if self.image_config.entry_point:
+                        run_kwargs["entrypoint"] = self.image_config.entry_point
+
                     # The requested image can be found in one of a few repos:
                     # - User-provided repo
                     # - mlupin/docker-lambda (the repo with up-to-date AWSLambda images
diff --git a/moto/resourcegroupstaggingapi/models.py b/moto/resourcegroupstaggingapi/models.py
index c54af77a2..db2002a96 100644
--- a/moto/resourcegroupstaggingapi/models.py
+++ b/moto/resourcegroupstaggingapi/models.py
@@ -19,6 +19,7 @@ from moto.moto_api._internal import mock_random
 from moto.rds.models import RDSBackend, rds_backends
 from moto.redshift.models import RedshiftBackend, redshift_backends
 from moto.s3.models import S3Backend, s3_backends
+from moto.sns.models import SNSBackend, sns_backends
 from moto.sqs.models import SQSBackend, sqs_backends
 from moto.utilities.tagging_service import TaggingService
 
@@ -97,6 +98,10 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend):
     def acm_backend(self) -> AWSCertificateManagerBackend:
         return acm_backends[self.account_id][self.region_name]
 
+    @property
+    def sns_backend(self) -> SNSBackend:
+        return sns_backends[self.account_id][self.region_name]
+
     @property
     def sqs_backend(self) -> SQSBackend:
         return sqs_backends[self.account_id][self.region_name]
@@ -340,6 +345,24 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend):
                     "Tags": tags,
                 }
 
+        # ELB (Classic Load Balancers)
+        if (
+            not resource_type_filters
+            or "elb" in resource_type_filters
+            or "elb:loadbalancer" in resource_type_filters
+        ):
+            for elb in self.elb_backend.load_balancers.values():
+                tags = format_tags(elb.tags)
+                if not tags or not tag_filter(
+                    tags
+                ):  # Skip if no tags, or invalid filter
+                    continue
+
+                yield {
+                    "ResourceARN": f"arn:aws:elasticloadbalancing:{self.region_name}:{self.account_id}:loadbalancer/{elb.name}",
+                    "Tags": tags,
+                }
+
         # TODO add these to the keys and values functions / combine functions
         # ELB, resource type elasticloadbalancing:loadbalancer
         if (
@@ -347,14 +370,14 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend):
             or "elasticloadbalancing" in resource_type_filters
             or "elasticloadbalancing:loadbalancer" in resource_type_filters
         ):
-            for elb in self.elbv2_backend.load_balancers.values():
+            for elbv2 in self.elbv2_backend.load_balancers.values():
                 tags = self.elbv2_backend.tagging_service.list_tags_for_resource(
-                    elb.arn
+                    elbv2.arn
                 )["Tags"]
                 if not tag_filter(tags):  # Skip if no tags, or invalid filter
                     continue
 
-                yield {"ResourceARN": f"{elb.arn}", "Tags": tags}
+                yield {"ResourceARN": f"{elbv2.arn}", "Tags": tags}
 
         # ELB Target Group, resource type elasticloadbalancing:targetgroup
         if (
@@ -474,6 +497,17 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend):
 
                 yield {"ResourceARN": f"{queue.queue_arn}", "Tags": tags}
 
+        # SNS
+        if not resource_type_filters or "sns" in resource_type_filters:
+
+            for topic in self.sns_backend.topics.values():
+                tags = format_tags(topic._tags)
+                if not tags or not tag_filter(
+                    tags
+                ):  # Skip if no tags, or invalid filter
+                    continue
+                yield {"ResourceARN": f"{topic.arn}", "Tags": tags}
+
         # VPC
         if (
             not resource_type_filters
diff --git a/scripts/implementation_coverage.py b/scripts/implementation_coverage.py
index 8e4ad0863..05adab0b8 100755
--- a/scripts/implementation_coverage.py
+++ b/scripts/implementation_coverage.py
@@ -15,7 +15,11 @@ alternative_service_names = {"lambda": "awslambda"}
 def get_moto_implementation(service_name):
     try:
         backends = get_backend(service_name)
-        backend = backends[0]["us-east-1"] if "us-east-1" in backends[0] else backends[0]["global"]
+        backend = (
+            backends[0]["us-east-1"]
+            if "us-east-1" in backends[0]
+            else backends[0]["global"]
+        )
         return backend, service_name
     except ModuleNotFoundError:
         return None, service_name
@@ -24,9 +28,9 @@ def get_moto_implementation(service_name):
 def get_module_name(o):
     klass = o.__class__
     module = klass.__module__
-    if module == 'builtins':
-        return klass.__qualname__ # avoid outputs like 'builtins.str'
-    return module + '.' + klass.__qualname__
+    if module == "builtins":
+        return klass.__qualname__  # avoid outputs like 'builtins.str'
+    return module + "." + klass.__qualname__
 
 
 def calculate_extended_implementation_coverage():
@@ -161,7 +165,9 @@ def write_implementation_coverage_to_file(coverage):
 
 
 def write_implementation_coverage_to_docs(coverage):
-    implementation_coverage_file = "{}/../docs/docs/services/index.rst".format(script_dir)
+    implementation_coverage_file = "{}/../docs/docs/services/index.rst".format(
+        script_dir
+    )
     # rewrite the implementation coverage file with updated values
     # try deleting the implementation coverage file
     try:
@@ -179,7 +185,9 @@ def write_implementation_coverage_to_docs(coverage):
         not_implemented = coverage.get(service_name)["not_implemented"]
         operations = sorted(list(implemented.keys()) + not_implemented)
 
-        service_coverage_file = "{}/../docs/docs/services/{}.rst".format(script_dir, service_name)
+        service_coverage_file = "{}/../docs/docs/services/{}.rst".format(
+            script_dir, service_name
+        )
         shorthand = service_name.replace(" ", "_")
 
         with open(service_coverage_file, "w+") as file:
@@ -202,7 +210,9 @@ def write_implementation_coverage_to_docs(coverage):
 
             if coverage[service_name]["docs"]:
                 # Only show auto-generated documentation if it exists
-                file.write(".. autoclass:: " + coverage[service_name].get("module_name"))
+                file.write(
+                    ".. autoclass:: " + coverage[service_name].get("module_name")
+                )
                 file.write("\n\n")
 
             file.write("|start-h3| Implemented features for this service |end-h3|\n\n")
@@ -217,7 +227,6 @@ def write_implementation_coverage_to_docs(coverage):
                     file.write("- [ ] {}\n".format(op))
             file.write("\n")
 
-
     with open(implementation_coverage_file, "w+") as file:
         file.write(".. _implemented_services:\n")
         file.write("\n")
@@ -227,7 +236,9 @@ def write_implementation_coverage_to_docs(coverage):
         file.write("Implemented Services\n")
         file.write("====================\n")
         file.write("\n")
-        file.write("Please see a list of all currently supported services. Each service will have a list of the endpoints that are implemented.\n")
+        file.write(
+            "Please see a list of all currently supported services. Each service will have a list of the endpoints that are implemented.\n"
+        )
 
         file.write("\n")
         file.write(".. toctree::\n")
diff --git a/scripts/update_backend_index.py b/scripts/update_backend_index.py
index e46ea9dc6..c956d61db 100755
--- a/scripts/update_backend_index.py
+++ b/scripts/update_backend_index.py
@@ -19,7 +19,20 @@ output_path = os.path.join(script_dir, "..", output_file)
 # Ignore S3bucket_path, as the functionality is covered in the S3 service
 # Ignore neptune, as it shares a URL with RDS
 # Ignore OpenSearch, as it shares a URL with ElasticSearch
-IGNORE_BACKENDS = ["moto_server", "moto_proxy", "apigatewayv2", "awslambda_simple", "batch_simple", "core", "dynamodb_v20111205", "packages", "utilities", "s3bucket_path", "neptune", "opensearch"]
+IGNORE_BACKENDS = [
+    "moto_server",
+    "moto_proxy",
+    "apigatewayv2",
+    "awslambda_simple",
+    "batch_simple",
+    "core",
+    "dynamodb_v20111205",
+    "packages",
+    "utilities",
+    "s3bucket_path",
+    "neptune",
+    "opensearch",
+]
 
 
 def iter_backend_url_patterns():


Test patch:
diff --git a/tests/test_awslambda/test_lambda_invoke.py b/tests/test_awslambda/test_lambda_invoke.py
index 0d6ed9a01..7000cc163 100644
--- a/tests/test_awslambda/test_lambda_invoke.py
+++ b/tests/test_awslambda/test_lambda_invoke.py
@@ -377,3 +377,39 @@ def test_invoke_lambda_with_proxy():
 
     expected_payload = {"id": vol.id, "state": vol.state, "size": vol.size}
     assert json.loads(payload) == expected_payload
+
+
+@mock_aws
+def test_invoke_lambda_with_entrypoint():
+    conn = boto3.client("lambda", _lambda_region)
+    function_name = str(uuid4())[0:6]
+    conn.create_function(
+        FunctionName=function_name,
+        Runtime=PYTHON_VERSION,
+        Role=get_role_name(),
+        Handler="lambda_function.lambda_handler",
+        Code={"ZipFile": get_test_zip_file1()},
+        ImageConfig={
+            "EntryPoint": [
+                "/var/rapid/init",
+                "--bootstrap",
+                "/var/runtime/bootstrap",
+                "--enable-msg-logs",
+            ],
+        },
+        Description="test lambda function",
+        Timeout=3,
+        MemorySize=128,
+        Publish=True,
+    )
+
+    in_data = {"hello": "world"}
+    result = conn.invoke(
+        FunctionName=function_name,
+        InvocationType="RequestResponse",
+        Payload=json.dumps(in_data),
+    )
+    assert result["StatusCode"] == 200
+    payload = result["Payload"].read().decode("utf-8")
+
+    assert json.loads(payload) == in_data
diff --git a/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py b/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py
index e01291123..4034621d9 100644
--- a/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py
+++ b/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py
@@ -745,6 +745,21 @@ def test_get_resources_sqs():
     assert {"Key": "Test", "Value": "1"} in resp["ResourceTagMappingList"][0]["Tags"]
 
 
+@mock_aws
+def test_get_resources_sns():
+    sns = boto3.client("sns", region_name="us-east-1")
+
+    sns.create_topic(Name="test", Tags=[{"Key": "Shape", "Value": "Square"}])
+
+    rtapi = boto3.client("resourcegroupstaggingapi", region_name="us-east-1")
+    resp = rtapi.get_resources(ResourceTypeFilters=["sns"])
+
+    assert len(resp["ResourceTagMappingList"]) == 1
+    assert {"Key": "Shape", "Value": "Square"} in resp["ResourceTagMappingList"][0][
+        "Tags"
+    ]
+
+
 @mock_aws
 def test_tag_resources_for_unknown_service():
     rtapi = boto3.client("resourcegroupstaggingapi", region_name="us-west-2")
@@ -757,3 +772,53 @@ def test_tag_resources_for_unknown_service():
         missing_resources["arn:aws:service_x"]["ErrorCode"]
         == "InternalServiceException"
     )
+
+
+@mock_aws
+def test_get_resources_elb():
+    client = boto3.client("elb", region_name="us-east-1")
+    lb_name = "my-lb"
+    lb2_name = "second-lb"
+    client.create_load_balancer(
+        LoadBalancerName=lb_name,
+        Listeners=[
+            {"Protocol": "tcp", "LoadBalancerPort": 80, "InstancePort": 8080},
+            {"Protocol": "http", "LoadBalancerPort": 81, "InstancePort": 9000},
+        ],
+        Scheme="internal",
+    )
+    client.add_tags(
+        LoadBalancerNames=[lb_name],
+        Tags=[
+            {"Key": "burger", "Value": "krabby patty"},
+        ],
+    )
+    client.create_load_balancer(
+        LoadBalancerName=lb2_name,
+        Listeners=[
+            {"Protocol": "tcp", "LoadBalancerPort": 80, "InstancePort": 8080},
+            {"Protocol": "http", "LoadBalancerPort": 81, "InstancePort": 9000},
+        ],
+        Scheme="internal",
+    )
+    client.add_tags(
+        LoadBalancerNames=[lb2_name],
+        Tags=[
+            {"Key": "color", "Value": "blue"},
+            {"Key": "shape", "Value": "square"},
+        ],
+    )
+    rgta_client = boto3.client("resourcegroupstaggingapi", region_name="us-east-1")
+    resources_no_filter = rgta_client.get_resources(
+        ResourceTypeFilters=["elb"],
+    )
+    assert len(resources_no_filter["ResourceTagMappingList"]) == 2
+
+    resources_burger_filter = rgta_client.get_resources(
+        TagFilters=[{"Key": "burger", "Values": ["krabby patty"]}]
+    )
+    assert len(resources_burger_filter["ResourceTagMappingList"]) == 1
+    assert (
+        f"arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/{lb_name}"
+        == resources_burger_filter["ResourceTagMappingList"][0]["ResourceARN"]
+    )