Repository: getmoto/moto
Pull request #7873 (2024-07-19 20:43:14)

Feature: Sagemaker compilation jobs
Adds support for Sagemaker compilation jobs create, describe, list, and stop.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/7873?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
Attention: Patch coverage is `94.44444%` with `6 lines` in your changes missing coverage. Please review.
> Project coverage is 94.35%. Comparing base [(`b6bb11e`)](https://app.codecov.io/gh/getmoto/moto/commit/b6bb11e6ef4c3c1e51e94278237ee7db65b0c89f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`660edcb`)](https://app.codecov.io/gh/getmoto/moto/commit/660edcbba0c0cbe3dace7ffe2895395bda39bfa3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).

| [Files](https://app.codecov.io/gh/getmoto/moto/pull/7873?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Patch % | Lines |
|---|---|---|
| [moto/sagemaker/models.py](https://app.codecov.io/gh/getmoto/moto/pull/7873?src=pr&el=tree&filepath=moto%2Fsagemaker%2Fmodels.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#diff-bW90by9zYWdlbWFrZXIvbW9kZWxzLnB5) | 92.00% | [6 Missing :warning: ](https://app.codecov.io/gh/getmoto/moto/pull/7873?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) |

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff            @@
##           master    #7873    +/-   ##
========================================
  Coverage   94.35%   94.35%            
========================================
  Files        1113     1113            
  Lines       94650    94758   +108     
========================================
+ Hits        89308    89411   +103     
- Misses       5342     5347     +5     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/7873/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/7873/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `29.19% <9.25%> (-0.03%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/7873/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `94.32% <94.44%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/getmoto/moto/pull/7873?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).


Patch:
diff --git a/docs/docs/configuration/index.rst b/docs/docs/configuration/index.rst
index 106986b90..151b48eba 100644
--- a/docs/docs/configuration/index.rst
+++ b/docs/docs/configuration/index.rst
@@ -23,6 +23,7 @@ If you are using the decorators, some options are configurable within the decora
         },
         "iam": {"load_aws_managed_policies": False},
         "stepfunctions": {"execute_state_machine": True},
+        "iot": {"use_valid_cert": False},
     })
 
 
@@ -57,6 +58,24 @@ Moto comes bundled with all Managed Policies that AWS exposes, which are updated
 
 Set `"iam": {"load_aws_managed_policies": True}` to load these policies for a single test.
 
+IoT Certificates Validity and Certificate ID computation
+-------------------------
+The IoT Client implementation for certificates, by default,
+computes the certificate ID incorrectly based on the
+sha256 hash of the certificate in PEM encoding and does not require a
+valid certificate.
+
+We now support proper certificate ID generation using 
+the sha256 of the DER certificate to ensure correct behavior for tests that
+use valid certificates.  To enable the correct
+certificate ID generation algorithm, set `"iot": {"use_valid_cert": True}`
+
+In the next major release we recommend changing the default to `True`
+which would a breaking change for tests that do not use valid certificates.
+For future compatiblity, update any existing code that does not use real
+certificates to explicitly set `use_valid_cert` to `False`
+
+
 Configuring MotoServer
 ----------------------
 The following options can also be configured when running the MotoServer:
diff --git a/moto/core/config.py b/moto/core/config.py
index 5958c44fc..1b6cfa72b 100644
--- a/moto/core/config.py
+++ b/moto/core/config.py
@@ -25,6 +25,10 @@ class _sfn_config(TypedDict, total=False):
     execute_state_machine: bool
 
 
+class _iot_config(TypedDict, total=False):
+    use_valid_cert: bool
+
+
 DefaultConfig = TypedDict(
     "DefaultConfig",
     {
@@ -33,6 +37,7 @@ DefaultConfig = TypedDict(
         "lambda": _docker_config,
         "iam": _iam_config,
         "stepfunctions": _sfn_config,
+        "iot": _iot_config,
     },
     total=False,
 )
@@ -47,6 +52,7 @@ default_user_config: DefaultConfig = {
     },
     "iam": {"load_aws_managed_policies": False},
     "stepfunctions": {"execute_state_machine": False},
+    "iot": {"use_valid_cert": False},
 }
 
 
diff --git a/moto/firehose/models.py b/moto/firehose/models.py
index 232a2b410..2af38c387 100644
--- a/moto/firehose/models.py
+++ b/moto/firehose/models.py
@@ -445,7 +445,7 @@ class FirehoseBackend(BaseBackend):
         # Path prefix pattern: myApp/YYYY/MM/DD/HH/
         # Object name pattern:
         # DeliveryStreamName-DeliveryStreamVersion-YYYY-MM-DD-HH-MM-SS-RandomString
-        prefix = f"{prefix}{'' if prefix.endswith('/') else '/'}"
+        prefix = f"{prefix}{'' if prefix.endswith('/') or prefix == '' else '/'}"
         now = utcnow()
         return (
             f"{prefix}{now.strftime('%Y/%m/%d/%H')}/"
diff --git a/moto/iot/models.py b/moto/iot/models.py
index fe2d0ba9d..b46d53e41 100644
--- a/moto/iot/models.py
+++ b/moto/iot/models.py
@@ -15,6 +15,7 @@ from moto.core.base_backend import BackendDict, BaseBackend
 from moto.core.common_models import BaseModel
 from moto.core.utils import utcnow
 from moto.moto_api._internal import mock_random as random
+from moto.settings import iot_use_valid_cert
 from moto.utilities.paginator import paginate
 from moto.utilities.utils import get_partition
 
@@ -192,11 +193,10 @@ class FakeCertificate(BaseModel):
         region_name: str,
         ca_certificate_id: Optional[str] = None,
     ):
-        m = hashlib.sha256()
-        m.update(certificate_pem.encode("utf-8"))
-        self.certificate_id = m.hexdigest()
+        self.certificate_id = self.compute_cert_id(certificate_pem)
         self.arn = f"arn:{get_partition(region_name)}:iot:{region_name}:{account_id}:cert/{self.certificate_id}"
         self.certificate_pem = certificate_pem
+
         self.status = status
 
         self.owner = account_id
@@ -207,6 +207,32 @@ class FakeCertificate(BaseModel):
         self.validity_not_after = time.time() + 86400
         self.ca_certificate_id = ca_certificate_id
 
+    def compute_cert_id(self, certificate_pem: str) -> str:
+        if iot_use_valid_cert():
+            return self.compute_der_cert_id(certificate_pem)
+        else:
+            return self.compute_pem_cert_id(certificate_pem)
+
+    def compute_pem_cert_id(self, certificate_pem: str) -> str:
+        """
+        Original (incorrect) PEM-based hash kept for backwards compatibility
+        with existing tests which may or may not use valid certs. Also useful
+        for simplifying tests that don't require a real cert.
+        """
+        m = hashlib.sha256()
+        m.update(certificate_pem.encode("utf-8"))
+        return m.hexdigest()
+
+    def compute_der_cert_id(self, certificate_pem: str) -> str:
+        """
+        Compute the certificate hash based on DER encoding
+        """
+        return hashlib.sha256(
+            x509.load_pem_x509_certificate(
+                certificate_pem.encode("utf-8")
+            ).public_bytes(serialization.Encoding.DER)
+        ).hexdigest()
+
     def to_dict(self) -> Dict[str, Any]:
         return {
             "certificateArn": self.arn,
diff --git a/moto/sagemaker/models.py b/moto/sagemaker/models.py
index a69fbe69f..f15a164d9 100644
--- a/moto/sagemaker/models.py
+++ b/moto/sagemaker/models.py
@@ -103,6 +103,12 @@ PAGINATION_MODEL = {
         "limit_default": 100,
         "unique_attribute": "endpoint_config_arn",
     },
+    "list_compilation_jobs": {
+        "input_token": "next_token",
+        "limit_key": "max_results",
+        "limit_default": 100,
+        "unique_attribute": "arn",
+    },
 }
 
 METRIC_INFO_TYPE = Dict[str, Union[str, int, float, datetime]]
@@ -1638,6 +1644,106 @@ class ClusterNode(BaseObject):
         }
 
 
+class CompilationJob(BaseObject):
+    def __init__(
+        self,
+        compilation_job_name: str,
+        role_arn: str,
+        region_name: str,
+        account_id: str,
+        output_config: Dict[str, Any],
+        stopping_condition: Dict[str, Any],
+        model_package_version_arn: Optional[str],
+        input_config: Optional[Dict[str, Any]],
+        vpc_config: Optional[Dict[str, Any]],
+        tags: Optional[List[Dict[str, str]]],
+    ):
+        self.compilation_job_name = compilation_job_name
+        if (
+            compilation_job_name
+            in sagemaker_backends[account_id][region_name].compilation_jobs
+        ):
+            raise ResourceInUseException(
+                message=f"Resource Already Exists: Compilation job with name {compilation_job_name} already exists. Choose a different name."
+            )
+        self.arn = arn_formatter(
+            "compilation-job", self.compilation_job_name, account_id, region_name
+        )
+        self.compilation_job_status = "COMPLETED"
+        self.compilation_start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+        self.compilation_end_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+        self.stopping_condition = stopping_condition
+        self.inference_image = "InferenceImage"
+        self.model_package_version_arn = model_package_version_arn
+        self.creation_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+        self.last_modified_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+        self.failure_reason = ""
+        self.model_artifacts = {"S3ModelArtifacts": output_config["S3OutputLocation"]}
+        self.model_digests = {
+            "ArtifactDigest": "786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce"
+        }
+        self.role_arn = role_arn
+        self.input_config = input_config
+        if input_config and model_package_version_arn:
+            raise ValidationError(
+                message="InputConfig and ModelPackageVersionArn cannot be specified at the same time."
+            )
+        if not input_config and not model_package_version_arn:
+            raise ValidationError(
+                message="Either InputConfig or ModelPackageVersionArn must be specified."
+            )
+        self.output_config = output_config
+        self.vpc_config = vpc_config
+        self.derived_information = {"DerivedDataInputConfig": "DerivedDataInputConfig"}
+        self.tags = tags
+
+    def describe(self) -> Dict[str, Any]:
+        return {
+            "CompilationJobName": self.compilation_job_name,
+            "CompilationJobArn": self.arn,
+            "CompilationJobStatus": self.compilation_job_status,
+            "CompilationStartTime": self.compilation_start_time,
+            "CompilationEndTime": self.compilation_end_time,
+            "StoppingCondition": self.stopping_condition,
+            "InferenceImage": self.inference_image,
+            "ModelPackageVersionArn": self.model_package_version_arn,
+            "CreationTime": self.creation_time,
+            "LastModifiedTime": self.last_modified_time,
+            "FailureReason": self.failure_reason,
+            "ModelArtifacts": self.model_artifacts,
+            "ModelDigests": self.model_digests,
+            "RoleArn": self.role_arn,
+            "InputConfig": self.input_config,
+            "OutputConfig": self.output_config,
+            "VpcConfig": self.vpc_config,
+            "DerivedInformation": self.derived_information,
+        }
+
+    def summary(self) -> Dict[str, Any]:
+        summary = {
+            "CompilationJobName": self.compilation_job_name,
+            "CompilationJobArn": self.arn,
+            "CreationTime": self.creation_time,
+            "CompilationStartTime": self.compilation_start_time,
+            "CompilationEndTime": self.compilation_end_time,
+            "LastModifiedTime": self.last_modified_time,
+            "CompilationJobStatus": self.compilation_job_status,
+        }
+        if "TargetDevice" in self.output_config:
+            summary["CompilationTargetDevice"] = self.output_config["TargetDevice"]
+        else:
+            summary["CompilationTargetPlatformOs"] = self.output_config[
+                "TargetPlatform"
+            ]["Os"]
+            summary["CompilationTargetPlatformArch"] = self.output_config[
+                "TargetPlatform"
+            ]["Arch"]
+            summary["CompilationTargetPlatformAccelerator"] = self.output_config[
+                "TargetPlatform"
+            ]["Accelerator"]
+        return summary
+
+
 class AutoMLJob(BaseObject):
     def __init__(
         self,
@@ -2281,6 +2387,7 @@ class SageMakerModelBackend(BaseBackend):
         self.feature_groups: Dict[str, FeatureGroup] = {}
         self.clusters: Dict[str, Cluster] = {}
         self.auto_ml_jobs: Dict[str, AutoMLJob] = {}
+        self.compilation_jobs: Dict[str, CompilationJob] = {}
 
     @staticmethod
     def default_vpc_endpoint_service(
@@ -2412,6 +2519,7 @@ class SageMakerModelBackend(BaseBackend):
             "model-package-group": self.model_package_groups,
             "cluster": self.clusters,
             "automl-job": self.auto_ml_jobs,
+            "compilation-job": self.compilation_jobs,
         }
         target_resource, target_name = arn.split(":")[-1].split("/")
         try:
@@ -4443,6 +4551,110 @@ class SageMakerModelBackend(BaseBackend):
             )
         return endpoint_configs
 
+    def create_compilation_job(
+        self,
+        compilation_job_name: str,
+        role_arn: str,
+        output_config: Dict[str, Any],
+        stopping_condition: Dict[str, Any],
+        model_package_version_arn: Optional[str],
+        input_config: Optional[Dict[str, Any]],
+        vpc_config: Optional[Dict[str, Any]],
+        tags: Optional[List[Dict[str, str]]],
+    ) -> str:
+        compilation_job = CompilationJob(
+            compilation_job_name=compilation_job_name,
+            role_arn=role_arn,
+            region_name=self.region_name,
+            account_id=self.account_id,
+            model_package_version_arn=model_package_version_arn,
+            input_config=input_config,
+            output_config=output_config,
+            vpc_config=vpc_config,
+            stopping_condition=stopping_condition,
+            tags=tags,
+        )
+        self.compilation_jobs[compilation_job_name] = compilation_job
+        return compilation_job.arn
+
+    def describe_compilation_job(self, compilation_job_name: str) -> Dict[str, Any]:
+        if compilation_job_name not in self.compilation_jobs:
+            raise ResourceNotFound(
+                message=f"Could not find compilation job '{compilation_job_name}'."
+            )
+        compilation_job = self.compilation_jobs[compilation_job_name]
+        return compilation_job.describe()
+
+    @paginate(pagination_model=PAGINATION_MODEL)
+    def list_compilation_jobs(
+        self,
+        creation_time_after: Optional[str],
+        creation_time_before: Optional[str],
+        last_modified_time_after: Optional[str],
+        last_modified_time_before: Optional[str],
+        name_contains: Optional[str],
+        status_equals: Optional[str],
+        sort_by: Optional[str],
+        sort_order: Optional[str],
+    ) -> List[CompilationJob]:
+        compilation_jobs = list(self.compilation_jobs.values())
+        if name_contains:
+            compilation_jobs = [
+                i for i in compilation_jobs if name_contains in i.compilation_job_name
+            ]
+        if creation_time_before:
+            compilation_jobs = [
+                i
+                for i in compilation_jobs
+                if i.creation_time < str(creation_time_before)
+            ]
+        if creation_time_after:
+            compilation_jobs = [
+                i
+                for i in compilation_jobs
+                if i.creation_time > str(creation_time_after)
+            ]
+        if last_modified_time_before:
+            compilation_jobs = [
+                i
+                for i in compilation_jobs
+                if i.last_modified_time < str(last_modified_time_before)
+            ]
+        if creation_time_after:
+            compilation_jobs = [
+                i
+                for i in compilation_jobs
+                if i.last_modified_time > str(last_modified_time_after)
+            ]
+        if status_equals:
+            compilation_jobs = [
+                i for i in compilation_jobs if i.compilation_job_status == status_equals
+            ]
+        reverse = sort_order == "Descending"
+        if sort_by == "Name":
+            compilation_jobs = sorted(
+                compilation_jobs, key=lambda x: x.compilation_job_name, reverse=reverse
+            )
+        if sort_by == "Status":
+            compilation_jobs = sorted(
+                compilation_jobs,
+                key=lambda x: x.compilation_job_status,
+                reverse=reverse,
+            )
+        if sort_by == "CreationTime" or sort_by is None:
+            compilation_jobs = sorted(
+                compilation_jobs, key=lambda x: x.creation_time, reverse=reverse
+            )
+        return compilation_jobs
+
+    def delete_compilation_job(self, compilation_job_name: str) -> None:
+        if compilation_job_name not in self.compilation_jobs:
+            raise ResourceNotFound(
+                message=f"Could not find compilation job '{compilation_job_name}'."
+            )
+        del self.compilation_jobs[compilation_job_name]
+        return
+
 
 class FakeExperiment(BaseObject):
     def __init__(
diff --git a/moto/sagemaker/responses.py b/moto/sagemaker/responses.py
index b7e3cc443..daa73851b 100644
--- a/moto/sagemaker/responses.py
+++ b/moto/sagemaker/responses.py
@@ -1140,3 +1140,68 @@ class SageMakerResponse(BaseResponse):
         return json.dumps(
             dict(EndpointConfigs=endpoint_summaries, NextToken=next_token)
         )
+
+    def create_compilation_job(self) -> str:
+        compilation_job_name = self._get_param("CompilationJobName")
+        role_arn = self._get_param("RoleArn")
+        model_package_version_arn = self._get_param("ModelPackageVersionArn")
+        input_config = self._get_param("InputConfig")
+        output_config = self._get_param("OutputConfig")
+        vpc_config = self._get_param("VpcConfig")
+        stopping_condition = self._get_param("StoppingCondition")
+        tags = self._get_param("Tags")
+        compilation_job_arn = self.sagemaker_backend.create_compilation_job(
+            compilation_job_name=compilation_job_name,
+            role_arn=role_arn,
+            model_package_version_arn=model_package_version_arn,
+            input_config=input_config,
+            output_config=output_config,
+            vpc_config=vpc_config,
+            stopping_condition=stopping_condition,
+            tags=tags,
+        )
+        return json.dumps(dict(CompilationJobArn=compilation_job_arn))
+
+    def describe_compilation_job(self) -> str:
+        compilation_job_name = self._get_param("CompilationJobName")
+        compilation_job_description = self.sagemaker_backend.describe_compilation_job(
+            compilation_job_name=compilation_job_name,
+        )
+        return json.dumps(compilation_job_description)
+
+    def list_compilation_jobs(self) -> str:
+        next_token = self._get_param("NextToken")
+        max_results = self._get_param("MaxResults")
+        creation_time_after = self._get_param("CreationTimeAfter")
+        creation_time_before = self._get_param("CreationTimeBefore")
+        last_modified_time_after = self._get_param("LastModifiedTimeAfter")
+        last_modified_time_before = self._get_param("LastModifiedTimeBefore")
+        name_contains = self._get_param("NameContains")
+        status_equals = self._get_param("StatusEquals")
+        sort_by = self._get_param("SortBy")
+        sort_order = self._get_param("SortOrder")
+        compilation_jobs, next_token = self.sagemaker_backend.list_compilation_jobs(
+            next_token=next_token,
+            max_results=max_results,
+            creation_time_after=creation_time_after,
+            creation_time_before=creation_time_before,
+            last_modified_time_after=last_modified_time_after,
+            last_modified_time_before=last_modified_time_before,
+            name_contains=name_contains,
+            status_equals=status_equals,
+            sort_by=sort_by,
+            sort_order=sort_order,
+        )
+        compilation_job_summaries = [x.summary() for x in compilation_jobs]
+        return json.dumps(
+            dict(
+                CompilationJobSummaries=compilation_job_summaries, NextToken=next_token
+            )
+        )
+
+    def delete_compilation_job(self) -> str:
+        compilation_job_name = self._get_param("CompilationJobName")
+        self.sagemaker_backend.delete_compilation_job(
+            compilation_job_name=compilation_job_name,
+        )
+        return json.dumps({})
diff --git a/moto/settings.py b/moto/settings.py
index a537d0143..09735052c 100644
--- a/moto/settings.py
+++ b/moto/settings.py
@@ -191,3 +191,11 @@ def load_iam_aws_managed_policies() -> bool:
         is True
         or os.environ.get("MOTO_IAM_LOAD_MANAGED_POLICIES", "").lower() == "true"
     )
+
+
+#
+# NOTE:  Recommend the next major release to set this to True for proper
+# default behavior
+#
+def iot_use_valid_cert() -> bool:
+    return default_user_config.get("iot", {}).get("use_valid_cert", False)


Test patch:
diff --git a/tests/test_firehose/test_firehose_put.py b/tests/test_firehose/test_firehose_put.py
index 7a8170ae1..87f3e4385 100644
--- a/tests/test_firehose/test_firehose_put.py
+++ b/tests/test_firehose/test_firehose_put.py
@@ -1,5 +1,7 @@
 """Unit tests verifying put-related delivery stream APIs."""
 
+import re
+
 import boto3
 
 from moto import mock_aws
@@ -143,3 +145,6 @@ def test_put_record_batch_extended_s3_destination():
         Bucket=bucket_name, Key=bucket_objects["Contents"][0]["Key"]
     )
     assert response["Body"].read() == b"onetwothree"
+    assert re.match(
+        r"^[0-9]{4}/[0-9]{2}/[0-9]{2}/[0-9]{2}/", bucket_objects["Contents"][0]["Key"]
+    )
diff --git a/tests/test_iot/test_iot_ca_certificates.py b/tests/test_iot/test_iot_ca_certificates.py
index 9b75b7be7..ec8e41566 100644
--- a/tests/test_iot/test_iot_ca_certificates.py
+++ b/tests/test_iot/test_iot_ca_certificates.py
@@ -1,12 +1,17 @@
+from unittest import SkipTest
+
 import boto3
 import pytest
 from botocore.exceptions import ClientError
 
-from moto import mock_aws
+from moto import mock_aws, settings
 
 
 @mock_aws
-def test_register_ca_certificate_simple():
+def test_register_ca_certificate_simple_invalid_cert():
+    """
+    Original test case with invalid cert.
+    """
     client = boto3.client("iot", region_name="us-east-1")
 
     resp = client.register_ca_certificate(
@@ -18,6 +23,38 @@ def test_register_ca_certificate_simple():
     assert "certificateId" in resp
 
 
+@pytest.fixture(name="make_cert")
+@mock_aws(config={"iot": {"use_valid_cert": True}})
+def fixture_make_cert():
+    """
+    Create a valid cert for testing
+    """
+    client = boto3.client("iot", region_name="us-east-1")
+    return client.create_keys_and_certificate(setAsActive=False)
+
+
+@mock_aws(config={"iot": {"use_valid_cert": True}})
+def test_register_ca_certificate_simple(make_cert):
+    if not settings.TEST_DECORATOR_MODE:
+        # Technically it doesn't have to be skipped
+        # in ServerMode the config is not set, so we'll compute the cert the old/invalid way
+        # But there are no assertions to actually verify the value of the computed cert
+        # This is just added here as a warning/reminder if this ever changes
+        raise SkipTest("Config cannot be easily set in ServerMode")
+
+    client = boto3.client("iot", region_name="us-east-1")
+    certInfo = make_cert
+
+    resp = client.register_ca_certificate(
+        caCertificate=certInfo["certificatePem"],
+        verificationCertificate="verification_certificate",
+    )
+
+    assert "certificateArn" in resp
+    assert "certificateId" in resp
+    assert resp["certificateId"] == certInfo["certificateId"]
+
+
 @mock_aws
 def test_describe_ca_certificate_unknown():
     client = boto3.client("iot", region_name="us-east-2")
@@ -76,6 +113,10 @@ def test_describe_ca_certificate_advanced():
 
 @mock_aws
 def test_list_certificates_by_ca():
+    """
+    test listing certificates by CA.  Don't use valid cert to make it simpler
+    for the test.
+    """
     client = boto3.client("iot", region_name="us-east-1")
 
     # create ca
diff --git a/tests/test_sagemaker/test_sagemaker_compilation_jobs.py b/tests/test_sagemaker/test_sagemaker_compilation_jobs.py
new file mode 100644
index 000000000..ae9424087
--- /dev/null
+++ b/tests/test_sagemaker/test_sagemaker_compilation_jobs.py
@@ -0,0 +1,512 @@
+"""Unit tests for sagemaker-supported APIs."""
+
+import datetime
+
+import boto3
+
+from moto import mock_aws
+from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
+
+# See our Development Tips on writing tests for hints on how to write good tests:
+# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html
+
+
+@mock_aws
+def test_create_compilation_job():
+    client = boto3.client("sagemaker", region_name="ap-southeast-1")
+    resp = client.create_compilation_job(
+        CompilationJobName="testcompilationjob",
+        RoleArn=f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole",
+        InputConfig={
+            "S3Uri": "s3://MyBucket/model.tar.gz",
+            "DataInputConfig": "{'input0':[1,3,224,224]}",
+            "Framework": "PYTORCH",
+            "FrameworkVersion": "2.0",
+        },
+        OutputConfig={
+            "S3OutputLocation": "s3://MyBucket/output",
+            "TargetDevice": "lambda",
+            "KmsKeyId": "1234abcd-12ab-34cd-56ef-1234567890ab",
+        },
+        VpcConfig={
+            "SecurityGroupIds": [
+                "sg-12345678901234567",
+            ],
+            "Subnets": [
+                "subnet-12345678901234567",
+            ],
+        },
+        StoppingCondition={
+            "MaxRuntimeInSeconds": 123,
+            "MaxWaitTimeInSeconds": 123,
+            "MaxPendingTimeInSeconds": 7200,
+        },
+        Tags=[
+            {"Key": "testkey", "Value": "testvalue"},
+        ],
+    )
+    assert (
+        resp["CompilationJobArn"]
+        == "arn:aws:sagemaker:ap-southeast-1:123456789012:compilation-job/testcompilationjob"
+    )
+
+
+@mock_aws
+def test_create_compilation_job_defaults():
+    client = boto3.client("sagemaker", region_name="ap-southeast-1")
+    resp = client.create_compilation_job(
+        CompilationJobName="testcompilationjob",
+        RoleArn=f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole",
+        ModelPackageVersionArn=f"arn:aws:sagemaker:ap-southeast-1:{ACCOUNT_ID}:model-package/FakeModelPackage",
+        OutputConfig={
+            "S3OutputLocation": "s3://MyBucket/output",
+            "TargetDevice": "lambda",
+            "KmsKeyId": "1234abcd-12ab-34cd-56ef-1234567890ab",
+        },
+        StoppingCondition={
+            "MaxRuntimeInSeconds": 123,
+            "MaxWaitTimeInSeconds": 123,
+            "MaxPendingTimeInSeconds": 7200,
+        },
+    )
+    assert (
+        resp["CompilationJobArn"]
+        == "arn:aws:sagemaker:ap-southeast-1:123456789012:compilation-job/testcompilationjob"
+    )
+
+
+@mock_aws
+def test_describe_compilation_job():
+    client = boto3.client("sagemaker", region_name="ap-southeast-1")
+    resp = client.create_compilation_job(
+        CompilationJobName="testcompilationjob",
+        RoleArn=f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole",
+        InputConfig={
+            "S3Uri": "s3://MyBucket/model.tar.gz",
+            "DataInputConfig": "{'input0':[1,3,224,224]}",
+            "Framework": "PYTORCH",
+            "FrameworkVersion": "2.0",
+        },
+        OutputConfig={
+            "S3OutputLocation": "s3://MyBucket/output",
+            "TargetPlatform": {
+                "Os": "LINUX",
+                "Arch": "X86_64",
+                "Accelerator": "NVIDIA",
+            },
+            "KmsKeyId": "1234abcd-12ab-34cd-56ef-1234567890ab",
+        },
+        VpcConfig={
+            "SecurityGroupIds": [
+                "sg-12345678901234567",
+            ],
+            "Subnets": [
+                "subnet-12345678901234567",
+            ],
+        },
+        StoppingCondition={
+            "MaxRuntimeInSeconds": 123,
+            "MaxWaitTimeInSeconds": 123,
+            "MaxPendingTimeInSeconds": 7200,
+        },
+        Tags=[
+            {"Key": "testkey", "Value": "testvalue"},
+        ],
+    )
+    resp = client.describe_compilation_job(CompilationJobName="testcompilationjob")
+    assert resp["CompilationJobName"] == "testcompilationjob"
+    assert (
+        resp["CompilationJobArn"]
+        == "arn:aws:sagemaker:ap-southeast-1:123456789012:compilation-job/testcompilationjob"
+    )
+    assert resp["CompilationJobStatus"] == "COMPLETED"
+    assert isinstance(resp["CompilationStartTime"], datetime.datetime)
+    assert isinstance(resp["CompilationEndTime"], datetime.datetime)
+    assert resp["StoppingCondition"] == {
+        "MaxRuntimeInSeconds": 123,
+        "MaxWaitTimeInSeconds": 123,
+        "MaxPendingTimeInSeconds": 7200,
+    }
+    assert resp["InferenceImage"] == "InferenceImage"
+    assert isinstance(resp["CreationTime"], datetime.datetime)
+    assert isinstance(resp["LastModifiedTime"], datetime.datetime)
+    assert resp["FailureReason"] == ""
+    assert resp["ModelArtifacts"] == {"S3ModelArtifacts": "s3://MyBucket/output"}
+    assert resp["ModelDigests"] == {
+        "ArtifactDigest": "786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce"
+    }
+    assert resp["RoleArn"] == f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole"
+    assert resp["InputConfig"] == {
+        "S3Uri": "s3://MyBucket/model.tar.gz",
+        "DataInputConfig": "{'input0':[1,3,224,224]}",
+        "Framework": "PYTORCH",
+        "FrameworkVersion": "2.0",
+    }
+    assert resp["OutputConfig"] == {
+        "S3OutputLocation": "s3://MyBucket/output",
+        "TargetPlatform": {"Os": "LINUX", "Arch": "X86_64", "Accelerator": "NVIDIA"},
+        "KmsKeyId": "1234abcd-12ab-34cd-56ef-1234567890ab",
+    }
+    assert resp["VpcConfig"] == {
+        "SecurityGroupIds": [
+            "sg-12345678901234567",
+        ],
+        "Subnets": [
+            "subnet-12345678901234567",
+        ],
+    }
+    assert resp["DerivedInformation"] == {
+        "DerivedDataInputConfig": "DerivedDataInputConfig"
+    }
+
+
+@mock_aws
+def test_describe_compilation_job_defaults():
+    client = boto3.client("sagemaker", region_name="ap-southeast-1")
+    resp = client.create_compilation_job(
+        CompilationJobName="testcompilationjob",
+        RoleArn=f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole",
+        ModelPackageVersionArn=f"arn:aws:sagemaker:ap-southeast-1:{ACCOUNT_ID}:model-package/FakeModelPackage",
+        OutputConfig={
+            "S3OutputLocation": "s3://MyBucket/output",
+            "TargetDevice": "lambda",
+            "KmsKeyId": "1234abcd-12ab-34cd-56ef-1234567890ab",
+        },
+        StoppingCondition={
+            "MaxRuntimeInSeconds": 123,
+            "MaxWaitTimeInSeconds": 123,
+            "MaxPendingTimeInSeconds": 7200,
+        },
+    )
+    assert (
+        resp["CompilationJobArn"]
+        == "arn:aws:sagemaker:ap-southeast-1:123456789012:compilation-job/testcompilationjob"
+    )
+
+    resp = client.describe_compilation_job(CompilationJobName="testcompilationjob")
+    assert resp["CompilationJobName"] == "testcompilationjob"
+    assert (
+        resp["CompilationJobArn"]
+        == "arn:aws:sagemaker:ap-southeast-1:123456789012:compilation-job/testcompilationjob"
+    )
+    assert resp["CompilationJobStatus"] == "COMPLETED"
+    assert isinstance(resp["CompilationStartTime"], datetime.datetime)
+    assert isinstance(resp["CompilationEndTime"], datetime.datetime)
+    assert resp["StoppingCondition"] == {
+        "MaxRuntimeInSeconds": 123,
+        "MaxWaitTimeInSeconds": 123,
+        "MaxPendingTimeInSeconds": 7200,
+    }
+    assert resp["InferenceImage"] == "InferenceImage"
+    assert (
+        resp["ModelPackageVersionArn"]
+        == f"arn:aws:sagemaker:ap-southeast-1:{ACCOUNT_ID}:model-package/FakeModelPackage"
+    )
+    assert isinstance(resp["CreationTime"], datetime.datetime)
+    assert isinstance(resp["LastModifiedTime"], datetime.datetime)
+    assert resp["FailureReason"] == ""
+    assert resp["ModelArtifacts"] == {"S3ModelArtifacts": "s3://MyBucket/output"}
+    assert resp["ModelDigests"] == {
+        "ArtifactDigest": "786a02f742015903c6c6fd852552d272912f4740e15847618a86e217f71f5419d25e1031afee585313896444934eb04b903a685b1448b755d56f701afe9be2ce"
+    }
+    assert resp["RoleArn"] == f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole"
+    assert resp["OutputConfig"] == {
+        "S3OutputLocation": "s3://MyBucket/output",
+        "TargetDevice": "lambda",
+        "KmsKeyId": "1234abcd-12ab-34cd-56ef-1234567890ab",
+    }
+    assert resp["DerivedInformation"] == {
+        "DerivedDataInputConfig": "DerivedDataInputConfig"
+    }
+
+
+@mock_aws
+def test_list_compilation_jobs():
+    client = boto3.client("sagemaker", region_name="us-east-2")
+    client.create_compilation_job(
+        CompilationJobName="testcompilationjob",
+        RoleArn=f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole",
+        ModelPackageVersionArn=f"arn:aws:sagemaker:ap-southeast-1:{ACCOUNT_ID}:model-package/FakeModelPackage",
+        OutputConfig={
+            "S3OutputLocation": "s3://MyBucket/output",
+            "TargetDevice": "lambda",
+            "KmsKeyId": "1234abcd-12ab-34cd-56ef-1234567890ab",
+        },
+        StoppingCondition={
+            "MaxRuntimeInSeconds": 123,
+            "MaxWaitTimeInSeconds": 123,
+            "MaxPendingTimeInSeconds": 7200,
+        },
+    )
+    client.create_compilation_job(
+        CompilationJobName="testcompilationjob2",
+        RoleArn=f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole",
+        InputConfig={
+            "S3Uri": "s3://MyBucket/model.tar.gz",
+            "DataInputConfig": "{'input0':[1,3,224,224]}",
+            "Framework": "PYTORCH",
+            "FrameworkVersion": "2.0",
+        },
+        OutputConfig={
+            "S3OutputLocation": "s3://MyBucket/output",
+            "TargetPlatform": {
+                "Os": "LINUX",
+                "Arch": "X86_64",
+                "Accelerator": "NVIDIA",
+            },
+            "KmsKeyId": "1234abcd-12ab-34cd-56ef-1234567890ab",
+        },
+        StoppingCondition={
+            "MaxRuntimeInSeconds": 123,
+            "MaxWaitTimeInSeconds": 123,
+            "MaxPendingTimeInSeconds": 7200,
+        },
+    )
+    resp = client.list_compilation_jobs()
+    assert len(resp["CompilationJobSummaries"]) == 2
+    assert (
+        resp["CompilationJobSummaries"][0]["CompilationJobName"] == "testcompilationjob"
+    )
+    assert (
+        resp["CompilationJobSummaries"][0]["CompilationJobArn"]
+        == "arn:aws:sagemaker:us-east-2:123456789012:compilation-job/testcompilationjob"
+    )
+    assert isinstance(
+        resp["CompilationJobSummaries"][0]["CreationTime"], datetime.datetime
+    )
+    assert isinstance(
+        resp["CompilationJobSummaries"][0]["CompilationStartTime"], datetime.datetime
+    )
+    assert isinstance(
+        resp["CompilationJobSummaries"][0]["CompilationEndTime"], datetime.datetime
+    )
+    assert resp["CompilationJobSummaries"][0]["CompilationTargetDevice"] == "lambda"
+    assert isinstance(
+        resp["CompilationJobSummaries"][0]["LastModifiedTime"], datetime.datetime
+    )
+    assert resp["CompilationJobSummaries"][0]["CompilationJobStatus"] == "COMPLETED"
+    assert (
+        resp["CompilationJobSummaries"][1]["CompilationJobName"]
+        == "testcompilationjob2"
+    )
+    assert (
+        resp["CompilationJobSummaries"][1]["CompilationJobArn"]
+        == "arn:aws:sagemaker:us-east-2:123456789012:compilation-job/testcompilationjob2"
+    )
+    assert isinstance(
+        resp["CompilationJobSummaries"][1]["CreationTime"], datetime.datetime
+    )
+    assert isinstance(
+        resp["CompilationJobSummaries"][1]["CompilationStartTime"], datetime.datetime
+    )
+    assert isinstance(
+        resp["CompilationJobSummaries"][1]["CompilationEndTime"], datetime.datetime
+    )
+    assert resp["CompilationJobSummaries"][1]["CompilationTargetPlatformOs"] == "LINUX"
+    assert (
+        resp["CompilationJobSummaries"][1]["CompilationTargetPlatformArch"] == "X86_64"
+    )
+    assert (
+        resp["CompilationJobSummaries"][1]["CompilationTargetPlatformAccelerator"]
+        == "NVIDIA"
+    )
+    assert isinstance(
+        resp["CompilationJobSummaries"][1]["LastModifiedTime"], datetime.datetime
+    )
+    assert resp["CompilationJobSummaries"][1]["CompilationJobStatus"] == "COMPLETED"
+
+
+@mock_aws
+def test_list_compilation_jobs_filters():
+    client = boto3.client("sagemaker", region_name="us-east-2")
+    client.create_compilation_job(
+        CompilationJobName="testcompilationjob",
+        RoleArn=f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole",
+        ModelPackageVersionArn=f"arn:aws:sagemaker:ap-southeast-1:{ACCOUNT_ID}:model-package/FakeModelPackage",
+        OutputConfig={
+            "S3OutputLocation": "s3://MyBucket/output",
+            "TargetDevice": "lambda",
+            "KmsKeyId": "1234abcd-12ab-34cd-56ef-1234567890ab",
+        },
+        StoppingCondition={
+            "MaxRuntimeInSeconds": 123,
+            "MaxWaitTimeInSeconds": 123,
+            "MaxPendingTimeInSeconds": 7200,
+        },
+    )
+    client.create_compilation_job(
+        CompilationJobName="testmycompilationjob",
+        RoleArn=f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole",
+        ModelPackageVersionArn=f"arn:aws:sagemaker:ap-southeast-1:{ACCOUNT_ID}:model-package/FakeModelPackage",
+        OutputConfig={
+            "S3OutputLocation": "s3://MyBucket/output",
+            "TargetDevice": "lambda",
+            "KmsKeyId": "1234abcd-12ab-34cd-56ef-1234567890ab",
+        },
+        StoppingCondition={
+            "MaxRuntimeInSeconds": 123,
+            "MaxWaitTimeInSeconds": 123,
+            "MaxPendingTimeInSeconds": 7200,
+        },
+    )
+    client.create_compilation_job(
+        CompilationJobName="testcompilationjob2",
+        RoleArn=f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole",
+        InputConfig={
+            "S3Uri": "s3://MyBucket/model.tar.gz",
+            "DataInputConfig": "{'input0':[1,3,224,224]}",
+            "Framework": "PYTORCH",
+            "FrameworkVersion": "2.0",
+        },
+        OutputConfig={
+            "S3OutputLocation": "s3://MyBucket/output",
+            "TargetPlatform": {
+                "Os": "LINUX",
+                "Arch": "X86_64",
+                "Accelerator": "NVIDIA",
+            },
+            "KmsKeyId": "1234abcd-12ab-34cd-56ef-1234567890ab",
+        },
+        StoppingCondition={
+            "MaxRuntimeInSeconds": 123,
+            "MaxWaitTimeInSeconds": 123,
+            "MaxPendingTimeInSeconds": 7200,
+        },
+    )
+    resp = client.list_compilation_jobs(
+        CreationTimeAfter=datetime.datetime(2015, 1, 1),
+        CreationTimeBefore=datetime.datetime(2099, 1, 1),
+        LastModifiedTimeAfter=datetime.datetime(2015, 1, 1),
+        LastModifiedTimeBefore=datetime.datetime(2099, 1, 1),
+        NameContains="testcompilationjob",
+        StatusEquals="COMPLETED",
+        SortBy="Name",
+        SortOrder="Ascending",
+    )
+    assert len(resp["CompilationJobSummaries"]) == 2
+    assert (
+        resp["CompilationJobSummaries"][0]["CompilationJobName"] == "testcompilationjob"
+    )
+    assert (
+        resp["CompilationJobSummaries"][0]["CompilationJobArn"]
+        == "arn:aws:sagemaker:us-east-2:123456789012:compilation-job/testcompilationjob"
+    )
+    assert isinstance(
+        resp["CompilationJobSummaries"][0]["CreationTime"], datetime.datetime
+    )
+    assert isinstance(
+        resp["CompilationJobSummaries"][0]["CompilationStartTime"], datetime.datetime
+    )
+    assert isinstance(
+        resp["CompilationJobSummaries"][0]["CompilationEndTime"], datetime.datetime
+    )
+    assert resp["CompilationJobSummaries"][0]["CompilationTargetDevice"] == "lambda"
+    assert isinstance(
+        resp["CompilationJobSummaries"][0]["LastModifiedTime"], datetime.datetime
+    )
+    assert resp["CompilationJobSummaries"][0]["CompilationJobStatus"] == "COMPLETED"
+    assert (
+        resp["CompilationJobSummaries"][1]["CompilationJobName"]
+        == "testcompilationjob2"
+    )
+    assert (
+        resp["CompilationJobSummaries"][1]["CompilationJobArn"]
+        == "arn:aws:sagemaker:us-east-2:123456789012:compilation-job/testcompilationjob2"
+    )
+    assert isinstance(
+        resp["CompilationJobSummaries"][1]["CreationTime"], datetime.datetime
+    )
+    assert isinstance(
+        resp["CompilationJobSummaries"][1]["CompilationStartTime"], datetime.datetime
+    )
+    assert isinstance(
+        resp["CompilationJobSummaries"][1]["CompilationEndTime"], datetime.datetime
+    )
+    assert resp["CompilationJobSummaries"][1]["CompilationTargetPlatformOs"] == "LINUX"
+    assert (
+        resp["CompilationJobSummaries"][1]["CompilationTargetPlatformArch"] == "X86_64"
+    )
+    assert (
+        resp["CompilationJobSummaries"][1]["CompilationTargetPlatformAccelerator"]
+        == "NVIDIA"
+    )
+    assert isinstance(
+        resp["CompilationJobSummaries"][1]["LastModifiedTime"], datetime.datetime
+    )
+    assert resp["CompilationJobSummaries"][1]["CompilationJobStatus"] == "COMPLETED"
+
+
+@mock_aws
+def test_delete_compilation_job():
+    client = boto3.client("sagemaker", region_name="ap-southeast-1")
+    client.create_compilation_job(
+        CompilationJobName="testcompilationjob",
+        RoleArn=f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole",
+        ModelPackageVersionArn=f"arn:aws:sagemaker:ap-southeast-1:{ACCOUNT_ID}:model-package/FakeModelPackage",
+        OutputConfig={
+            "S3OutputLocation": "s3://MyBucket/output",
+            "TargetDevice": "lambda",
+            "KmsKeyId": "1234abcd-12ab-34cd-56ef-1234567890ab",
+        },
+        StoppingCondition={
+            "MaxRuntimeInSeconds": 123,
+            "MaxWaitTimeInSeconds": 123,
+            "MaxPendingTimeInSeconds": 7200,
+        },
+    )
+    client.create_compilation_job(
+        CompilationJobName="testcompilationjob2",
+        RoleArn=f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole",
+        ModelPackageVersionArn=f"arn:aws:sagemaker:ap-southeast-1:{ACCOUNT_ID}:model-package/FakeModelPackage",
+        OutputConfig={
+            "S3OutputLocation": "s3://MyBucket/output",
+            "TargetDevice": "lambda",
+            "KmsKeyId": "1234abcd-12ab-34cd-56ef-1234567890ab",
+        },
+        StoppingCondition={
+            "MaxRuntimeInSeconds": 123,
+            "MaxWaitTimeInSeconds": 123,
+            "MaxPendingTimeInSeconds": 7200,
+        },
+    )
+    client.delete_compilation_job(CompilationJobName="testcompilationjob")
+    resp = client.list_compilation_jobs()
+    assert len(resp["CompilationJobSummaries"]) == 1
+    assert (
+        resp["CompilationJobSummaries"][0]["CompilationJobName"]
+        == "testcompilationjob2"
+    )
+
+
+@mock_aws
+def test_tag_compilation_job():
+    client = boto3.client("sagemaker", region_name="us-east-1")
+    client.create_compilation_job(
+        CompilationJobName="testcompilationjob",
+        RoleArn=f"arn:aws:iam::{ACCOUNT_ID}:role/FakeRole",
+        ModelPackageVersionArn=f"arn:aws:sagemaker:us-east-1:{ACCOUNT_ID}:model-package/FakeModelPackage",
+        OutputConfig={
+            "S3OutputLocation": "s3://MyBucket/output",
+            "TargetDevice": "lambda",
+            "KmsKeyId": "1234abcd-12ab-34cd-56ef-1234567890ab",
+        },
+        StoppingCondition={
+            "MaxRuntimeInSeconds": 123,
+            "MaxWaitTimeInSeconds": 123,
+            "MaxPendingTimeInSeconds": 7200,
+        },
+        Tags=[
+            {"Key": "testkey", "Value": "testvalue"},
+        ],
+    )
+    client.add_tags(
+        ResourceArn="arn:aws:sagemaker:us-east-1:123456789012:compilation-job/testcompilationjob",
+        Tags=[{"Key": "testkey2", "Value": "testvalue2"}],
+    )
+    resp = client.list_tags(
+        ResourceArn="arn:aws:sagemaker:us-east-1:123456789012:compilation-job/testcompilationjob"
+    )
+    assert resp["Tags"] == [
+        {"Key": "testkey", "Value": "testvalue"},
+        {"Key": "testkey2", "Value": "testvalue2"},
+    ]