diff --git a/moto/greengrass/models.py b/moto/greengrass/models.py index 787121619ea..9041abac1bf 100644 --- a/moto/greengrass/models.py +++ b/moto/greengrass/models.py @@ -167,6 +167,58 @@ def to_dict(self): } +class FakeFunctionDefinition(BaseModel): + def __init__(self, region_name, name, initial_version): + self.region_name = region_name + self.id = str(uuid.uuid4()) + self.arn = f"arn:aws:greengrass:{self.region_name}:{get_account_id()}:/greengrass/definition/functions/{self.id}" + self.created_at_datetime = datetime.utcnow() + self.update_at_datetime = datetime.utcnow() + self.latest_version = "" + self.latest_version_arn = "" + self.name = name + self.initial_version = initial_version + + def to_dict(self): + res = { + "Arn": self.arn, + "CreationTimestamp": iso_8601_datetime_with_milliseconds( + self.created_at_datetime + ), + "Id": self.id, + "LastUpdatedTimestamp": iso_8601_datetime_with_milliseconds( + self.update_at_datetime + ), + "LatestVersion": self.latest_version, + "LatestVersionArn": self.latest_version_arn, + } + if self.name is not None: + res["Name"] = self.name + return res + + +class FakeFunctionDefinitionVersion(BaseModel): + def __init__(self, region_name, function_definition_id, functions, default_config): + self.region_name = region_name + self.function_definition_id = function_definition_id + self.functions = functions + self.default_config = default_config + self.version = str(uuid.uuid4()) + self.arn = f"arn:aws:greengrass:{self.region_name}:{get_account_id()}:/greengrass/definition/functions/{self.function_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 + ), + "Definition": {"Functions": self.functions}, + "Id": self.function_definition_id, + "Version": self.version, + } + + class GreengrassBackend(BaseBackend): def __init__(self, region_name, account_id): super().__init__(region_name, account_id) @@ -410,5 +462,88 @@ def _validate_resources(resources): f", but got: {device_source_path}])", ) + def create_function_definition(self, name, initial_version): + func_def = FakeFunctionDefinition(self.region_name, name, initial_version) + self.function_definitions[func_def.id] = func_def + init_ver = func_def.initial_version + init_func_def = init_ver.get("Functions", {}) + init_config = init_ver.get("DefaultConfig", {}) + self.create_function_definition_version(func_def.id, init_func_def, init_config) + + return func_def + + def list_function_definitions(self): + return self.function_definitions.values() + + def get_function_definition(self, function_definition_id): + + if function_definition_id not in self.function_definitions: + raise IdNotFoundException("That Lambda List Definition does not exist.") + return self.function_definitions[function_definition_id] + + def delete_function_definition(self, function_definition_id): + if function_definition_id not in self.function_definitions: + raise IdNotFoundException("That lambdas definition does not exist.") + del self.function_definitions[function_definition_id] + del self.function_definition_versions[function_definition_id] + + def update_function_definition(self, function_definition_id, name): + + if name == "": + raise InvalidContainerDefinitionException( + "Input does not contain any attributes to be updated" + ) + if function_definition_id not in self.function_definitions: + raise IdNotFoundException("That lambdas definition does not exist.") + self.function_definitions[function_definition_id].name = name + + def create_function_definition_version( + self, function_definition_id, functions, default_config + ): + + if function_definition_id not in self.function_definitions: + raise IdNotFoundException("That lambdas does not exist.") + + func_ver = FakeFunctionDefinitionVersion( + self.region_name, function_definition_id, functions, default_config + ) + func_vers = self.function_definition_versions.get( + func_ver.function_definition_id, {} + ) + func_vers[func_ver.version] = func_ver + self.function_definition_versions[func_ver.function_definition_id] = func_vers + self.function_definitions[ + function_definition_id + ].latest_version = func_ver.version + self.function_definitions[ + function_definition_id + ].latest_version_arn = func_ver.arn + + return func_ver + + def list_function_definition_versions(self, function_definition_id): + if function_definition_id not in self.function_definition_versions: + raise IdNotFoundException("That lambdas definition does not exist.") + return self.function_definition_versions[function_definition_id] + + def get_function_definition_version( + self, function_definition_id, function_definition_version_id + ): + + if function_definition_id not in self.function_definition_versions: + raise IdNotFoundException("That lambdas definition does not exist.") + + if ( + function_definition_version_id + not in self.function_definition_versions[function_definition_id] + ): + raise IdNotFoundException( + f"Version {function_definition_version_id} of Lambda List Definition {function_definition_id} does not exist." + ) + + return self.function_definition_versions[function_definition_id][ + function_definition_version_id + ] + greengrass_backends = BackendDict(GreengrassBackend, "greengrass") diff --git a/moto/greengrass/responses.py b/moto/greengrass/responses.py index 49d6ae4a717..fed4a8b3b14 100644 --- a/moto/greengrass/responses.py +++ b/moto/greengrass/responses.py @@ -260,3 +260,110 @@ def create_resource_definition_version(self): resource_definition_id=resource_definition_id, resources=resources ) return 201, {"status": 201}, json.dumps(res.to_dict()) + + def function_definitions(self, request, full_url, headers): + self.setup_class(request, full_url, headers) + + if self.method == "POST": + return self.create_function_definition() + + if self.method == "GET": + return self.list_function_definitions() + + def create_function_definition(self): + + initial_version = self._get_param("InitialVersion") + name = self._get_param("Name") + res = self.greengrass_backend.create_function_definition( + name=name, initial_version=initial_version + ) + return 201, {"status": 201}, json.dumps(res.to_dict()) + + def list_function_definitions(self): + res = self.greengrass_backend.list_function_definitions() + return ( + 200, + {"status": 200}, + json.dumps( + {"Definitions": [func_definition.to_dict() for func_definition in res]} + ), + ) + + def function_definition(self, request, full_url, headers): + self.setup_class(request, full_url, headers) + + if self.method == "GET": + return self.get_function_definition() + + if self.method == "DELETE": + return self.delete_function_definition() + + if self.method == "PUT": + return self.update_function_definition() + + def get_function_definition(self): + function_definition_id = self.path.split("/")[-1] + res = self.greengrass_backend.get_function_definition( + function_definition_id=function_definition_id, + ) + return 200, {"status": 200}, json.dumps(res.to_dict()) + + def delete_function_definition(self): + function_definition_id = self.path.split("/")[-1] + self.greengrass_backend.delete_function_definition( + function_definition_id=function_definition_id, + ) + return 200, {"status": 200}, json.dumps({}) + + def update_function_definition(self): + function_definition_id = self.path.split("/")[-1] + name = self._get_param("Name") + self.greengrass_backend.update_function_definition( + function_definition_id=function_definition_id, name=name + ) + return 200, {"status": 200}, json.dumps({}) + + def function_definition_versions(self, request, full_url, headers): + self.setup_class(request, full_url, headers) + + if self.method == "POST": + return self.create_function_definition_version() + + if self.method == "GET": + return self.list_function_definition_versions() + + def create_function_definition_version(self): + + default_config = self._get_param("DefaultConfig") + function_definition_id = self.path.split("/")[-2] + functions = self._get_param("Functions") + + res = self.greengrass_backend.create_function_definition_version( + default_config=default_config, + function_definition_id=function_definition_id, + functions=functions, + ) + return 201, {"status": 201}, json.dumps(res.to_dict()) + + def list_function_definition_versions(self): + function_definition_id = self.path.split("/")[-2] + res = self.greengrass_backend.list_function_definition_versions( + function_definition_id=function_definition_id + ) + versions = [i.to_dict() for i in res.values()] + return 200, {"status": 200}, json.dumps({"Versions": versions}) + + def function_definition_version(self, request, full_url, headers): + self.setup_class(request, full_url, headers) + + if self.method == "GET": + return self.get_function_definition_version() + + def get_function_definition_version(self): + function_definition_id = self.path.split("/")[-3] + function_definition_version_id = self.path.split("/")[-1] + res = self.greengrass_backend.get_function_definition_version( + function_definition_id=function_definition_id, + function_definition_version_id=function_definition_version_id, + ) + return 200, {"status": 200}, json.dumps(res.to_dict()) diff --git a/moto/greengrass/urls.py b/moto/greengrass/urls.py index 5a237da0a4d..1ce85893cc0 100644 --- a/moto/greengrass/urls.py +++ b/moto/greengrass/urls.py @@ -17,6 +17,10 @@ "{0}/greengrass/definition/devices/(?P[^/]+)/?$": response.device_definition, "{0}/greengrass/definition/devices/(?P[^/]+)/versions$": response.device_definition_versions, "{0}/greengrass/definition/devices/(?P[^/]+)/versions/(?P[^/]+)/?$": response.device_definition_version, + "{0}/greengrass/definition/functions$": response.function_definitions, + "{0}/greengrass/definition/functions/(?P[^/]+)/?$": response.function_definition, + "{0}/greengrass/definition/functions/(?P[^/]+)/versions$": response.function_definition_versions, + "{0}/greengrass/definition/functions/(?P[^/]+)/versions/(?P[^/]+)/?$": response.function_definition_version, "{0}/greengrass/definition/resources$": response.resource_definitions, "{0}/greengrass/definition/resources/(?P[^/]+)/versions$": response.resource_definition_versions, } diff --git a/tests/test_greengrass/test_greengrass_functions.py b/tests/test_greengrass/test_greengrass_functions.py new file mode 100644 index 00000000000..39a614de727 --- /dev/null +++ b/tests/test_greengrass/test_greengrass_functions.py @@ -0,0 +1,484 @@ +import boto3 +from botocore.client import ClientError +import freezegun +import pytest + + +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_function_definition(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + init_ver = { + "Functions": [ + { + "FunctionArn": "arn:aws:lambda:ap-northeast-1:123456789012:function:test-func:1", + "Id": "1234567890", + "FunctionConfiguration": { + "MemorySize": 16384, + "EncodingType": "binary", + "Pinned": True, + "Timeout": 3, + }, + } + ] + } + func_name = "TestFunc" + res = client.create_function_definition(InitialVersion=init_ver, Name=func_name) + res.should.have.key("Arn") + res.should.have.key("Id") + res.should.have.key("LatestVersion") + res.should.have.key("LatestVersionArn") + res.should.have.key("Name").equals(func_name) + res["ResponseMetadata"]["HTTPStatusCode"].should.equal(201) + + 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") + + +@freezegun.freeze_time("2022-06-01 12:00:00") +@mock_greengrass +def test_list_function_definitions(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + init_ver = { + "Functions": [ + { + "FunctionArn": "arn:aws:lambda:ap-northeast-1:123456789012:function:test-func:1", + "Id": "1234567890", + "FunctionConfiguration": { + "MemorySize": 16384, + "EncodingType": "binary", + "Pinned": True, + "Timeout": 3, + }, + } + ] + } + func_name = "TestFunc" + client.create_function_definition(InitialVersion=init_ver, Name=func_name) + + res = client.list_function_definitions() + func_def = res["Definitions"][0] + + func_def.should.have.key("Name").equals(func_name) + func_def.should.have.key("Arn") + func_def.should.have.key("Id") + func_def.should.have.key("LatestVersion") + func_def.should.have.key("LatestVersionArn") + if not TEST_SERVER_MODE: + func_def.should.have.key("CreationTimestamp").equal("2022-06-01T12:00:00.000Z") + func_def.should.have.key("LastUpdatedTimestamp").equals( + "2022-06-01T12:00:00.000Z" + ) + + +@freezegun.freeze_time("2022-06-01 12:00:00") +@mock_greengrass +def test_get_function_definition(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + init_ver = { + "Functions": [ + { + "FunctionArn": "arn:aws:lambda:ap-northeast-1:123456789012:function:test-func:1", + "Id": "1234567890", + "FunctionConfiguration": { + "MemorySize": 16384, + "EncodingType": "binary", + "Pinned": True, + "Timeout": 3, + }, + } + ] + } + func_name = "TestFunc" + create_res = client.create_function_definition( + InitialVersion=init_ver, Name=func_name + ) + + func_def_id = create_res["Id"] + arn = create_res["Arn"] + latest_version = create_res["LatestVersion"] + latest_version_arn = create_res["LatestVersionArn"] + + get_res = client.get_function_definition(FunctionDefinitionId=func_def_id) + + get_res.should.have.key("Name").equals(func_name) + get_res.should.have.key("Arn").equals(arn) + get_res.should.have.key("Id").equals(func_def_id) + get_res.should.have.key("LatestVersion").equals(latest_version) + get_res.should.have.key("LatestVersionArn").equals(latest_version_arn) + get_res["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + if not TEST_SERVER_MODE: + get_res.should.have.key("CreationTimestamp").equal("2022-06-01T12:00:00.000Z") + get_res.should.have.key("LastUpdatedTimestamp").equals( + "2022-06-01T12:00:00.000Z" + ) + + +@mock_greengrass +def test_get_function_definition_with_invalid_id(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + with pytest.raises(ClientError) as ex: + client.get_function_definition( + FunctionDefinitionId="b552443b-1888-469b-81f8-0ebc5ca92949" + ) + ex.value.response["Error"]["Message"].should.equal( + "That Lambda List Definition does not exist." + ) + ex.value.response["Error"]["Code"].should.equal("IdNotFoundException") + + +@mock_greengrass +def test_delete_function_definition(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + init_ver = { + "Functions": [ + { + "FunctionArn": "arn:aws:lambda:ap-northeast-1:123456789012:function:test-func:1", + "Id": "1234567890", + "FunctionConfiguration": { + "MemorySize": 16384, + "EncodingType": "binary", + "Pinned": True, + "Timeout": 3, + }, + } + ] + } + create_res = client.create_function_definition( + InitialVersion=init_ver, Name="TestFunc" + ) + + func_def_id = create_res["Id"] + del_res = client.delete_function_definition(FunctionDefinitionId=func_def_id) + del_res["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + +@mock_greengrass +def test_delete_function_definition_with_invalid_id(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + + with pytest.raises(ClientError) as ex: + client.delete_function_definition( + FunctionDefinitionId="6fbffc21-989e-4d29-a793-a42f450a78c6" + ) + ex.value.response["Error"]["Message"].should.equal( + "That lambdas definition does not exist." + ) + ex.value.response["Error"]["Code"].should.equal("IdNotFoundException") + + +@mock_greengrass +def test_update_function_definition(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + init_ver = { + "Functions": [ + { + "FunctionArn": "arn:aws:lambda:ap-northeast-1:123456789012:function:test-func:1", + "Id": "1234567890", + "FunctionConfiguration": { + "MemorySize": 16384, + "EncodingType": "binary", + "Pinned": True, + "Timeout": 3, + }, + } + ] + } + create_res = client.create_function_definition( + InitialVersion=init_ver, Name="TestFunc" + ) + + func_def_id = create_res["Id"] + updated_func_name = "UpdatedFunction" + update_res = client.update_function_definition( + FunctionDefinitionId=func_def_id, Name=updated_func_name + ) + update_res["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + get_res = client.get_function_definition(FunctionDefinitionId=func_def_id) + get_res.should.have.key("Name").equals(updated_func_name) + + +@mock_greengrass +def test_update_function_definition_with_empty_name(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + init_ver = { + "Functions": [ + { + "FunctionArn": "arn:aws:lambda:ap-northeast-1:123456789012:function:test-func:1", + "Id": "1234567890", + "FunctionConfiguration": { + "MemorySize": 16384, + "EncodingType": "binary", + "Pinned": True, + "Timeout": 3, + }, + } + ] + } + create_res = client.create_function_definition( + InitialVersion=init_ver, Name="TestFunc" + ) + + func_def_id = create_res["Id"] + + with pytest.raises(ClientError) as ex: + client.update_function_definition(FunctionDefinitionId=func_def_id, Name="") + ex.value.response["Error"]["Message"].should.equal( + "Input does not contain any attributes to be updated" + ) + ex.value.response["Error"]["Code"].should.equal( + "InvalidContainerDefinitionException" + ) + + +@mock_greengrass +def test_update_function_definition_with_invalid_id(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + + with pytest.raises(ClientError) as ex: + client.update_function_definition( + FunctionDefinitionId="6fbffc21-989e-4d29-a793-a42f450a78c6", Name="123" + ) + ex.value.response["Error"]["Message"].should.equal( + "That lambdas definition does not exist." + ) + ex.value.response["Error"]["Code"].should.equal("IdNotFoundException") + + +@freezegun.freeze_time("2022-06-01 12:00:00") +@mock_greengrass +def test_create_function_definition_version(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + v1_functions = [ + { + "FunctionArn": "arn:aws:lambda:ap-northeast-1:123456789012:function:test-func:1", + "Id": "1234567890", + "FunctionConfiguration": { + "MemorySize": 16384, + "EncodingType": "binary", + "Pinned": True, + "Timeout": 3, + }, + } + ] + + initial_version = {"Functions": v1_functions} + func_def_res = client.create_function_definition( + InitialVersion=initial_version, Name="TestFunction" + ) + func_def_id = func_def_res["Id"] + + v2_functions = [ + { + "FunctionArn": "arn:aws:lambda:ap-northeast-1:123456789012:function:test-func:2", + "Id": "987654321", + "FunctionConfiguration": { + "MemorySize": 128, + "EncodingType": "binary", + "Pinned": False, + "Timeout": 5, + }, + } + ] + + func_def_ver_res = client.create_function_definition_version( + FunctionDefinitionId=func_def_id, Functions=v2_functions + ) + func_def_ver_res.should.have.key("Arn") + func_def_ver_res.should.have.key("CreationTimestamp") + func_def_ver_res.should.have.key("Id").equals(func_def_id) + func_def_ver_res.should.have.key("Version") + + if not TEST_SERVER_MODE: + func_def_ver_res["CreationTimestamp"].should.equal("2022-06-01T12:00:00.000Z") + + +@mock_greengrass +def test_create_function_definition_version_with_invalid_id(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + functions = [ + { + "FunctionArn": "arn:aws:lambda:ap-northeast-1:123456789012:function:test-func:1", + "Id": "1234567890", + "FunctionConfiguration": { + "MemorySize": 16384, + "EncodingType": "binary", + "Pinned": True, + "Timeout": 3, + }, + } + ] + with pytest.raises(ClientError) as ex: + client.create_function_definition_version( + FunctionDefinitionId="7b0bdeae-54c7-47cf-9f93-561e672efd9c", + Functions=functions, + ) + ex.value.response["Error"]["Message"].should.equal("That lambdas does not exist.") + ex.value.response["Error"]["Code"].should.equal("IdNotFoundException") + + +@freezegun.freeze_time("2022-06-01 12:00:00") +@mock_greengrass +def test_list_function_definition_versions(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + functions = [ + { + "FunctionArn": "arn:aws:lambda:ap-northeast-1:123456789012:function:test-func:1", + "Id": "1234567890", + "FunctionConfiguration": { + "MemorySize": 16384, + "EncodingType": "binary", + "Pinned": True, + "Timeout": 3, + }, + } + ] + + initial_version = {"Functions": functions} + create_res = client.create_function_definition( + InitialVersion=initial_version, Name="TestFunction" + ) + func_def_id = create_res["Id"] + + func_def_vers_res = client.list_function_definition_versions( + FunctionDefinitionId=func_def_id + ) + + func_def_vers_res.should.have.key("Versions") + device_def_ver = func_def_vers_res["Versions"][0] + device_def_ver.should.have.key("Arn") + device_def_ver.should.have.key("CreationTimestamp") + + if not TEST_SERVER_MODE: + device_def_ver["CreationTimestamp"].should.equal("2022-06-01T12:00:00.000Z") + device_def_ver.should.have.key("Id").equals(func_def_id) + device_def_ver.should.have.key("Version") + + +@mock_greengrass +def test_list_function_definition_versions_with_invalid_id(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + + with pytest.raises(ClientError) as ex: + client.list_function_definition_versions( + FunctionDefinitionId="7b0bdeae-54c7-47cf-9f93-561e672efd9c" + ) + ex.value.response["Error"]["Message"].should.equal( + "That lambdas definition does not exist." + ) + ex.value.response["Error"]["Code"].should.equal("IdNotFoundException") + + +@freezegun.freeze_time("2022-06-01 12:00:00") +@mock_greengrass +def test_get_function_definition_version(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + functions = [ + { + "FunctionArn": "arn:aws:lambda:ap-northeast-1:123456789012:function:test-func:1", + "Id": "1234567890", + "FunctionConfiguration": { + "MemorySize": 16384, + "EncodingType": "binary", + "Pinned": True, + "Timeout": 3, + }, + } + ] + + initial_version = {"Functions": functions} + create_res = client.create_function_definition( + InitialVersion=initial_version, Name="TestFunction" + ) + + func_def_id = create_res["Id"] + func_def_ver_id = create_res["LatestVersion"] + + func_def_ver_res = client.get_function_definition_version( + FunctionDefinitionId=func_def_id, FunctionDefinitionVersionId=func_def_ver_id + ) + + func_def_ver_res.should.have.key("Arn") + func_def_ver_res.should.have.key("CreationTimestamp") + func_def_ver_res.should.have.key("Definition").should.equal(initial_version) + func_def_ver_res.should.have.key("Id").equals(func_def_id) + func_def_ver_res.should.have.key("Version") + + if not TEST_SERVER_MODE: + func_def_ver_res["CreationTimestamp"].should.equal("2022-06-01T12:00:00.000Z") + + +@mock_greengrass +def test_get_function_definition_version_with_invalid_id(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + + with pytest.raises(ClientError) as ex: + client.get_function_definition_version( + FunctionDefinitionId="7b0bdeae-54c7-47cf-9f93-561e672efd9c", + FunctionDefinitionVersionId="7b0bdeae-54c7-47cf-9f93-561e672efd9c", + ) + ex.value.response["Error"]["Message"].should.equal( + "That lambdas definition does not exist." + ) + ex.value.response["Error"]["Code"].should.equal("IdNotFoundException") + + +@mock_greengrass +def test_get_function_definition_version_with_invalid_version_id(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + functions = [ + { + "FunctionArn": "arn:aws:lambda:ap-northeast-1:123456789012:function:test-func:1", + "Id": "1234567890", + "FunctionConfiguration": { + "MemorySize": 16384, + "EncodingType": "binary", + "Pinned": True, + "Timeout": 3, + }, + } + ] + + initial_version = {"Functions": functions} + create_res = client.create_function_definition( + InitialVersion=initial_version, Name="TestFunction" + ) + + func_def_id = create_res["Id"] + invalid_func_def_ver_id = "7b0bdeae-54c7-47cf-9f93-561e672efd9c" + + with pytest.raises(ClientError) as ex: + client.get_function_definition_version( + FunctionDefinitionId=func_def_id, + FunctionDefinitionVersionId=invalid_func_def_ver_id, + ) + ex.value.response["Error"]["Message"].should.equal( + f"Version {invalid_func_def_ver_id} of Lambda List Definition {func_def_id} does not exist." + ) + ex.value.response["Error"]["Code"].should.equal("IdNotFoundException")