diff --git a/moto/__init__.py b/moto/__init__.py index 04e36221b..9c3463f24 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -103,6 +103,7 @@ mock_es = lazy_load(".es", "mock_es") mock_events = lazy_load(".events", "mock_events") mock_firehose = lazy_load(".firehose", "mock_firehose") mock_forecast = lazy_load(".forecast", "mock_forecast") +mock_greengrass = lazy_load(".greengrass", "mock_greengrass") mock_glacier = lazy_load(".glacier", "mock_glacier") mock_glue = lazy_load(".glue", "mock_glue") mock_guardduty = lazy_load(".guardduty", "mock_guardduty") diff --git a/moto/backend_index.py b/moto/backend_index.py index e29332752..6dd464cc5 100644 --- a/moto/backend_index.py +++ b/moto/backend_index.py @@ -70,6 +70,7 @@ backend_url_patterns = [ ("forecast", re.compile("https?://forecast\\.(.+)\\.amazonaws\\.com")), ("glacier", re.compile("https?://glacier\\.(.+)\\.amazonaws.com")), ("glue", re.compile("https?://glue\\.(.+)\\.amazonaws\\.com")), + ("greengrass", re.compile("https?://greengrass\\.(.+)\\.amazonaws.com")), ("guardduty", re.compile("https?://guardduty\\.(.+)\\.amazonaws\\.com")), ("iam", re.compile("https?://iam\\.(.*\\.)?amazonaws\\.com")), ("iot", re.compile("https?://iot\\.(.+)\\.amazonaws\\.com")), diff --git a/moto/greengrass/__init__.py b/moto/greengrass/__init__.py new file mode 100644 index 000000000..d6dd58b5b --- /dev/null +++ b/moto/greengrass/__init__.py @@ -0,0 +1,4 @@ +from .models import greengrass_backends +from ..core.models import base_decorator + +mock_greengrass = base_decorator(greengrass_backends) diff --git a/moto/greengrass/models.py b/moto/greengrass/models.py new file mode 100644 index 000000000..53df9ce9f --- /dev/null +++ b/moto/greengrass/models.py @@ -0,0 +1,100 @@ +import uuid +from collections import OrderedDict +from datetime import datetime + +from moto.core import BaseBackend, BaseModel, get_account_id +from moto.core.utils import BackendDict, iso_8601_datetime_with_milliseconds + + +class FakeCoreDefinition(BaseModel): + def __init__(self, region_name, name): + self.region_name = region_name + self.name = name + self.id = str(uuid.uuid4()) + self.arn = f"arn:aws:greengrass:{region_name}:{get_account_id()}:greengrass/definition/cores/{self.id}" + self.created_at_datetime = datetime.utcnow() + self.latest_version = "" + self.latest_version_arn = "" + + def to_dict(self): + return { + "Arn": self.arn, + "CreationTimestamp": iso_8601_datetime_with_milliseconds( + self.created_at_datetime + ), + "Id": self.id, + "LastUpdatedTimestamp": iso_8601_datetime_with_milliseconds( + self.created_at_datetime + ), + "LatestVersion": self.latest_version, + "LatestVersionArn": self.latest_version_arn, + "Name": self.name, + } + + +class FakeCoreDefinitionVersion(BaseModel): + def __init__(self, region_name, core_definition_id, definition): + self.region_name = region_name + self.core_definition_id = core_definition_id + self.definition = definition + self.version = str(uuid.uuid4()) + self.arn = f"arn:aws:greengrass:{region_name}:{get_account_id()}:greengrass/definition/cores/{self.core_definition_id}/versions/{self.version}" + self.created_at_datetime = datetime.utcnow() + + def to_dict(self): + return { + "Arn": self.arn, + "CreationTimestamp": iso_8601_datetime_with_milliseconds( + self.created_at_datetime + ), + "Id": self.core_definition_id, + "Version": self.version, + } + + +class GreengrassBackend(BaseBackend): + def __init__(self, region_name=None): + super().__init__() + self.region_name = region_name + self.groups = OrderedDict() + self.group_versions = OrderedDict() + self.core_definitions = OrderedDict() + self.core_definition_versions = OrderedDict() + self.device_definitions = OrderedDict() + self.device_definition_versions = OrderedDict() + self.function_definitions = OrderedDict() + self.function_definition_versions = OrderedDict() + self.resource_definitions = OrderedDict() + self.resource_definition_versions = OrderedDict() + self.subscription_definitions = OrderedDict() + self.subscription_definition_versions = OrderedDict() + self.deployments = OrderedDict() + + def create_core_definition(self, name, initial_version): + + core_definition = FakeCoreDefinition(self.region_name, name) + self.core_definitions[core_definition.id] = core_definition + self.create_core_definition_version( + core_definition.id, initial_version["Cores"] + ) + return core_definition + + def create_core_definition_version(self, core_definition_id, cores): + + definition = {"Cores": cores} + core_def_ver = FakeCoreDefinitionVersion( + self.region_name, core_definition_id, definition + ) + core_def_vers = self.core_definition_versions.get( + core_def_ver.core_definition_id, {} + ) + core_def_vers[core_def_ver.version] = core_def_ver + self.core_definition_versions[core_def_ver.core_definition_id] = core_def_vers + + self.core_definitions[core_definition_id].latest_version = core_def_ver.version + self.core_definitions[core_definition_id].latest_version_arn = core_def_ver.arn + + return core_def_ver + + +greengrass_backends = BackendDict(GreengrassBackend, "greengrass") diff --git a/moto/greengrass/responses.py b/moto/greengrass/responses.py new file mode 100644 index 000000000..3110066a7 --- /dev/null +++ b/moto/greengrass/responses.py @@ -0,0 +1,31 @@ +import json + +from moto.core.responses import BaseResponse +from .models import greengrass_backends + + +class GreengrassResponse(BaseResponse): + SERVICE_NAME = "greengrass" + + @property + def greengrass_backend(self): + return greengrass_backends[self.region] + + def create_core_definition(self, request, full_url, headers): + self.setup_class(request, full_url, headers) + name = self._get_param("Name") + initial_version = self._get_param("InitialVersion") + res = self.greengrass_backend.create_core_definition( + name=name, initial_version=initial_version + ) + return 201, {"status": 201}, json.dumps(res.to_dict()) + + def create_core_definition_version(self, request, full_url, headers): + self.setup_class(request, full_url, headers) + core_definition_id = self.path.split("/")[-2] + cores = self._get_param("Cores") + + res = self.greengrass_backend.create_core_definition_version( + core_definition_id=core_definition_id, cores=cores + ) + return 201, {"status": 201}, json.dumps(res.to_dict()) diff --git a/moto/greengrass/urls.py b/moto/greengrass/urls.py new file mode 100644 index 000000000..f27bea333 --- /dev/null +++ b/moto/greengrass/urls.py @@ -0,0 +1,14 @@ +from .responses import GreengrassResponse + +url_bases = [ + r"https?://greengrass\.(.+)\.amazonaws.com", +] + + +response = GreengrassResponse() + + +url_paths = { + "{0}/greengrass/definition/cores$": response.create_core_definition, + "{0}/greengrass/definition/cores/(?P[^/]+)/versions$": response.create_core_definition_version, +} diff --git a/tests/test_greengrass/__init__.py b/tests/test_greengrass/__init__.py new file mode 100644 index 000000000..08a1c1568 --- /dev/null +++ b/tests/test_greengrass/__init__.py @@ -0,0 +1 @@ +# This file is intentionally left blank. diff --git a/tests/test_greengrass/test_greengrass_core.py b/tests/test_greengrass/test_greengrass_core.py new file mode 100644 index 000000000..69f13fc3b --- /dev/null +++ b/tests/test_greengrass/test_greengrass_core.py @@ -0,0 +1,74 @@ +import boto3 +import freezegun + +from moto import mock_greengrass +from moto.core import get_account_id +from moto.settings import TEST_SERVER_MODE + +ACCOUNT_ID = get_account_id() + + +@freezegun.freeze_time("2022-06-01 12:00:00") +@mock_greengrass +def test_create_core_definition(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + cores = [ + { + "CertificateArn": f"arn:aws:iot:ap-northeast-1:{ACCOUNT_ID}:cert/36ed61be9c6271ae8da174e29d0e033c06af149d7b21672f3800fe322044554d", + "Id": "123456789", + "ThingArn": f"arn:aws:iot:ap-northeast-1:{ACCOUNT_ID}:thing/CoreThing", + } + ] + + initial_version = {"Cores": cores} + + core_name = "TestCore" + res = client.create_core_definition(InitialVersion=initial_version, Name=core_name) + res.should.have.key("Arn") + res.should.have.key("Id") + if not TEST_SERVER_MODE: + res.should.have.key("CreationTimestamp").equals("2022-06-01T12:00:00.000Z") + res.should.have.key("LastUpdatedTimestamp").equals("2022-06-01T12:00:00.000Z") + res.should.have.key("LatestVersionArn") + res.should.have.key("Name").equals(core_name) + res["ResponseMetadata"]["HTTPStatusCode"].should.equal(201) + + +@freezegun.freeze_time("2022-06-01 12:00:00") +@mock_greengrass +def test_create_core_definition_version(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + v1_cores = [ + { + "CertificateArn": f"arn:aws:iot:ap-northeast-1:{ACCOUNT_ID}:cert/36ed61be9c6271ae8da174e29d0e033c06af149d7b21672f3800fe322044554d", + "Id": "123456789", + "ThingArn": f"arn:aws:iot:ap-northeast-1:{ACCOUNT_ID}:thing/v1Thing", + } + ] + + initial_version = {"Cores": v1_cores} + + core_def_res = client.create_core_definition( + InitialVersion=initial_version, Name="TestCore" + ) + core_def_id = core_def_res["Id"] + + v2_cores = [ + { + "CertificateArn": f"arn:aws:iot:ap-northeast-1:{ACCOUNT_ID}:cert/277a6a15293c1ed5fa1aa74bae890b1827f80959537bfdcf10f63e661d54ebe1", + "Id": "987654321", + "ThingArn": f"arn:aws:iot:ap-northeast-1:{ACCOUNT_ID}:thing/v2Thing", + } + ] + + core_def_ver_res = client.create_core_definition_version( + CoreDefinitionId=core_def_id, Cores=v2_cores + ) + core_def_ver_res.should.have.key("Arn") + core_def_ver_res.should.have.key("CreationTimestamp") + if not TEST_SERVER_MODE: + core_def_ver_res["CreationTimestamp"].should.equal("2022-06-01T12:00:00.000Z") + core_def_ver_res.should.have.key("Id").equals(core_def_id) + core_def_ver_res.should.have.key("Version")