Repository: getmoto/moto

Issue:
create_endpoint_config doesn't support Serverless deployment
It is currently not possible to use moto to mock `create_endpoint_config` if the endpoint config involves using serverless deployment.

To reproduce:
```
from moto import mock_sagemaker
import boto3

with mock_sagemaker():
    client = boto3.client("sagemaker")
    endpoint_conf_name = 'test'
    
    client.create_endpoint_config(
        EndpointConfigName=endpoint_conf_name,
        ProductionVariants=[
            {'VariantName': 'dummy',
                'ModelName': 'dummy',
                'ServerlessConfig': {
                'MemorySizeInMB': 1024,
                'MaxConcurrency': 20
            }}
        ],
    )
```

Stacktrace:
```
self = <moto.sagemaker.models.FakeEndpointConfig object at 0x7f90f8934850>, production_variants = [{'ModelName': 'test', 'ServerlessConfig': {'MaxConcurrency': 20, 'MemorySizeInMB': 6144}, 'VariantName': 'default-variant-test'}]

    def validate_production_variants(self, production_variants):
        for production_variant in production_variants:
>           self.validate_instance_type(production_variant["InstanceType"])
E           KeyError: 'InstanceType'

ml_framework/venv/lib/python3.7/site-packages/moto/sagemaker/models.py:401: KeyError
```

The error is triggered when moto creates a new `FakeEndpointConfig`: https://github.com/spulec/moto/blob/930c58bd1366b073e8630a82fa7f34624babd867/moto/sagemaker/models.py#L399-L401

When deploying using `ServerlessConfig`, there is no `InstanceType` present in the ProductionVariant (see documentation: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sagemaker.html#SageMaker.Client.create_endpoint_config)

Ideally, we should condition based on whether we have `InstanceType` or `ServerlessConfig` in the ProductionVariant, and add some checks on the Serverless Config parameters.

Happy to get a PR out to fix this :) 


Discussion:
Hi @arnaudstiegler, thanks for raising this. PR's are always welcome - let me know if you need any help or pointers.
Sounds good, will work on this

Resolving patch:
diff --git a/moto/sagemaker/models.py b/moto/sagemaker/models.py
--- a/moto/sagemaker/models.py
+++ b/moto/sagemaker/models.py
@@ -398,7 +398,25 @@ def __init__(
 
     def validate_production_variants(self, production_variants):
         for production_variant in production_variants:
-            self.validate_instance_type(production_variant["InstanceType"])
+            if "InstanceType" in production_variant.keys():
+                self.validate_instance_type(production_variant["InstanceType"])
+            elif "ServerlessConfig" in production_variant.keys():
+                self.validate_serverless_config(production_variant["ServerlessConfig"])
+            else:
+                message = "Invalid Keys for ProductionVariant: received {} but expected it to contain one of {}".format(
+                    production_variant.keys(), ["InstanceType", "ServerlessConfig"]
+                )
+                raise ValidationError(message=message)
+
+    def validate_serverless_config(self, serverless_config):
+        VALID_SERVERLESS_MEMORY_SIZE = [1024, 2048, 3072, 4096, 5120, 6144]
+        if not validators.is_one_of(
+            serverless_config["MemorySizeInMB"], VALID_SERVERLESS_MEMORY_SIZE
+        ):
+            message = "Value '{}' at 'MemorySizeInMB' failed to satisfy constraint: Member must satisfy enum value set: {}".format(
+                serverless_config["MemorySizeInMB"], VALID_SERVERLESS_MEMORY_SIZE
+            )
+            raise ValidationError(message=message)
 
     def validate_instance_type(self, instance_type):
         VALID_INSTANCE_TYPES = [


Test patch:
diff --git a/tests/test_sagemaker/test_sagemaker_endpoint.py b/tests/test_sagemaker/test_sagemaker_endpoint.py
--- a/tests/test_sagemaker/test_sagemaker_endpoint.py
+++ b/tests/test_sagemaker/test_sagemaker_endpoint.py
@@ -20,6 +20,8 @@
 TEST_ENDPOINT_CONFIG_NAME = "MyEndpointConfig"
 TEST_VARIANT_NAME = "MyProductionVariant"
 TEST_INSTANCE_TYPE = "ml.t2.medium"
+TEST_MEMORY_SIZE = 1024
+TEST_CONCURRENCY = 10
 TEST_PRODUCTION_VARIANTS = [
     {
         "VariantName": TEST_VARIANT_NAME,
@@ -28,6 +30,16 @@
         "InstanceType": TEST_INSTANCE_TYPE,
     },
 ]
+TEST_SERVERLESS_PRODUCTION_VARIANTS = [
+    {
+        "VariantName": TEST_VARIANT_NAME,
+        "ModelName": TEST_MODEL_NAME,
+        "ServerlessConfig": {
+            "MemorySizeInMB": TEST_MEMORY_SIZE,
+            "MaxConcurrency": TEST_CONCURRENCY,
+        },
+    },
+]
 
 
 @pytest.fixture
@@ -35,19 +47,12 @@ def sagemaker_client():
     return boto3.client("sagemaker", region_name=TEST_REGION_NAME)
 
 
-@mock_sagemaker
-def test_create_endpoint_config(sagemaker_client):
-    with pytest.raises(ClientError) as e:
-        sagemaker_client.create_endpoint_config(
-            EndpointConfigName=TEST_ENDPOINT_CONFIG_NAME,
-            ProductionVariants=TEST_PRODUCTION_VARIANTS,
-        )
-    assert e.value.response["Error"]["Message"].startswith("Could not find model")
-
+def create_endpoint_config_helper(sagemaker_client, production_variants):
     _create_model(sagemaker_client, TEST_MODEL_NAME)
+
     resp = sagemaker_client.create_endpoint_config(
         EndpointConfigName=TEST_ENDPOINT_CONFIG_NAME,
-        ProductionVariants=TEST_PRODUCTION_VARIANTS,
+        ProductionVariants=production_variants,
     )
     resp["EndpointConfigArn"].should.match(
         r"^arn:aws:sagemaker:.*:.*:endpoint-config/{}$".format(
@@ -64,7 +69,33 @@ def test_create_endpoint_config(sagemaker_client):
         )
     )
     resp["EndpointConfigName"].should.equal(TEST_ENDPOINT_CONFIG_NAME)
-    resp["ProductionVariants"].should.equal(TEST_PRODUCTION_VARIANTS)
+    resp["ProductionVariants"].should.equal(production_variants)
+
+
+@mock_sagemaker
+def test_create_endpoint_config(sagemaker_client):
+    with pytest.raises(ClientError) as e:
+        sagemaker_client.create_endpoint_config(
+            EndpointConfigName=TEST_ENDPOINT_CONFIG_NAME,
+            ProductionVariants=TEST_PRODUCTION_VARIANTS,
+        )
+    assert e.value.response["Error"]["Message"].startswith("Could not find model")
+
+    # Testing instance-based endpoint configuration
+    create_endpoint_config_helper(sagemaker_client, TEST_PRODUCTION_VARIANTS)
+
+
+@mock_sagemaker
+def test_create_endpoint_config_serverless(sagemaker_client):
+    with pytest.raises(ClientError) as e:
+        sagemaker_client.create_endpoint_config(
+            EndpointConfigName=TEST_ENDPOINT_CONFIG_NAME,
+            ProductionVariants=TEST_SERVERLESS_PRODUCTION_VARIANTS,
+        )
+    assert e.value.response["Error"]["Message"].startswith("Could not find model")
+
+    # Testing serverless endpoint configuration
+    create_endpoint_config_helper(sagemaker_client, TEST_SERVERLESS_PRODUCTION_VARIANTS)
 
 
 @mock_sagemaker
@@ -129,6 +160,26 @@ def test_create_endpoint_invalid_instance_type(sagemaker_client):
     assert expected_message in e.value.response["Error"]["Message"]
 
 
+@mock_sagemaker
+def test_create_endpoint_invalid_memory_size(sagemaker_client):
+    _create_model(sagemaker_client, TEST_MODEL_NAME)
+
+    memory_size = 1111
+    production_variants = TEST_SERVERLESS_PRODUCTION_VARIANTS
+    production_variants[0]["ServerlessConfig"]["MemorySizeInMB"] = memory_size
+
+    with pytest.raises(ClientError) as e:
+        sagemaker_client.create_endpoint_config(
+            EndpointConfigName=TEST_ENDPOINT_CONFIG_NAME,
+            ProductionVariants=production_variants,
+        )
+    assert e.value.response["Error"]["Code"] == "ValidationException"
+    expected_message = "Value '{}' at 'MemorySizeInMB' failed to satisfy constraint: Member must satisfy enum value set: [".format(
+        memory_size
+    )
+    assert expected_message in e.value.response["Error"]["Message"]
+
+
 @mock_sagemaker
 def test_create_endpoint(sagemaker_client):
     with pytest.raises(ClientError) as e: