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

localstack / localstack / 20942662173

12 Jan 2026 04:45PM UTC coverage: 86.905% (-0.03%) from 86.936%
20942662173

push

github

web-flow
Allow authenticated pull and push of docker images (#13569)

34 of 51 new or added lines in 4 files covered. (66.67%)

247 existing lines in 15 files now uncovered.

70218 of 80799 relevant lines covered (86.9%)

0.87 hits per line

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

82.1
/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 os
1✔
11
import re
1✔
12
import threading
1✔
13
from abc import ABC, abstractmethod
1✔
14
from collections.abc import Callable
1✔
15
from functools import lru_cache, partial
1✔
16
from random import choice
1✔
17
from socket import socket
1✔
18
from typing import Any, Generic, TypedDict, TypeVar
1✔
19

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

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

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

49

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

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

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

74

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

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

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

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

92

93
def config_equality_patch(self, other: object) -> bool:
1✔
94
    return type(self) is type(other) and self._user_provided_options == other._user_provided_options
1✔
95

96

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

100

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

104

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

119

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

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

130

131
#
132
# Data transfer object
133
#
134

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

138

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

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

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

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

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

157

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

163

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

167

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

170

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

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

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

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

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

210

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

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

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

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

242

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

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

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

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

279
        self._create_client_lock = threading.RLock()
1✔
280

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

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

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

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

347
        metadata = {}
1✔
348
        if service_principal:
1✔
349
            metadata["service_principal"] = service_principal
1✔
350

351
        sts_client = sts_client.request_metadata(**metadata)
1✔
352
        credentials = sts_client.assume_role(RoleArn=role_arn, RoleSessionName=session_name)[
1✔
353
            "Credentials"
354
        ]
355

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

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

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

382
        Any modifications to the client can be implemented here in subclasses
383
        without affecting the caching mechanism.
384
        """
385
        return client
1✔
386

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

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

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

442
        return self._get_client_post_hook(client)
1✔
443

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

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

462

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

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

475
        if localstack_config.IN_MEMORY_CLIENT:
1✔
476
            # this make the client call the gateway directly
477
            from localstack.aws.client import GatewayShortCircuit
×
478
            from localstack.runtime import get_current_runtime
×
479

480
            GatewayShortCircuit.modify_client(client, get_current_runtime().components.gateway)
×
481

482
        return client
1✔
483

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

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

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

515
        if config is None:
1✔
516
            config = self._config
1✔
517
        else:
518
            config = self._config.merge(config)
1✔
519

520
        endpoint_url = endpoint_url or self._endpoint or get_service_endpoint()
1✔
521
        if (
1✔
522
            endpoint_url
523
            and service_name == "s3"
524
            and re.match(r"https?://localhost(:[0-9]+)?", endpoint_url)
525
        ):
526
            endpoint_url = endpoint_url.replace("://localhost", f"://{get_s3_hostname()}")
1✔
527

528
        return self._get_client(
1✔
529
            service_name=service_name,
530
            region_name=region_name or self._get_region(),
531
            use_ssl=self._use_ssl,
532
            verify=self._verify,
533
            endpoint_url=endpoint_url,
534
            aws_access_key_id=aws_access_key_id or INTERNAL_AWS_ACCESS_KEY_ID,
535
            aws_secret_access_key=aws_secret_access_key or INTERNAL_AWS_SECRET_ACCESS_KEY,
536
            aws_session_token=aws_session_token,
537
            config=config,
538
        )
539

540

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

555
        If the region is set to None, it is loaded from following
556
        locations:
557
        - AWS environment variables
558
        - Credentials file `~/.aws/credentials`
559
        - Config file `~/.aws/config`
560

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

579
        # Boto has an odd behaviour when using a non-default (any other region than us-east-1) in config
580
        # If the region in arg is non-default, it gives the arg the precedence
581
        # But if the region in arg is default (us-east-1), it gives precedence to one in config
582
        # Below: always give precedence to arg region
583
        if (
1✔
584
            config
585
            and config.region_name != AWS_REGION_US_EAST_1
586
            and region_name == AWS_REGION_US_EAST_1
587
        ):
588
            config = config.merge(Config(region_name=region_name))
1✔
589

590
        endpoint_url = endpoint_url or self._endpoint or get_service_endpoint()
1✔
591
        if (
1✔
592
            endpoint_url
593
            and service_name == "s3"
594
            and re.match(r"https?://localhost(:[0-9]+)?", endpoint_url)
595
        ):
596
            endpoint_url = endpoint_url.replace("://localhost", f"://{get_s3_hostname()}")
1✔
597

598
        # Prevent `PartialCredentialsError` when only access key ID is provided
599
        # The value of secret access key is insignificant and can be set to anything
600
        if aws_access_key_id:
1✔
601
            aws_secret_access_key = aws_secret_access_key or INTERNAL_AWS_SECRET_ACCESS_KEY
1✔
602

603
        return self._get_client(
1✔
604
            service_name=service_name,
605
            region_name=region_name or config.region_name or self._get_region(),
606
            use_ssl=self._use_ssl,
607
            verify=self._verify,
608
            endpoint_url=endpoint_url,
609
            aws_access_key_id=aws_access_key_id,
610
            aws_secret_access_key=aws_secret_access_key,
611
            aws_session_token=aws_session_token,
612
            config=config,
613
        )
614

615

616
class ExternalAwsClientFactory(ClientFactory):
1✔
617
    def get_client(
1✔
618
        self,
619
        service_name: str,
620
        region_name: str | None = None,
621
        aws_access_key_id: str | None = None,
622
        aws_secret_access_key: str | None = None,
623
        aws_session_token: str | None = None,
624
        endpoint_url: str | None = None,
625
        config: Config | None = None,
626
    ) -> BaseClient:
627
        """
628
        Build and return client for connections originating outside LocalStack and targeting AWS.
629

630
        If either of the access keys or region are set to None, they are loaded from following
631
        locations:
632
        - AWS environment variables
633
        - Credentials file `~/.aws/credentials`
634
        - Config file `~/.aws/config`
635

636
        :param service_name: Service to build the client for, eg. `s3`
637
        :param region_name: Name of the AWS region to be associated with the client
638
            If set to None, loads from botocore session.
639
        :param aws_access_key_id: Access key to use for the client.
640
            If set to None, loads from botocore session.
641
        :param aws_secret_access_key: Secret key to use for the client.
642
            If set to None, loads from botocore session.
643
        :param aws_session_token: Session token to use for the client.
644
            Not being used if not set.
645
        :param endpoint_url: Full endpoint URL to be used by the client.
646
            Defaults to appropriate AWS endpoint.
647
        :param config: Boto config for advanced use.
648
        """
649
        if config is None:
1✔
650
            config = self._config
1✔
651
        else:
652
            config = self._config.merge(config)
×
653

654
        return self._get_client(
1✔
655
            config=config,
656
            service_name=service_name,
657
            region_name=region_name or self._get_session_region(),
658
            endpoint_url=endpoint_url,
659
            use_ssl=True,
660
            verify=True,
661
            aws_access_key_id=aws_access_key_id,
662
            aws_secret_access_key=aws_secret_access_key,
663
            aws_session_token=aws_session_token,
664
        )
665

666

667
def resolve_dns_from_upstream(hostname: str) -> str:
1✔
668
    from localstack.dns.server import get_fallback_dns_server
×
669

670
    upstream_dns = get_fallback_dns_server()
×
671
    request = dns.message.make_query(hostname, "A")
×
672
    response = dns.query.udp(request, upstream_dns, port=53, timeout=5)
×
673
    if len(response.answer) == 0:
×
674
        raise ValueError(f"No DNS response found for hostname '{hostname}'")
×
675

676
    ip_addresses = []
×
677
    for answer in response.answer:
×
678
        if answer.match(dns.rdataclass.IN, dns.rdatatype.A, dns.rdatatype.NONE):
×
679
            ip_addresses.extend(answer.items.keys())
×
680

681
    if not ip_addresses:
×
682
        raise ValueError(f"No DNS records of type 'A' found for hostname '{hostname}'")
×
683

684
    return choice(ip_addresses).address
×
685

686

687
class ExternalBypassDnsClientFactory(ExternalAwsClientFactory):
1✔
688
    """
689
    Client factory that makes requests against AWS ensuring that DNS resolution is not affected by the LocalStack DNS
690
    server.
691
    """
692

693
    def __init__(
1✔
694
        self,
695
        session: Session = None,
696
        config: Config = None,
697
    ):
698
        if ca_cert := os.getenv("REQUESTS_CA_BUNDLE"):
×
699
            LOG.debug("Creating External AWS Client with REQUESTS_CA_BUNDLE=%s", ca_cert)
×
700

701
        proxy_config = Config(
×
702
            proxies={
703
                "http": localstack_config.OUTBOUND_HTTP_PROXY,
704
                "https": localstack_config.OUTBOUND_HTTPS_PROXY,
705
            }
706
        )
707

UNCOV
708
        super().__init__(
×
709
            use_ssl=localstack_config.is_env_not_false("USE_SSL"),
710
            verify=ca_cert or True,
711
            session=session,
712
            config=config.merge(proxy_config) if config else proxy_config,
713
        )
714

715
    def _get_client_post_hook(self, client: BaseClient) -> BaseClient:
1✔
UNCOV
716
        client = super()._get_client_post_hook(client)
×
UNCOV
717
        client._endpoint.http_session = ExternalBypassDnsSession(
×
718
            verify=self._verify, proxies=self._config.proxies
719
        )
720
        return client
×
721

722

723
class ExternalBypassDnsHTTPConnection(AWSHTTPConnection):
1✔
724
    """
725
    Connection class that bypasses the LocalStack DNS server for HTTP connections
726
    """
727

728
    def _new_conn(self) -> socket:
1✔
UNCOV
729
        orig_host = self._dns_host
×
UNCOV
730
        try:
×
UNCOV
731
            self._dns_host = resolve_dns_from_upstream(self._dns_host)
×
UNCOV
732
            return super()._new_conn()
×
733
        finally:
734
            self._dns_host = orig_host
×
735

736

737
class ExternalBypassDnsHTTPSConnection(AWSHTTPSConnection):
1✔
738
    """
739
    Connection class that bypasses the LocalStack DNS server for HTTPS connections
740
    """
741

742
    def _new_conn(self) -> socket:
1✔
UNCOV
743
        orig_host = self._dns_host
×
UNCOV
744
        try:
×
UNCOV
745
            self._dns_host = resolve_dns_from_upstream(self._dns_host)
×
UNCOV
746
            return super()._new_conn()
×
747
        finally:
UNCOV
748
            self._dns_host = orig_host
×
749

750

751
class ExternalBypassDnsHTTPConnectionPool(AWSHTTPConnectionPool):
1✔
752
    ConnectionCls = ExternalBypassDnsHTTPConnection
1✔
753

754

755
class ExternalBypassDnsHTTPSConnectionPool(AWSHTTPSConnectionPool):
1✔
756
    ConnectionCls = ExternalBypassDnsHTTPSConnection
1✔
757

758

759
class ExternalBypassDnsSession(URLLib3Session):
1✔
760
    """
761
    urllib3 session wrapper that uses our custom connection pool.
762
    """
763

764
    def __init__(self, *args, **kwargs):
1✔
UNCOV
765
        super().__init__(*args, **kwargs)
×
766

UNCOV
767
        self._pool_classes_by_scheme["https"] = ExternalBypassDnsHTTPSConnectionPool
×
UNCOV
768
        self._pool_classes_by_scheme["http"] = ExternalBypassDnsHTTPConnectionPool
×
769

770

771
connect_to = InternalClientFactory(use_ssl=localstack_config.DISTRIBUTED_MODE)
1✔
772
connect_externally_to = ExternalClientFactory()
1✔
773

774

775
#
776
# Handlers
777
#
778

779

780
def _handler_create_request_parameters(params: dict[str, Any], context: dict[str, Any], **kwargs):
1✔
781
    """
782
    Construct the data transfer object at the time of parsing the client
783
    parameters and proxy it via the Boto context dict.
784

785
    This handler enables the use of additional keyword parameters in Boto API
786
    operation functions.
787

788
    It uses the `InternalRequestParameters` type annotations to handle supported parameters.
789
    The keys supported by this type will be converted to method parameters by prefixing it with an underscore `_`
790
    and converting the snake case to camel case.
791
    Example:
792
        service_principal -> _ServicePrincipal
793
    """
794

795
    # Names of arguments that can be passed to Boto API operation functions.
796
    # These must correspond to entries on the data transfer object.
797
    dto = InternalRequestParameters()
1✔
798
    for member in InternalRequestParameters.__annotations__.keys():
1✔
799
        parameter = f"_{''.join([part.title() for part in member.split('_')])}"
1✔
800
        if parameter in params:
1✔
801
            dto[member] = params.pop(parameter)
1✔
802

803
    context["_localstack"] = dto
1✔
804

805

806
def _handler_inject_dto_header(params: dict[str, Any], context: dict[str, Any], **kwargs):
1✔
807
    """
808
    Retrieve the data transfer object from the Boto context dict and serialise
809
    it as part of the request headers.
810
    """
811
    if (dto := context.pop("_localstack", None)) is not None:
1✔
812
        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