Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DSQL: implement create_cluster() #8425

Merged
merged 2 commits into from
Dec 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions moto/backend_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
("directconnect", re.compile("https?://directconnect\\.(.+)\\.amazonaws\\.com")),
("dms", re.compile("https?://dms\\.(.+)\\.amazonaws\\.com")),
("ds", re.compile("https?://ds\\.(.+)\\.amazonaws\\.com")),
("dsql", re.compile("https?://dsql\\.(.+)\\.api\\.aws")),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DSQL Endpoint is sub domain of api.aws like dsql.us-east-1.api.aws not amazonaws.com

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. I wonder whether that's an outlier, or whether they're going to host other (existing?) services there as well.

("dynamodb", re.compile("https?://dynamodb\\.(.+)\\.amazonaws\\.com")),
(
"dynamodbstreams",
Expand Down
4 changes: 4 additions & 0 deletions moto/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
from moto.directconnect.models import DirectConnectBackend
from moto.dms.models import DatabaseMigrationServiceBackend
from moto.ds.models import DirectoryServiceBackend
from moto.dsql.models import AuroraDSQLBackend
from moto.dynamodb.models import DynamoDBBackend
from moto.dynamodb_v20111205.models import (
DynamoDBBackend as DynamoDBBackend_v20111205,
Expand Down Expand Up @@ -225,6 +226,7 @@ def get_service_from_url(url: str) -> Optional[str]:
"Literal['directconnect']",
"Literal['dms']",
"Literal['ds']",
"Literal['dsql']",
"Literal['dynamodb']",
"Literal['dynamodb_v20111205']",
"Literal['dynamodbstreams']",
Expand Down Expand Up @@ -440,6 +442,8 @@ def get_backend(
@overload
def get_backend(name: "Literal['ds']") -> "BackendDict[DirectoryServiceBackend]": ...
@overload
def get_backend(name: "Literal['dsql']") -> "BackendDict[AuroraDSQLBackend]": ...
@overload
def get_backend(name: "Literal['dynamodb']") -> "BackendDict[DynamoDBBackend]": ...
@overload
def get_backend(
Expand Down
1 change: 1 addition & 0 deletions moto/dsql/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .models import dsql_backends # noqa: F401
1 change: 1 addition & 0 deletions moto/dsql/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Exceptions raised by the dsql service."""
81 changes: 81 additions & 0 deletions moto/dsql/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""AuroraDSQLBackend class with methods for supported APIs."""

from collections import OrderedDict
from typing import Any, Dict, Optional

from moto.core.base_backend import BackendDict, BaseBackend
from moto.core.common_models import BaseModel
from moto.core.utils import iso_8601_datetime_with_milliseconds, utcnow
from moto.moto_api._internal import mock_random
from moto.moto_api._internal.managed_state_model import ManagedState
from moto.utilities.utils import get_partition


class Cluster(BaseModel, ManagedState):
"""Model for an AuroraDSQL cluster."""

def to_dict(self) -> Dict[str, Any]:
dct = {
"identifier": self.identifier,
"arn": self.arn,
"status": self.status,
"creationTime": iso_8601_datetime_with_milliseconds(self.creation_time),
"deletionProtectionEnabled": self.deletion_protection_enabled,
}
return {k: v for k, v in dct.items() if v is not None}

def __init__(
self,
region_name: str,
account_id: str,
deletion_protection_enabled: Optional[bool],
tags: Optional[Dict[str, str]],
client_token: Optional[str],
):
ManagedState.__init__(
self, "dsql::cluster", transitions=[("CREATING", "ACTIVE")]
)
self.region_name = region_name
self.account_id = account_id
self.identifier = mock_random.get_random_hex(26)
self.arn = f"arn:{get_partition(self.region_name)}:dsql:{self.region_name}:{self.account_id}:cluster/{self.identifier}"
self.creation_time = utcnow()
self.deletion_protection_enabled = deletion_protection_enabled
self.tags = tags
self.client_token = client_token


class AuroraDSQLBackend(BaseBackend):
"""Implementation of AuroraDSQL APIs."""

def __init__(self, region_name: str, account_id: str):
super().__init__(region_name, account_id)
self.region_name = region_name
self.account_id = account_id
self.partition = get_partition(region_name)
self.clusters: Dict[str, Cluster] = OrderedDict()

def create_cluster(
self,
deletion_protection_enabled: bool,
tags: Optional[Dict[str, str]],
client_token: Optional[str],
) -> Cluster:
cluster = Cluster(
self.region_name,
self.account_id,
deletion_protection_enabled,
tags,
client_token,
)
return cluster


dsql_backends = BackendDict(
AuroraDSQLBackend,
"dsql",
# currently botocore does not provide a dsql endpoint
# https://github.com/boto/botocore/blob/e07cddc333fe4fb90efcd5d04324dd83f9cc3a57/botocore/data/endpoints.json
use_boto3_regions=False,
additional_regions=["us-east-1", "us-east-2"],
)
32 changes: 32 additions & 0 deletions moto/dsql/responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Handles incoming dsql requests, invokes methods, returns responses."""

import json

from moto.core.responses import BaseResponse

from .models import AuroraDSQLBackend, dsql_backends


class AuroraDSQLResponse(BaseResponse):
"""Handler for AuroraDSQL requests and responses."""

def __init__(self) -> None:
super().__init__(service_name="dsql")

@property
def dsql_backend(self) -> AuroraDSQLBackend:
"""Return backend instance specific for this region."""
return dsql_backends[self.current_account][self.region]

def create_cluster(self) -> str:
params = self._get_params()
deletion_protection_enabled = params.get("deletionProtectionEnabled", True)
tags = params.get("tags")
client_token = params.get("clientToken")
cluster = self.dsql_backend.create_cluster(
deletion_protection_enabled=deletion_protection_enabled,
tags=tags,
client_token=client_token,
)

return json.dumps(dict(cluster.to_dict()))
11 changes: 11 additions & 0 deletions moto/dsql/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""dsql base URL and path."""

from .responses import AuroraDSQLResponse

url_bases = [
r"https?://dsql\.(.+)\.api\.aws",
]

url_paths = {
"{0}/cluster$": AuroraDSQLResponse.dispatch,
}
3 changes: 3 additions & 0 deletions moto/moto_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
state_manager.register_default_transition(
model_name="dax::cluster", transition={"progression": "manual", "times": 4}
)
state_manager.register_default_transition(
"dsql::cluster", transition={"progression": "manual", "times": 1}
)
state_manager.register_default_transition(
model_name="ecs::task", transition={"progression": "manual", "times": 1}
)
Expand Down
2 changes: 2 additions & 0 deletions moto/moto_server/werkzeug_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@
# All MediaStore API calls have a target header
# If no target is set, assume we're trying to reach the mediastore-data service
host = f"data.{service}.{region}.amazonaws.com"
elif service == "dsql":
host = f"{service}.{region}.api.aws"

Check warning on line 140 in moto/moto_server/werkzeug_app.py

View check run for this annotation

Codecov / codecov/patch

moto/moto_server/werkzeug_app.py#L140

Added line #L140 was not covered by tests
elif service == "dynamodb":
if environ["HTTP_X_AMZ_TARGET"].startswith("DynamoDBStreams"):
host = "dynamodbstreams"
Expand Down
Empty file added tests/test_dsql/__init__.py
Empty file.
24 changes: 24 additions & 0 deletions tests/test_dsql/test_dsql.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Unit tests for dsql-supported APIs."""

from datetime import datetime

import boto3
from dateutil.tz import tzutc
from freezegun import freeze_time

from moto import mock_aws, settings


@mock_aws
def test_create_cluster():
client = boto3.client("dsql", region_name="us-east-1")
with freeze_time("2024-12-22 12:34:00"):
resp = client.create_cluster()

identifier = resp["identifier"]
assert identifier is not None
assert resp["arn"] == f"arn:aws:dsql:us-east-1:123456789012:cluster/{identifier}"
assert resp["deletionProtectionEnabled"] is True
assert resp["status"] == "CREATING"
if not settings.TEST_SERVER_MODE:
assert resp["creationTime"] == datetime(2024, 12, 22, 12, 34, tzinfo=tzutc())
Loading