Repository: getmoto/moto
Pull request #7889 (2024-07-23 20:54:35)

Feature: Sagemaker Domains
Adds support for Sagemaker Domains create, describe, list, and delete.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/7889?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 `96.20253%` with `3 lines` in your changes missing coverage. Please review.
> Project coverage is 94.35%. Comparing base [(`ba76bca`)](https://app.codecov.io/gh/getmoto/moto/commit/ba76bca5ce480efcea58462e93ebacd714e58ce9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`2524701`)](https://app.codecov.io/gh/getmoto/moto/commit/252470120aa6896a3d3abd90c8a21e8291dcb614?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).
> Report is 12 commits behind head on master.

| [Files](https://app.codecov.io/gh/getmoto/moto/pull/7889?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/7889?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) | 93.87% | [3 Missing :warning: ](https://app.codecov.io/gh/getmoto/moto/pull/7889?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    #7889    +/-   ##
========================================
  Coverage   94.34%   94.35%            
========================================
  Files        1113     1113            
  Lines       94417    94720   +303     
========================================
+ Hits        89081    89375   +294     
- Misses       5336     5345     +9     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/7889/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/7889/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `29.20% <12.65%> (-0.05%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/7889/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `94.32% <96.20%> (+<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/7889?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/moto/rds/models.py b/moto/rds/models.py
index f880b958f..3e14fa973 100644
--- a/moto/rds/models.py
+++ b/moto/rds/models.py
@@ -69,7 +69,31 @@ def find_cluster(cluster_arn: str) -> "DBCluster":
     return rds_backends[account][region].describe_db_clusters(cluster_arn)[0]
 
 
-class RDSBaseModel(BaseModel):
+class TaggingMixin:
+    _tags: List[Dict[str, str]] = []
+
+    @property
+    def tags(self) -> List[Dict[str, str]]:
+        return self._tags
+
+    @tags.setter
+    def tags(self, value: List[Dict[str, str]]) -> None:
+        self._tags = value
+
+    def get_tags(self) -> List[Dict[str, str]]:
+        return self.tags
+
+    def add_tags(self, tags: List[Dict[str, str]]) -> List[Dict[str, str]]:
+        new_keys = [tag_set["Key"] for tag_set in tags]
+        self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in new_keys]
+        self.tags.extend(tags)
+        return self.tags
+
+    def remove_tags(self, tag_keys: List[str]) -> None:
+        self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in tag_keys]
+
+
+class RDSBaseModel(TaggingMixin, BaseModel):
     resource_type: str
 
     def __init__(self, backend: "RDSBackend"):
@@ -545,18 +569,6 @@ class DBCluster(RDSBaseModel):
             "postgres": {"gp2": 20, "io1": 100, "standard": 5},
         }[engine][storage_type]
 
-    def get_tags(self) -> List[Dict[str, str]]:
-        return self.tags
-
-    def add_tags(self, tags: List[Dict[str, str]]) -> List[Dict[str, str]]:
-        new_keys = [tag_set["Key"] for tag_set in tags]
-        self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in new_keys]
-        self.tags.extend(tags)
-        return self.tags
-
-    def remove_tags(self, tag_keys: List[str]) -> None:
-        self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in tag_keys]
-
 
 class DBClusterSnapshot(RDSBaseModel):
     resource_type = "cluster-snapshot"
@@ -633,18 +645,6 @@ class DBClusterSnapshot(RDSBaseModel):
         )
         return template.render(snapshot=self, cluster=self.cluster)
 
-    def get_tags(self) -> List[Dict[str, str]]:
-        return self.tags
-
-    def add_tags(self, tags: List[Dict[str, str]]) -> List[Dict[str, str]]:
-        new_keys = [tag_set["Key"] for tag_set in tags]
-        self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in new_keys]
-        self.tags.extend(tags)
-        return self.tags
-
-    def remove_tags(self, tag_keys: List[str]) -> None:
-        self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in tag_keys]
-
 
 class DBInstance(CloudFormationModel, RDSBaseModel):
     SUPPORTED_FILTERS = {
@@ -1115,82 +1115,6 @@ class DBInstance(CloudFormationModel, RDSBaseModel):
             database = rds_backend.create_db_instance(db_kwargs)
         return database
 
-    def to_json(self) -> str:
-        template = Template(
-            """{
-        "AllocatedStorage": 10,
-        "AutoMinorVersionUpgrade": "{{ database.auto_minor_version_upgrade }}",
-        "AvailabilityZone": "{{ database.availability_zone }}",
-        "BackupRetentionPeriod": "{{ database.backup_retention_period }}",
-        "CharacterSetName": {%- if database.character_set_name -%}{{ database.character_set_name }}{%- else %} null{%- endif -%},
-        "DBInstanceClass": "{{ database.db_instance_class }}",
-        {%- if database.db_cluster_identifier -%}"DBClusterIdentifier": "{{ database.db_cluster_identifier }}",{%- endif -%}
-        "DBInstanceIdentifier": "{{ database.db_instance_identifier }}",
-        "DBInstanceStatus": "{{ database.status }}",
-        "DBName": {%- if database.db_name -%}"{{ database.db_name }}"{%- else %} null{%- endif -%},
-        {% if database.db_parameter_group_name -%}"DBParameterGroups": {
-            "DBParameterGroup": {
-            "ParameterApplyStatus": "in-sync",
-            "DBParameterGroupName": "{{ database.db_parameter_group_name }}"
-          }
-        },{%- endif %}
-        "DBSecurityGroups": [
-          {% for security_group in database.security_groups -%}{%- if loop.index != 1 -%},{%- endif -%}
-          {"DBSecurityGroup": {
-            "Status": "active",
-            "DBSecurityGroupName": "{{ security_group }}"
-          }}{% endfor %}
-        ],
-        {%- if database.db_subnet_group -%}{{ database.db_subnet_group.to_json() }},{%- endif %}
-        "Engine": "{{ database.engine }}",
-        "EngineVersion": "{{ database.engine_version }}",
-        "LatestRestorableTime": null,
-        "LicenseModel": "{{ database.license_model }}",
-        "MasterUsername": "{{ database.master_username }}",
-        "MultiAZ": "{{ database.multi_az }}",{% if database.option_group_name %}
-        "OptionGroupMemberships": [{
-          "OptionGroupMembership": {
-            "OptionGroupName": "{{ database.option_group_name }}",
-            "Status": "in-sync"
-          }
-        }],{%- endif %}
-        "PendingModifiedValues": { "MasterUserPassword": "****" },
-        "PreferredBackupWindow": "{{ database.preferred_backup_window }}",
-        "PreferredMaintenanceWindow": "{{ database.preferred_maintenance_window }}",
-        "PubliclyAccessible": "{{ database.publicly_accessible }}",
-        "CopyTagsToSnapshot": "{{ database.copy_tags_to_snapshot }}",
-        "AllocatedStorage": "{{ database.allocated_storage }}",
-        "Endpoint": {
-            "Address": "{{ database.address }}",
-            "Port": "{{ database.port }}"
-        },
-        "InstanceCreateTime": "{{ database.instance_create_time }}",
-        "Iops": null,
-        "ReadReplicaDBInstanceIdentifiers": [{%- for replica in database.replicas -%}
-            {%- if not loop.first -%},{%- endif -%}
-            "{{ replica }}"
-        {%- endfor -%}
-        ],
-        {%- if database.source_db_identifier -%}
-        "ReadReplicaSourceDBInstanceIdentifier": "{{ database.source_db_identifier }}",
-        {%- else -%}
-        "ReadReplicaSourceDBInstanceIdentifier": null,
-        {%- endif -%}
-        "SecondaryAvailabilityZone": null,
-        "StatusInfos": null,
-        "VpcSecurityGroups": [
-            {% for vpc_security_group_id in database.vpc_security_group_ids %}
-            {
-                "Status": "active",
-                "VpcSecurityGroupId": "{{ vpc_security_group_id }}"
-            }
-            {% endfor %}
-        ],
-        "DBInstanceArn": "{{ database.db_instance_arn }}"
-      }"""
-        )
-        return template.render(database=self)
-
     def get_tags(self) -> List[Dict[str, str]]:
         return self.tags
 
@@ -1208,7 +1132,7 @@ class DBInstance(CloudFormationModel, RDSBaseModel):
         backend.delete_db_instance(self.db_instance_identifier)  # type: ignore[arg-type]
 
 
-class DBInstanceSnapshot(RDSBaseModel):
+class DBSnapshot(RDSBaseModel):
     resource_type = "snapshot"
     SUPPORTED_FILTERS = {
         "db-instance-id": FilterDef(
@@ -1297,23 +1221,11 @@ class DBInstanceSnapshot(RDSBaseModel):
         )
         return template.render(snapshot=self, database=self.database)
 
-    def get_tags(self) -> List[Dict[str, str]]:
-        return self.tags
-
-    def add_tags(self, tags: List[Dict[str, str]]) -> List[Dict[str, str]]:
-        new_keys = [tag_set["Key"] for tag_set in tags]
-        self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in new_keys]
-        self.tags.extend(tags)
-        return self.tags
-
-    def remove_tags(self, tag_keys: List[str]) -> None:
-        self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in tag_keys]
-
 
 class ExportTask(BaseModel):
     def __init__(
         self,
-        snapshot: Union[DBInstanceSnapshot, DBClusterSnapshot],
+        snapshot: Union[DBSnapshot, DBClusterSnapshot],
         kwargs: Dict[str, Any],
     ):
         self.snapshot = snapshot
@@ -1328,9 +1240,7 @@ class ExportTask(BaseModel):
 
         self.status = "complete"
         self.created_at = iso_8601_datetime_with_milliseconds()
-        self.source_type = (
-            "SNAPSHOT" if type(snapshot) is DBInstanceSnapshot else "CLUSTER"
-        )
+        self.source_type = "SNAPSHOT" if type(snapshot) is DBSnapshot else "CLUSTER"
 
     def to_xml(self) -> str:
         template = Template(
@@ -1415,18 +1325,6 @@ class EventSubscription(RDSBaseModel):
         )
         return template.render(subscription=self)
 
-    def get_tags(self) -> List[Dict[str, str]]:
-        return self.tags
-
-    def add_tags(self, tags: List[Dict[str, str]]) -> List[Dict[str, str]]:
-        new_keys = [tag_set["Key"] for tag_set in tags]
-        self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in new_keys]
-        self.tags.extend(tags)
-        return self.tags
-
-    def remove_tags(self, tag_keys: List[str]) -> None:
-        self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in tag_keys]
-
 
 class DBSecurityGroup(CloudFormationModel, RDSBaseModel):
     resource_type = "secgrp"
@@ -1481,23 +1379,6 @@ class DBSecurityGroup(CloudFormationModel, RDSBaseModel):
         )
         return template.render(security_group=self)
 
-    def to_json(self) -> str:
-        template = Template(
-            """{
-            "DBSecurityGroupDescription": "{{ security_group.description }}",
-            "DBSecurityGroupName": "{{ security_group.group_name }}",
-            "EC2SecurityGroups": {{ security_group.ec2_security_groups }},
-            "IPRanges": [{%- for ip in security_group.ip_ranges -%}
-                         {%- if loop.index != 1 -%},{%- endif -%}
-                         "{{ ip }}"
-                         {%- endfor -%}
-                        ],
-            "OwnerId": "{{ security_group.owner_id }}",
-            "VpcId": "{{ security_group.vpc_id }}"
-        }"""
-        )
-        return template.render(security_group=self)
-
     def authorize_cidr(self, cidr_ip: str) -> None:
         self.ip_ranges.append(cidr_ip)
 
@@ -1545,18 +1426,6 @@ class DBSecurityGroup(CloudFormationModel, RDSBaseModel):
                     security_group.authorize_security_group(subnet)  # type: ignore[arg-type]
         return security_group
 
-    def get_tags(self) -> List[Dict[str, str]]:
-        return self.tags
-
-    def add_tags(self, tags: List[Dict[str, str]]) -> List[Dict[str, str]]:
-        new_keys = [tag_set["Key"] for tag_set in tags]
-        self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in new_keys]
-        self.tags.extend(tags)
-        return self.tags
-
-    def remove_tags(self, tag_keys: List[str]) -> None:
-        self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in tag_keys]
-
     def delete(self, account_id: str, region_name: str) -> None:
         backend = rds_backends[account_id][region_name]
         backend.delete_security_group(self.group_name)
@@ -1609,29 +1478,6 @@ class DBSubnetGroup(CloudFormationModel, RDSBaseModel):
         )
         return template.render(subnet_group=self)
 
-    def to_json(self) -> str:
-        template = Template(
-            """"DBSubnetGroup": {
-                "VpcId": "{{ subnet_group.vpc_id }}",
-                "SubnetGroupStatus": "{{ subnet_group.status }}",
-                "DBSubnetGroupDescription": "{{ subnet_group.description }}",
-                "DBSubnetGroupName": "{{ subnet_group.subnet_name }}",
-                "Subnets": {
-                  "Subnet": [
-                    {% for subnet in subnet_group.subnets %}{
-                      "SubnetStatus": "Active",
-                      "SubnetIdentifier": "{{ subnet.id }}",
-                      "SubnetAvailabilityZone": {
-                        "Name": "{{ subnet.availability_zone }}",
-                        "ProvisionedIopsCapable": "false"
-                      }
-                    }{%- if not loop.last -%},{%- endif -%}{% endfor %}
-                  ]
-                }
-            }"""
-        )
-        return template.render(subnet_group=self)
-
     @staticmethod
     def cloudformation_name_type() -> str:
         return "DBSubnetGroupName"
@@ -1667,18 +1513,6 @@ class DBSubnetGroup(CloudFormationModel, RDSBaseModel):
         )
         return subnet_group
 
-    def get_tags(self) -> List[Dict[str, str]]:
-        return self.tags
-
-    def add_tags(self, tags: List[Dict[str, str]]) -> List[Dict[str, str]]:
-        new_keys = [tag_set["Key"] for tag_set in tags]
-        self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in new_keys]
-        self.tags.extend(tags)
-        return self.tags
-
-    def remove_tags(self, tag_keys: List[str]) -> None:
-        self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in tag_keys]
-
     def delete(self, account_id: str, region_name: str) -> None:
         backend = rds_backends[account_id][region_name]
         backend.delete_subnet_group(self.subnet_name)
@@ -1750,18 +1584,6 @@ class DBProxy(RDSBaseModel):
     def name(self) -> str:
         return self.db_proxy_name
 
-    def get_tags(self) -> List[Dict[str, str]]:
-        return self.tags
-
-    def add_tags(self, tags: List[Dict[str, str]]) -> List[Dict[str, str]]:
-        new_keys = [tag_set["Key"] for tag_set in tags]
-        self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in new_keys]
-        self.tags.extend(tags)
-        return self.tags
-
-    def remove_tags(self, tag_keys: List[str]) -> None:
-        self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in tag_keys]
-
     def to_xml(self) -> str:
         template = Template(
             """
@@ -1816,7 +1638,7 @@ class RDSBackend(BaseBackend):
         self.clusters: Dict[str, DBCluster] = OrderedDict()
         self.global_clusters: Dict[str, GlobalCluster] = OrderedDict()
         self.databases: Dict[str, DBInstance] = OrderedDict()
-        self.database_snapshots: Dict[str, DBInstanceSnapshot] = OrderedDict()
+        self.database_snapshots: Dict[str, DBSnapshot] = OrderedDict()
         self.cluster_snapshots: Dict[str, DBClusterSnapshot] = OrderedDict()
         self.export_tasks: Dict[str, ExportTask] = OrderedDict()
         self.event_subscriptions: Dict[str, EventSubscription] = OrderedDict()
@@ -1877,7 +1699,7 @@ class RDSBackend(BaseBackend):
         self,
         db_instance_identifier: str,
         db_snapshot_identifier: str,
-    ) -> DBInstanceSnapshot:
+    ) -> DBSnapshot:
         return self.create_db_snapshot(
             db_instance_identifier, db_snapshot_identifier, snapshot_type="automated"
         )
@@ -1888,7 +1710,7 @@ class RDSBackend(BaseBackend):
         db_snapshot_identifier: str,
         snapshot_type: str = "manual",
         tags: Optional[List[Dict[str, str]]] = None,
-    ) -> DBInstanceSnapshot:
+    ) -> DBSnapshot:
         if isinstance(db_instance, str):
             database = self.databases.get(db_instance)
             if not database:
@@ -1906,7 +1728,7 @@ class RDSBackend(BaseBackend):
             tags = list()
         if database.copy_tags_to_snapshot and not tags:
             tags = database.get_tags()
-        snapshot = DBInstanceSnapshot(
+        snapshot = DBSnapshot(
             self, database, db_snapshot_identifier, snapshot_type, tags
         )
         self.database_snapshots[db_snapshot_identifier] = snapshot
@@ -1917,7 +1739,7 @@ class RDSBackend(BaseBackend):
         source_snapshot_identifier: str,
         target_snapshot_identifier: str,
         tags: Optional[List[Dict[str, str]]] = None,
-    ) -> DBInstanceSnapshot:
+    ) -> DBSnapshot:
         if source_snapshot_identifier not in self.database_snapshots:
             raise DBSnapshotNotFoundError(source_snapshot_identifier)
 
@@ -1932,7 +1754,7 @@ class RDSBackend(BaseBackend):
             tags=tags,
         )
 
-    def delete_db_snapshot(self, db_snapshot_identifier: str) -> DBInstanceSnapshot:
+    def delete_db_snapshot(self, db_snapshot_identifier: str) -> DBSnapshot:
         if db_snapshot_identifier not in self.database_snapshots:
             raise DBSnapshotNotFoundError(db_snapshot_identifier)
 
@@ -1981,7 +1803,7 @@ class RDSBackend(BaseBackend):
         db_instance_identifier: Optional[str],
         db_snapshot_identifier: str,
         filters: Optional[Dict[str, Any]] = None,
-    ) -> List[DBInstanceSnapshot]:
+    ) -> List[DBSnapshot]:
         snapshots = self.database_snapshots
         if db_instance_identifier:
             filters = merge_filters(
@@ -1992,7 +1814,7 @@ class RDSBackend(BaseBackend):
                 filters, {"db-snapshot-id": [db_snapshot_identifier]}
             )
         if filters:
-            snapshots = self._filter_resources(snapshots, filters, DBInstanceSnapshot)
+            snapshots = self._filter_resources(snapshots, filters, DBSnapshot)
         if db_snapshot_identifier and not snapshots and not db_instance_identifier:
             raise DBSnapshotNotFoundError(db_snapshot_identifier)
         return list(snapshots.values())
@@ -2714,9 +2536,9 @@ class RDSBackend(BaseBackend):
             raise DBClusterSnapshotNotFoundError(snapshot_id)
 
         if snapshot_type == "snapshot":
-            snapshot: Union[DBInstanceSnapshot, DBClusterSnapshot] = (
-                self.database_snapshots[snapshot_id]
-            )
+            snapshot: Union[DBSnapshot, DBClusterSnapshot] = self.database_snapshots[
+                snapshot_id
+            ]
         else:
             snapshot = self.cluster_snapshots[snapshot_id]
 
@@ -3083,10 +2905,9 @@ class RDSBackend(BaseBackend):
             if attribute["AttributeName"] == attribute_name:
                 attribute_present = True
                 if values_to_add:
-                    attribute["AttributeValues"] = (
-                        values_to_add["AttributeValue"].values()
-                        + attribute["AttributeValues"]
-                    )
+                    attribute["AttributeValues"] = list(
+                        attribute["AttributeValues"]
+                    ) + list(values_to_add["AttributeValue"].values())
                 if values_to_remove:
                     attribute["AttributeValues"] = [
                         i
@@ -3127,10 +2948,9 @@ class RDSBackend(BaseBackend):
             if attribute["AttributeName"] == attribute_name:
                 attribute_present = True
                 if values_to_add:
-                    attribute["AttributeValues"] = (
-                        values_to_add["AttributeValue"].values()
-                        + attribute["AttributeValues"]
-                    )
+                    attribute["AttributeValues"] = list(
+                        attribute["AttributeValues"]
+                    ) + list(values_to_add["AttributeValue"].values())
                 if values_to_remove:
                     attribute["AttributeValues"] = [
                         i
@@ -3221,21 +3041,6 @@ class OptionGroup(RDSBaseModel):
     def name(self) -> str:
         return self._name
 
-    def to_json(self) -> str:
-        template = Template(
-            """{
-    "VpcId": null,
-    "MajorEngineVersion": "{{ option_group.major_engine_version }}",
-    "OptionGroupDescription": "{{ option_group.description }}",
-    "AllowsVpcAndNonVpcInstanceMemberships": "{{ option_group.vpc_and_non_vpc_instance_memberships }}",
-    "EngineName": "{{ option_group.engine_name }}",
-    "Options": [],
-    "OptionGroupName": "{{ option_group.name }},
-    "OptionGroupArn": "{{ option_group.arn }}
-}"""
-        )
-        return template.render(option_group=self)
-
     def to_xml(self) -> str:
         template = Template(
             """<OptionGroup>
@@ -3266,18 +3071,6 @@ class OptionGroup(RDSBaseModel):
         # error
         return
 
-    def get_tags(self) -> List[Dict[str, str]]:
-        return self.tags
-
-    def add_tags(self, tags: List[Dict[str, str]]) -> List[Dict[str, str]]:
-        new_keys = [tag_set["Key"] for tag_set in tags]
-        self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in new_keys]
-        self.tags.extend(tags)
-        return self.tags
-
-    def remove_tags(self, tag_keys: List[str]) -> None:
-        self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in tag_keys]
-
 
 class DBParameterGroup(CloudFormationModel, RDSBaseModel):
     resource_type = "pg"
@@ -3312,18 +3105,6 @@ class DBParameterGroup(CloudFormationModel, RDSBaseModel):
         )
         return template.render(param_group=self)
 
-    def get_tags(self) -> List[Dict[str, Any]]:
-        return self.tags
-
-    def add_tags(self, tags: List[Dict[str, str]]) -> List[Dict[str, Any]]:
-        new_keys = [tag_set["Key"] for tag_set in tags]
-        self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in new_keys]
-        self.tags.extend(tags)
-        return self.tags
-
-    def remove_tags(self, tag_keys: List[str]) -> None:
-        self.tags = [tag_set for tag_set in self.tags if tag_set["Key"] not in tag_keys]
-
     def update_parameters(self, new_parameters: Iterable[Dict[str, Any]]) -> None:
         for new_parameter in new_parameters:
             parameter = self.parameters[new_parameter["ParameterName"]]
diff --git a/moto/sagemaker/models.py b/moto/sagemaker/models.py
index f15a164d9..ec9816958 100644
--- a/moto/sagemaker/models.py
+++ b/moto/sagemaker/models.py
@@ -109,6 +109,12 @@ PAGINATION_MODEL = {
         "limit_default": 100,
         "unique_attribute": "arn",
     },
+    "list_domains": {
+        "input_token": "next_token",
+        "limit_key": "max_results",
+        "limit_default": 100,
+        "unique_attribute": "arn",
+    },
 }
 
 METRIC_INFO_TYPE = Dict[str, Union[str, int, float, datetime]]
@@ -1992,6 +1998,101 @@ class AutoMLJob(BaseObject):
         }
 
 
+class Domain(BaseObject):
+    def __init__(
+        self,
+        domain_name: str,
+        auth_mode: str,
+        default_user_settings: Dict[str, Any],
+        subnet_ids: List[str],
+        vpc_id: str,
+        account_id: str,
+        region_name: str,
+        domain_settings: Optional[Dict[str, Any]],
+        tags: Optional[List[Dict[str, str]]],
+        app_network_access_type: Optional[str],
+        home_efs_file_system_kms_key_id: Optional[str],
+        kms_key_id: Optional[str],
+        app_security_group_management: Optional[str],
+        default_space_settings: Optional[Dict[str, Any]],
+    ):
+        self.domain_name = domain_name
+        if domain_name in sagemaker_backends[account_id][region_name].domains:
+            raise ResourceInUseException(
+                message=f"Resource Already Exists: Domain with name {domain_name} already exists. Choose a different name."
+            )
+        self.auth_mode = auth_mode
+        self.default_user_settings = default_user_settings
+        self.subnet_ids = subnet_ids
+        self.vpc_id = vpc_id
+        self.account_id = account_id
+        self.region_name = region_name
+        self.domain_settings = domain_settings
+        self.tags = tags
+        self.app_network_access_type = (
+            app_network_access_type if app_network_access_type else "PublicInternetOnly"
+        )
+        self.home_efs_file_system_kms_key_id = (
+            home_efs_file_system_kms_key_id
+            if home_efs_file_system_kms_key_id
+            else kms_key_id
+        )
+        self.kms_key_id = kms_key_id
+        self.app_security_group_management = app_security_group_management
+        self.default_space_settings = default_space_settings
+
+        self.id = f"d-{domain_name}"
+        self.arn = arn_formatter("domain", self.id, account_id, region_name)
+        self.home_efs_file_system_id = f"{domain_name}-efs-id"
+        self.single_sign_on_managed_application_instance_id = f"{domain_name}-sso-id"
+        self.single_sign_on_managed_application_arn = arn_formatter(
+            "sso", f"application/{domain_name}/apl-{domain_name}", account_id, ""
+        )
+        self.status = "InService"
+        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.security_group_id_for_domain_boundary = f"sg-{domain_name}"
+        self.url = f"{domain_name}.{region_name}.sagemaker.test.com"
+
+    def describe(self) -> Dict[str, Any]:
+        return {
+            "DomainArn": self.arn,
+            "DomainId": self.id,
+            "DomainName": self.domain_name,
+            "HomeEfsFileSystemId": self.home_efs_file_system_id,
+            "SingleSignOnManagedApplicationInstanceId": self.single_sign_on_managed_application_instance_id,
+            "SingleSignOnApplicationArn": self.single_sign_on_managed_application_arn,
+            "Status": self.status,
+            "CreationTime": self.creation_time,
+            "LastModifiedTime": self.last_modified_time,
+            "FailureReason": self.failure_reason,
+            "SecurityGroupIdForDomainBoundary": self.security_group_id_for_domain_boundary,
+            "AuthMode": self.auth_mode,
+            "DefaultUserSettings": self.default_user_settings,
+            "DomainSetting": self.domain_settings,
+            "AppNetworkAccessType": self.app_network_access_type,
+            "HomeEfsFileSystemKmsKeyId": self.home_efs_file_system_kms_key_id,
+            "SubnetIds": self.subnet_ids,
+            "Url": self.url,
+            "VpcId": self.vpc_id,
+            "KmsKeyId": self.kms_key_id,
+            "AppSecurityGroupManagement": self.app_security_group_management,
+            "DefaultSpaceSettings": self.default_space_settings,
+        }
+
+    def summary(self) -> Dict[str, Any]:
+        return {
+            "DomainArn": self.arn,
+            "DomainId": self.id,
+            "DomainName": self.domain_name,
+            "Status": self.status,
+            "CreationTime": self.creation_time,
+            "LastModifiedTime": self.last_modified_time,
+            "Url": self.url,
+        }
+
+
 class VpcConfig(BaseObject):
     def __init__(self, security_group_ids: List[str], subnets: List[str]):
         self.security_group_ids = security_group_ids
@@ -2388,6 +2489,7 @@ class SageMakerModelBackend(BaseBackend):
         self.clusters: Dict[str, Cluster] = {}
         self.auto_ml_jobs: Dict[str, AutoMLJob] = {}
         self.compilation_jobs: Dict[str, CompilationJob] = {}
+        self.domains: Dict[str, Domain] = {}
 
     @staticmethod
     def default_vpc_endpoint_service(
@@ -2520,6 +2622,7 @@ class SageMakerModelBackend(BaseBackend):
             "cluster": self.clusters,
             "automl-job": self.auto_ml_jobs,
             "compilation-job": self.compilation_jobs,
+            "domain": self.domains,
         }
         target_resource, target_name = arn.split(":")[-1].split("/")
         try:
@@ -4655,6 +4758,57 @@ class SageMakerModelBackend(BaseBackend):
         del self.compilation_jobs[compilation_job_name]
         return
 
+    def create_domain(
+        self,
+        domain_name: str,
+        auth_mode: str,
+        default_user_settings: Dict[str, Any],
+        subnet_ids: List[str],
+        vpc_id: str,
+        domain_settings: Optional[Dict[str, Any]],
+        tags: Optional[List[Dict[str, str]]],
+        app_network_access_type: Optional[str],
+        home_efs_file_system_kms_key_id: Optional[str],
+        kms_key_id: Optional[str],
+        app_security_group_management: Optional[str],
+        default_space_settings: Optional[Dict[str, Any]],
+    ) -> Dict[str, Any]:
+        domain = Domain(
+            domain_name=domain_name,
+            auth_mode=auth_mode,
+            default_user_settings=default_user_settings,
+            subnet_ids=subnet_ids,
+            vpc_id=vpc_id,
+            domain_settings=domain_settings,
+            tags=tags,
+            app_network_access_type=app_network_access_type,
+            home_efs_file_system_kms_key_id=home_efs_file_system_kms_key_id,
+            kms_key_id=kms_key_id,
+            app_security_group_management=app_security_group_management,
+            default_space_settings=default_space_settings,
+            region_name=self.region_name,
+            account_id=self.account_id,
+        )
+        self.domains[domain.id] = domain
+        return {"DomainArn": domain.arn, "Url": domain.url}
+
+    def describe_domain(self, domain_id: str) -> Dict[str, Any]:
+        if domain_id not in self.domains:
+            raise ValidationError(message=f"Could not find domain '{domain_id}'.")
+        return self.domains[domain_id].describe()
+
+    @paginate(pagination_model=PAGINATION_MODEL)
+    def list_domains(self) -> List[Domain]:
+        return list(self.domains.values())
+
+    def delete_domain(
+        self, domain_id: str, retention_policy: Optional[Dict[str, str]]
+    ) -> None:
+        # 'retention_policy' parameter is not used
+        if domain_id not in self.domains:
+            raise ValidationError(message=f"Could not find domain '{domain_id}'.")
+        del self.domains[domain_id]
+
 
 class FakeExperiment(BaseObject):
     def __init__(
diff --git a/moto/sagemaker/responses.py b/moto/sagemaker/responses.py
index daa73851b..49fc687c9 100644
--- a/moto/sagemaker/responses.py
+++ b/moto/sagemaker/responses.py
@@ -1205,3 +1205,58 @@ class SageMakerResponse(BaseResponse):
             compilation_job_name=compilation_job_name,
         )
         return json.dumps({})
+
+    def create_domain(self) -> str:
+        domain_name = self._get_param("DomainName")
+        auth_mode = self._get_param("AuthMode")
+        default_user_settings = self._get_param("DefaultUserSettings")
+        domain_settings = self._get_param("DomainSettings")
+        subnet_ids = self._get_param("SubnetIds")
+        vpc_id = self._get_param("VpcId")
+        tags = self._get_param("Tags")
+        app_network_access_type = self._get_param("AppNetworkAccessType")
+        home_efs_file_system_kms_key_id = self._get_param("HomeEfsFileSystemKmsKeyId")
+        kms_key_id = self._get_param("KmsKeyId")
+        app_security_group_management = self._get_param("AppSecurityGroupManagement")
+        default_space_settings = self._get_param("DefaultSpaceSettings")
+        resp = self.sagemaker_backend.create_domain(
+            domain_name=domain_name,
+            auth_mode=auth_mode,
+            default_user_settings=default_user_settings,
+            domain_settings=domain_settings,
+            subnet_ids=subnet_ids,
+            vpc_id=vpc_id,
+            tags=tags,
+            app_network_access_type=app_network_access_type,
+            home_efs_file_system_kms_key_id=home_efs_file_system_kms_key_id,
+            kms_key_id=kms_key_id,
+            app_security_group_management=app_security_group_management,
+            default_space_settings=default_space_settings,
+        )
+        return json.dumps(resp)
+
+    def describe_domain(self) -> str:
+        domain_id = self._get_param("DomainId")
+        domain_description = self.sagemaker_backend.describe_domain(
+            domain_id=domain_id,
+        )
+        return json.dumps(domain_description)
+
+    def list_domains(self) -> str:
+        next_token = self._get_param("NextToken")
+        max_results = self._get_param("MaxResults")
+        domains, next_token = self.sagemaker_backend.list_domains(
+            next_token=next_token,
+            max_results=max_results,
+        )
+        domain_summaries = [domain.summary() for domain in domains]
+        return json.dumps(dict(Domains=domain_summaries, NextToken=next_token))
+
+    def delete_domain(self) -> str:
+        domain_id = self._get_param("DomainId")
+        retention_policy = self._get_param("RetentionPolicy")
+        self.sagemaker_backend.delete_domain(
+            domain_id=domain_id,
+            retention_policy=retention_policy,
+        )
+        return json.dumps(dict())


Test patch:
diff --git a/.github/workflows/tests_sdk_ruby.yml b/.github/workflows/tests_sdk_ruby.yml
index 411ab3d32..87f08bcb7 100644
--- a/.github/workflows/tests_sdk_ruby.yml
+++ b/.github/workflows/tests_sdk_ruby.yml
@@ -13,7 +13,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
       - name: Set up Ruby ${{ matrix.ruby-version }}
-        uses: ruby/setup-ruby@50ba3386b050ad5b97a41fcb81240cbee1d1821f
+        uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854
         with:
           ruby-version: ${{ matrix.ruby-version }}
       - name: Set up Python 3.8
diff --git a/tests/test_rds/test_rds.py b/tests/test_rds/test_rds.py
index bf06b5645..ac70f9c57 100644
--- a/tests/test_rds/test_rds.py
+++ b/tests/test_rds/test_rds.py
@@ -2780,6 +2780,11 @@ def test_modify_db_snapshot_attribute():
         AttributeName="restore",
         ValuesToRemove=["Test"],
     )
+    resp = client.modify_db_snapshot_attribute(
+        DBSnapshotIdentifier="snapshot-1",
+        AttributeName="restore",
+        ValuesToAdd=["Test3"],
+    )
 
     assert (
         resp["DBSnapshotAttributesResult"]["DBSnapshotAttributes"][0]["AttributeName"]
@@ -2787,7 +2792,7 @@ def test_modify_db_snapshot_attribute():
     )
     assert resp["DBSnapshotAttributesResult"]["DBSnapshotAttributes"][0][
         "AttributeValues"
-    ] == ["Test2"]
+    ] == ["Test2", "Test3"]
 
 
 def validation_helper(exc):
diff --git a/tests/test_rds/test_rds_clusters.py b/tests/test_rds/test_rds_clusters.py
index d7bd70aa9..f4fabf822 100644
--- a/tests/test_rds/test_rds_clusters.py
+++ b/tests/test_rds/test_rds_clusters.py
@@ -1090,6 +1090,11 @@ def test_modify_db_cluster_snapshot_attribute():
         AttributeName="restore",
         ValuesToRemove=["test"],
     )
+    resp = conn.modify_db_cluster_snapshot_attribute(
+        DBClusterSnapshotIdentifier="g-1",
+        AttributeName="restore",
+        ValuesToAdd=["test3"],
+    )
     assert (
         resp["DBClusterSnapshotAttributesResult"]["DBClusterSnapshotIdentifier"]
         == "g-1"
@@ -1102,7 +1107,7 @@ def test_modify_db_cluster_snapshot_attribute():
     )
     assert resp["DBClusterSnapshotAttributesResult"]["DBClusterSnapshotAttributes"][0][
         "AttributeValues"
-    ] == ["test2"]
+    ] == ["test2", "test3"]
 
 
 @mock_aws
diff --git a/tests/test_sagemaker/test_sagemaker_domains.py b/tests/test_sagemaker/test_sagemaker_domains.py
new file mode 100644
index 000000000..ef91123be
--- /dev/null
+++ b/tests/test_sagemaker/test_sagemaker_domains.py
@@ -0,0 +1,244 @@
+"""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_domain():
+    client = boto3.client("sagemaker", region_name="us-east-2")
+    resp = client.create_domain(
+        DomainName="testdomain",
+        AuthMode="IAM",
+        DefaultUserSettings={
+            "ExecutionRole": f"arn:aws:iam::{ACCOUNT_ID}:role/iamrole-domain-execution",
+            "SecurityGroups": ["sg-12345678901234567"],
+            "SharingSettings": {
+                "NotebookOutputOption": "Allowed",
+                "S3OutputPath": "s3://output",
+            },
+        },
+        SubnetIds=["subnet-12345678901234567"],
+        VpcId="VpcId",
+        AppNetworkAccessType="PublicInternetOnly",
+        Tags=[{"Key": "string", "Value": "string"}],
+    )
+
+    assert (
+        resp["DomainArn"]
+        == f"arn:aws:sagemaker:us-east-2:{ACCOUNT_ID}:domain/d-testdomain"
+    )
+    assert resp["Url"] == "testdomain.us-east-2.sagemaker.test.com"
+
+
+@mock_aws
+def test_describe_domain():
+    client = boto3.client("sagemaker", region_name="ap-southeast-1")
+    client.create_domain(
+        DomainName="testdomain",
+        AuthMode="IAM",
+        DefaultUserSettings={
+            "ExecutionRole": f"arn:aws:iam::{ACCOUNT_ID}:role/iamrole-domain-execution",
+            "SecurityGroups": ["sg-12345678901234567"],
+            "SharingSettings": {
+                "NotebookOutputOption": "Allowed",
+                "S3OutputPath": "s3://output",
+            },
+        },
+        SubnetIds=["subnet-12345678901234567"],
+        VpcId="VpcId",
+        AppNetworkAccessType="PublicInternetOnly",
+        HomeEfsFileSystemKmsKeyId="testHomeEfsFileSystemKmsKeyId",
+        KmsKeyId="testKmsKeyId",
+        DefaultSpaceSettings={
+            "ExecutionRole": f"arn:aws:iam::{ACCOUNT_ID}:role/iamrole-space-execution",
+            "SecurityGroups": [
+                "sg-12345678901234564",
+            ],
+            "JupyterServerAppSettings": {
+                "DefaultResourceSpec": {
+                    "SageMakerImageArn": "SageMakerImageArn",
+                    "SageMakerImageVersionArn": "SageMakerImageVersionArn",
+                    "SageMakerImageVersionAlias": "SageMakerImageVersionAlias",
+                    "InstanceType": "system",
+                    "LifecycleConfigArn": "LifecycleConfigArn",
+                },
+                "LifecycleConfigArns": [
+                    "LifecycleConfigArns",
+                ],
+                "CodeRepositories": [
+                    {"RepositoryUrl": "RepositoryUrl"},
+                ],
+            },
+        },
+        Tags=[{"Key": "string", "Value": "string"}],
+    )
+    resp = client.describe_domain(DomainId="d-testdomain")
+    assert (
+        resp["DomainArn"]
+        == f"arn:aws:sagemaker:ap-southeast-1:{ACCOUNT_ID}:domain/d-testdomain"
+    )
+    assert resp["DomainId"] == "d-testdomain"
+    assert resp["DomainName"] == "testdomain"
+    assert resp["HomeEfsFileSystemId"] == "testdomain-efs-id"
+    assert resp["SingleSignOnManagedApplicationInstanceId"] == "testdomain-sso-id"
+    assert (
+        resp["SingleSignOnApplicationArn"]
+        == "arn:aws:sagemaker::123456789012:sso/application/testdomain/apl-testdomain"
+    )
+    assert resp["Status"] == "InService"
+    assert isinstance(resp["CreationTime"], datetime.datetime)
+    assert isinstance(resp["LastModifiedTime"], datetime.datetime)
+    assert resp["FailureReason"] == ""
+    assert resp["SecurityGroupIdForDomainBoundary"] == "sg-testdomain"
+    assert resp["AuthMode"] == "IAM"
+    assert resp["DefaultUserSettings"] == {
+        "ExecutionRole": f"arn:aws:iam::{ACCOUNT_ID}:role/iamrole-domain-execution",
+        "SecurityGroups": ["sg-12345678901234567"],
+        "SharingSettings": {
+            "NotebookOutputOption": "Allowed",
+            "S3OutputPath": "s3://output",
+        },
+    }
+
+
+@mock_aws
+def test_describe_domain_defaults():
+    client = boto3.client("sagemaker", region_name="ap-southeast-1")
+    resp = client.create_domain(
+        DomainName="testdomain",
+        AuthMode="IAM",
+        DefaultUserSettings={},
+        SubnetIds=["subnet-12345678901234567"],
+        VpcId="VpcId",
+    )
+    resp = client.describe_domain(DomainId="d-testdomain")
+    assert (
+        resp["DomainArn"]
+        == f"arn:aws:sagemaker:ap-southeast-1:{ACCOUNT_ID}:domain/d-testdomain"
+    )
+    assert resp["DomainId"] == "d-testdomain"
+    assert resp["DomainName"] == "testdomain"
+    assert resp["HomeEfsFileSystemId"] == "testdomain-efs-id"
+    assert resp["SingleSignOnManagedApplicationInstanceId"] == "testdomain-sso-id"
+    assert (
+        resp["SingleSignOnApplicationArn"]
+        == "arn:aws:sagemaker::123456789012:sso/application/testdomain/apl-testdomain"
+    )
+    assert resp["Status"] == "InService"
+    assert isinstance(resp["CreationTime"], datetime.datetime)
+    assert isinstance(resp["LastModifiedTime"], datetime.datetime)
+    assert resp["FailureReason"] == ""
+    assert resp["SecurityGroupIdForDomainBoundary"] == "sg-testdomain"
+    assert resp["AuthMode"] == "IAM"
+    assert resp["DefaultUserSettings"] == {}
+
+
+@mock_aws
+def test_list_domains():
+    client = boto3.client("sagemaker", region_name="us-east-2")
+    client.create_domain(
+        DomainName="testdomain",
+        AuthMode="IAM",
+        DefaultUserSettings={},
+        SubnetIds=["subnet-12345678901234567"],
+        VpcId="VpcId",
+    )
+    client.create_domain(
+        DomainName="testdomain2",
+        AuthMode="IAM",
+        DefaultUserSettings={},
+        SubnetIds=["subnet-12345678901234567"],
+        VpcId="VpcId",
+    )
+    resp = client.list_domains()
+    assert len(resp["Domains"]) == 2
+    assert (
+        resp["Domains"][0]["DomainArn"]
+        == "arn:aws:sagemaker:us-east-2:123456789012:domain/d-testdomain"
+    )
+    assert resp["Domains"][0]["DomainId"] == "d-testdomain"
+    assert resp["Domains"][0]["DomainName"] == "testdomain"
+    assert resp["Domains"][0]["Status"] == "InService"
+    assert isinstance(resp["Domains"][0]["CreationTime"], datetime.datetime)
+    assert isinstance(resp["Domains"][0]["LastModifiedTime"], datetime.datetime)
+    assert resp["Domains"][0]["Url"] == "testdomain.us-east-2.sagemaker.test.com"
+    assert (
+        resp["Domains"][1]["DomainArn"]
+        == "arn:aws:sagemaker:us-east-2:123456789012:domain/d-testdomain2"
+    )
+    assert resp["Domains"][1]["DomainId"] == "d-testdomain2"
+    assert resp["Domains"][1]["DomainName"] == "testdomain2"
+    assert resp["Domains"][1]["Status"] == "InService"
+    assert isinstance(resp["Domains"][1]["CreationTime"], datetime.datetime)
+    assert isinstance(resp["Domains"][1]["LastModifiedTime"], datetime.datetime)
+    assert resp["Domains"][1]["Url"] == "testdomain2.us-east-2.sagemaker.test.com"
+
+
+@mock_aws
+def test_delete_domain():
+    client = boto3.client("sagemaker", region_name="us-east-2")
+    client.create_domain(
+        DomainName="testdomain",
+        AuthMode="IAM",
+        DefaultUserSettings={},
+        SubnetIds=["subnet-12345678901234567"],
+        VpcId="VpcId",
+    )
+    client.create_domain(
+        DomainName="testdomain2",
+        AuthMode="IAM",
+        DefaultUserSettings={},
+        SubnetIds=["subnet-12345678901234567"],
+        VpcId="VpcId",
+    )
+    client.delete_domain(DomainId="d-testdomain2")
+    resp = client.list_domains()
+    assert len(resp["Domains"]) == 1
+    assert (
+        resp["Domains"][0]["DomainArn"]
+        == "arn:aws:sagemaker:us-east-2:123456789012:domain/d-testdomain"
+    )
+    assert resp["Domains"][0]["DomainId"] == "d-testdomain"
+    assert resp["Domains"][0]["DomainName"] == "testdomain"
+
+
+@mock_aws
+def test_tag_domain():
+    client = boto3.client("sagemaker", region_name="us-east-2")
+    resp = client.create_domain(
+        DomainName="testdomain",
+        AuthMode="IAM",
+        DefaultUserSettings={
+            "ExecutionRole": f"arn:aws:iam::{ACCOUNT_ID}:role/iamrole-domain-execution",
+            "SecurityGroups": ["sg-12345678901234567"],
+            "SharingSettings": {
+                "NotebookOutputOption": "Allowed",
+                "S3OutputPath": "s3://output",
+            },
+        },
+        SubnetIds=["subnet-12345678901234567"],
+        VpcId="VpcId",
+        AppNetworkAccessType="PublicInternetOnly",
+        Tags=[{"Key": "testkey", "Value": "testvalue"}],
+    )
+    tags1 = client.list_tags(ResourceArn=resp["DomainArn"])
+    assert tags1["Tags"] == [{"Key": "testkey", "Value": "testvalue"}]
+    client.add_tags(
+        ResourceArn=resp["DomainArn"], Tags=[{"Key": "testkey2", "Value": "testvalue2"}]
+    )
+    tags2 = client.list_tags(ResourceArn=resp["DomainArn"])
+    assert tags2["Tags"] == [
+        {"Key": "testkey", "Value": "testvalue"},
+        {"Key": "testkey2", "Value": "testvalue2"},
+    ]
+    client.delete_tags(ResourceArn=resp["DomainArn"], TagKeys=["testkey"])
+    tags3 = client.list_tags(ResourceArn=resp["DomainArn"])
+    assert tags3["Tags"] == [{"Key": "testkey2", "Value": "testvalue2"}]