-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New AWS client #7240
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
Merged
Merged
New AWS client #7240
Changes from 51 commits
Commits
Show all changes
57 commits
Select commit
Hold shift + click to select a range
add7928
add mob-programming based client prototype
dfangl f57429d
WIP
viren-nadkarni ab0c79f
Merge branch 'master' into aws-client
viren-nadkarni 89e43f5
Fix imports
viren-nadkarni 247340c
Merge branch 'master' into aws-client
viren-nadkarni 0352fae
Updates
viren-nadkarni 548ced3
Fallback to default internal credentials
viren-nadkarni f6f37fa
Proper loading of default credentials
viren-nadkarni f392242
Move to its own module
viren-nadkarni f8edc9c
Fix datetime
viren-nadkarni b53e068
Allow module to be used for external clients also
viren-nadkarni f8d8d8e
Use headers for internal call arg func
viren-nadkarni 9f67ea9
Add tests
viren-nadkarni ff8e9a4
Remove dev comments
viren-nadkarni b551b2a
Default access keys
viren-nadkarni a4461c0
Fixes
viren-nadkarni 161e0ae
Merge branch 'master' into aws-client
viren-nadkarni a457707
Use separate functions for internal and external use
viren-nadkarni c0a01e1
Enhancements
viren-nadkarni 94a9170
Update tests
viren-nadkarni cc93146
Merge branch 'master' into aws-stack-dto
viren-nadkarni d616ff6
WIP
viren-nadkarni 2b2c463
Remove assertion from prod code
viren-nadkarni b6cfdea
Revamp hook logic
viren-nadkarni e39fabf
Merge branch 'master' into aws-stack-dto
viren-nadkarni 6819110
Merge branch 'aws-client' into aws-stack-dto
viren-nadkarni 70bf9f4
Merge branch 'master' into aws-client
viren-nadkarni 206f295
Fixes
viren-nadkarni cac43ed
Merge branch 'aws-client' into aws-client-dto
viren-nadkarni aeed818
Add new enricher
viren-nadkarni 0150e36
Fixes
viren-nadkarni 2dbebad
Merge branch 'aws-client' into aws-client-dto
viren-nadkarni d4c251f
Fixes
viren-nadkarni 3597fe8
Merge branch 'aws-client' into aws-client-dto
viren-nadkarni cc7a47c
Override region from target ARN
viren-nadkarni 30a4e7c
Minor touches
viren-nadkarni 2e91751
Allow no region when it is overridden
viren-nadkarni ac93d4c
Override account ID along with region for internal calls with TargetArns
viren-nadkarni d45e6ce
Remove SourceService
viren-nadkarni 8036dcf
Prevent fallback account ID for internal calls
viren-nadkarni 806f321
Update tests
viren-nadkarni 9f3f3a2
Update note
viren-nadkarni daee913
Merge branch 'master' into aws-client
viren-nadkarni 0d9ca21
Rename to ClientFactory
viren-nadkarni afaef80
Use inheritance to specialise factories
viren-nadkarni 8bdcb10
Remove internal call helper
viren-nadkarni cc693d5
Fix tests
viren-nadkarni 10b7d86
Merge branch 'master' into aws-client
8000
viren-nadkarni 46d690e
Merge branch 'master' into aws-client
viren-nadkarni f790be7
Merge branch 'master' into aws-client
dfangl 48cdd07
add some preliminary changes
dfangl 5d49524
add typed interface, add some tests + some test scaffolds
dfangl 4b6ff91
Merge branch 'master' into aws-client
dfangl 0641ae3
add more tests, fix internal call detection
dfangl f10f983
Apply suggestions from code review
dfangl db858a3
fix imports, remove unnecessary tests
dfangl dc7c36f
fix nits
dfangl File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,341 @@ | ||
""" | ||
LocalStack client stack. | ||
|
||
This module provides the interface to perform cross-service communication between | ||
LocalStack providers. | ||
""" | ||
import json | ||
import logging | ||
import threading | ||
from abc import ABC, abstractmethod | ||
from functools import cache | ||
from typing import Optional, TypedDict | ||
|
||
from boto3.session import Session | ||
from botocore.client import BaseClient | ||
from botocore.config import Config | ||
|
||
from localstack import config | ||
from localstack.constants import ( | ||
INTERNAL_AWS_ACCESS_KEY_ID, | ||
INTERNAL_AWS_SECRET_ACCESS_KEY, | ||
MAX_POOL_CONNECTIONS, | ||
) | ||
from localstack.utils.aws.aws_stack import get_local_service_url | ||
from localstack.utils.aws.request_context import get_region_from_request_context | ||
|
||
LOG = logging.getLogger(__name__) | ||
|
||
# | ||
# Data transfer object | ||
# | ||
|
||
INTERNAL_REQUEST_PARAMS_HEADER = "x-localstack-data" | ||
"""Request header which contains the data transfer object.""" | ||
|
||
|
||
class InternalRequestParameters(TypedDict): | ||
""" | ||
LocalStack Data Transfer Object. | ||
|
||
This is sent with every internal request and contains any additional information | ||
LocalStack might need for the purpose of policy enforcement. It is serialised | ||
into text and sent in the request header. | ||
|
||
Attributes can be added as needed. The keys should roughly correspond to: | ||
https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html | ||
""" | ||
|
||
source_arn: str | None | ||
"""ARN of resource which is triggering the call""" | ||
|
||
target_arn: str | None | ||
"""ARN of the resource being accessed.""" | ||
|
||
service_principal: str | None | ||
"""Service principal making this call""" | ||
|
||
|
||
def dump_dto(data: InternalRequestParameters) -> str: | ||
# TODO@viren: Minification can be improved using custom JSONEncoder that uses shortened keys | ||
|
||
dfangl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# To produce a compact JSON representation of DTO, remove spaces from separators | ||
return json.dumps(data, separators=(",", ":")) | ||
|
||
|
||
def load_dto(data: str) -> InternalRequestParameters: | ||
return json.loads(data) | ||
|
||
|
||
# | ||
# Factory | ||
# | ||
|
||
|
||
class ClientFactory(ABC): | ||
""" | ||
Factory to build the AWS client. | ||
|
||
Boto client creation is resource intensive. This class caches all Boto | ||
clients it creates and must be used instead of directly using boto lib. | ||
""" | ||
|
||
def __init__( | ||
self, | ||
use_ssl: bool = False, | ||
verify: bool = False, | ||
session: Session = None, | ||
config: Config = None, | ||
): | ||
""" | ||
:param use_ssl: Whether to use SSL | ||
:param verify: Whether to verify SSL certificates | ||
:param session: Session to be used for client creation. Will create a new session if not provided. | ||
Please note that sessions are not generally thread safe. | ||
Either create a new session for each factory or make sure the session is not shared with another thread. | ||
The factory itself has a lock for the session, so as long as you only use the session in one factory, | ||
it should be fine using the factory in a multithreaded context. | ||
:param config: Config used as default for client creation. | ||
""" | ||
self._use_ssl = use_ssl | ||
self._verify = verify | ||
self._config: Config = config or Config(max_pool_connections=MAX_POOL_CONNECTIONS) | ||
self._session: Session = session or Session() | ||
self._create_client_lock = threading.RLock() | ||
|
||
def __call__(self, *args, **kwargs) -> BaseClient: | ||
return self.get_client(*args, **kwargs) | ||
|
||
@abstractmethod | ||
def get_client( | ||
self, | ||
service_name: str, | ||
region_name: Optional[str], | ||
aws_access_key_id: Optional[str] = None, | ||
aws_secret_access_key: Optional[str] = None, | ||
aws_session_token: Optional[str] = None, | ||
endpoint_url: str = None, | ||
config: Config = None, | ||
): | ||
raise NotImplementedError() | ||
|
||
def _get_client_post_hook(self, client: BaseClient) -> BaseClient: | ||
""" | ||
This is called after the client is created by Boto. | ||
|
||
Any modifications to the client can be implemented here in subclasses | ||
without affecting the caching mechanism. | ||
""" | ||
return client | ||
|
||
@cache | ||
def _get_client( | ||
self, | ||
service_name: str, | ||
region_name: str, | ||
use_ssl: bool, | ||
verify: bool, | ||
endpoint_url: str, | ||
aws_access_key_id: str, | ||
aws_secret_access_key: str, | ||
aws_session_token: str, | ||
config: Config, | ||
) -> BaseClient: | ||
""" | ||
Returns a boto3 client with the given configuration, and the hooks added by `_get_client_post_hook`. | ||
This is a cached call, so modifications to the used client will affect others. | ||
Please use another instance of the factory, should you want to modify clients. | ||
Client creation is behind a lock as it is not generally thread safe. | ||
|
||
:param service_name: Service to build the client for, eg. `s3` | ||
:param region_name: Name of the AWS region to be associated with the client | ||
If set to None, loads from botocore session. | ||
:param aws_access_key_id: Access key to use for the client. | ||
If set to None, loads from botocore session. | ||
:param aws_secret_access_key: Secret key to use for the client. | ||
If set to None, loads from botocore session. | ||
:param aws_session_token: Session token to use for the client. | ||
Not being used if not set. | ||
:param endpoint_url: Full endpoint URL to be used by the client. | ||
Defaults to appropriate LocalStack endpoint. | ||
:param config: Boto config for advanced use. | ||
:return: Boto3 client. | ||
""" | ||
with self._create_client_lock: | ||
client = self._session.client( | ||
service_name=service_name, | ||
region_name=region_name, | ||
use_ssl=use_ssl, | ||
verify=verify, | ||
endpoint_url=endpoint_url, | ||
aws_access_key_id=aws_access_key_id, | ||
aws_secret_access_key=aws_secret_access_key, | ||
aws_session_token=aws_session_token, | ||
config=config, | ||
) | ||
|
||
return self._get_client_post_hook(client) | ||
alexrashed marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# | ||
# Boto session utilities | ||
# | ||
def _get_session_region(self) -> str: | ||
""" | ||
Return AWS region as set in the Boto session. | ||
""" | ||
return self._session.region_name | ||
|
||
def _get_region(self) -> str: | ||
""" | ||
Return the AWS region name from following sources, in order of availability. | ||
- LocalStack request context | ||
- LocalStack default region | ||
- Boto session | ||
""" | ||
return ( | ||
get_region_from_request_context() | ||
or config.DEFAULT_REGION | ||
alexrashed marked this conversation as resolved.
Show resolved
Hide resolved
|
||
or self._get_session_region() # TODO this will never be called, as DEFAULT_REGION defaults to 'us-east-1' | ||
) | ||
|
||
|
||
class InternalClientFactory(ClientFactory): | ||
def _get_client_post_hook(self, client: BaseClient) -> BaseClient: | ||
""" | ||
Register handlers that enable internal data object transfer mechanism | ||
for internal clients. | ||
""" | ||
client.meta.events.register( | ||
"provide-client-params.*.*", handler=_handler_create_request_parameters | ||
) | ||
client.meta.events.register("before-call.*.*", handler=_handler_inject_dto_header) | ||
|
||
return client | ||
|
||
def get_client( | ||
self, | ||
service_name: str, | ||
region_name: Optional[str], | ||
aws_access_key_id: Optional[str] = None, | ||
aws_secret_access_key: Optional[str] = None, | ||
aws_session_token: Optional[str] = None, | ||
endpoint_url: str = None, | ||
config: Config = None, | ||
) -> BaseClient: | ||
""" | ||
Build and return client for connections originating within LocalStack. | ||
|
||
All API operation methods (such as `.list_buckets()` or `.run_instances()` | ||
take additional args that start with `_` prefix. These are used to pass | ||
additional information to LocalStack server during internal calls. | ||
|
||
:param service_name: Service to build the client for, eg. `s3` | ||
:param region_name: Region name. See note above. | ||
If set to None, loads from botocore session. | ||
:param aws_access_key_id: Access key to use for the client. | ||
Defaults to LocalStack internal credentials. | ||
:param aws_secret_access_key: Secret key to use for the client. | ||
Defaults to LocalStack internal credentials. | ||
:param aws_session_token: Session token to use for the client. | ||
Not being used if not set. | ||
:param endpoint_url: Full endpoint URL to be used by the client. | ||
Defaults to appropriate LocalStack endpoint. | ||
:param config: Boto config for advanced use. | ||
""" | ||
|
||
return self._get_client( | ||
service_name=service_name, | ||
region_name=region_name or self._get_region(), | ||
use_ssl=self._use_ssl, | ||
verify=self._verify, | ||
endpoint_url=endpoint_url or get_local_service_url(service_name), | ||
aws_access_key_id=aws_access_key_id or INTERNAL_AWS_ACCESS_KEY_ID, | ||
aws_secret_access_key=aws_secret_access_key or INTERNAL_AWS_SECRET_ACCESS_KEY, | ||
alexrashed marked this conversation as resolved.
Show resolved
Hide resolved
|
||
aws_session_token=aws_session_token, | ||
config=config or self._config, | ||
) | ||
|
||
|
||
class ExternalClientFactory(ClientFactory): | ||
def get_client( | ||
self, | ||
service_name: str, | ||
region_name: Optional[str], | ||
aws_access_key_id: Optional[str] = None, | ||
aws_secret_access_key: Optional[str] = None, | ||
aws_session_token: Optional[str] = None, | ||
endpoint_url: str = None, | ||
config: Config = None, | ||
) -> BaseClient: | ||
""" | ||
Build and return client for connections originating outside LocalStack. | ||
|
||
If either of the access keys or region are set to None, they are loaded from following | ||
locations: | ||
- AWS environment variables | ||
- Credentials file `~/.aws/credentials` | ||
- Config file `~/.aws/config` | ||
|
||
:param service_name: Service to build the client for, eg. `s3` | ||
:param region_name: Name of the AWS region to be associated with the client | ||
If set to None, loads from botocore session. | ||
:param aws_access_key_id: Access key to use for the client. | ||
If set to None, loads from botocore session. | ||
:param aws_secret_access_key: Secret key to use for the client. | ||
If set to None, loads from botocore session. | ||
:param aws_session_token: Session token to use for the client. | ||
Not being used if not set. | ||
:param endpoint_url: Full endpoint URL to be used by the client. | ||
Defaults to appropriate LocalStack endpoint. | ||
:param config: Boto config for advanced use. | ||
""" | ||
|
||
return self._get_client( | ||
service_name=service_name, | ||
region_name=region_name or self._get_region(), | ||
use_ssl=self._use_ssl, | ||
verify=self._verify, | ||
endpoint_url=endpoint_url or get_local_service_url(service_name), | ||
aws_access_key_id=aws_access_key_id, | ||
aws_secret_access_key=aws_secret_access_key, | ||
aws_session_token=aws_session_token, | ||
config=config or self._config, | ||
) | ||
|
||
|
||
connect_to = InternalClientFactory() | ||
connect_externally_to = ExternalClientFactory() | ||
|
||
# | ||
# Handlers | ||
# | ||
|
||
|
||
def _handler_create_request_parameters(params, model, context, **kwargs): | ||
dfangl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
Construct the data transfer object at the time of parsing the client | ||
parameters and proxy it via the Boto context dict. | ||
|
||
This handler enables the use of additional keyword parameters in Boto API | ||
operation functions. | ||
""" | ||
|
||
# Names of arguments that can be passed to Boto API operation functions. | ||
# These must correspond to entries on the data transfer object. | ||
dto = InternalRequestParameters() | ||
for member in InternalRequestParameters.__annotations__.keys(): | ||
parameter = f"_{''.join([part.title() for part in member.split('_')])}" | ||
alexrashed marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if parameter in params: | ||
dto[member] = params.pop(parameter) | ||
|
||
if dto: | ||
context["_localstack"] = dto | ||
|
||
|
||
def _handler_inject_dto_header(model, params, request_signer, context, **kwargs): | ||
""" | ||
Retrieve the data transfer object from the Boto context dict and serialise | ||
it as part of the request headers. | ||
""" | ||
if dto := context.pop("_localstack", None): | ||
params["headers"][INTERNAL_REQUEST_PARAMS_HEADER] = dump_dto(dto) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.