Skip to content

WIP: add S3Control native provider #22

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
17 changes: 17 additions & 0 deletions localstack/aws/api/s3control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@
TagValueString = str
TrafficDialPercentage = int
VpcId = str
HostId = str
URI = str


class AsyncOperationName(str):
Expand Down Expand Up @@ -430,6 +432,7 @@ class NoSuchPublicAccessBlockConfiguration(ServiceException):
code: str = "NoSuchPublicAccessBlockConfiguration"
sender_fault: bool = False
status_code: int = 404
AccountId: Optional[AccountId]


class NotFoundException(ServiceException):
Expand All @@ -450,6 +453,20 @@ class TooManyTagsException(ServiceException):
status_code: int = 400


class NoSuchAccessPoint(ServiceException):
code: str = "NoSuchAccessPoint"
sender_fault: bool = False
status_code: int = 404
AccessPointName: Optional[AccessPointName]


class InvalidURI(ServiceException):
code: str = "InvalidURI"
sender_fault: bool = False
status_code: int = 400
URI: Optional[URI]


class AbortIncompleteMultipartUpload(TypedDict, total=False):
DaysAfterInitiation: Optional[DaysAfterInitiation]

Expand Down
48 changes: 48 additions & 0 deletions localstack/aws/protocol/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1586,6 +1586,53 @@ def _timestamp_iso8601(value: datetime) -> str:
return value.strftime("%Y-%m-%dT%H:%M:%S.000Z")


class S3ControlResponseSerializer(RestXMLResponseSerializer):
"""
The ``S3ResponseSerializer`` adds some minor logic to handle S3 specific peculiarities with the error response
serialization and the root node tag.
"""

def _serialize_error(
self,
error: ServiceException,
response: HttpResponse,
shape: StructureShape,
operation_model: OperationModel,
mime_type: str,
request_id: str,
) -> None:
# Check if we need to add a namespace
attr = (
{"xmlns": operation_model.metadata.get("xmlNamespace")}
if "xmlNamespace" in operation_model.metadata
else {}
)
root = ETree.Element("ErrorResponse", attr)

error_tag = ETree.SubElement(root, "Error")
# the difference for S3Control is here: it adds additional error tags inside the Error tags, unlike other
# rest-xml services
self._add_error_tags(error, error_tag, mime_type)
request_id_element = ETree.SubElement(root, "RequestId")
request_id_element.text = request_id

host_id_element = ETree.SubElement(root, "HostId")
host_id_element.text = (
"9Gjjt1m+cjU4OPvX9O9/8RuvnG41MRb/18Oux2o5H5MY7ISNTlXN+Dz9IG62/ILVxhAGI0qyPfg="
)

self._add_additional_error_tags(vars(error), error_tag, shape, mime_type)
response.set_response(self._encode_payload(self._node_to_string(root, mime_type)))

@staticmethod
def _timestamp_iso8601(value: datetime) -> str:
"""
This is very specific to S3, S3 returns an ISO8601 timestamp but with milliseconds always set to 000
Some SDKs are very picky about the length
"""
return value.strftime("%Y-%m-%dT%H:%M:%S.000Z")


class SqsQueryResponseSerializer(QueryResponseSerializer):
"""
Unfortunately, SQS uses a rare interpretation of the XML protocol: It uses HTML entities within XML tag text nodes.
Expand Down Expand Up @@ -1761,6 +1808,7 @@ def create_serializer(service: ServiceModel) -> ResponseSerializer:
"sqs-query": SqsQueryResponseSerializer,
"sqs": SqsResponseSerializer,
"s3": S3ResponseSerializer,
"s3control": S3ControlResponseSerializer,
}
protocol_specific_serializers = {
"query": QueryResponseSerializer,
Expand Down
64 changes: 64 additions & 0 deletions localstack/aws/spec-patches.json
Original file line number Diff line number Diff line change
Expand Up @@ -1153,5 +1153,69 @@
"exception": true
}
}
],
"s3control/2018-08-20/service-2": [
{
"op": "add",
"path": "/operations/DeletePublicAccessBlock/http/responseCode",
"value": 204
},
{
"op": "add",
"path": "/shapes/HostId",
"value": {
"type": "string"
}
},
{
"op": "add",
"path": "/shapes/NoSuchPublicAccessBlockConfiguration/members/AccountId",
"value": {
"shape": "AccountId"
}
},
{
"op": "add",
"path": "/shapes/NoSuchAccessPoint",
"value": {
"type": "structure",
"members": {
"AccessPointName": {
"shape": "AccessPointName"
}
},
"error": {
"httpStatusCode": 404
},
"documentation": "<p>The specified accesspoint does not exist</p>",
"exception": true
}
},
{
"op": "add",
"path": "/operations/DeleteAccessPoint/http/responseCode",
"value": 204
},
{
"op": "add",
"path": "/shapes/URI",
"value": {
"type": "string"
}
},
{
"op": "add",
"path": "/shapes/InvalidURI",
"value": {
"type": "structure",
"members": {
"URI": {
"shape": "URI"
}
},
"documentation": "<p>Couldn't parse the specified URI.</p>",
"exception": true
}
}
]
}
7 changes: 7 additions & 0 deletions localstack/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,13 @@ def in_docker():
# whether the S3 legacy V2/ASF provider is enabled
LEGACY_V2_S3_PROVIDER = os.environ.get("PROVIDER_OVERRIDE_S3", "") in ("v2", "legacy_v2", "asf")

# force the native provider for tests
if not os.environ.get("PROVIDER_OVERRIDE_S3CONTROL"):
os.environ["PROVIDER_OVERRIDE_S3CONTROL"] = "v2"

# whether the S3 Control native provider is enabled
NATIVE_S3_CONTROL_PROVIDER = os.environ.get("PROVIDER_OVERRIDE_S3CONTROL", "") == "v2"

# Whether to report internal failures as 500 or 501 errors.
FAIL_FAST = is_env_true("FAIL_FAST")

Expand Down
8 changes: 8 additions & 0 deletions localstack/services/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,14 @@ def s3control():
return Service.for_provider(provider, dispatch_table_factory=MotoFallbackDispatcher)


@aws_provider(api="s3control", name="v2")
def s3control_v2():
from localstack.services.s3control.v2.provider import S3ControlProvider

provider = S3ControlProvider()
return Service.for_provider(provider)


@aws_provider()
def scheduler():
from localstack.services.moto import MotoFallbackDispatcher
Expand Down
Empty file.
28 changes: 28 additions & 0 deletions localstack/services/s3control/v2/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from localstack.aws.api.s3control import (
AccessPointName,
Alias,
BucketName,
GetAccessPointResult,
PublicAccessBlockConfiguration,
)
from localstack.services.stores import (
AccountRegionBundle,
BaseStore,
CrossAccountAttribute,
CrossRegionAttribute,
LocalAttribute,
)


class S3ControlStore(BaseStore):
# buckets: dict[BucketName, S3Bucket] = CrossRegionAttribute(default=dict)
public_access_block: PublicAccessBlockConfiguration = CrossRegionAttribute(default=dict)
access_points: dict[AccessPointName, GetAccessPointResult] = LocalAttribute(
default=dict
) # TODO: check locality
# TODO: check for accross-region accesses
Copy link

Choose a reason for hiding this comment

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

syntax: Fix typo: 'accross' should be 'across'

access_point_alias: dict[Alias, BucketName] = CrossAccountAttribute(default=dict)
# global_bucket_map: dict[BucketName, AccountId] = CrossAccountAttribute(default=dict)


s3control_stores = AccountRegionBundle[S3ControlStore]("s3control", S3ControlStore)
Loading