Repository: getmoto/moto
Pull request #8588 (2025-02-12 15:26:12)

ECS: tasks include containers objects
Resolves https://github.com/getmoto/moto/issues/8319

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/8588?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 `90.00000%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 92.65%. Comparing base [(`777ca53`)](https://app.codecov.io/gh/getmoto/moto/commit/777ca53026b94c7dc9237de34c84ee41e6e1f2f6?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`173ca6e`)](https://app.codecov.io/gh/getmoto/moto/commit/173ca6e6d36547bee3e6c546925384657cc10a84?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).

| [Files with missing lines](https://app.codecov.io/gh/getmoto/moto/pull/8588?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/ecs/models.py](https://app.codecov.io/gh/getmoto/moto/pull/8588?src=pr&el=tree&filepath=moto%2Fecs%2Fmodels.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#diff-bW90by9lY3MvbW9kZWxzLnB5) | 90.00% | [2 Missing :warning: ](https://app.codecov.io/gh/getmoto/moto/pull/8588?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    #8588      +/-   ##
==========================================
- Coverage   92.65%   92.65%   -0.01%     
==========================================
  Files        1236     1236              
  Lines      106945   106964      +19     
==========================================
+ Hits        99094    99111      +17     
- Misses       7851     7853       +2     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/8588/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/8588/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `28.11% <15.00%> (-0.01%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/8588/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `92.63% <90.00%> (-0.01%)` | :arrow_down: |

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/8588?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/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index cd69f6351..8a77afccf 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,22 +1,126 @@
 {
   "name": "moto",
-  "image": "mcr.microsoft.com/devcontainers/python:0-3.11",
-  "remoteUser": "root",
+  "image": "mcr.microsoft.com/devcontainers/python:3.11",
   "features": {
-    "ghcr.io/devcontainers/features/docker-in-docker:2": {}
+    "ghcr.io/devcontainers/features/docker-in-docker:2": {},
+    "ghcr.io/devcontainers/features/github-cli:1": {}
   },
+  "portsAttributes": {
+    "5000": {
+      "onAutoForward": "silent",
+      "elevateIfNeeded": true,
+      "protocol": "http",
+      "requireLocalPort": false
+    }
+  },
+  "postCreateCommand": "python -m venv .venv",
+  "postStartCommand": ". .venv/bin/activate && make init",
   "customizations": {
     "vscode": {
-      "extensions": ["ms-vscode.makefile-tools", "ms-python.python", "ms-python.black-formatter"],
+      "extensions": [
+        "ms-vscode.makefile-tools",
+        "ms-python.python",
+        "ms-python.black-formatter"
+      ],
       "settings": {
+        "editor.formatOnSave": true,
         "python.defaultInterpreterPath": "${workspaceFolder}/.venv",
         "python.formatting.provider": "none",
         "[python]": {
           "editor.defaultFormatter": "ms-python.black-formatter"
+        },
+        "tasks": {
+          "version": "2.0.0",
+          "tasks": [
+            {
+              "label": "Open Port 5000",
+              "type": "shell",
+              "command": "bash",
+              "args": [
+                "-c",
+                "until gh codespace ports visibility 5000:public --codespace ${CODESPACE_NAME} 2>/dev/null; do sleep 1; done"
+              ],
+              "presentation": {
+                "reveal": "silent",
+                "focus": false
+              }
+            },
+            {
+              "label": "Kill MotoServer",
+              "type": "shell",
+              "command": "pkill -f '${workspaceFolder}/moto/server.py'",
+              "problemMatcher": [],
+              "presentation": {
+                "reveal": "silent",
+                "focus": false
+              }
+            }
+          ]
+        },
+        "launch": {
+          "version": "0.2.0",
+          "configurations": [
+            {
+              "name": "Run MotoServer",
+              "type": "debugpy",
+              "request": "launch",
+              "program": "${workspaceFolder}/moto/server.py",
+              "args": [
+                "--reload"
+              ],
+              "console": "internalConsole",
+              "presentation": {
+                "order": 11
+              }
+            },
+            {
+              "name": "Run MotoServer Tests",
+              "type": "debugpy",
+              "request": "launch",
+              "module": "pytest",
+              "console": "integratedTerminal",
+              "preLaunchTask": "Open Port 5000",
+              "postDebugTask": "Kill MotoServer",
+              "env": {
+                "TEST_SERVER_MODE": "true",
+                "TEST_SERVER_MODE_ENDPOINT": "https://${env:CODESPACE_NAME}-5000.${env:GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}"
+              },
+              "presentation": {
+                "order": 12
+              },
+              "args": [
+                "-sv",
+                "--cov=moto",
+                "--cov-report=xml",
+                "${workspaceFolder}/tests/",
+                "-k",
+                "${input:pytestFilter}"
+              ]
+            }
+          ],
+          "inputs": [
+            {
+              "id": "pytestFilter",
+              "type": "promptString",
+              "description": "pytest -k filter (leave empty to run all tests)",
+              "default": ""
+            }
+          ],
+          "compounds": [
+            {
+              "name": "MotoServer Tests",
+              "configurations": [
+                "Run MotoServer Tests",
+                "Run MotoServer"
+              ],
+              "presentation": {
+                "order": 1
+              },
+              "stopAll": true
+            }
+          ]
         }
       }
     }
-  },
-  "postCreateCommand": "python -m venv .venv",
-  "postStartCommand": ". .venv/bin/activate && make init"
-}
+  }
+}
\ No newline at end of file
diff --git a/moto/backend_index.py b/moto/backend_index.py
index 213550592..e0d9e277f 100644
--- a/moto/backend_index.py
+++ b/moto/backend_index.py
@@ -205,6 +205,10 @@ backend_url_patterns = [
     ("support", re.compile("https?://support\\.(.+)\\.amazonaws\\.com")),
     ("swf", re.compile("https?://swf\\.(.+)\\.amazonaws\\.com")),
     ("textract", re.compile("https?://textract\\.(.+)\\.amazonaws\\.com")),
+    (
+        "timestreaminfluxdb",
+        re.compile("https?://timestream-influxdb\\.(.+)\\.amazonaws\\.com"),
+    ),
     (
         "timestreamquery",
         re.compile("https?://query\\.timestream\\.(.+)\\.amazonaws\\.com"),
diff --git a/moto/backends.py b/moto/backends.py
index aa0fe4c1b..0d918c6ed 100644
--- a/moto/backends.py
+++ b/moto/backends.py
@@ -146,6 +146,7 @@ if TYPE_CHECKING:
     from moto.support.models import SupportBackend
     from moto.swf.models import SWFBackend
     from moto.textract.models import TextractBackend
+    from moto.timestreaminfluxdb.models import TimestreamInfluxDBBackend
     from moto.timestreamquery.models import TimestreamQueryBackend
     from moto.timestreamwrite.models import TimestreamWriteBackend
     from moto.transcribe.models import TranscribeBackend
@@ -330,6 +331,7 @@ SERVICE_NAMES = Union[
     "Literal['support']",
     "Literal['swf']",
     "Literal['textract']",
+    "Literal['timestream-influxdb']",
     "Literal['timestream-query']",
     "Literal['timestream-write']",
     "Literal['transcribe']",
@@ -734,6 +736,10 @@ def get_backend(name: "Literal['swf']") -> "BackendDict[SWFBackend]": ...
 @overload
 def get_backend(name: "Literal['textract']") -> "BackendDict[TextractBackend]": ...
 @overload
+def get_backend(
+    name: "Literal['timestream-influxdb']",
+) -> "BackendDict[TimestreamInfluxDBBackend]": ...
+@overload
 def get_backend(
     name: "Literal['timestream-query']",
 ) -> "BackendDict[TimestreamQueryBackend]": ...
diff --git a/moto/ecs/models.py b/moto/ecs/models.py
index bcb8c5fbe..15fa3b03a 100644
--- a/moto/ecs/models.py
+++ b/moto/ecs/models.py
@@ -386,7 +386,7 @@ class Task(BaseObject, ManagedState):
         self.desired_status = "RUNNING"
         self.task_definition_arn = task_definition.arn
         self.overrides = overrides or {}
-        self.containers: List[Dict[str, Any]] = []
+        self.containers = [Container(task_definition)]
         self.started_by = started_by
         self.tags = tags or []
         self.launch_type = launch_type
@@ -451,6 +451,7 @@ class Task(BaseObject, ManagedState):
             response_object.pop("tags", None)
         response_object["taskArn"] = self.task_arn
         response_object["lastStatus"] = self.last_status
+        response_object["containers"] = [self.containers[0].response_object]
         return response_object
 
 
@@ -757,6 +758,29 @@ class Service(BaseObject, CloudFormationModel):
         raise UnformattedGetAttTemplateException()
 
 
+class Container(BaseObject, CloudFormationModel):
+    def __init__(
+        self,
+        task_def: TaskDefinition,
+    ):
+        self.container_arn = f"{task_def.arn}/{str(mock_random.uuid4())}"
+        self.task_arn = task_def.arn
+
+        container_def = task_def.container_definitions[0]
+        self.image = container_def.get("image")
+        self.last_status = "PENDING"
+        self.exitCode = 0
+
+        self.network_interfaces: List[Dict[str, Any]] = []
+        self.health_status = "HEALTHY"
+
+        self.cpu = container_def.get("cpu")
+        self.memory = container_def.get("memory")
+        self.environment = container_def.get("environment")
+        self.name = container_def.get("name")
+        self.command = container_def.get("command")
+
+
 class ContainerInstance(BaseObject):
     def __init__(
         self,
diff --git a/moto/timestreaminfluxdb/__init__.py b/moto/timestreaminfluxdb/__init__.py
new file mode 100644
index 000000000..e051c6f8d
--- /dev/null
+++ b/moto/timestreaminfluxdb/__init__.py
@@ -0,0 +1,1 @@
+from .models import timestreaminfluxdb_backends  # noqa: F401
diff --git a/moto/timestreaminfluxdb/exceptions.py b/moto/timestreaminfluxdb/exceptions.py
new file mode 100644
index 000000000..4208034cb
--- /dev/null
+++ b/moto/timestreaminfluxdb/exceptions.py
@@ -0,0 +1,28 @@
+"""Exceptions raised by the timestreaminfluxdb service."""
+
+from moto.core.exceptions import JsonRESTError
+
+
+class TimestreamInfluxDBException(JsonRESTError):
+    pass
+
+
+class ValidationException(TimestreamInfluxDBException):
+    code = 400
+
+    def __init__(self, message: str):
+        super().__init__("ValidationException", message)
+
+
+class ConflictException(TimestreamInfluxDBException):
+    code = 400
+
+    def __init__(self, message: str):
+        super().__init__("ConflictException", message)
+
+
+class ResourceNotFoundException(TimestreamInfluxDBException):
+    code = 400
+
+    def __init__(self, message: str):
+        super().__init__("ResourceNotFoundException", message)
diff --git a/moto/timestreaminfluxdb/models.py b/moto/timestreaminfluxdb/models.py
new file mode 100644
index 000000000..ce92e2a29
--- /dev/null
+++ b/moto/timestreaminfluxdb/models.py
@@ -0,0 +1,286 @@
+"""TimestreamInfluxDBBackend class with methods for supported APIs."""
+
+from enum import Enum
+from typing import Any, Dict, List, Optional
+
+from moto.core.base_backend import BackendDict, BaseBackend
+from moto.core.common_models import BaseModel
+from moto.utilities.tagging_service import TaggingService
+
+from .exceptions import (
+    ConflictException,
+    ResourceNotFoundException,
+    ValidationException,
+)
+from .utils import random_id, validate_name
+
+
+class InstanceStatus(str, Enum):
+    CREATING = "CREATING"
+    AVAILABLE = "AVAILABLE"
+    DELETING = "DELETING"
+    MODIFYING = "MODIFYING"
+    UPDATING = "UPDATING"
+    DELETED = "DELETED"
+    FAILED = "FAILED"
+    UPDATING_DEPLOYMENT_TYPE = "UPDATING_DEPLOYMENT_TYPE"
+    UPDATING_INSTANCE_TYPE = "UPDATING_INSTANCE_TYPE"
+
+
+class NetworkType(str, Enum):
+    IPV4 = "IPV4"
+    DUAL = "DUAL"
+
+
+class InstanceType(str, Enum):
+    DB_INFLUX_MEDIUM = "db.influx.medium"
+    DB_INFLUX_LARGE = "db.influx.large"
+    DB_INFLUX_XLARGE = "db.influx.xlarge"
+    DB_INFLUX_2XLARGE = "db.influx.2xlarge"
+    DB_INFLUX_4XLARGE = "db.influx.4xlarge"
+    DB_INFLUX_8XLARGE = "db.influx.8xlarge"
+    DB_INFLUX_12XLARGE = "db.influx.12xlarge"
+    DB_INFLUX_16XLARGE = "db.influx.16xlarge"
+
+
+class DBStorageType(str, Enum):
+    InfluxIOIncludedT1 = "InfluxIOIncludedT1"
+    InfluxIOIncludedT2 = "InfluxIOIncludedT2"
+    InfluxIOIncludedT3 = "InfluxIOIncludedT3"
+
+
+class DeploymentType(str, Enum):
+    SINGLE_AZ = "SINGLE_AZ"
+    WITH_MULTIAZ_STANDBY = "WITH_MULTIAZ_STANDBY"
+
+
+class DBInstance(BaseModel):
+    def __init__(
+        self,
+        name: str,
+        username: Optional[str],
+        password: str,
+        organization: str,
+        bucket: str,
+        dbInstanceType: str,
+        vpcSubnetIds: List[str],
+        vpcSecurityGroupIds: List[str],
+        publiclyAccessible: bool,
+        dbStorageType: str,
+        allocatedStorage: int,
+        dbParameterGroupIdentifier: Optional[str],
+        deploymentType: str,
+        logDeliveryConfiguration: Optional[Dict[str, Any]],
+        tags: Optional[Dict[str, Any]],
+        port: int,
+        networkType: str,
+        region_name: str,
+        account_id: str,
+        endpoint_id: str,
+    ):
+        # Generate a random id of size 10
+        self.id = random_id()
+
+        self.name = name
+        self.username = username
+        self.password = password
+        self.organization = organization
+        self.bucket = bucket
+        self.db_instance_type = dbInstanceType
+        self.vpc_subnet_ids = vpcSubnetIds
+        self.vpc_security_group_ids = vpcSecurityGroupIds
+        self.publicly_accessible = publiclyAccessible
+        self.db_storage_type = dbStorageType
+        self.allocated_storage = allocatedStorage
+        self.db_parameter_group_id = dbParameterGroupIdentifier
+        self.deployment_type = deploymentType
+        self.log_delivery_configuration = logDeliveryConfiguration
+        self.port = port
+        self.network_type = networkType
+        self.status = InstanceStatus.CREATING
+        self.arn = f"arn:aws:timestream-influxdb:{region_name}:{account_id}:db-instance/{self.id}"
+        self.endpoint = (
+            f"{self.id}-{endpoint_id}.timestream-influxdb.{region_name}.on.aws"
+        )
+        # Before 12/09/2024, there was a different endpoint format.
+        self.endpoint_old = (
+            f"{self.name}-{endpoint_id}.timestream-influxdb.{region_name}.on.aws"
+        )
+
+        self.availability_zone = ""  # TODO implement this
+        self.secondary_availability_zone = ""  # TODO implement this
+
+    def to_dict(self) -> Dict[str, Any]:
+        return {
+            "id": self.id,
+            "name": self.name,
+            "arn": self.arn,
+            "status": self.status,
+            "endpoint": self.endpoint,
+            "port": self.port,
+            "networkType": self.network_type,
+            "dbInstanceType": self.db_instance_type,
+            "dbStorageType": self.db_storage_type,
+            "allocatedStorage": self.allocated_storage,
+            "deploymentType": self.deployment_type,
+            "vpcSubnetIds": self.vpc_subnet_ids,
+            "publiclyAccessible": self.publicly_accessible,
+            "vpcSecurityGroupIds": self.vpc_security_group_ids,
+            "dbParameterGroupIdentifier": self.db_parameter_group_id,  # TODO implement this
+            "availabilityZone": self.availability_zone,  # TODO implement this
+            "secondaryAvailabilityZone": self.secondary_availability_zone,  # TODO implement this
+            "logDeliveryConfiguration": self.log_delivery_configuration,  # TODO implement this
+            "influxAuthParametersSecretArn": "",  # TODO implement this
+        }
+
+
+class TimestreamInfluxDBBackend(BaseBackend):
+    """Implementation of TimestreamInfluxDB APIs."""
+
+    def __init__(self, region_name: str, account_id: str):
+        super().__init__(region_name, account_id)
+
+        # the endpoint identifier is unique per account and per region
+        # https://docs.aws.amazon.com/timestream/latest/developerguide/timestream-for-influxdb.html
+        self.endpoint_id: str = random_id(10)
+        self.db_instances: Dict[str, DBInstance] = {}
+        self.tagger = TaggingService()
+
+    def create_db_instance(
+        self,
+        name: str,
+        username: Optional[str],  # required if using InfluxDB UI though
+        password: str,
+        organization: str,
+        bucket: str,
+        db_instance_type: str,
+        vpc_subnet_ids: List[str],
+        vpc_security_group_ids: List[str],
+        db_storage_type: str,
+        publicly_accessible: bool,
+        allocated_storage: int,
+        db_parameter_group_identifier: str,
+        deployment_type: str,
+        log_delivery_configuration: Optional[Dict[str, Any]],
+        tags: Optional[Dict[str, str]],
+        port: int,
+        network_type: str,
+    ) -> DBInstance:
+        """
+        dbParameterGroupIdentifier argument is not yet handled
+        deploymentType currently is auto set to 'SINGLE_AZ' if not passed in.
+        publicAccessible is not yet handled
+        logDeliveryConfiguration is not yet handled
+        AvailabilityZone and SecondaryAvailabilityZone are not yet handled
+        influxAuthParametersSecretArn is not yet handled
+        """
+
+        # Checks:
+        for db_instance in self.db_instances.values():
+            if db_instance.name == name:
+                raise ConflictException(
+                    f"A DB Instance with the name {name} already exists"
+                )
+
+        validate_name(name)
+
+        if db_storage_type not in [t.value for t in DBStorageType]:
+            raise ValidationException(f"Unknown DB storage type {db_storage_type}")
+
+        if db_instance_type not in [t.value for t in InstanceType]:
+            raise ValidationException(f"Unknown DB instance type {db_instance_type}")
+
+        new_instance = DBInstance(
+            name,
+            username,
+            password,
+            organization,
+            bucket,
+            db_instance_type,
+            vpc_subnet_ids,
+            vpc_security_group_ids,
+            publicly_accessible,
+            db_storage_type,
+            allocated_storage,
+            db_parameter_group_identifier,
+            deployment_type,
+            log_delivery_configuration,
+            tags,
+            port,
+            network_type,
+            self.region_name,
+            self.account_id,
+            self.endpoint_id,
+        )
+
+        # add to the list
+        self.db_instances[new_instance.id] = new_instance
+
+        # add tags
+        if tags:
+            self.tag_resource(new_instance.arn, tags)
+
+        return new_instance
+
+    def delete_db_instance(self, id: str) -> DBInstance:
+        if id not in self.db_instances:
+            raise ResourceNotFoundException(f"DB Instance with id {id} not found")
+
+        # mark as deleting
+        self.db_instances[id].status = InstanceStatus.DELETING
+        return self.db_instances.pop(id)
+
+    def get_db_instance(self, id: str) -> DBInstance:
+        if id not in self.db_instances:
+            raise ResourceNotFoundException(f"DB Instance with id {id} not found")
+
+        return self.db_instances[id]
+
+    def list_db_instances(self) -> List[Dict[str, Any]]:
+        """
+        Pagination is not yet implemented
+        """
+        return [
+            {
+                "allocatedStorage": instance.allocated_storage,
+                "arn": instance.arn,
+                "dbInstanceType": instance.db_instance_type,
+                "dbStorageType": instance.db_storage_type,
+                "deploymentType": instance.deployment_type,
+                "endpoint": instance.endpoint,
+                "id": instance.id,
+                "name": instance.name,
+                "networkType": instance.network_type,
+                "port": instance.port,
+                "status": instance.status,
+            }
+            for instance in self.db_instances.values()
+        ]
+
+    def tag_resource(self, resource_arn: str, tags: Dict[str, str]) -> None:
+        tag_list = self.tagger.convert_dict_to_tags_input(tags)
+        errmsg = self.tagger.validate_tags(tag_list)
+        if errmsg:
+            raise ValidationException(errmsg)
+        self.tagger.tag_resource(resource_arn, tag_list)
+
+    def untag_resource(self, resource_arn: str, tag_keys: List[str]) -> None:
+        self.tagger.untag_resource_using_names(resource_arn, tag_keys)
+
+    def list_tags_for_resource(self, resource_arn: str) -> Dict[str, str]:
+        return self.tagger.get_tag_dict_for_resource(resource_arn)
+
+
+timestreaminfluxdb_backends = BackendDict(
+    TimestreamInfluxDBBackend,
+    "timestream-influxdb",
+    additional_regions=[
+        "us-east-1",
+        "us-east-2",
+        "us-west-2",
+        "eu-central-1",
+        "eu-west-1",
+        "ap-southeast-2",
+        "ap-northeast-1",
+    ],
+)
diff --git a/moto/timestreaminfluxdb/responses.py b/moto/timestreaminfluxdb/responses.py
new file mode 100644
index 000000000..b6b9dece9
--- /dev/null
+++ b/moto/timestreaminfluxdb/responses.py
@@ -0,0 +1,105 @@
+import json
+
+from moto.core.responses import BaseResponse
+
+from .models import (
+    DBStorageType,
+    DeploymentType,
+    NetworkType,
+    TimestreamInfluxDBBackend,
+    timestreaminfluxdb_backends,
+)
+
+
+class TimestreamInfluxDBResponse(BaseResponse):
+    """Handler for TimestreamInfluxDB requests and responses."""
+
+    def __init__(self) -> None:
+        super().__init__(service_name="timestream-influxdb")
+
+    @property
+    def timestreaminfluxdb_backend(self) -> TimestreamInfluxDBBackend:
+        return timestreaminfluxdb_backends[self.current_account][self.region]
+
+    def create_db_instance(self) -> str:
+        params = json.loads(self.body)
+        name = params.get("name")
+        username = params.get("username")
+        password = params.get("password")
+        organization = params.get("organization")
+        bucket = params.get("bucket")
+        db_instance_type = params.get("dbInstanceType")
+        vpc_subnet_ids = params.get("vpcSubnetIds")
+        vpc_security_group_ids = params.get("vpcSecurityGroupIds")
+        publicly_accessible = params.get("publiclyAccessible", False)
+        db_storage_type = params.get("dbStorageType", DBStorageType.InfluxIOIncludedT1)
+        allocated_storage = params.get("allocatedStorage")
+        db_parameter_group_identifier = params.get("dbParameterGroupIdentifier")
+        deployment_type = params.get("deploymentType", DeploymentType.SINGLE_AZ)
+        log_delivery_configuration = params.get("logDeliveryConfiguration", {})
+        tags = params.get("tags", {})
+        port = int(params.get("port", 8086))
+        network_type = params.get("networkType", NetworkType.IPV4)
+
+        created_instance = self.timestreaminfluxdb_backend.create_db_instance(
+            name=name,
+            username=username,
+            password=password,
+            organization=organization,
+            bucket=bucket,
+            db_instance_type=db_instance_type,
+            vpc_subnet_ids=vpc_subnet_ids,
+            vpc_security_group_ids=vpc_security_group_ids,
+            publicly_accessible=publicly_accessible,
+            db_storage_type=db_storage_type,
+            allocated_storage=allocated_storage,
+            db_parameter_group_identifier=db_parameter_group_identifier,
+            deployment_type=deployment_type,
+            log_delivery_configuration=log_delivery_configuration,
+            tags=tags,
+            port=port,
+            network_type=network_type,
+        )
+        return json.dumps(created_instance.to_dict())
+
+    def delete_db_instance(self) -> str:
+        params = json.loads(self.body)
+        id = params.get("identifier")
+        deleted_instance = self.timestreaminfluxdb_backend.delete_db_instance(id=id)
+        return json.dumps(deleted_instance.to_dict())
+
+    def get_db_instance(self) -> str:
+        params = json.loads(self.body)
+        id = params.get("identifier")
+        instance = self.timestreaminfluxdb_backend.get_db_instance(id=id)
+        return json.dumps(instance.to_dict())
+
+    def list_db_instances(self) -> str:
+        """
+        Pagination is not yet implemented
+        """
+        instances = self.timestreaminfluxdb_backend.list_db_instances()
+
+        return json.dumps({"items": instances})
+
+    def tag_resource(self) -> str:
+        params = json.loads(self.body)
+        arn = params.get("resourceArn")
+        tags = params.get("tags")
+        self.timestreaminfluxdb_backend.tag_resource(resource_arn=arn, tags=tags)
+        return "{}"
+
+    def untag_resource(self) -> str:
+        params = json.loads(self.body)
+        arn = params.get("resourceArn")
+        tag_keys = params.get("tagKeys")
+        self.timestreaminfluxdb_backend.untag_resource(
+            resource_arn=arn, tag_keys=tag_keys
+        )
+        return "{}"
+
+    def list_tags_for_resource(self) -> str:
+        params = json.loads(self.body)
+        arn = params.get("resourceArn")
+        tags = self.timestreaminfluxdb_backend.list_tags_for_resource(resource_arn=arn)
+        return json.dumps({"tags": tags})
diff --git a/moto/timestreaminfluxdb/urls.py b/moto/timestreaminfluxdb/urls.py
new file mode 100644
index 000000000..ea9c57eaf
--- /dev/null
+++ b/moto/timestreaminfluxdb/urls.py
@@ -0,0 +1,11 @@
+"""timestreaminfluxdb base URL and path."""
+
+from .responses import TimestreamInfluxDBResponse
+
+url_bases = [
+    r"https?://timestream-influxdb\.(.+)\.amazonaws\.com",
+]
+
+url_paths = {
+    "{0}/$": TimestreamInfluxDBResponse.dispatch,
+}
diff --git a/moto/timestreaminfluxdb/utils.py b/moto/timestreaminfluxdb/utils.py
new file mode 100644
index 000000000..1aa3390e4
--- /dev/null
+++ b/moto/timestreaminfluxdb/utils.py
@@ -0,0 +1,34 @@
+import random
+import re
+import string
+
+from .exceptions import ValidationException
+
+
+def random_id(size: int = 10) -> str:
+    chars = list(range(10)) + list(string.ascii_lowercase)
+    return "".join(str(random.choice(chars)) for x in range(size))
+
+
+def validate_name(name: str) -> None:
+    """
+    The name that uniquely identifies the DB instance when interacting with the Amazon Timestream for InfluxDB API and CLI commands.
+    This name will also be a prefix included in the endpoint.
+    DB instance names must be unique per customer and per region.
+    Length Constraints: Minimum length of 3. Maximum length of 40.
+    Pattern: ^[a-zA-Z][a-zA-Z0-9]*(-[a-zA-Z0-9]+)*$
+
+    """
+
+    if len(name) < 3:
+        raise ValidationException(
+            f"Expected name to have a minimum length of 3, got {len(name)}"
+        )
+    elif len(name) > 40:
+        raise ValidationException(
+            f"Expected name to have a maximum length of 40, got {len(name)}"
+        )
+    elif not re.match(r"^[a-zA-Z][a-zA-Z0-9]*(-[a-zA-Z0-9]+)*$", name):
+        raise ValidationException(
+            f"Expected name to match the pattern ^[a-zA-Z][a-zA-Z0-9]*(-[a-zA-Z0-9]+)*$, got {name}"
+        )


Test patch:
diff --git a/tests/test_ecs/test_ecs_boto3.py b/tests/test_ecs/test_ecs_boto3.py
index 465178f1b..f0f1e988d 100644
--- a/tests/test_ecs/test_ecs_boto3.py
+++ b/tests/test_ecs/test_ecs_boto3.py
@@ -1854,31 +1854,27 @@ def test_update_container_instances_state_by_arn():
 
 @mock_aws
 def test_run_task():
+    # Setup
+    test_cluster_name = "test_ecs_cluster"
+    image = "docker/hello-world:latest"
     client = boto3.client("ecs", region_name=ECS_REGION)
     ec2 = boto3.resource("ec2", region_name=ECS_REGION)
-
-    test_cluster_name = "test_ecs_cluster"
-
     client.create_cluster(clusterName=test_cluster_name)
-
     test_instance = ec2.create_instances(
         ImageId=EXAMPLE_AMI_ID, MinCount=1, MaxCount=1
     )[0]
-
     instance_id_document = json.dumps(
         ec2_utils.generate_instance_identity_document(test_instance)
     )
-
-    response = client.register_container_instance(
+    client.register_container_instance(
         cluster=test_cluster_name, instanceIdentityDocument=instance_id_document
     )
-
-    client.register_task_definition(
+    td = client.register_task_definition(
         family="test_ecs_task",
         containerDefinitions=[
             {
                 "name": "hello_world",
-                "image": "docker/hello-world:latest",
+                "image": image,
                 "cpu": 1024,
                 "memory": 400,
                 "essential": True,
@@ -1889,12 +1885,16 @@ def test_run_task():
             }
         ],
     )
+
+    # Execute
     response = client.run_task(
         cluster="test_ecs_cluster",
         overrides={},
         taskDefinition="test_ecs_task",
         startedBy="moto",
     )
+
+    # Verify
     assert len(response["tasks"]) == 1
     response = client.run_task(
         cluster="test_ecs_cluster",
@@ -1928,6 +1928,13 @@ def test_run_task():
     assert task["startedBy"] == "moto"
     assert task["stoppedReason"] == ""
     assert task["tags"][0].get("value") == "tagValue0"
+    assert len(task["containers"]) == 1
+
+    con = task["containers"][0]
+    assert td["taskDefinition"]["taskDefinitionArn"] in con["containerArn"]
+    assert con["taskArn"] == td["taskDefinition"]["taskDefinitionArn"]
+    assert con["lastStatus"] == "PENDING"
+    assert con["image"] == image
 
 
 @mock_aws
diff --git a/tests/test_timestreaminfluxdb/__init__.py b/tests/test_timestreaminfluxdb/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_timestreaminfluxdb/test_timestreaminfluxdb.py b/tests/test_timestreaminfluxdb/test_timestreaminfluxdb.py
new file mode 100644
index 000000000..8721ba2b7
--- /dev/null
+++ b/tests/test_timestreaminfluxdb/test_timestreaminfluxdb.py
@@ -0,0 +1,280 @@
+"""Unit tests for timestreaminfluxdb-supported APIs."""
+
+import boto3
+import pytest
+from botocore.exceptions import ClientError
+
+from moto import mock_aws
+
+# 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
+
+
+"""Unit tests for timestreaminfluxdb-supported APIs."""
+
+
+@mock_aws
+def test_create_db_instance_success():
+    client = boto3.client("timestream-influxdb", region_name="us-east-1")
+
+    response = client.create_db_instance(
+        name="test-instance",
+        password="password123",
+        dbInstanceType="db.influx.medium",
+        vpcSubnetIds=["subnet-0123456789abcdef0"],
+        vpcSecurityGroupIds=["sg-0123456789abcdef0"],
+        allocatedStorage=123,
+    )
+
+    assert response["allocatedStorage"] == 123
+    assert response["dbInstanceType"] == "db.influx.medium"
+    assert response["name"] == "test-instance"
+    assert response["vpcSecurityGroupIds"] == ["sg-0123456789abcdef0"]
+    assert response["vpcSubnetIds"] == ["subnet-0123456789abcdef0"]
+
+    assert "arn" in response
+    assert "availabilityZone" in response
+    assert "dbStorageType" in response
+    assert "deploymentType" in response
+    assert "endpoint" in response
+    assert "id" in response
+    assert "influxAuthParametersSecretArn" in response
+    assert "networkType" in response
+    assert "port" in response
+    assert "publiclyAccessible" in response
+    assert "secondaryAvailabilityZone" in response
+    assert "status" in response
+    assert "logDeliveryConfiguration" in response
+
+
+@mock_aws
+def test_create_db_instance_duplicate_identifier():
+    client = boto3.client("timestream-influxdb", region_name="us-east-1")
+
+    client.create_db_instance(
+        name="test-instance",
+        password="password123",
+        dbInstanceType="db.influx.medium",
+        vpcSubnetIds=["subnet-0123456789abcdef0"],
+        vpcSecurityGroupIds=["sg-0123456789abcdef0"],
+        allocatedStorage=123,
+    )
+
+    with pytest.raises(ClientError) as exc:
+        client.create_db_instance(
+            name="test-instance",
+            password="password123",
+            dbInstanceType="db.influx.medium",
+            vpcSubnetIds=["subnet-0123456789abcdef0"],
+            vpcSecurityGroupIds=["sg-0123456789abcdef0"],
+            allocatedStorage=123,
+        )
+
+    assert exc.value.response["Error"]["Code"] == "ConflictException"
+
+
+@mock_aws
+def test_create_db_instance_name_invalid():
+    invalid_names = [
+        "1muststartwithletter",
+        "cantendwithhyphen_",
+        "nodoublehyphen--",
+        "no",
+        "longlonglonglonglonglonglonglonglonglongname",
+    ]
+    client = boto3.client("timestream-influxdb", region_name="us-east-1")
+
+    for invalid_name in invalid_names:
+        with pytest.raises(ClientError) as exc:
+            client.create_db_instance(
+                name="invalid_name",
+                password="password123",
+                dbInstanceType="db.influx.medium",
+                vpcSubnetIds=["subnet-0123456789abcdef0"],
+                vpcSecurityGroupIds=["sg-0123456789abcdef0"],
+                allocatedStorage=123,
+            )
+        assert exc.value.response["Error"]["Code"] == "ValidationException"
+
+
+@mock_aws
+def test_create_db_instance_invalid_storage_type():
+    client = boto3.client("timestream-influxdb", region_name="us-east-1")
+    with pytest.raises(ClientError) as exc:
+        client.create_db_instance(
+            name="test-instance",
+            password="password123",
+            dbInstanceType="db.influx.medium",
+            vpcSubnetIds=["subnet-0123456789abcdef0"],
+            vpcSecurityGroupIds=["sg-0123456789abcdef0"],
+            allocatedStorage=123,
+            dbStorageType="invalid",
+        )
+
+        assert exc.value.response["Error"]["Code"] == "ValidationException"
+
+
+@mock_aws
+def test_create_db_instance_invalid_instance_type():
+    client = boto3.client("timestream-influxdb", region_name="us-east-1")
+
+    with pytest.raises(ClientError) as exc:
+        client.create_db_instance(
+            name="test-instance",
+            password="password123",
+            vpcSubnetIds=["subnet-0123456789abcdef0"],
+            vpcSecurityGroupIds=["sg-0123456789abcdef0"],
+            allocatedStorage=123,
+            dbInstanceType="invalid",
+        )
+
+    assert exc.value.response["Error"]["Code"] == "ValidationException"
+
+
+@mock_aws
+def test_delete_db_instance():
+    client = boto3.client("timestream-influxdb", region_name="us-east-1")
+
+    response = client.create_db_instance(
+        name="test-instance",
+        password="password123",
+        dbInstanceType="db.influx.medium",
+        vpcSubnetIds=["subnet-0123456789abcdef0"],
+        vpcSecurityGroupIds=["sg-0123456789abcdef0"],
+        allocatedStorage=123,
+    )
+
+    id = response["id"]
+
+    response = client.delete_db_instance(identifier=id)
+    assert response["allocatedStorage"] == 123
+    assert response["dbInstanceType"] == "db.influx.medium"
+    assert response["name"] == "test-instance"
+    assert response["vpcSecurityGroupIds"] == ["sg-0123456789abcdef0"]
+    assert response["vpcSubnetIds"] == ["subnet-0123456789abcdef0"]
+
+    assert "arn" in response
+    assert "availabilityZone" in response
+    assert "dbStorageType" in response
+    assert "deploymentType" in response
+    assert "endpoint" in response
+    assert "id" in response
+    assert "influxAuthParametersSecretArn" in response
+    assert "networkType" in response
+    assert "port" in response
+    assert "publiclyAccessible" in response
+    assert "secondaryAvailabilityZone" in response
+    assert "logDeliveryConfiguration" in response
+
+    assert response["status"] == "DELETING"
+
+
+@mock_aws
+def test_delete_db_instance_invalid_name():
+    client = boto3.client("timestream-influxdb", region_name="us-east-1")
+
+    id = "-100000"
+    with pytest.raises(ClientError) as exc:
+        client.delete_db_instance(identifier=id)
+
+    assert exc.value.response["Error"]["Code"] == "ResourceNotFoundException"
+
+
+@mock_aws
+def test_get_db_instance():
+    client = boto3.client("timestream-influxdb", region_name="us-east-1")
+
+    created_instance = client.create_db_instance(
+        name="test-instance",
+        password="password123",
+        dbInstanceType="db.influx.medium",
+        vpcSubnetIds=["subnet-0123456789abcdef0"],
+        vpcSecurityGroupIds=["sg-0123456789abcdef0"],
+        allocatedStorage=123,
+    )
+
+    id = created_instance["id"]
+    response = client.get_db_instance(identifier=id)
+    assert response["allocatedStorage"] == 123
+    assert response["dbInstanceType"] == "db.influx.medium"
+    assert response["name"] == "test-instance"
+    assert response["vpcSecurityGroupIds"] == ["sg-0123456789abcdef0"]
+    assert response["vpcSubnetIds"] == ["subnet-0123456789abcdef0"]
+
+    assert "arn" in response
+    assert "availabilityZone" in response
+    assert "dbStorageType" in response
+    assert "deploymentType" in response
+    assert "endpoint" in response
+    assert "id" in response
+    assert "influxAuthParametersSecretArn" in response
+    assert "networkType" in response
+    assert "port" in response
+    assert "publiclyAccessible" in response
+    assert "secondaryAvailabilityZone" in response
+    assert "logDeliveryConfiguration" in response
+    assert "status" in response
+
+
+@mock_aws
+def test_get_db_instance_invalid():
+    client = boto3.client("timestream-influxdb", region_name="us-east-1")
+
+    id = "-100000"
+    with pytest.raises(ClientError) as exc:
+        client.get_db_instance(identifier=id)
+
+    assert exc.value.response["Error"]["Code"] == "ResourceNotFoundException"
+
+
+@mock_aws
+def test_list_db_instances_empty():
+    client = boto3.client("timestream-influxdb", region_name="us-east-1")
+    response = client.list_db_instances()
+    assert len(response["items"]) == 0
+
+
+@mock_aws
+def test_list_db_instances():
+    client = boto3.client("timestream-influxdb", region_name="us-east-1")
+    client.create_db_instance(
+        name="test-instance1",
+        password="password123",
+        dbInstanceType="db.influx.medium",
+        vpcSubnetIds=["subnet-0123456789abcdef0"],
+        vpcSecurityGroupIds=["sg-0123456789abcdef0"],
+        allocatedStorage=123,
+    )
+
+    client.create_db_instance(
+        name="test-instance2",
+        password="password123",
+        dbInstanceType="db.influx.medium",
+        vpcSubnetIds=["subnet-0123456789abcdef0"],
+        vpcSecurityGroupIds=["sg-0123456789abcdef0"],
+        allocatedStorage=123,
+    )
+
+    response = client.list_db_instances()
+    resources = response["items"]
+    assert len(resources) == 2
+
+    for resource in resources:
+        assert "allocatedStorage" in resource
+        assert "arn" in resource
+        assert "dbInstanceType" in resource
+        assert "dbStorageType" in resource
+        assert "deploymentType" in resource
+        assert "endpoint" in resource
+        assert "id" in resource
+        assert "name" in resource
+        assert "networkType" in resource
+        assert "port" in resource
+        assert "status" in resource
+
+    # delete one and confirm only one left
+    client.delete_db_instance(identifier=resources[0]["id"])
+
+    response = client.list_db_instances()
+    resources = response["items"]
+    assert len(resources) == 1
diff --git a/tests/test_timestreaminfluxdb/test_timestreaminfluxdb_tagging.py b/tests/test_timestreaminfluxdb/test_timestreaminfluxdb_tagging.py
new file mode 100644
index 000000000..823f3dcb5
--- /dev/null
+++ b/tests/test_timestreaminfluxdb/test_timestreaminfluxdb_tagging.py
@@ -0,0 +1,69 @@
+import boto3
+
+from moto import mock_aws
+
+
+@mock_aws
+def test_list_tags_for_db_instance():
+    client = boto3.client("timestream-influxdb", region_name="us-east-1")
+
+    response = client.create_db_instance(
+        name="test-instance",
+        password="password123",
+        dbInstanceType="db.influx.medium",
+        vpcSubnetIds=["subnet-0123456789abcdef0"],
+        vpcSecurityGroupIds=["sg-0123456789abcdef0"],
+        allocatedStorage=123,
+        tags={"key1": "value1", "key2": "value2"},
+    )
+
+    arn = response["arn"]
+    response = client.list_tags_for_resource(resourceArn=arn)
+    assert response["tags"] == {"key1": "value1", "key2": "value2"}
+
+
+@mock_aws
+def test_tag_resources():
+    client = boto3.client("timestream-influxdb", region_name="us-east-1")
+
+    response = client.create_db_instance(
+        name="test-instance",
+        password="password123",
+        dbInstanceType="db.influx.medium",
+        vpcSubnetIds=["subnet-0123456789abcdef0"],
+        vpcSecurityGroupIds=["sg-0123456789abcdef0"],
+        allocatedStorage=123,
+        tags={"key1": "value1", "key2": "value2"},
+    )
+
+    arn = response["arn"]
+
+    # add a tag
+    additional_tags = {"key3": "value3"}
+    response = client.tag_resource(resourceArn=arn, tags=additional_tags)
+
+    response = client.list_tags_for_resource(resourceArn=arn)
+    assert response["tags"] == {"key1": "value1", "key2": "value2", "key3": "value3"}
+
+
+@mock_aws
+def test_untag_resources():
+    client = boto3.client("timestream-influxdb", region_name="us-east-1")
+
+    response = client.create_db_instance(
+        name="test-instance",
+        password="password123",
+        dbInstanceType="db.influx.medium",
+        vpcSubnetIds=["subnet-0123456789abcdef0"],
+        vpcSecurityGroupIds=["sg-0123456789abcdef0"],
+        allocatedStorage=123,
+        tags={"key1": "value1", "key2": "value2"},
+    )
+
+    arn = response["arn"]
+
+    # remove tags
+    response = client.untag_resource(resourceArn=arn, tagKeys=["key1"])
+
+    response = client.list_tags_for_resource(resourceArn=arn)
+    assert response["tags"] == {"key2": "value2"}