Repository: getmoto/moto

Issue:
Full ECR coverage
Is full coverage of ECR planned?

As of now mocking any workflow that involves docker push to an ECR private repository is not supported.


Discussion:
Hi @volphy, Moto's main goal is to mock the SDK's - implementing the Docker protocol is out of scope as far as I'm concerned. 

I would happily look at PR's that implement the [ECR-methods that we have not yet implemented](http://docs.getmoto.org/en/latest/docs/services/ecr.html), but as far as I can tell, that doesn't solve your problem - as `docker push` calls a different API.
To be precise my Lambda function I would like to test using mocks requires the following IAM actions from ECR:

1. ecr:BatchGetRepositoryScanningConfiguration
2. ecr:DescribeImages
3. ecr:DescribeImageScanFindings

I can see that as of now only the item no. 1 is missing.

Will I be able to mock 'ecr:DescribeImages' without "pseudo-pushing" Docker images to mocked AWS ECR repository?


ECR has the `put_image` API, which is implemented by Moto. So you could use that as part of the test setup to create some images, that will then be returned by `describe_images`.
See our internal test here: 

https://github.com/getmoto/moto/blob/90fec47f5f9275200a0b659ea659234a3146a2db/tests/test_ecr/test_ecr_boto3.py#L247

Thanks for detailed answer.

I hoped that there would be an easier way but I understand your point.

Resolving patch:
diff --git a/moto/ecr/models.py b/moto/ecr/models.py
--- a/moto/ecr/models.py
+++ b/moto/ecr/models.py
@@ -3,7 +3,7 @@
 import re
 from collections import namedtuple
 from datetime import datetime, timezone
-from typing import Any, Dict, List, Iterable, Optional
+from typing import Any, Dict, List, Iterable, Optional, Tuple
 
 from botocore.exceptions import ParamValidationError
 
@@ -93,6 +93,13 @@ def __init__(
         self.policy: Optional[str] = None
         self.lifecycle_policy: Optional[str] = None
         self.images: List[Image] = []
+        self.scanning_config = {
+            "repositoryArn": self.arn,
+            "repositoryName": self.name,
+            "scanOnPush": False,
+            "scanFrequency": "MANUAL",
+            "appliedScanFilters": [],
+        }
 
     def _determine_encryption_config(
         self, encryption_config: Optional[Dict[str, str]]
@@ -789,6 +796,20 @@ def batch_delete_image(
 
         return response
 
+    def batch_get_repository_scanning_configuration(
+        self, names: List[str]
+    ) -> Tuple[List[Dict[str, Any]], List[str]]:
+        configs = []
+        failing = []
+        for name in names:
+            try:
+                configs.append(
+                    self._get_repository(name=name, registry_id=None).scanning_config
+                )
+            except RepositoryNotFoundException:
+                failing.append(name)
+        return configs, failing
+
     def list_tags_for_resource(self, arn: str) -> Dict[str, List[Dict[str, str]]]:
         resource = self._parse_resource_arn(arn)
         repo = self._get_repository(resource.repo_name, resource.account_id)
@@ -1093,6 +1114,17 @@ def put_replication_configuration(
 
         return {"replicationConfiguration": replication_config}
 
+    def put_registry_scanning_configuration(self, rules: List[Dict[str, Any]]) -> None:
+        for rule in rules:
+            for repo_filter in rule["repositoryFilters"]:
+                for repo in self.repositories.values():
+                    if repo_filter["filter"] == repo.name or re.match(
+                        repo_filter["filter"], repo.name
+                    ):
+                        repo.scanning_config["scanFrequency"] = rule["scanFrequency"]
+                        # AWS testing seems to indicate that this is always overwritten
+                        repo.scanning_config["appliedScanFilters"] = [repo_filter]
+
     def describe_registry(self) -> Dict[str, Any]:
         return {
             "registryId": self.account_id,
diff --git a/moto/ecr/responses.py b/moto/ecr/responses.py
--- a/moto/ecr/responses.py
+++ b/moto/ecr/responses.py
@@ -108,6 +108,25 @@ def batch_get_image(self) -> str:
         )
         return json.dumps(response)
 
+    def batch_get_repository_scanning_configuration(self) -> str:
+        names = self._get_param("repositoryNames")
+        configs, missing = self.ecr_backend.batch_get_repository_scanning_configuration(
+            names
+        )
+        return json.dumps(
+            {
+                "scanningConfigurations": configs,
+                "failures": [
+                    {
+                        "repositoryName": m,
+                        "failureCode": "REPOSITORY_NOT_FOUND",
+                        "failureReason": "REPOSITORY_NOT_FOUND",
+                    }
+                    for m in missing
+                ],
+            }
+        )
+
     def complete_layer_upload(self) -> None:
         self.error_on_dryrun()
         raise NotImplementedError("ECR.complete_layer_upload is not yet implemented")
@@ -303,5 +322,11 @@ def put_replication_configuration(self) -> str:
             )
         )
 
+    def put_registry_scanning_configuration(self) -> str:
+        scan_type = self._get_param("scanType")
+        rules = self._get_param("rules")
+        self.ecr_backend.put_registry_scanning_configuration(rules)
+        return json.dumps({"scanType": scan_type, "rules": rules})
+
     def describe_registry(self) -> str:
         return json.dumps(self.ecr_backend.describe_registry())


Test patch:
diff --git a/tests/test_ecr/test_ecr_scanning_config.py b/tests/test_ecr/test_ecr_scanning_config.py
new file mode 100644
--- /dev/null
+++ b/tests/test_ecr/test_ecr_scanning_config.py
@@ -0,0 +1,140 @@
+import boto3
+from moto import mock_ecr
+
+
+@mock_ecr
+def test_batch_get_repository_scanning_configuration():
+    client = boto3.client("ecr", region_name="us-east-1")
+    repo_name = "test-repo"
+    repo_arn = client.create_repository(repositoryName=repo_name)["repository"][
+        "repositoryArn"
+    ]
+
+    # Default ScanningConfig is returned
+    resp = client.batch_get_repository_scanning_configuration(
+        repositoryNames=[repo_name]
+    )
+
+    assert resp["scanningConfigurations"] == [
+        {
+            "repositoryArn": repo_arn,
+            "repositoryName": repo_name,
+            "scanOnPush": False,
+            "scanFrequency": "MANUAL",
+            "appliedScanFilters": [],
+        }
+    ]
+    assert resp["failures"] == []
+
+    # Non-existing repositories are returned as failures
+    resp = client.batch_get_repository_scanning_configuration(
+        repositoryNames=["unknown"]
+    )
+
+    assert resp["scanningConfigurations"] == []
+    assert resp["failures"] == [
+        {
+            "repositoryName": "unknown",
+            "failureCode": "REPOSITORY_NOT_FOUND",
+            "failureReason": "REPOSITORY_NOT_FOUND",
+        }
+    ]
+
+
+@mock_ecr
+def test_put_registry_scanning_configuration():
+    client = boto3.client("ecr", region_name="us-east-1")
+    repo_name = "test-repo"
+    repo_arn = client.create_repository(repositoryName=repo_name)["repository"][
+        "repositoryArn"
+    ]
+
+    #####
+    # Creating a ScanningConfig where
+    #      filter == repo_name
+    ####
+    client.put_registry_scanning_configuration(
+        scanType="ENHANCED",
+        rules=[
+            {
+                "scanFrequency": "CONTINUOUS_SCAN",
+                "repositoryFilters": [{"filter": repo_name, "filterType": "WILDCARD"}],
+            }
+        ],
+    )
+
+    resp = client.batch_get_repository_scanning_configuration(
+        repositoryNames=[repo_name]
+    )
+
+    assert resp["scanningConfigurations"] == [
+        {
+            "repositoryArn": repo_arn,
+            "repositoryName": repo_name,
+            "scanOnPush": False,
+            "scanFrequency": "CONTINUOUS_SCAN",
+            "appliedScanFilters": [{"filter": repo_name, "filterType": "WILDCARD"}],
+        }
+    ]
+
+    #####
+    # Creating a ScanningConfig where
+    #      filter == repo*
+    ####
+    client.put_registry_scanning_configuration(
+        scanType="ENHANCED",
+        rules=[
+            {
+                "scanFrequency": "SCAN_ON_PUSH",
+                "repositoryFilters": [
+                    {"filter": f"{repo_name[:4]}*", "filterType": "WILDCARD"}
+                ],
+            }
+        ],
+    )
+
+    resp = client.batch_get_repository_scanning_configuration(
+        repositoryNames=[repo_name]
+    )
+
+    assert resp["scanningConfigurations"] == [
+        {
+            "repositoryArn": repo_arn,
+            "repositoryName": repo_name,
+            "scanOnPush": False,
+            "scanFrequency": "SCAN_ON_PUSH",
+            "appliedScanFilters": [
+                {"filter": f"{repo_name[:4]}*", "filterType": "WILDCARD"}
+            ],
+        }
+    ]
+
+    #####
+    # Creating a ScanningConfig where
+    #      filter == unknown_repo
+    ####
+    client.put_registry_scanning_configuration(
+        scanType="ENHANCED",
+        rules=[
+            {
+                "scanFrequency": "MANUAL",
+                "repositoryFilters": [{"filter": "unknown", "filterType": "WILDCARD"}],
+            }
+        ],
+    )
+
+    resp = client.batch_get_repository_scanning_configuration(
+        repositoryNames=[repo_name]
+    )
+
+    assert resp["scanningConfigurations"] == [
+        {
+            "repositoryArn": repo_arn,
+            "repositoryName": repo_name,
+            "scanOnPush": False,
+            "scanFrequency": "SCAN_ON_PUSH",
+            "appliedScanFilters": [
+                {"filter": f"{repo_name[:4]}*", "filterType": "WILDCARD"}
+            ],
+        }
+    ]