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

feat: implement mock for S3Tables service #8470

Merged
merged 57 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
d803f46
feat: modify backend_index negative lookahead
felixscherz Dec 30, 2024
b1e52b1
feat: explicitly set s3tables regions
felixscherz Jan 1, 2025
14d7093
feat: implement create_table_bucket
felixscherz Jan 1, 2025
c57f3b9
feat: add scaffolding for other table_bucket methods
felixscherz Jan 1, 2025
3e23356
fix: add s3tables to typing
felixscherz Jan 1, 2025
55821cd
fix: update s3 urls to not overlap with s3tables
felixscherz Jan 2, 2025
9ed68e9
feat: implement list_table_buckets
felixscherz Jan 2, 2025
32514de
feat: implement get_table_bucket
felixscherz Jan 2, 2025
39e0fa3
feat: implement delete_table_bucket
felixscherz Jan 2, 2025
98d928a
chore: run formatter
felixscherz Jan 2, 2025
2d0cffe
chore: add type hints
felixscherz Jan 3, 2025
dde45d0
fix: no longer return empty continuationToken
felixscherz Jan 3, 2025
8ad9d5a
refactor: move response formatting out of models
felixscherz Jan 3, 2025
8068329
feat: validate table bucket name
felixscherz Jan 3, 2025
2403717
fix: response headers
felixscherz Jan 3, 2025
f742e86
chore: format
felixscherz Jan 4, 2025
7790f6d
chore: add types
felixscherz Jan 4, 2025
641b3aa
fix: clean up path variables
felixscherz Jan 5, 2025
460b282
fix: change url patterns to work wiht slashes in path
felixscherz Jan 5, 2025
ac04831
chore: remove comments
felixscherz Jan 5, 2025
ea1a555
feat: scaffold namespace operations
felixscherz Jan 10, 2025
c0ba657
feat: implement create_namespace
felixscherz Jan 10, 2025
d038686
feat: implement get_namespace
felixscherz Jan 10, 2025
76584a5
feat: implement list_namespaces
felixscherz Jan 10, 2025
9e3c0a5
feat: implement delete_namespace
felixscherz Jan 10, 2025
db550e1
chore: format
felixscherz Jan 10, 2025
a817c9b
chore: add type hints
felixscherz Jan 11, 2025
0c3088b
test: add server test for create_namespace
felixscherz Jan 11, 2025
33a2956
feat: adapt how pagination works
felixscherz Jan 11, 2025
463afbc
chore: add type hints
felixscherz Jan 11, 2025
c3922f4
feat: add error handling for buckets
felixscherz Jan 11, 2025
659da73
test: use random bucket names for tests
felixscherz Jan 11, 2025
19b54da
fix: change how to handle path variables
felixscherz Jan 11, 2025
3d4cf86
feat: implement create_table
felixscherz Jan 11, 2025
0be049f
feat: implement get_table
felixscherz Jan 12, 2025
d8a13f7
feat: implement list_tables
felixscherz Jan 12, 2025
21da908
feat: implement delete_table
felixscherz Jan 12, 2025
6e7d1bf
chore: format
felixscherz Jan 12, 2025
67d93e5
fix: url handling for tables
felixscherz Jan 12, 2025
9ff07e4
feat: start modeling s3 tables
felixscherz Jan 12, 2025
74df3e2
feat: create s3 buckets when creating s3 tables
felixscherz Jan 12, 2025
8916bed
feat: implement get/update_table_metadata_location
felixscherz Jan 12, 2025
0e24ad2
test: write metadata to underlying s3 bucket
felixscherz Jan 15, 2025
57f56cd
fix: type hints for python 3.8
felixscherz Jan 15, 2025
d187aed
feat: improve excpetion handling
felixscherz Jan 16, 2025
3b5ca32
feat: improve handling of s3 table storage
felixscherz Jan 17, 2025
6752429
feat: disable specific s3 methods for table storage buckets
felixscherz Jan 17, 2025
04d616d
feat: implement rename_table
felixscherz Jan 18, 2025
968cc5b
docs: document implemented services
felixscherz Jan 18, 2025
e5ef686
fix: urls for rename_table
felixscherz Jan 18, 2025
a77ca1d
test: assert delete_table raises on version_token mismatch
felixscherz Jan 20, 2025
4cd5c73
feat: simplify creating underlying s3 table storage buckets
felixscherz Jan 20, 2025
04b9fb7
chore: run formatter
felixscherz Jan 20, 2025
fdafeb8
refactor: use moto pagination decorator
felixscherz Jan 20, 2025
60670e8
fix: rely on internal random.uuid4 instead of uuid.uuid4
felixscherz Jan 20, 2025
daa0f42
feat: check metadata location before updating it
felixscherz Jan 20, 2025
c7888a1
test: validate error messages
felixscherz Jan 20, 2025
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
45 changes: 45 additions & 0 deletions docs/docs/services/s3tables.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
.. _implementedservice_s3tables:

.. |start-h3| raw:: html

<h3>

.. |end-h3| raw:: html

</h3>

========
s3tables
========

.. autoclass:: moto.s3tables.models.S3TablesBackend

|start-h3| Implemented features for this service |end-h3|

- [X] create_namespace
- [X] create_table
- [X] create_table_bucket
- [X] delete_namespace
- [X] delete_table
- [X] delete_table_bucket
- [ ] delete_table_bucket_policy
- [ ] delete_table_policy
- [X] get_namespace
- [X] get_table
- [X] get_table_bucket
- [ ] get_table_bucket_maintenance_configuration
- [ ] get_table_bucket_policy
- [ ] get_table_maintenance_configuration
- [ ] get_table_maintenance_job_status
- [ ] get_table_metadata_location
- [ ] get_table_policy
- [X] list_namespaces
- [X] list_table_buckets
- [X] list_tables
- [ ] put_table_bucket_maintenance_configuration
- [ ] put_table_bucket_policy
- [ ] put_table_maintenance_configuration
- [ ] put_table_policy
- [X] rename_table
- [X] update_table_metadata_location

5 changes: 3 additions & 2 deletions moto/backend_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,17 +160,18 @@
"route53resolver",
re.compile("https?://route53resolver\\.(.+)\\.amazonaws\\.com"),
),
("s3", re.compile("https?://s3(?!-control)(.*)\\.amazonaws.com")),
("s3", re.compile("https?://s3(?!(-control|tables))(.*)\\.amazonaws.com")),
(
"s3",
re.compile(
"https?://(?P<bucket_name>[a-zA-Z0-9\\-_.]*)\\.?s3(?!-control)(.*)\\.amazonaws.com"
"https?://(?P<bucket_name>[a-zA-Z0-9\\-_.]*)\\.?s3(?!(-control|tables))(.*)\\.amazonaws.com"
),
),
(
"s3control",
re.compile("https?://([0-9]+)\\.s3-control\\.(.+)\\.amazonaws\\.com"),
),
("s3tables", re.compile("https?://s3tables\\.(.+)\\.amazonaws\\.com")),
("sagemaker", re.compile("https?://api\\.sagemaker\\.(.+)\\.amazonaws.com")),
(
"sagemakermetrics",
Expand Down
4 changes: 4 additions & 0 deletions moto/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@
from moto.route53resolver.models import Route53ResolverBackend
from moto.s3.models import S3Backend
from moto.s3control.models import S3ControlBackend
from moto.s3tables.models import S3TablesBackend
from moto.sagemaker.models import SageMakerModelBackend
from moto.sagemakermetrics.models import SageMakerMetricsBackend
from moto.sagemakerruntime.models import SageMakerRuntimeBackend
Expand Down Expand Up @@ -307,6 +308,7 @@ def get_service_from_url(url: str) -> Optional[str]:
"Literal['s3']",
"Literal['s3bucket_path']",
"Literal['s3control']",
"Literal['s3tables']",
"Literal['sagemaker']",
"Literal['sagemaker-metrics']",
"Literal['sagemaker-runtime']",
Expand Down Expand Up @@ -753,6 +755,8 @@ def get_backend(
) -> "BackendDict[WorkSpacesWebBackend]": ...
@overload
def get_backend(name: "Literal['xray']") -> "BackendDict[XRayBackend]": ...
@overload
def get_backend(name: "Literal['s3tables']") -> "BackendDict[S3TablesBackend]": ...


def get_backend(name: SERVICE_NAMES) -> "BackendDict[SERVICE_BACKEND]":
Expand Down
10 changes: 10 additions & 0 deletions moto/s3/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -635,3 +635,13 @@ def __init__(self) -> None:
"DaysMustProvidedExceptForSelectRequest",
"`Days` must be provided except for select requests",
)


class MethodNotAllowed(S3ClientError):
code = 405

def __init__(self) -> None:
super().__init__(
"MethodNotAllowed",
"The specified method is not allowed against this resource.",
)
40 changes: 39 additions & 1 deletion moto/s3/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
InvalidTagError,
InvalidTargetBucketForLogging,
MalformedXML,
MethodNotAllowed,
MissingBucket,
MissingKey,
NoSuchPublicAccessBlockConfiguration,
Expand Down Expand Up @@ -1646,6 +1647,9 @@
return now.strftime("%Y-%m-%dT%H:%M:%SZ")


class FakeTableStorageBucket(FakeBucket): ...


class S3Backend(BaseBackend, CloudWatchMetricProvider):
"""
Custom S3 endpoints are supported, if you are using a S3-compatible storage solution like Ceph.
Expand Down Expand Up @@ -1711,6 +1715,7 @@
def __init__(self, region_name: str, account_id: str):
super().__init__(region_name, account_id)
self.buckets: Dict[str, FakeBucket] = {}
self.table_buckets: Dict[str, FakeTableStorageBucket] = {}
self.tagger = TaggingService()
self._pagination_tokens: Dict[str, str] = {}

Expand All @@ -1719,7 +1724,9 @@
# Ensure that these TemporaryFile-objects are closed, and leave no filehandles open
#
# First, check all known buckets/keys
for bucket in self.buckets.values():
for bucket in itertools.chain(
self.buckets.values(), self.table_buckets.values()
):
for key in bucket.keys.values(): # type: ignore
if isinstance(key, FakeKey):
key.dispose()
Expand Down Expand Up @@ -1876,13 +1883,25 @@

return new_bucket

def create_table_storage_bucket(self, region_name: str) -> FakeTableStorageBucket:
# every s3 table is assigned a unique s3 bucket with a random name
bucket_name = f"{str(random.uuid4())}--table-s3"
new_bucket = FakeTableStorageBucket(
name=bucket_name, account_id=self.account_id, region_name=region_name
)
self.table_buckets[bucket_name] = new_bucket
return new_bucket

def list_buckets(self) -> List[FakeBucket]:
return list(self.buckets.values())

def get_bucket(self, bucket_name: str) -> FakeBucket:
if bucket_name in self.buckets:
return self.buckets[bucket_name]

if bucket_name in self.table_buckets:
return self.table_buckets[bucket_name]

if bucket_name in s3_backends.bucket_accounts:
if not s3_allow_crossdomain_access():
raise AccessDeniedByLock
Expand All @@ -1903,6 +1922,19 @@
s3_backends.bucket_accounts.pop(bucket_name, None)
return self.buckets.pop(bucket_name)

def delete_table_storage_bucket(self, bucket_name: str) -> Optional[FakeBucket]:
bucket = self.get_bucket(bucket_name)
assert isinstance(bucket, FakeTableStorageBucket)
# table storage buckets can be deleted while not empty
if bucket.keys:
for key in bucket.keys.values(): # type: ignore
if isinstance(key, FakeKey):
key.dispose()
for part in bucket.multiparts.values():
part.dispose()

Check warning on line 1934 in moto/s3/models.py

View check run for this annotation

Codecov / codecov/patch

moto/s3/models.py#L1930-L1934

Added lines #L1930 - L1934 were not covered by tests
s3_backends.bucket_accounts.pop(bucket_name, None)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This doesn't seem necessary - we're never adding the bucket to bucket_accounts

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The bucket is added to bucket_accounts as part of the FakeBucket.__init__ method that's why we're removing it here. It does need to be part of bucket_accounts to be able to lookup the actual account the bucket is in I think.

return self.table_buckets.pop(bucket_name)

def get_bucket_accelerate_configuration(self, bucket_name: str) -> Optional[str]:
bucket = self.get_bucket(bucket_name)
return bucket.accelerate_configuration
Expand Down Expand Up @@ -2644,6 +2676,8 @@

MOTO_S3_DEFAULT_MAX_KEYS=5
"""
if isinstance(bucket, FakeTableStorageBucket):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does AWS throw the same error here?

It seems like a useful feature to let users look at the actual data that was stored, but this is the first time that I'm looking at this service, so maybe I'm missing something obvious 🙂

Copy link
Contributor Author

Choose a reason for hiding this comment

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

AWS does throw the same error, I think it has to do with the way they want users to use the service i.e. not rely on list operations. They also disable delete_object which raises the same exception. I think its important to replicate this behaviour since AWS not supporting it can throw a lot of people off:)

raise MethodNotAllowed()
key_results = set()
folder_results = set()
if prefix:
Expand Down Expand Up @@ -2702,6 +2736,8 @@

MOTO_S3_DEFAULT_MAX_KEYS=5
"""
if isinstance(bucket, FakeTableStorageBucket):
raise MethodNotAllowed()
result_keys, result_folders, _, _ = self.list_objects(
bucket, prefix, delimiter, marker=None, max_keys=None
)
Expand Down Expand Up @@ -2784,6 +2820,8 @@
bypass: bool = False,
) -> Tuple[bool, Dict[str, Any]]:
bucket = self.get_bucket(bucket_name)
if isinstance(bucket, FakeTableStorageBucket):
raise MethodNotAllowed()

response_meta = {}
delete_key = bucket.keys.get(key_name)
Expand Down
4 changes: 2 additions & 2 deletions moto/s3/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

# Catch s3.amazonaws.com, but not s3-control.amazonaws.com
url_bases = [
r"https?://s3(?!-control)(.*)\.amazonaws.com",
r"https?://(?P<bucket_name>[a-zA-Z0-9\-_.]*)\.?s3(?!-control)(.*)\.amazonaws.com",
r"https?://s3(?!(-control|tables))(.*)\.amazonaws.com",
r"https?://(?P<bucket_name>[a-zA-Z0-9\-_.]*)\.?s3(?!(-control|tables))(.*)\.amazonaws.com",
]

url_bases.extend(settings.get_s3_custom_endpoints())
Expand Down
1 change: 1 addition & 0 deletions moto/s3tables/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .models import s3tables_backends # noqa: F401
101 changes: 101 additions & 0 deletions moto/s3tables/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Exceptions raised by the s3tables service."""

from moto.core.exceptions import JsonRESTError


class BadRequestException(JsonRESTError):
code = 400

def __init__(self, message: str) -> None:
super().__init__("BadRequestException", message)


class InvalidContinuationToken(BadRequestException):
msg = "The continuation token is not valid."

def __init__(self) -> None:
super().__init__(self.msg)

Check warning on line 17 in moto/s3tables/exceptions.py

View check run for this annotation

Codecov / codecov/patch

moto/s3tables/exceptions.py#L17

Added line #L17 was not covered by tests


class InvalidTableBucketName(BadRequestException):
msg = "The specified bucket name is not valid."

def __init__(self) -> None:
super().__init__(self.msg)


class InvalidTableName(BadRequestException):
template = "1 validation error detected: Value '%s' at 'name' failed to satisfy constraint: Member must satisfy regular expression pattern: [0-9a-z_]*"

def __init__(self, name: str) -> None:
super().__init__(self.template.format(name))

Check warning on line 31 in moto/s3tables/exceptions.py

View check run for this annotation

Codecov / codecov/patch

moto/s3tables/exceptions.py#L31

Added line #L31 was not covered by tests


class InvalidNamespaceName(BadRequestException):
msg = "The specified namespace name is not valid."

def __init__(self) -> None:
super().__init__(self.msg)

Check warning on line 38 in moto/s3tables/exceptions.py

View check run for this annotation

Codecov / codecov/patch

moto/s3tables/exceptions.py#L38

Added line #L38 was not covered by tests


class InvalidMetadataLocation(BadRequestException):
msg = "The specified metadata location is not valid."

def __init__(self) -> None:
super().__init__(self.msg)


class NothingToRename(BadRequestException):
msg = "Neither a new namespace name nor a new table name is specified."

def __init__(self) -> None:
super().__init__(self.msg)


class NotFoundException(JsonRESTError):
code = 404

def __init__(self, message: str) -> None:
super().__init__("NotFoundException", message)


class NamespaceDoesNotExist(NotFoundException):
msg = "The specified namespace does not exist."

def __init__(self) -> None:
super().__init__(self.msg)

Check warning on line 66 in moto/s3tables/exceptions.py

View check run for this annotation

Codecov / codecov/patch

moto/s3tables/exceptions.py#L66

Added line #L66 was not covered by tests


class DestinationNamespaceDoesNotExist(NotFoundException):
msg = "The specified destination namespace does not exist."

def __init__(self) -> None:
super().__init__(self.msg)


class TableDoesNotExist(NotFoundException):
msg = "The specified table does not exist."

def __init__(self) -> None:
super().__init__(self.msg)

Check warning on line 80 in moto/s3tables/exceptions.py

View check run for this annotation

Codecov / codecov/patch

moto/s3tables/exceptions.py#L80

Added line #L80 was not covered by tests


class ConflictException(JsonRESTError):
code = 409

def __init__(self, message: str) -> None:
super().__init__("ConflictException", message)


class VersionTokenMismatch(ConflictException):
msg = "Provided version token does not match the table version token."

def __init__(self) -> None:
super().__init__(self.msg)


class TableAlreadyExists(ConflictException):
msg = "A table with an identical name already exists in the namespace."

def __init__(self) -> None:
super().__init__(self.msg)

Check warning on line 101 in moto/s3tables/exceptions.py

View check run for this annotation

Codecov / codecov/patch

moto/s3tables/exceptions.py#L101

Added line #L101 was not covered by tests
Loading
Loading