• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

localstack / localstack / 16820655284

07 Aug 2025 05:03PM UTC coverage: 86.841% (-0.05%) from 86.892%
16820655284

push

github

web-flow
CFNV2: support CDK bootstrap and deployment (#12967)

32 of 38 new or added lines in 5 files covered. (84.21%)

2013 existing lines in 125 files now uncovered.

66606 of 76699 relevant lines covered (86.84%)

0.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.26
/localstack-core/localstack/aws/connect.py
1
"""
2
LocalStack client stack.
3

4
This module provides the interface to perform cross-service communication between
5
LocalStack providers.
6
"""
7

8
import json
1✔
9
import logging
1✔
10
import re
1✔
11
import threading
1✔
12
from abc import ABC, abstractmethod
1✔
13
from collections.abc import Callable
1✔
14
from functools import lru_cache, partial
1✔
15
from random import choice
1✔
16
from socket import socket
1✔
17
from typing import Any, Generic, TypedDict, TypeVar
1✔
18

19
import dns.message
1✔
20
import dns.query
1✔
21
from boto3.session import Session
1✔
22
from botocore.awsrequest import (
1✔
23
    AWSHTTPConnection,
24
    AWSHTTPConnectionPool,
25
    AWSHTTPSConnection,
26
    AWSHTTPSConnectionPool,
27
)
28
from botocore.client import BaseClient
1✔
29
from botocore.config import Config
1✔
30
from botocore.httpsession import URLLib3Session
1✔
31
from botocore.waiter import Waiter
1✔
32

33
from localstack import config as localstack_config
1✔
34
from localstack.aws.spec import LOCALSTACK_BUILTIN_DATA_PATH
1✔
35
from localstack.constants import (
1✔
36
    AWS_REGION_US_EAST_1,
37
    INTERNAL_AWS_ACCESS_KEY_ID,
38
    INTERNAL_AWS_SECRET_ACCESS_KEY,
39
    MAX_POOL_CONNECTIONS,
40
)
41
from localstack.utils.aws.aws_stack import get_s3_hostname
1✔
42
from localstack.utils.aws.client_types import ServicePrincipal, TypedServiceClientFactory
1✔
43
from localstack.utils.patch import patch
1✔
44
from localstack.utils.strings import short_uid
1✔
45

46
LOG = logging.getLogger(__name__)
1✔
47

48

49
@patch(target=Waiter.wait, pass_target=True)
1✔
50
def my_patch(fn, self, **kwargs):
1✔
51
    """
52
    We're patching defaults in here that will override the defaults specified in the waiter spec since these are usually way too long
53

54
    Alternatively we could also try to find a solution where we patch the loader used in the generated clients
55
    so that we can dynamically fix the waiter config when it's loaded instead of when it's being used for wait execution
56
    """
57

58
    if localstack_config.DISABLE_CUSTOM_BOTO_WAITER_CONFIG:
1✔
UNCOV
59
        return fn(self, **kwargs)
×
60
    else:
61
        patched_kwargs = {
1✔
62
            **kwargs,
63
            "WaiterConfig": {
64
                "Delay": localstack_config.BOTO_WAITER_DELAY,
65
                "MaxAttempts": localstack_config.BOTO_WAITER_MAX_ATTEMPTS,
66
                **kwargs.get(
67
                    "WaiterConfig", {}
68
                ),  # we still allow client users to override these defaults
69
            },
70
        }
71
        return fn(self, **patched_kwargs)
1✔
72

73

74
# patch the botocore.Config object to be comparable and hashable.
75
# this solution does not validates the hashable (https://docs.python.org/3/glossary.html#term-hashable) definition on python
76
# It would do so only when someone accesses the internals of the Config option to change the dict directly.
77
# Since this is not a proper way to use the config object (but via config.merge), this should be fine
78
def make_hash(o):
1✔
79
    if isinstance(o, (set, tuple, list)):
1✔
UNCOV
80
        return tuple([make_hash(e) for e in o])
×
81

82
    elif not isinstance(o, dict):
1✔
83
        return hash(o)
1✔
84

85
    new_o = {}
1✔
86
    for k, v in o.items():
1✔
87
        new_o[k] = make_hash(v)
1✔
88

89
    return hash(frozenset(sorted(new_o.items())))
1✔
90

91

92
def config_equality_patch(self, other: object):
1✔
93
    return type(self) == type(other) and self._user_provided_options == other._user_provided_options
1✔
94

95

96
def config_hash_patch(self):
1✔
97
    return make_hash(self._user_provided_options)
1✔
98

99

100
Config.__eq__ = config_equality_patch
1✔
101
Config.__hash__ = config_hash_patch
1✔
102

103

104
def attribute_name_to_service_name(attribute_name):
1✔
105
    """
106
    Converts a python-compatible attribute name to the boto service name
107
    :param attribute_name: Python compatible attribute name using the following replacements:
108
                            a) Add an underscore suffix `_` to any reserved Python keyword (PEP-8).
109
                            b) Replace any dash `-` with an underscore `_`
110
    :return:
111
    """
112
    if attribute_name.endswith("_"):
1✔
113
        # lambda_ -> lambda
114
        attribute_name = attribute_name[:-1]
1✔
115
    # replace all _ with -: cognito_idp -> cognito-idp
116
    return attribute_name.replace("_", "-")
1✔
117

118

119
def get_service_endpoint() -> str | None:
1✔
120
    """
121
    Returns the endpoint the client should target.
122

123
    :return: Endpoint url
124
    """
125
    if localstack_config.DISTRIBUTED_MODE:
1✔
UNCOV
126
        return None
×
127
    return localstack_config.internal_service_url()
1✔
128

129

130
#
131
# Data transfer object
132
#
133

134
INTERNAL_REQUEST_PARAMS_HEADER = "x-localstack-data"
1✔
135
"""Request header which contains the data transfer object."""
1✔
136

137

138
class InternalRequestParameters(TypedDict):
1✔
139
    """
140
    LocalStack Data Transfer Object.
141

142
    This is sent with every internal request and contains any additional information
143
    LocalStack might need for the purpose of policy enforcement. It is serialised
144
    into text and sent in the request header.
145

146
    Attributes can be added as needed. The keys should roughly correspond to:
147
    https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html
148
    """
149

150
    source_arn: str | None
1✔
151
    """ARN of resource which is triggering the call"""
1✔
152

153
    service_principal: str | None
1✔
154
    """Service principal making this call"""
1✔
155

156

157
def dump_dto(data: InternalRequestParameters) -> str:
1✔
158
    # To produce a compact JSON representation of DTO, remove spaces from separators
159
    # If possible, we could use a custom encoder to further decrease header size in the future
160
    return json.dumps(data, separators=(",", ":"))
1✔
161

162

163
def load_dto(data: str) -> InternalRequestParameters:
1✔
164
    return json.loads(data)
1✔
165

166

167
T = TypeVar("T")
1✔
168

169

170
class MetadataRequestInjector(Generic[T]):
1✔
171
    def __init__(self, client: T, params: dict[str, str] | None = None):
1✔
172
        self._client = client
1✔
173
        self._params = params
1✔
174

175
    def __getattr__(self, item):
1✔
176
        target = getattr(self._client, item)
1✔
177
        if not isinstance(target, Callable):
1✔
178
            return target
1✔
179
        if self._params:
1✔
180
            return partial(target, **self._params)
1✔
181
        else:
182
            return target
1✔
183

184
    def request_metadata(
1✔
185
        self, source_arn: str | None = None, service_principal: str | None = None
186
    ) -> T:
187
        """
188
        Returns a new client instance preset with the given request metadata.
189
        Identical to providing _ServicePrincipal and _SourceArn directly as operation arguments but typing
190
        compatible.
191

192
        Raw example: lambda_client.invoke(FunctionName="fn", _SourceArn="...")
193
        Injector example: lambda_client.request_metadata(source_arn="...").invoke(FunctionName="fn")
194
        Cannot be called on objects where the parameters are already set.
195

196
        :param source_arn: Arn on which behalf the calls of this client shall be made
197
        :param service_principal: Service principal on which behalf the calls of this client shall be made
198
        :return: A new version of the MetadataRequestInjector
199
        """
200
        if self._params is not None:
1✔
UNCOV
201
            raise TypeError("Request_data cannot be called on it's own return value")
×
202
        params = {}
1✔
203
        if source_arn:
1✔
204
            params["_SourceArn"] = source_arn
1✔
205
        if service_principal:
1✔
206
            params["_ServicePrincipal"] = service_principal
1✔
207
        return MetadataRequestInjector(client=self._client, params=params)
1✔
208

209

210
#
211
# Factory
212
#
213
class ServiceLevelClientFactory(TypedServiceClientFactory):
1✔
214
    """
215
    A service level client factory, preseeded with parameters for the boto3 client creation.
216
    Will create any service client with parameters already provided by the ClientFactory.
217
    """
218

219
    def __init__(
1✔
220
        self,
221
        *,
222
        factory: "ClientFactory",
223
        client_creation_params: dict[str, str | Config | None],
224
        request_wrapper_clazz: type,
225
    ):
226
        self._factory = factory
1✔
227
        self._client_creation_params = client_creation_params
1✔
228
        self._request_wrapper_clazz = request_wrapper_clazz
1✔
229

230
    def get_client(self, service: str):
1✔
231
        return self._request_wrapper_clazz(
1✔
232
            client=self._factory.get_client(service_name=service, **self._client_creation_params)
233
        )
234

235
    def __getattr__(self, service: str):
1✔
236
        service = attribute_name_to_service_name(service)
1✔
237
        return self._request_wrapper_clazz(
1✔
238
            client=self._factory.get_client(service_name=service, **self._client_creation_params)
239
        )
240

241

242
class ClientFactory(ABC):
1✔
243
    """
244
    Factory to build the AWS client.
245

246
    Boto client creation is resource intensive. This class caches all Boto
247
    clients it creates and must be used instead of directly using boto lib.
248
    """
249

250
    def __init__(
1✔
251
        self,
252
        use_ssl: bool = False,
253
        verify: bool = False,
254
        session: Session = None,
255
        config: Config = None,
256
    ):
257
        """
258
        :param use_ssl: Whether to use SSL
259
        :param verify: Whether to verify SSL certificates
260
        :param session: Session to be used for client creation. Will create a new session if not provided.
261
            Please note that sessions are not generally thread safe.
262
            Either create a new session for each factory or make sure the session is not shared with another thread.
263
            The factory itself has a lock for the session, so as long as you only use the session in one factory,
264
            it should be fine using the factory in a multithreaded context.
265
        :param config: Config used as default for client creation.
266
        """
267
        self._use_ssl = use_ssl
1✔
268
        self._verify = verify
1✔
269
        self._config: Config = config or Config(max_pool_connections=MAX_POOL_CONNECTIONS)
1✔
270
        self._session: Session = session or Session()
1✔
271

272
        # make sure we consider our custom data paths for legacy specs (like SQS query protocol)
273
        if LOCALSTACK_BUILTIN_DATA_PATH not in self._session._loader.search_paths:
1✔
274
            self._session._loader.search_paths.insert(0, LOCALSTACK_BUILTIN_DATA_PATH)
1✔
275

276
        self._create_client_lock = threading.RLock()
1✔
277

278
    def __call__(
1✔
279
        self,
280
        *,
281
        region_name: str | None = None,
282
        aws_access_key_id: str | None = None,
283
        aws_secret_access_key: str | None = None,
284
        aws_session_token: str | None = None,
285
        endpoint_url: str = None,
286
        config: Config = None,
287
    ) -> ServiceLevelClientFactory:
288
        """
289
        Get back an object which lets you select the typed service you want to access with the given attributes
290

291
        :param region_name: Name of the AWS region to be associated with the client
292
            If set to None, loads from botocore session.
293
        :param aws_access_key_id: Access key to use for the client.
294
            If set to None, loads from botocore session.
295
        :param aws_secret_access_key: Secret key to use for the client.
296
            If set to None, loads from botocore session.
297
        :param aws_session_token: Session token to use for the client.
298
            Not being used if not set.
299
        :param endpoint_url: Full endpoint URL to be used by the client.
300
            Defaults to appropriate LocalStack endpoint.
301
        :param config: Boto config for advanced use.
302
        :return: Service Region Client Creator
303
        """
304
        params = {
1✔
305
            "region_name": region_name,
306
            "aws_access_key_id": aws_access_key_id,
307
            "aws_secret_access_key": aws_secret_access_key,
308
            "aws_session_token": aws_session_token,
309
            "endpoint_url": endpoint_url,
310
            "config": config,
311
        }
312
        return ServiceLevelClientFactory(
1✔
313
            factory=self,
314
            client_creation_params=params,
315
            request_wrapper_clazz=MetadataRequestInjector,
316
        )
317

318
    def with_assumed_role(
1✔
319
        self,
320
        *,
321
        role_arn: str,
322
        service_principal: ServicePrincipal | None = None,
323
        session_name: str | None = None,
324
        region_name: str | None = None,
325
        endpoint_url: str | None = None,
326
        config: Config | None = None,
327
    ) -> ServiceLevelClientFactory:
328
        """
329
        Create a service level client factory with credentials from assuming the given role ARN.
330
        The service_principal will only be used for the assume_role call, for all succeeding calls it has to be provided
331
        separately, either as call attribute or using request_metadata()
332

333
        :param role_arn: Role to assume
334
        :param service_principal: Service the role should be assumed as, must not be set for test clients
335
        :param session_name: Session name for the role session
336
        :param region_name: Region for the returned client
337
        :param endpoint_url: Endpoint for both the assume_role call and the returned client
338
        :param config: Config for both the assume_role call and the returned client
339
        :return: Service Level Client Factory
340
        """
341
        session_name = session_name or f"session-{short_uid()}"
1✔
342
        sts_client = self(endpoint_url=endpoint_url, config=config, region_name=region_name).sts
1✔
343

344
        metadata = {}
1✔
345
        if service_principal:
1✔
346
            metadata["service_principal"] = service_principal
1✔
347

348
        sts_client = sts_client.request_metadata(**metadata)
1✔
349
        credentials = sts_client.assume_role(RoleArn=role_arn, RoleSessionName=session_name)[
1✔
350
            "Credentials"
351
        ]
352

353
        return self(
1✔
354
            region_name=region_name,
355
            aws_access_key_id=credentials["AccessKeyId"],
356
            aws_secret_access_key=credentials["SecretAccessKey"],
357
            aws_session_token=credentials["SessionToken"],
358
            endpoint_url=endpoint_url,
359
            config=config,
360
        )
361

362
    @abstractmethod
1✔
363
    def get_client(
1✔
364
        self,
365
        service_name: str,
366
        region_name: str | None = None,
367
        aws_access_key_id: str | None = None,
368
        aws_secret_access_key: str | None = None,
369
        aws_session_token: str | None = None,
370
        endpoint_url: str | None = None,
371
        config: Config | None = None,
372
    ):
373
        raise NotImplementedError()
374

375
    def _get_client_post_hook(self, client: BaseClient) -> BaseClient:
1✔
376
        """
377
        This is called after the client is created by Boto.
378

379
        Any modifications to the client can be implemented here in subclasses
380
        without affecting the caching mechanism.
381
        """
382
        return client
1✔
383

384
    # TODO @lru_cache here might result in a memory leak, as it keeps a reference to `self`
385
    # We might need an alternative caching decorator with a weak ref to `self`
386
    # Otherwise factories might never be garbage collected
387
    @lru_cache(maxsize=256)
1✔
388
    def _get_client(
1✔
389
        self,
390
        service_name: str,
391
        region_name: str,
392
        use_ssl: bool,
393
        verify: bool | None,
394
        endpoint_url: str | None,
395
        aws_access_key_id: str | None,
396
        aws_secret_access_key: str | None,
397
        aws_session_token: str | None,
398
        config: Config,
399
    ) -> BaseClient:
400
        """
401
        Returns a boto3 client with the given configuration, and the hooks added by `_get_client_post_hook`.
402
        This is a cached call, so modifications to the used client will affect others.
403
        Please use another instance of the factory, should you want to modify clients.
404
        Client creation is behind a lock as it is not generally thread safe.
405

406
        :param service_name: Service to build the client for, eg. `s3`
407
        :param region_name: Name of the AWS region to be associated with the client
408
            If set to None, loads from botocore session.
409
        :param aws_access_key_id: Access key to use for the client.
410
            If set to None, loads from botocore session.
411
        :param aws_secret_access_key: Secret key to use for the client.
412
            If set to None, loads from botocore session.
413
        :param aws_session_token: Session token to use for the client.
414
            Not being used if not set.
415
        :param endpoint_url: Full endpoint URL to be used by the client.
416
            Defaults to appropriate LocalStack endpoint.
417
        :param config: Boto config for advanced use.
418
        :return: Boto3 client.
419
        """
420
        with self._create_client_lock:
1✔
421
            default_config = (
1✔
422
                Config(retries={"max_attempts": 0})
423
                if localstack_config.DISABLE_BOTO_RETRIES
424
                else Config()
425
            )
426

427
            client = self._session.client(
1✔
428
                service_name=service_name,
429
                region_name=region_name,
430
                use_ssl=use_ssl,
431
                verify=verify,
432
                endpoint_url=endpoint_url,
433
                aws_access_key_id=aws_access_key_id,
434
                aws_secret_access_key=aws_secret_access_key,
435
                aws_session_token=aws_session_token,
436
                config=config.merge(default_config),
437
            )
438

439
        return self._get_client_post_hook(client)
1✔
440

441
    #
442
    # Boto session utilities
443
    #
444
    def _get_session_region(self) -> str:
1✔
445
        """
446
        Return AWS region as set in the Boto session.
447
        """
448
        return self._session.region_name
1✔
449

450
    def _get_region(self) -> str:
1✔
451
        """
452
        Return the AWS region name from following sources, in order of availability.
453
        - LocalStack request context
454
        - Boto session
455
        - us-east-1
456
        """
457
        return self._get_session_region() or AWS_REGION_US_EAST_1
1✔
458

459

460
class InternalClientFactory(ClientFactory):
1✔
461
    def _get_client_post_hook(self, client: BaseClient) -> BaseClient:
1✔
462
        """
463
        Register handlers that enable internal data object transfer mechanism
464
        for internal clients.
465
        """
466
        client.meta.events.register(
1✔
467
            "provide-client-params.*.*", handler=_handler_create_request_parameters
468
        )
469

470
        client.meta.events.register("before-call.*.*", handler=_handler_inject_dto_header)
1✔
471

472
        if localstack_config.IN_MEMORY_CLIENT:
1✔
473
            # this make the client call the gateway directly
474
            from localstack.aws.client import GatewayShortCircuit
×
UNCOV
475
            from localstack.runtime import get_current_runtime
×
476

UNCOV
477
            GatewayShortCircuit.modify_client(client, get_current_runtime().components.gateway)
×
478

479
        return client
1✔
480

481
    def get_client(
1✔
482
        self,
483
        service_name: str,
484
        region_name: str | None = None,
485
        aws_access_key_id: str | None = None,
486
        aws_secret_access_key: str | None = None,
487
        aws_session_token: str | None = None,
488
        endpoint_url: str | None = None,
489
        config: Config | None = None,
490
    ) -> BaseClient:
491
        """
492
        Build and return client for connections originating within LocalStack.
493

494
        All API operation methods (such as `.list_buckets()` or `.run_instances()`
495
        take additional args that start with `_` prefix. These are used to pass
496
        additional information to LocalStack server during internal calls.
497

498
        :param service_name: Service to build the client for, eg. `s3`
499
        :param region_name: Region name. See note above.
500
            If set to None, loads from botocore session.
501
        :param aws_access_key_id: Access key to use for the client.
502
            Defaults to LocalStack internal credentials.
503
        :param aws_secret_access_key: Secret key to use for the client.
504
            Defaults to LocalStack internal credentials.
505
        :param aws_session_token: Session token to use for the client.
506
            Not being used if not set.
507
        :param endpoint_url: Full endpoint URL to be used by the client.
508
            Defaults to appropriate LocalStack endpoint.
509
        :param config: Boto config for advanced use.
510
        """
511

512
        if config is None:
1✔
513
            config = self._config
1✔
514
        else:
515
            config = self._config.merge(config)
1✔
516

517
        endpoint_url = endpoint_url or get_service_endpoint()
1✔
518
        if service_name == "s3" and endpoint_url:
1✔
519
            if re.match(r"https?://localhost(:[0-9]+)?", endpoint_url):
1✔
520
                endpoint_url = endpoint_url.replace("://localhost", f"://{get_s3_hostname()}")
1✔
521

522
        return self._get_client(
1✔
523
            service_name=service_name,
524
            region_name=region_name or self._get_region(),
525
            use_ssl=self._use_ssl,
526
            verify=self._verify,
527
            endpoint_url=endpoint_url,
528
            aws_access_key_id=aws_access_key_id or INTERNAL_AWS_ACCESS_KEY_ID,
529
            aws_secret_access_key=aws_secret_access_key or INTERNAL_AWS_SECRET_ACCESS_KEY,
530
            aws_session_token=aws_session_token,
531
            config=config,
532
        )
533

534

535
class ExternalClientFactory(ClientFactory):
1✔
536
    def get_client(
1✔
537
        self,
538
        service_name: str,
539
        region_name: str | None = None,
540
        aws_access_key_id: str | None = None,
541
        aws_secret_access_key: str | None = None,
542
        aws_session_token: str | None = None,
543
        endpoint_url: str | None = None,
544
        config: Config | None = None,
545
    ) -> BaseClient:
546
        """
547
        Build and return client for connections originating outside LocalStack and targeting Localstack.
548

549
        If the region is set to None, it is loaded from following
550
        locations:
551
        - AWS environment variables
552
        - Credentials file `~/.aws/credentials`
553
        - Config file `~/.aws/config`
554

555
        :param service_name: Service to build the client for, eg. `s3`
556
        :param region_name: Name of the AWS region to be associated with the client
557
            If set to None, loads from botocore session.
558
        :param aws_access_key_id: Access key to use for the client.
559
            If set to None, loads from botocore session.
560
        :param aws_secret_access_key: Secret key to use for the client.
561
            If set to None, uses a placeholder value
562
        :param aws_session_token: Session token to use for the client.
563
            Not being used if not set.
564
        :param endpoint_url: Full endpoint URL to be used by the client.
565
            Defaults to appropriate LocalStack endpoint.
566
        :param config: Boto config for advanced use.
567
        """
568
        if config is None:
1✔
569
            config = self._config
1✔
570
        else:
571
            config = self._config.merge(config)
1✔
572

573
        # Boto has an odd behaviour when using a non-default (any other region than us-east-1) in config
574
        # If the region in arg is non-default, it gives the arg the precedence
575
        # But if the region in arg is default (us-east-1), it gives precedence to one in config
576
        # Below: always give precedence to arg region
577
        if config and config.region_name != AWS_REGION_US_EAST_1:
1✔
578
            if region_name == AWS_REGION_US_EAST_1:
1✔
579
                config = config.merge(Config(region_name=region_name))
1✔
580

581
        endpoint_url = endpoint_url or get_service_endpoint()
1✔
582
        if service_name == "s3":
1✔
583
            if re.match(r"https?://localhost(:[0-9]+)?", endpoint_url):
1✔
584
                endpoint_url = endpoint_url.replace("://localhost", f"://{get_s3_hostname()}")
1✔
585

586
        # Prevent `PartialCredentialsError` when only access key ID is provided
587
        # The value of secret access key is insignificant and can be set to anything
588
        if aws_access_key_id:
1✔
589
            aws_secret_access_key = aws_secret_access_key or INTERNAL_AWS_SECRET_ACCESS_KEY
1✔
590

591
        return self._get_client(
1✔
592
            service_name=service_name,
593
            region_name=region_name or config.region_name or self._get_region(),
594
            use_ssl=self._use_ssl,
595
            verify=self._verify,
596
            endpoint_url=endpoint_url,
597
            aws_access_key_id=aws_access_key_id,
598
            aws_secret_access_key=aws_secret_access_key,
599
            aws_session_token=aws_session_token,
600
            config=config,
601
        )
602

603

604
class ExternalAwsClientFactory(ClientFactory):
1✔
605
    def get_client(
1✔
606
        self,
607
        service_name: str,
608
        region_name: str | None = None,
609
        aws_access_key_id: str | None = None,
610
        aws_secret_access_key: str | None = None,
611
        aws_session_token: str | None = None,
612
        endpoint_url: str | None = None,
613
        config: Config | None = None,
614
    ) -> BaseClient:
615
        """
616
        Build and return client for connections originating outside LocalStack and targeting AWS.
617

618
        If either of the access keys or region are set to None, they are loaded from following
619
        locations:
620
        - AWS environment variables
621
        - Credentials file `~/.aws/credentials`
622
        - Config file `~/.aws/config`
623

624
        :param service_name: Service to build the client for, eg. `s3`
625
        :param region_name: Name of the AWS region to be associated with the client
626
            If set to None, loads from botocore session.
627
        :param aws_access_key_id: Access key to use for the client.
628
            If set to None, loads from botocore session.
629
        :param aws_secret_access_key: Secret key to use for the client.
630
            If set to None, loads from botocore session.
631
        :param aws_session_token: Session token to use for the client.
632
            Not being used if not set.
633
        :param endpoint_url: Full endpoint URL to be used by the client.
634
            Defaults to appropriate AWS endpoint.
635
        :param config: Boto config for advanced use.
636
        """
637
        if config is None:
1✔
638
            config = self._config
1✔
639
        else:
UNCOV
640
            config = self._config.merge(config)
×
641

642
        return self._get_client(
1✔
643
            config=config,
644
            service_name=service_name,
645
            region_name=region_name or self._get_session_region(),
646
            endpoint_url=endpoint_url,
647
            use_ssl=True,
648
            verify=True,
649
            aws_access_key_id=aws_access_key_id,
650
            aws_secret_access_key=aws_secret_access_key,
651
            aws_session_token=aws_session_token,
652
        )
653

654

655
def resolve_dns_from_upstream(hostname: str) -> str:
1✔
UNCOV
656
    from localstack.dns.server import get_fallback_dns_server
×
657

658
    upstream_dns = get_fallback_dns_server()
×
659
    request = dns.message.make_query(hostname, "A")
×
660
    response = dns.query.udp(request, upstream_dns, port=53, timeout=5)
×
661
    if len(response.answer) == 0:
×
UNCOV
662
        raise ValueError(f"No DNS response found for hostname '{hostname}'")
×
663

664
    ip_addresses = []
×
665
    for answer in response.answer:
×
666
        if answer.match(dns.rdataclass.IN, dns.rdatatype.A, dns.rdatatype.NONE):
×
UNCOV
667
            ip_addresses.extend(answer.items.keys())
×
668

669
    if not ip_addresses:
×
UNCOV
670
        raise ValueError(f"No DNS records of type 'A' found for hostname '{hostname}'")
×
671

UNCOV
672
    return choice(ip_addresses).address
×
673

674

675
class ExternalBypassDnsClientFactory(ExternalAwsClientFactory):
1✔
676
    """
677
    Client factory that makes requests against AWS ensuring that DNS resolution is not affected by the LocalStack DNS
678
    server.
679
    """
680

681
    def __init__(
1✔
682
        self,
683
        session: Session = None,
684
        config: Config = None,
685
    ):
UNCOV
686
        super().__init__(use_ssl=True, verify=True, session=session, config=config)
×
687

688
    def _get_client_post_hook(self, client: BaseClient) -> BaseClient:
1✔
689
        client = super()._get_client_post_hook(client)
×
690
        client._endpoint.http_session = ExternalBypassDnsSession()
×
UNCOV
691
        return client
×
692

693

694
class ExternalBypassDnsHTTPConnection(AWSHTTPConnection):
1✔
695
    """
696
    Connection class that bypasses the LocalStack DNS server for HTTP connections
697
    """
698

699
    def _new_conn(self) -> socket:
1✔
700
        orig_host = self._dns_host
×
701
        try:
×
702
            self._dns_host = resolve_dns_from_upstream(self._dns_host)
×
UNCOV
703
            return super()._new_conn()
×
704
        finally:
UNCOV
705
            self._dns_host = orig_host
×
706

707

708
class ExternalBypassDnsHTTPSConnection(AWSHTTPSConnection):
1✔
709
    """
710
    Connection class that bypasses the LocalStack DNS server for HTTPS connections
711
    """
712

713
    def _new_conn(self) -> socket:
1✔
714
        orig_host = self._dns_host
×
715
        try:
×
716
            self._dns_host = resolve_dns_from_upstream(self._dns_host)
×
UNCOV
717
            return super()._new_conn()
×
718
        finally:
UNCOV
719
            self._dns_host = orig_host
×
720

721

722
class ExternalBypassDnsHTTPConnectionPool(AWSHTTPConnectionPool):
1✔
723
    ConnectionCls = ExternalBypassDnsHTTPConnection
1✔
724

725

726
class ExternalBypassDnsHTTPSConnectionPool(AWSHTTPSConnectionPool):
1✔
727
    ConnectionCls = ExternalBypassDnsHTTPSConnection
1✔
728

729

730
class ExternalBypassDnsSession(URLLib3Session):
1✔
731
    """
732
    urllib3 session wrapper that uses our custom connection pool.
733
    """
734

735
    def __init__(self, *args, **kwargs):
1✔
UNCOV
736
        super().__init__(*args, **kwargs)
×
737

738
        self._pool_classes_by_scheme["https"] = ExternalBypassDnsHTTPSConnectionPool
×
UNCOV
739
        self._pool_classes_by_scheme["http"] = ExternalBypassDnsHTTPConnectionPool
×
740

741

742
connect_to = InternalClientFactory(use_ssl=localstack_config.DISTRIBUTED_MODE)
1✔
743
connect_externally_to = ExternalClientFactory()
1✔
744

745

746
#
747
# Handlers
748
#
749

750

751
def _handler_create_request_parameters(params: dict[str, Any], context: dict[str, Any], **kwargs):
1✔
752
    """
753
    Construct the data transfer object at the time of parsing the client
754
    parameters and proxy it via the Boto context dict.
755

756
    This handler enables the use of additional keyword parameters in Boto API
757
    operation functions.
758

759
    It uses the `InternalRequestParameters` type annotations to handle supported parameters.
760
    The keys supported by this type will be converted to method parameters by prefixing it with an underscore `_`
761
    and converting the snake case to camel case.
762
    Example:
763
        service_principal -> _ServicePrincipal
764
    """
765

766
    # Names of arguments that can be passed to Boto API operation functions.
767
    # These must correspond to entries on the data transfer object.
768
    dto = InternalRequestParameters()
1✔
769
    for member in InternalRequestParameters.__annotations__.keys():
1✔
770
        parameter = f"_{''.join([part.title() for part in member.split('_')])}"
1✔
771
        if parameter in params:
1✔
772
            dto[member] = params.pop(parameter)
1✔
773

774
    context["_localstack"] = dto
1✔
775

776

777
def _handler_inject_dto_header(params: dict[str, Any], context: dict[str, Any], **kwargs):
1✔
778
    """
779
    Retrieve the data transfer object from the Boto context dict and serialise
780
    it as part of the request headers.
781
    """
782
    if (dto := context.pop("_localstack", None)) is not None:
1✔
783
        params["headers"][INTERNAL_REQUEST_PARAMS_HEADER] = dump_dto(dto)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc