From 33024619ebae8c6725af8981eb261f93089a0291 Mon Sep 17 00:00:00 2001 From: Bogdan Girman Date: Thu, 25 Jan 2024 23:34:04 +0100 Subject: [PATCH] SageMaker: Add exception when resource already exists (#7239) --- moto/sagemaker/exceptions.py | 4 ++ moto/sagemaker/models.py | 15 ++++++++ .../test_sagemaker_feature_groups.py | 38 +++++++++++++++++-- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/moto/sagemaker/exceptions.py b/moto/sagemaker/exceptions.py index 8f5750573..c2286b9fe 100644 --- a/moto/sagemaker/exceptions.py +++ b/moto/sagemaker/exceptions.py @@ -41,6 +41,10 @@ class AWSValidationException(AWSError): TYPE = "ValidationException" +class ResourceInUseException(AWSError): + TYPE = "ResourceInUse" + + class ResourceNotFound(JsonRESTError): def __init__(self, message: str): super().__init__(__class__.__name__, message) # type: ignore diff --git a/moto/sagemaker/models.py b/moto/sagemaker/models.py index c284c65ec..241dbdfa3 100644 --- a/moto/sagemaker/models.py +++ b/moto/sagemaker/models.py @@ -14,6 +14,7 @@ from moto.utilities.paginator import paginate from .exceptions import ( AWSValidationException, MissingModel, + ResourceInUseException, ResourceNotFound, ValidationError, ) @@ -1006,6 +1007,9 @@ class FeatureGroup(BaseObject): "Catalog": "AwsDataCatalog", "Database": "sagemaker_featurestore", } + offline_store_config["S3StorageConfig"][ + "ResolvedOutputS3Uri" + ] = f'{offline_store_config["S3StorageConfig"]["S3Uri"]}/{account_id}/{region_name}/offline-store/{feature_group_name}-{int(datetime.now().timestamp())}/data' self.offline_store_config = offline_store_config self.role_arn = role_arn @@ -3529,6 +3533,17 @@ class SageMakerModelBackend(BaseBackend): role_arn: str, tags: Any, ) -> str: + feature_group_arn = arn_formatter( + region_name=self.region_name, + account_id=self.account_id, + _type="feature-group", + _id=f"{feature_group_name.lower()}", + ) + if feature_group_arn in self.feature_groups: + raise ResourceInUseException( + message=f"An error occurred (ResourceInUse) when calling the CreateFeatureGroup operation: Resource Already Exists: FeatureGroup with name {feature_group_name} already exists. Choose a different name.\nInfo: Feature Group '{feature_group_name}' already exists." + ) + feature_group = FeatureGroup( feature_group_name=feature_group_name, record_identifier_feature_name=record_identifier_feature_name, diff --git a/tests/test_sagemaker/test_sagemaker_feature_groups.py b/tests/test_sagemaker/test_sagemaker_feature_groups.py index c3f4a1475..029b042c3 100644 --- a/tests/test_sagemaker/test_sagemaker_feature_groups.py +++ b/tests/test_sagemaker/test_sagemaker_feature_groups.py @@ -3,6 +3,8 @@ import re from datetime import datetime import boto3 +import pytest +from botocore.exceptions import ClientError from moto import mock_sagemaker @@ -34,6 +36,29 @@ def test_create_feature_group(): == "arn:aws:sagemaker:us-east-2:123456789012:feature-group/some-feature-group-name" ) + with pytest.raises(ClientError) as raised_exception: + client.create_feature_group( + FeatureGroupName="some-feature-group-name", + RecordIdentifierFeatureName="some_record_identifier", + EventTimeFeatureName="EventTime", + FeatureDefinitions=[ + {"FeatureName": "some_feature", "FeatureType": "String"}, + {"FeatureName": "EventTime", "FeatureType": "Fractional"}, + {"FeatureName": "some_record_identifier", "FeatureType": "String"}, + ], + RoleArn="arn:aws:iam::123456789012:role/AWSFeatureStoreAccess", + OfflineStoreConfig={ + "DisableGlueTableCreation": False, + "S3StorageConfig": {"S3Uri": "s3://mybucket"}, + }, + ) + + assert raised_exception.value.response["Error"]["Code"] == "ResourceInUse" + assert ( + raised_exception.value.response["Error"]["Message"] + == "An error occurred (ResourceInUse) when calling the CreateFeatureGroup operation: Resource Already Exists: FeatureGroup with name some-feature-group-name already exists. Choose a different name.\nInfo: Feature Group 'some-feature-group-name' already exists." + ) + @mock_sagemaker def test_describe_feature_group(): @@ -55,7 +80,7 @@ def test_describe_feature_group(): RoleArn=role_arn, OfflineStoreConfig={ "DisableGlueTableCreation": False, - "S3StorageConfig": {"S3Uri": "s3://mybucket"}, + "S3StorageConfig": {"S3Uri": "s3://mybucket/some-folder/some-subfolder"}, }, ) resp = client.describe_feature_group(FeatureGroupName=feature_group_name) @@ -70,7 +95,7 @@ def test_describe_feature_group(): assert resp["FeatureDefinitions"] == feature_definitions assert resp["RoleArn"] == role_arn assert re.match( - r"^some_feature_group_name_[0-9]+$", + f"^{feature_group_name.replace('-', '_')}_[0-9]+$", resp["OfflineStoreConfig"]["DataCatalogConfig"]["TableName"], ) assert ( @@ -80,6 +105,13 @@ def test_describe_feature_group(): resp["OfflineStoreConfig"]["DataCatalogConfig"]["Database"] == "sagemaker_featurestore" ) - assert resp["OfflineStoreConfig"]["S3StorageConfig"]["S3Uri"] == "s3://mybucket" + assert ( + resp["OfflineStoreConfig"]["S3StorageConfig"]["S3Uri"] + == "s3://mybucket/some-folder/some-subfolder" + ) + assert re.match( + f"^s3://mybucket/some-folder/some-subfolder/123456789012/us-east-2/offline-store/{feature_group_name}-[0-9]+/data$", + resp["OfflineStoreConfig"]["S3StorageConfig"]["ResolvedOutputS3Uri"], + ) assert isinstance(resp["CreationTime"], datetime) assert resp["FeatureGroupStatus"] == "Created"