-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
Changes from all commits
d803f46
b1e52b1
14d7093
c57f3b9
3e23356
55821cd
9ed68e9
32514de
39e0fa3
98d928a
2d0cffe
dde45d0
8ad9d5a
8068329
2403717
f742e86
7790f6d
641b3aa
460b282
ac04831
ea1a555
c0ba657
d038686
76584a5
9e3c0a5
db550e1
a817c9b
0c3088b
33a2956
463afbc
c3922f4
659da73
19b54da
3d4cf86
0be049f
d8a13f7
21da908
6e7d1bf
67d93e5
9ff07e4
74df3e2
8916bed
0e24ad2
57f56cd
d187aed
3b5ca32
6752429
04d616d
968cc5b
e5ef686
a77ca1d
4cd5c73
04b9fb7
fdafeb8
60670e8
daa0f42
c7888a1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -56,6 +56,7 @@ | |
InvalidTagError, | ||
InvalidTargetBucketForLogging, | ||
MalformedXML, | ||
MethodNotAllowed, | ||
MissingBucket, | ||
MissingKey, | ||
NoSuchPublicAccessBlockConfiguration, | ||
|
@@ -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. | ||
|
@@ -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] = {} | ||
|
||
|
@@ -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() | ||
|
@@ -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 | ||
|
@@ -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() | ||
s3_backends.bucket_accounts.pop(bucket_name, None) | ||
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 | ||
|
@@ -2644,6 +2676,8 @@ | |
|
||
MOTO_S3_DEFAULT_MAX_KEYS=5 | ||
""" | ||
if isinstance(bucket, FakeTableStorageBucket): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 🙂 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
|
@@ -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 | ||
) | ||
|
@@ -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) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .models import s3tables_backends # noqa: F401 |
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) | ||
|
||
|
||
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)) | ||
|
||
|
||
class InvalidNamespaceName(BadRequestException): | ||
msg = "The specified namespace name is not valid." | ||
|
||
def __init__(self) -> None: | ||
super().__init__(self.msg) | ||
|
||
|
||
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) | ||
|
||
|
||
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) | ||
|
||
|
||
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) | ||
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 theFakeBucket.__init__
method that's why we're removing it here. It does need to be part ofbucket_accounts
to be able to lookup the actual account the bucket is in I think.