diff --git a/moto/greengrass/models.py b/moto/greengrass/models.py index e36f2a995..787121619 100644 --- a/moto/greengrass/models.py +++ b/moto/greengrass/models.py @@ -5,6 +5,7 @@ from datetime import datetime from moto.core import BaseBackend, BaseModel, get_account_id from moto.core.utils import BackendDict, iso_8601_datetime_with_milliseconds from .exceptions import ( + GreengrassClientError, IdNotFoundException, InvalidContainerDefinitionException, VersionNotFoundException, @@ -117,6 +118,55 @@ class FakeDeviceDefinitionVersion(BaseModel): return obj +class FakeResourceDefinition(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:{region_name}:{get_account_id()}:/greengrass/definition/resources/{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): + return { + "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, + "Name": self.name, + } + + +class FakeResourceDefinitionVersion(BaseModel): + def __init__(self, region_name, resource_definition_id, resources): + self.region_name = region_name + self.resource_definition_id = resource_definition_id + self.resources = resources + self.version = str(uuid.uuid4()) + self.arn = f"arn:aws:greengrass:{region_name}:{get_account_id()}:/greengrass/definition/resources/{self.resource_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": {"Resources": self.resources}, + "Id": self.resource_definition_id, + "Version": self.version, + } + + class GreengrassBackend(BaseBackend): def __init__(self, region_name, account_id): super().__init__(region_name, account_id) @@ -291,5 +341,74 @@ class GreengrassBackend(BaseBackend): device_definition_version_id ] + def create_resource_definition(self, name, initial_version): + + resources = initial_version.get("Resources", []) + GreengrassBackend._validate_resources(resources) + + resource_def = FakeResourceDefinition(self.region_name, name, initial_version) + self.resource_definitions[resource_def.id] = resource_def + init_ver = resource_def.initial_version + resources = init_ver.get("Resources", {}) + self.create_resource_definition_version(resource_def.id, resources) + + return resource_def + + def create_resource_definition_version(self, resource_definition_id, resources): + + if resource_definition_id not in self.resource_definitions: + raise IdNotFoundException("That resource definition does not exist.") + + GreengrassBackend._validate_resources(resources) + + resource_def_ver = FakeResourceDefinitionVersion( + self.region_name, resource_definition_id, resources + ) + + resources_ver = self.resource_definition_versions.get( + resource_def_ver.resource_definition_id, {} + ) + resources_ver[resource_def_ver.version] = resource_def_ver + self.resource_definition_versions[ + resource_def_ver.resource_definition_id + ] = resources_ver + + self.resource_definitions[ + resource_definition_id + ].latest_version = resource_def_ver.version + + self.resource_definitions[ + resource_definition_id + ].latest_version_arn = resource_def_ver.arn + + return resource_def_ver + + @staticmethod + def _validate_resources(resources): + for resource in resources: + volume_source_path = ( + resource.get("ResourceDataContainer", {}) + .get("LocalVolumeResourceData", {}) + .get("SourcePath", "") + ) + if volume_source_path == "/sys" or volume_source_path.startswith("/sys/"): + raise GreengrassClientError( + "400", + "The resources definition is invalid. (ErrorDetails: [Accessing /sys is prohibited])", + ) + + local_device_resource_data = resource.get("ResourceDataContainer", {}).get( + "LocalDeviceResourceData", {} + ) + if local_device_resource_data: + device_source_path = local_device_resource_data["SourcePath"] + if not device_source_path.startswith("/dev"): + raise GreengrassClientError( + "400", + f"The resources definition is invalid. (ErrorDetails: [Device resource path should begin with " + "/dev" + f", but got: {device_source_path}])", + ) + greengrass_backends = BackendDict(GreengrassBackend, "greengrass") diff --git a/moto/greengrass/responses.py b/moto/greengrass/responses.py index b1d30f761..49d6ae4a7 100644 --- a/moto/greengrass/responses.py +++ b/moto/greengrass/responses.py @@ -229,3 +229,34 @@ class GreengrassResponse(BaseResponse): device_definition_version_id=device_definition_version_id, ) return 200, {"status": 200}, json.dumps(res.to_dict(include_detail=True)) + + def resource_definitions(self, request, full_url, headers): + self.setup_class(request, full_url, headers) + + if self.method == "POST": + return self.create_resource_definition() + + def create_resource_definition(self): + + initial_version = self._get_param("InitialVersion") + name = self._get_param("Name") + res = self.greengrass_backend.create_resource_definition( + name=name, initial_version=initial_version + ) + return 201, {"status": 201}, json.dumps(res.to_dict()) + + def resource_definition_versions(self, request, full_url, headers): + self.setup_class(request, full_url, headers) + + if self.method == "POST": + return self.create_resource_definition_version() + + def create_resource_definition_version(self): + + resource_definition_id = self.path.split("/")[-2] + resources = self._get_param("Resources") + + res = self.greengrass_backend.create_resource_definition_version( + resource_definition_id=resource_definition_id, resources=resources + ) + return 201, {"status": 201}, json.dumps(res.to_dict()) diff --git a/moto/greengrass/urls.py b/moto/greengrass/urls.py index 9f390d970..5a237da0a 100644 --- a/moto/greengrass/urls.py +++ b/moto/greengrass/urls.py @@ -17,4 +17,6 @@ url_paths = { "{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/resources$": response.resource_definitions, + "{0}/greengrass/definition/resources/(?P[^/]+)/versions$": response.resource_definition_versions, } diff --git a/tests/test_greengrass/test_greengrass_resource.py b/tests/test_greengrass/test_greengrass_resource.py new file mode 100644 index 000000000..272b96d46 --- /dev/null +++ b/tests/test_greengrass/test_greengrass_resource.py @@ -0,0 +1,272 @@ +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_resource_definition(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + init_ver = { + "Resources": [ + { + "Id": "123", + "Name": "test_directory", + "ResourceDataContainer": { + "LocalVolumeResourceData": { + "DestinationPath": "/test_dir", + "GroupOwnerSetting": {"AutoAddGroupOwner": True}, + "SourcePath": "/home/ggc_user/test_dir", + } + }, + } + ] + } + + resource_name = "TestResource" + res = client.create_resource_definition(InitialVersion=init_ver, Name=resource_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(resource_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") + + +@mock_greengrass +def test_create_resource_definition_with_invalid_volume_resource(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + init_ver = { + "Resources": [ + { + "Id": "123", + "Name": "test_directory", + "ResourceDataContainer": { + "LocalVolumeResourceData": { + "DestinationPath": "/test_dir", + "GroupOwnerSetting": {"AutoAddGroupOwner": True}, + "SourcePath": "/sys/foo", + } + }, + } + ] + } + + with pytest.raises(ClientError) as ex: + client.create_resource_definition(InitialVersion=init_ver) + ex.value.response["Error"]["Message"].should.equal( + "The resources definition is invalid. (ErrorDetails: [Accessing /sys is prohibited])" + ) + ex.value.response["Error"]["Code"].should.equal("400") + + +@mock_greengrass +def test_create_resource_definition_with_invalid_local_device_resource(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + source_path = "/foo/bar" + init_ver = { + "Resources": [ + { + "Id": "123", + "Name": "test_directory", + "ResourceDataContainer": { + "LocalDeviceResourceData": { + "SourcePath": source_path, + } + }, + } + ] + } + + with pytest.raises(ClientError) as ex: + client.create_resource_definition(InitialVersion=init_ver) + ex.value.response["Error"]["Message"].should.equal( + f"The resources definition is invalid. (ErrorDetails: [Device resource path should begin with " + "/dev" + f", but got: {source_path}])" + ) + ex.value.response["Error"]["Code"].should.equal("400") + + +@freezegun.freeze_time("2022-06-01 12:00:00") +@mock_greengrass +def test_create_resource_definition_version(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + v1_resources = [ + { + "Id": "123", + "Name": "test_directory", + "ResourceDataContainer": { + "LocalVolumeResourceData": { + "DestinationPath": "/test_dir", + "GroupOwnerSetting": {"AutoAddGroupOwner": True}, + "SourcePath": "/home/ggc_user/test_dir", + } + }, + } + ] + + initial_version = {"Resources": v1_resources} + resource_def_res = client.create_resource_definition( + InitialVersion=initial_version, Name="TestResource" + ) + resource_def_id = resource_def_res["Id"] + + v2_resources = [ + { + "Id": "234", + "Name": "test_directory2", + "ResourceDataContainer": { + "LocalVolumeResourceData": { + "DestinationPath": "/test_dir2", + "GroupOwnerSetting": {"AutoAddGroupOwner": True}, + "SourcePath": "/home/ggc_user/test_dir2", + } + }, + } + ] + + device_def_ver_res = client.create_resource_definition_version( + ResourceDefinitionId=resource_def_id, Resources=v2_resources + ) + device_def_ver_res.should.have.key("Arn") + device_def_ver_res.should.have.key("CreationTimestamp") + device_def_ver_res.should.have.key("Id").equals(resource_def_id) + device_def_ver_res.should.have.key("Version") + + if not TEST_SERVER_MODE: + device_def_ver_res["CreationTimestamp"].should.equal("2022-06-01T12:00:00.000Z") + + +@mock_greengrass +def test_create_resources_definition_version_with_invalid_id(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + resources = [ + { + "Id": "123", + "Name": "test_directory", + "ResourceDataContainer": { + "LocalVolumeResourceData": { + "DestinationPath": "/test_dir", + "GroupOwnerSetting": {"AutoAddGroupOwner": True}, + "SourcePath": "/home/ggc_user/test_dir", + } + }, + } + ] + with pytest.raises(ClientError) as ex: + client.create_resource_definition_version( + ResourceDefinitionId="7b0bdeae-54c7-47cf-9f93-561e672efd9c", + Resources=resources, + ) + ex.value.response["Error"]["Message"].should.equal( + "That resource definition does not exist." + ) + ex.value.response["Error"]["Code"].should.equal("IdNotFoundException") + + +@mock_greengrass +def test_create_resources_definition_version_with_volume_resource(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + v1_resources = [ + { + "Id": "123", + "Name": "test_directory", + "ResourceDataContainer": { + "LocalVolumeResourceData": { + "DestinationPath": "/test_dir", + "GroupOwnerSetting": {"AutoAddGroupOwner": True}, + "SourcePath": "/home/ggc_user/test_dir", + } + }, + } + ] + + initial_version = {"Resources": v1_resources} + resource_def_res = client.create_resource_definition( + InitialVersion=initial_version, Name="TestResource" + ) + resource_def_id = resource_def_res["Id"] + + v2_resources = [ + { + "Id": "123", + "Name": "test_directory", + "ResourceDataContainer": { + "LocalVolumeResourceData": { + "DestinationPath": "/test_dir", + "GroupOwnerSetting": {"AutoAddGroupOwner": True}, + "SourcePath": "/sys/block/sda", + } + }, + } + ] + with pytest.raises(ClientError) as ex: + client.create_resource_definition_version( + ResourceDefinitionId=resource_def_id, Resources=v2_resources + ) + ex.value.response["Error"]["Message"].should.equal( + "The resources definition is invalid. (ErrorDetails: [Accessing /sys is prohibited])" + ) + ex.value.response["Error"]["Code"].should.equal("400") + + +@mock_greengrass +def test_create_resources_definition_version_with_invalid_local_device_resource(): + + client = boto3.client("greengrass", region_name="ap-northeast-1") + v1_resources = [ + { + "Id": "123", + "Name": "test_directory", + "ResourceDataContainer": { + "LocalDeviceResourceData": { + "SourcePath": "/dev/null", + } + }, + } + ] + + initial_version = {"Resources": v1_resources} + resource_def_res = client.create_resource_definition( + InitialVersion=initial_version, Name="TestResource" + ) + resource_def_id = resource_def_res["Id"] + + source_path = "/foo/bar" + v2_resources = [ + { + "Id": "123", + "Name": "test_directory", + "ResourceDataContainer": { + "LocalDeviceResourceData": {"SourcePath": source_path} + }, + } + ] + with pytest.raises(ClientError) as ex: + client.create_resource_definition_version( + ResourceDefinitionId=resource_def_id, Resources=v2_resources + ) + ex.value.response["Error"]["Message"].should.equal( + f"The resources definition is invalid. (ErrorDetails: [Device resource path should begin with " + "/dev" + f", but got: {source_path}])" + ) + ex.value.response["Error"]["Code"].should.equal("400")