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

localstack / localstack / 22709357475

05 Mar 2026 08:35AM UTC coverage: 59.732% (-27.2%) from 86.974%
22709357475

Pull #13880

github

web-flow
Merge 28fcab93c into 710618057
Pull Request #13880: Firehose: Replace TaggingService

12 of 12 new or added lines in 2 files covered. (100.0%)

20464 existing lines in 510 files now uncovered.

45290 of 75822 relevant lines covered (59.73%)

0.6 hits per line

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

86.67
/localstack-core/localstack/config.py
1
import ipaddress
1✔
2
import logging
1✔
3
import os
1✔
4
import platform
1✔
5
import re
1✔
6
import socket
1✔
7
import subprocess
1✔
8
import tempfile
1✔
9
import time
1✔
10
import warnings
1✔
11
from collections import defaultdict
1✔
12
from collections.abc import Mapping
1✔
13
from typing import Any, TypeVar
1✔
14

15
from localstack import constants
1✔
16
from localstack.constants import (
1✔
17
    DEFAULT_BUCKET_MARKER_LOCAL,
18
    DEFAULT_DEVELOP_PORT,
19
    DEFAULT_VOLUME_DIR,
20
    ENV_INTERNAL_TEST_COLLECT_METRIC,
21
    ENV_INTERNAL_TEST_RUN,
22
    ENV_INTERNAL_TEST_STORE_METRICS_IN_LOCALSTACK,
23
    FALSE_STRINGS,
24
    LOCALHOST,
25
    LOCALHOST_IP,
26
    LOCALSTACK_ROOT_FOLDER,
27
    LOG_LEVELS,
28
    TRACE_LOG_LEVELS,
29
    TRUE_STRINGS,
30
)
31

32
T = TypeVar("T", str, int)
1✔
33

34
# keep track of start time, for performance debugging
35
load_start_time = time.time()
1✔
36

37

38
class Directories:
1✔
39
    """
40
    Holds different directories available to localstack. Some directories are shared between the host and the
41
    localstack container, some live only on the host and others in the container.
42

43
    Attributes:
44
        static_libs: container only; binaries and libraries statically packaged with the image
45
        var_libs:    shared; binaries and libraries+data computed at runtime: lazy-loaded binaries, ssl cert, ...
46
        cache:       shared; ephemeral data that has to persist across localstack runs and reboots
47
        tmp:         container only; ephemeral data that has to persist across localstack runs but not reboots
48
        mounted_tmp: shared; same as above, but shared for persistence across different containers, tests, ...
49
        functions:   shared; volume to communicate between host<->lambda containers
50
        data:        shared; holds localstack state, pods, ...
51
        config:      host only; pre-defined configuration values, cached credentials, machine id, ...
52
        init:        shared; user-defined provisioning scripts executed in the container when it starts
53
        logs:        shared; log files produced by localstack
54
    """
55

56
    static_libs: str
1✔
57
    var_libs: str
1✔
58
    cache: str
1✔
59
    tmp: str
1✔
60
    mounted_tmp: str
1✔
61
    functions: str
1✔
62
    data: str
1✔
63
    config: str
1✔
64
    init: str
1✔
65
    logs: str
1✔
66

67
    def __init__(
1✔
68
        self,
69
        static_libs: str,
70
        var_libs: str,
71
        cache: str,
72
        tmp: str,
73
        mounted_tmp: str,
74
        functions: str,
75
        data: str,
76
        config: str,
77
        init: str,
78
        logs: str,
79
    ) -> None:
80
        super().__init__()
1✔
81
        self.static_libs = static_libs
1✔
82
        self.var_libs = var_libs
1✔
83
        self.cache = cache
1✔
84
        self.tmp = tmp
1✔
85
        self.mounted_tmp = mounted_tmp
1✔
86
        self.functions = functions
1✔
87
        self.data = data
1✔
88
        self.config = config
1✔
89
        self.init = init
1✔
90
        self.logs = logs
1✔
91

92
    @staticmethod
1✔
93
    def defaults() -> "Directories":
1✔
94
        """Returns Localstack directory paths based on the localstack filesystem hierarchy."""
95
        return Directories(
1✔
96
            static_libs="/usr/lib/localstack",
97
            var_libs=f"{DEFAULT_VOLUME_DIR}/lib",
98
            cache=f"{DEFAULT_VOLUME_DIR}/cache",
99
            tmp=os.path.join(tempfile.gettempdir(), "localstack"),
100
            mounted_tmp=f"{DEFAULT_VOLUME_DIR}/tmp",
101
            functions=f"{DEFAULT_VOLUME_DIR}/tmp",  # FIXME: remove - this was misconceived
102
            data=f"{DEFAULT_VOLUME_DIR}/state",
103
            logs=f"{DEFAULT_VOLUME_DIR}/logs",
104
            config="/etc/localstack/conf.d",  # for future use
105
            init="/etc/localstack/init",
106
        )
107

108
    @staticmethod
1✔
109
    def for_container() -> "Directories":
1✔
110
        """
111
        Returns Localstack directory paths as they are defined within the container. Everything shared and writable
112
        lives in /var/lib/localstack or {tempfile.gettempdir()}/localstack.
113

114
        :returns: Directories object
115
        """
116
        defaults = Directories.defaults()
1✔
117

118
        return Directories(
1✔
119
            static_libs=defaults.static_libs,
120
            var_libs=defaults.var_libs,
121
            cache=defaults.cache,
122
            tmp=defaults.tmp,
123
            mounted_tmp=defaults.mounted_tmp,
124
            functions=defaults.functions,
125
            data=defaults.data if PERSISTENCE else os.path.join(defaults.tmp, "state"),
126
            config=defaults.config,
127
            logs=defaults.logs,
128
            init=defaults.init,
129
        )
130

131
    @staticmethod
1✔
132
    def for_host() -> "Directories":
1✔
133
        """Return directories used for running localstack in host mode. Note that these are *not* the directories
134
        that are mounted into the container when the user starts localstack."""
135
        root = os.environ.get("FILESYSTEM_ROOT") or os.path.join(
1✔
136
            LOCALSTACK_ROOT_FOLDER, ".filesystem"
137
        )
138
        root = os.path.abspath(root)
1✔
139

140
        defaults = Directories.for_container()
1✔
141

142
        tmp = os.path.join(root, defaults.tmp.lstrip("/"))
1✔
143
        data = os.path.join(root, defaults.data.lstrip("/"))
1✔
144

145
        return Directories(
1✔
146
            static_libs=os.path.join(root, defaults.static_libs.lstrip("/")),
147
            var_libs=os.path.join(root, defaults.var_libs.lstrip("/")),
148
            cache=os.path.join(root, defaults.cache.lstrip("/")),
149
            tmp=tmp,
150
            mounted_tmp=os.path.join(root, defaults.mounted_tmp.lstrip("/")),
151
            functions=os.path.join(root, defaults.functions.lstrip("/")),
152
            data=data if PERSISTENCE else os.path.join(tmp, "state"),
153
            config=os.path.join(root, defaults.config.lstrip("/")),
154
            init=os.path.join(root, defaults.init.lstrip("/")),
155
            logs=os.path.join(root, defaults.logs.lstrip("/")),
156
        )
157

158
    @staticmethod
1✔
159
    def for_cli() -> "Directories":
1✔
160
        """Returns directories used for when running localstack CLI commands from the host system. Unlike
161
        ``for_container``, these needs to be cross-platform. Ideally, this should not be needed at all,
162
        because the localstack runtime and CLI do not share any control paths. There are a handful of
163
        situations where directories or files may be created lazily for CLI commands. Some paths are
164
        intentionally set to None to provoke errors if these paths are used from the CLI - which they
165
        shouldn't. This is a symptom of not having a clear separation between CLI/runtime code, which will
166
        be a future project."""
167
        import tempfile
×
168

169
        from localstack.utils import files
×
170

171
        tmp_dir = os.path.join(tempfile.gettempdir(), "localstack-cli")
×
172
        cache_dir = (files.get_user_cache_dir()).absolute() / "localstack-cli"
×
173

174
        return Directories(
×
175
            static_libs=None,
176
            var_libs=None,
177
            cache=str(cache_dir),  # used by analytics metadata
178
            tmp=tmp_dir,
179
            mounted_tmp=tmp_dir,
180
            functions=None,
181
            data=os.path.join(tmp_dir, "state"),  # used by localstack-pro config TODO: remove
182
            logs=os.path.join(tmp_dir, "logs"),  # used for container logs
183
            config=None,  # in the context of the CLI, config.CONFIG_DIR should be used
184
            init=None,
185
        )
186

187
    def mkdirs(self):
1✔
188
        for folder in [
1✔
189
            self.static_libs,
190
            self.var_libs,
191
            self.cache,
192
            self.tmp,
193
            self.mounted_tmp,
194
            self.functions,
195
            self.data,
196
            self.config,
197
            self.init,
198
            self.logs,
199
        ]:
200
            if folder and not os.path.exists(folder):
1✔
201
                try:
1✔
202
                    os.makedirs(folder)
1✔
203
                except Exception:
×
204
                    # this can happen due to a race condition when starting
205
                    # multiple processes in parallel. Should be safe to ignore
206
                    pass
×
207

208
    def __str__(self):
1✔
209
        return str(self.__dict__)
×
210

211

212
def eval_log_type(env_var_name: str) -> str | bool:
1✔
213
    """Get the log type from environment variable"""
214
    ls_log = os.environ.get(env_var_name, "").lower().strip()
1✔
215
    return ls_log if ls_log in LOG_LEVELS else False
1✔
216

217

218
def parse_boolean_env(env_var_name: str) -> bool | None:
1✔
219
    """Parse the value of the given env variable and return True/False, or None if it is not a boolean value."""
220
    value = os.environ.get(env_var_name, "").lower().strip()
1✔
221
    if value in TRUE_STRINGS:
1✔
222
        return True
×
223
    if value in FALSE_STRINGS:
1✔
224
        return False
×
225
    return None
1✔
226

227

228
def parse_comma_separated_list(env_var_name: str) -> list[str]:
1✔
229
    """Parse a comma separated list from the given environment variable."""
230
    return os.environ.get(env_var_name, "").strip().split(",")
1✔
231

232

233
def is_env_true(env_var_name: str) -> bool:
1✔
234
    """Whether the given environment variable has a truthy value."""
235
    return os.environ.get(env_var_name, "").lower().strip() in TRUE_STRINGS
1✔
236

237

238
def is_env_not_false(env_var_name: str) -> bool:
1✔
239
    """Whether the given environment variable is empty or has a truthy value."""
240
    return os.environ.get(env_var_name, "").lower().strip() not in FALSE_STRINGS
1✔
241

242

243
def load_environment(profiles: str = None, env=os.environ) -> list[str]:
1✔
244
    """Loads the environment variables from ~/.localstack/{profile}.env, for each profile listed in the profiles.
245
    :param env: environment to load profile to. Defaults to `os.environ`
246
    :param profiles: a comma separated list of profiles to load (defaults to "default")
247
    :returns str: the list of the actually loaded profiles (might be the fallback)
248
    """
249
    if not profiles:
1✔
250
        profiles = "default"
1✔
251

252
    profiles = profiles.split(",")
1✔
253
    environment = {}
1✔
254
    import dotenv
1✔
255

256
    for profile in profiles:
1✔
257
        profile = profile.strip()
1✔
258
        path = os.path.join(CONFIG_DIR, f"{profile}.env")
1✔
259
        if not os.path.exists(path):
1✔
260
            continue
1✔
261
        environment.update(dotenv.dotenv_values(path))
1✔
262

263
    for k, v in environment.items():
1✔
264
        # we do not want to override the environment
265
        if k not in env and v is not None:
1✔
266
            env[k] = v
1✔
267

268
    return profiles
1✔
269

270

271
def is_persistence_enabled() -> bool:
1✔
272
    return PERSISTENCE and dirs.data
1✔
273

274

275
def is_linux() -> bool:
1✔
276
    return platform.system() == "Linux"
1✔
277

278

279
def is_macos() -> bool:
1✔
280
    return platform.system() == "Darwin"
1✔
281

282

283
def is_windows() -> bool:
1✔
284
    return platform.system().lower() == "windows"
1✔
285

286

287
def is_wsl() -> bool:
1✔
288
    return platform.system().lower() == "linux" and os.environ.get("WSL_DISTRO_NAME") is not None
1✔
289

290

291
def ping(host):
1✔
292
    """Returns True if the host responds to a ping request"""
293
    is_in_windows = is_windows()
1✔
294
    ping_opts = "-n 1 -w 2000" if is_in_windows else "-c 1 -W 2"
1✔
295
    args = f"ping {ping_opts} {host}"
1✔
296
    return (
1✔
297
        subprocess.call(
298
            args, shell=not is_in_windows, stdout=subprocess.PIPE, stderr=subprocess.PIPE
299
        )
300
        == 0
301
    )
302

303

304
def in_docker():
1✔
305
    """
306
    Returns True if running in a docker container, else False
307
    Ref. https://docs.docker.com/config/containers/runmetrics/#control-groups
308
    """
309
    if OVERRIDE_IN_DOCKER is not None:
1✔
310
        return OVERRIDE_IN_DOCKER
×
311

312
    # check some marker files that we create in our Dockerfiles
313
    for path in [
1✔
314
        "/usr/lib/localstack/.community-version",
315
        "/usr/lib/localstack/.pro-version",
316
        "/tmp/localstack/.marker",
317
    ]:
318
        if os.path.isfile(path):
1✔
319
            return True
1✔
320

321
    # details: https://github.com/localstack/localstack/pull/4352
322
    if os.path.exists("/.dockerenv"):
1✔
323
        return True
×
324
    if os.path.exists("/run/.containerenv"):
1✔
325
        return True
×
326

327
    if not os.path.exists("/proc/1/cgroup"):
1✔
328
        return False
×
329
    try:
1✔
330
        if any(
1✔
331
            [
332
                os.path.exists("/sys/fs/cgroup/memory/docker/"),
333
                any(
334
                    "docker-" in file_names
335
                    for file_names in os.listdir("/sys/fs/cgroup/memory/system.slice")
336
                ),
337
                os.path.exists("/sys/fs/cgroup/docker/"),
338
                any(
339
                    "docker-" in file_names
340
                    for file_names in os.listdir("/sys/fs/cgroup/system.slice/")
341
                ),
342
            ]
343
        ):
344
            return False
×
345
    except Exception:
1✔
346
        pass
1✔
347
    with open("/proc/1/cgroup") as ifh:
1✔
348
        content = ifh.read()
1✔
349
        if "docker" in content or "buildkit" in content:
1✔
350
            return True
×
351
        os_hostname = socket.gethostname()
1✔
352
        if os_hostname and os_hostname in content:
1✔
353
            return True
×
354

355
    # containerd does not set any specific file or config, but it does use
356
    # io.containerd.snapshotter.v1.overlayfs as the overlay filesystem for `/`.
357
    try:
1✔
358
        with open("/proc/mounts") as infile:
1✔
359
            for line in infile:
1✔
360
                line = line.strip()
1✔
361

362
                if not line:
1✔
363
                    continue
×
364

365
                # skip comments
366
                if line[0] == "#":
1✔
367
                    continue
×
368

369
                # format (man 5 fstab)
370
                # <spec> <mount point> <type> <options> <rest>...
371
                parts = line.split()
1✔
372
                if len(parts) < 4:
1✔
373
                    # badly formatted line
374
                    continue
×
375

376
                mount_point = parts[1]
1✔
377
                options = parts[3]
1✔
378

379
                # only consider the root filesystem
380
                if mount_point != "/":
1✔
381
                    continue
1✔
382

383
                if "io.containerd" in options:
1✔
384
                    return True
×
385

386
    except FileNotFoundError:
×
387
        pass
×
388

389
    return False
1✔
390

391

392
# whether the `in_docker` check should always return True or False
393
OVERRIDE_IN_DOCKER = parse_boolean_env("OVERRIDE_IN_DOCKER")
1✔
394

395
is_in_docker = in_docker()
1✔
396
is_in_linux = is_linux()
1✔
397
is_in_macos = is_macos()
1✔
398
is_in_windows = is_windows()
1✔
399
is_in_wsl = is_wsl()
1✔
400
default_ip = "0.0.0.0" if is_in_docker else "127.0.0.1"
1✔
401

402
# CLI specific: the configuration profile to load
403
CONFIG_PROFILE = os.environ.get("CONFIG_PROFILE", "").strip()
1✔
404

405
# CLI specific: host configuration directory
406
CONFIG_DIR = os.environ.get("CONFIG_DIR", os.path.expanduser("~/.localstack"))
1✔
407

408
# keep this on top to populate the environment
409
try:
1✔
410
    # CLI specific: the actually loaded configuration profile
411
    LOADED_PROFILES = load_environment(CONFIG_PROFILE)
1✔
412
except ImportError:
×
413
    # dotenv may not be available in lambdas or other environments where config is loaded
414
    LOADED_PROFILES = None
×
415

416
# loaded components name - default: all components are loaded and the first one is chosen
417
RUNTIME_COMPONENTS = os.environ.get("RUNTIME_COMPONENTS", "").strip()
1✔
418

419
# directory for persisting data (TODO: deprecated, simply use PERSISTENCE=1)
420
DATA_DIR = os.environ.get("DATA_DIR", "").strip()
1✔
421

422
# whether localstack should persist service state across localstack runs
423
PERSISTENCE = is_env_true("PERSISTENCE")
1✔
424

425
# the strategy for loading snapshots from disk when `PERSISTENCE=1` is used (on_startup, on_request, manual)
426
SNAPSHOT_LOAD_STRATEGY = os.environ.get("SNAPSHOT_LOAD_STRATEGY", "").upper()
1✔
427

428
# the strategy saving snapshots to disk when `PERSISTENCE=1` is used (on_shutdown, on_request, scheduled, manual)
429
SNAPSHOT_SAVE_STRATEGY = os.environ.get("SNAPSHOT_SAVE_STRATEGY", "").upper()
1✔
430

431
# the flush interval (in seconds) for persistence when the snapshot save strategy is set to "scheduled"
432
SNAPSHOT_FLUSH_INTERVAL = int(os.environ.get("SNAPSHOT_FLUSH_INTERVAL") or 15)
1✔
433

434
# whether to clear config.dirs.tmp on startup and shutdown
435
CLEAR_TMP_FOLDER = is_env_not_false("CLEAR_TMP_FOLDER")
1✔
436

437
# folder for temporary files and data
438
TMP_FOLDER = os.path.join(tempfile.gettempdir(), "localstack")
1✔
439

440
# this is exclusively for the CLI to configure the container mount into /var/lib/localstack
441
VOLUME_DIR = os.environ.get("LOCALSTACK_VOLUME_DIR", "").strip() or TMP_FOLDER
1✔
442

443
# fix for Mac OS, to be able to mount /var/folders in Docker
444
if TMP_FOLDER.startswith("/var/folders/") and os.path.exists(f"/private{TMP_FOLDER}"):
1✔
445
    TMP_FOLDER = f"/private{TMP_FOLDER}"
×
446

447
# whether to enable verbose debug logging ("LOG" is used when using the CLI with LOCALSTACK_LOG instead of LS_LOG)
448
LS_LOG = eval_log_type("LS_LOG") or eval_log_type("LOG")
1✔
449
DEBUG = is_env_true("DEBUG") or LS_LOG in TRACE_LOG_LEVELS
1✔
450

451
# PUBLIC PREVIEW: 0 (default), 1 (preview)
452
# When enabled it triggers specialised workflows for the debugging.
453
LAMBDA_DEBUG_MODE = is_env_true("LAMBDA_DEBUG_MODE")
1✔
454

455
# path to the lambda debug mode configuration file.
456
LAMBDA_DEBUG_MODE_CONFIG_PATH = os.environ.get("LAMBDA_DEBUG_MODE_CONFIG_PATH")
1✔
457

458
# EXPERIMENTAL: allow setting custom log levels for individual loggers
459
LOG_LEVEL_OVERRIDES = os.environ.get("LOG_LEVEL_OVERRIDES", "")
1✔
460

461
# whether to enable debugpy
462
DEVELOP = is_env_true("DEVELOP")
1✔
463

464
# PORT FOR DEBUGGER
465
DEVELOP_PORT = int(os.environ.get("DEVELOP_PORT", "").strip() or DEFAULT_DEVELOP_PORT)
1✔
466

467
# whether to make debugpy wait for a debbuger client
468
WAIT_FOR_DEBUGGER = is_env_true("WAIT_FOR_DEBUGGER")
1✔
469

470
# whether to assume http or https for `get_protocol`
471
USE_SSL = is_env_true("USE_SSL")
1✔
472

473
# Whether to report internal failures as 500 or 501 errors.
474
FAIL_FAST = is_env_true("FAIL_FAST")
1✔
475

476
# whether to run in TF compatibility mode for TF integration tests
477
# (e.g., returning verbatim ports for ELB resources, rather than edge port 4566, etc.)
478
TF_COMPAT_MODE = is_env_true("TF_COMPAT_MODE")
1✔
479

480
# default encoding used to convert strings to byte arrays (mainly for Python 3 compatibility)
481
DEFAULT_ENCODING = "utf-8"
1✔
482

483
# path to local Docker UNIX domain socket
484
DOCKER_SOCK = os.environ.get("DOCKER_SOCK", "").strip() or "/var/run/docker.sock"
1✔
485

486
# additional flags to pass to "docker run" when starting the stack in Docker
487
DOCKER_FLAGS = os.environ.get("DOCKER_FLAGS", "").strip()
1✔
488

489
# command used to run Docker containers (e.g., set to "sudo docker" to run as sudo)
490
DOCKER_CMD = os.environ.get("DOCKER_CMD", "").strip() or "docker"
1✔
491

492
# use the command line docker client instead of the new sdk version, might get removed in the future
493
LEGACY_DOCKER_CLIENT = is_env_true("LEGACY_DOCKER_CLIENT")
1✔
494

495
# Docker image to use when starting up containers for port checks
496
PORTS_CHECK_DOCKER_IMAGE = os.environ.get("PORTS_CHECK_DOCKER_IMAGE", "").strip()
1✔
497

498
# global prefix to prepend to Docker image names (e.g., for using a custom registry mirror)
499
DOCKER_GLOBAL_IMAGE_PREFIX = os.environ.get("DOCKER_GLOBAL_IMAGE_PREFIX", "").strip()
1✔
500

501

502
def is_trace_logging_enabled():
1✔
503
    if LS_LOG:
1✔
504
        log_level = str(LS_LOG).upper()
×
505
        return log_level.lower() in TRACE_LOG_LEVELS
×
506
    return False
1✔
507

508

509
# set log levels immediately, but will be overwritten later by setup_logging
510
if DEBUG:
1✔
511
    logging.getLogger("").setLevel(logging.DEBUG)
1✔
512
    logging.getLogger("localstack").setLevel(logging.DEBUG)
1✔
513

514
LOG = logging.getLogger(__name__)
1✔
515
if is_trace_logging_enabled():
1✔
516
    load_end_time = time.time()
×
517
    LOG.debug(
×
518
        "Initializing the configuration took %s ms", int((load_end_time - load_start_time) * 1000)
519
    )
520

521

522
def is_ipv6_address(host: str) -> bool:
1✔
523
    """
524
    Returns True if the given host is an IPv6 address.
525
    """
526

527
    if not host:
1✔
528
        return False
×
529

530
    try:
1✔
531
        ipaddress.IPv6Address(host)
1✔
532
        return True
1✔
533
    except ipaddress.AddressValueError:
1✔
534
        return False
1✔
535

536

537
class HostAndPort:
1✔
538
    """
539
    Definition of an address for a server to listen to.
540

541
    Includes a `parse` method to convert from `str`, allowing for default fallbacks, as well as
542
    some helper methods to help tests - particularly testing for equality and a hash function
543
    so that `HostAndPort` instances can be used as keys to dictionaries.
544
    """
545

546
    host: str
1✔
547
    port: int
1✔
548

549
    def __init__(self, host: str, port: int):
1✔
550
        self.host = host
1✔
551
        self.port = port
1✔
552

553
    @classmethod
1✔
554
    def parse(
1✔
555
        cls,
556
        input: str,
557
        default_host: str,
558
        default_port: int,
559
    ) -> "HostAndPort":
560
        """
561
        Parse a `HostAndPort` from strings like:
562
            - 0.0.0.0:4566 -> host=0.0.0.0, port=4566
563
            - 0.0.0.0      -> host=0.0.0.0, port=`default_port`
564
            - :4566        -> host=`default_host`, port=4566
565
            - [::]:4566    -> host=[::], port=4566
566
            - [::1]        -> host=[::1], port=`default_port`
567
        """
568
        host, port = default_host, default_port
1✔
569

570
        # recognize IPv6 addresses (+ port)
571
        if input.startswith("["):
1✔
572
            ipv6_pattern = re.compile(r"^\[(?P<host>[^]]+)\](:(?P<port>\d+))?$")
1✔
573
            match = ipv6_pattern.match(input)
1✔
574

575
            if match:
1✔
576
                host = match.group("host")
1✔
577
                if not is_ipv6_address(host):
1✔
578
                    raise ValueError(
1✔
579
                        f"input looks like an IPv6 address (is enclosed in square brackets), but is not valid: {host}"
580
                    )
581
                port_s = match.group("port")
1✔
582
                if port_s:
1✔
583
                    port = cls._validate_port(port_s)
1✔
584
            else:
585
                raise ValueError(
×
586
                    f'input looks like an IPv6 address, but is invalid. Should be formatted "[ip]:port": {input}'
587
                )
588

589
        # recognize IPv4 address + port
590
        elif ":" in input:
1✔
591
            hostname, port_s = input.split(":", 1)
1✔
592
            if hostname.strip():
1✔
593
                host = hostname.strip()
1✔
594
            port = cls._validate_port(port_s)
1✔
595
        else:
596
            if input.strip():
1✔
597
                host = input.strip()
1✔
598

599
        # validation
600
        if port < 0 or port >= 2**16:
1✔
601
            raise ValueError("port out of range")
1✔
602

603
        return cls(host=host, port=port)
1✔
604

605
    @classmethod
1✔
606
    def _validate_port(cls, port_s: str) -> int:
1✔
607
        try:
1✔
608
            port = int(port_s)
1✔
609
        except ValueError as e:
1✔
610
            raise ValueError(f"specified port {port_s} not a number") from e
1✔
611

612
        return port
1✔
613

614
    def _get_unprivileged_port_range_start(self) -> int:
1✔
615
        try:
×
616
            with open("/proc/sys/net/ipv4/ip_unprivileged_port_start") as unprivileged_port_start:
×
617
                port = unprivileged_port_start.read()
×
618
                return int(port.strip())
×
619
        except Exception:
×
620
            return 1024
×
621

622
    def is_unprivileged(self) -> bool:
1✔
623
        return self.port >= self._get_unprivileged_port_range_start()
×
624

625
    def host_and_port(self) -> str:
1✔
626
        formatted_host = f"[{self.host}]" if is_ipv6_address(self.host) else self.host
1✔
627
        return f"{formatted_host}:{self.port}" if self.port is not None else formatted_host
1✔
628

629
    def __hash__(self) -> int:
1✔
630
        return hash((self.host, self.port))
×
631

632
    # easier tests
633
    def __eq__(self, other: "str | HostAndPort") -> bool:
1✔
634
        if isinstance(other, self.__class__):
1✔
635
            return self.host == other.host and self.port == other.port
1✔
636
        elif isinstance(other, str):
1✔
637
            return str(self) == other
1✔
638
        else:
639
            raise TypeError(f"cannot compare {self.__class__} to {other.__class__}")
×
640

641
    def __str__(self) -> str:
1✔
642
        return self.host_and_port()
1✔
643

644
    def __repr__(self) -> str:
645
        return f"HostAndPort(host={self.host}, port={self.port})"
646

647

648
class UniqueHostAndPortList(list[HostAndPort]):
1✔
649
    """
650
    Container type that ensures that ports added to the list are unique based
651
    on these rules:
652
        - :: "trumps" any other binding on the same port, including both IPv6 and IPv4
653
          addresses. All other bindings for this port are removed, since :: already
654
          covers all interfaces. For example, adding 127.0.0.1:4566, [::1]:4566,
655
          and [::]:4566 would result in only [::]:4566 being preserved.
656
        - 0.0.0.0 "trumps" any other binding on IPv4 addresses only. IPv6 addresses
657
          are not removed.
658
        - Identical identical hosts and ports are de-duped
659
    """
660

661
    def __init__(self, iterable: list[HostAndPort] | None = None):
1✔
662
        super().__init__(iterable or [])
1✔
663
        self._ensure_unique()
1✔
664

665
    def _ensure_unique(self):
1✔
666
        """
667
        Ensure that all bindings on the same port are de-duped.
668
        """
669
        if len(self) <= 1:
1✔
670
            return
1✔
671

672
        unique: list[HostAndPort] = []
1✔
673

674
        # Build a dictionary of hosts by port
675
        hosts_by_port: dict[int, list[str]] = defaultdict(list)
1✔
676
        for item in self:
1✔
677
            hosts_by_port[item.port].append(item.host)
1✔
678

679
        # For any given port, dedupe the hosts
680
        for port, hosts in hosts_by_port.items():
1✔
681
            deduped_hosts = set(hosts)
1✔
682

683
            # IPv6 all interfaces: this is the most general binding.
684
            # Any others should be removed.
685
            if "::" in deduped_hosts:
1✔
686
                unique.append(HostAndPort(host="::", port=port))
1✔
687
                continue
1✔
688
            # IPv4 all interfaces: this is the next most general binding.
689
            # Any others should be removed.
690
            if "0.0.0.0" in deduped_hosts:
1✔
691
                unique.append(HostAndPort(host="0.0.0.0", port=port))
1✔
692
                continue
1✔
693

694
            # All other bindings just need to be unique
695
            unique.extend([HostAndPort(host=host, port=port) for host in deduped_hosts])
1✔
696

697
        self.clear()
1✔
698
        self.extend(unique)
1✔
699

700
    def append(self, value: HostAndPort):
1✔
701
        super().append(value)
1✔
702
        self._ensure_unique()
1✔
703

704

705
def populate_edge_configuration(
1✔
706
    environment: Mapping[str, str],
707
) -> tuple[HostAndPort, UniqueHostAndPortList]:
708
    """Populate the LocalStack edge configuration from environment variables."""
709
    localstack_host_raw = environment.get("LOCALSTACK_HOST")
1✔
710
    gateway_listen_raw = environment.get("GATEWAY_LISTEN")
1✔
711

712
    # parse gateway listen from multiple components
713
    if gateway_listen_raw is not None:
1✔
714
        gateway_listen = []
1✔
715
        for address in gateway_listen_raw.split(","):
1✔
716
            gateway_listen.append(
1✔
717
                HostAndPort.parse(
718
                    address.strip(),
719
                    default_host=default_ip,
720
                    default_port=constants.DEFAULT_PORT_EDGE,
721
                )
722
            )
723
    else:
724
        # use default if gateway listen is not defined
725
        gateway_listen = [HostAndPort(host=default_ip, port=constants.DEFAULT_PORT_EDGE)]
1✔
726

727
    # the actual value of the LOCALSTACK_HOST port now depends on what gateway listen actually listens to.
728
    if localstack_host_raw is None:
1✔
729
        localstack_host = HostAndPort(
1✔
730
            host=constants.LOCALHOST_HOSTNAME, port=gateway_listen[0].port
731
        )
732
    else:
733
        localstack_host = HostAndPort.parse(
1✔
734
            localstack_host_raw,
735
            default_host=constants.LOCALHOST_HOSTNAME,
736
            default_port=gateway_listen[0].port,
737
        )
738

739
    assert gateway_listen is not None
1✔
740
    assert localstack_host is not None
1✔
741

742
    return (
1✔
743
        localstack_host,
744
        UniqueHostAndPortList(gateway_listen),
745
    )
746

747

748
# How to access LocalStack
749
(
1✔
750
    # -- Cosmetic
751
    LOCALSTACK_HOST,
752
    # -- Edge configuration
753
    # Main configuration of the listen address of the hypercorn proxy. Of the form
754
    # <ip_address>:<port>(,<ip_address>:port>)*
755
    GATEWAY_LISTEN,
756
) = populate_edge_configuration(os.environ)
757

758
GATEWAY_WORKER_COUNT = int(os.environ.get("GATEWAY_WORKER_COUNT") or 1000)
1✔
759

760
# the gateway server that should be used (supported: hypercorn, twisted dev: werkzeug)
761
GATEWAY_SERVER = os.environ.get("GATEWAY_SERVER", "").strip() or "twisted"
1✔
762

763
# IP of the docker bridge used to enable access between containers
764
DOCKER_BRIDGE_IP = os.environ.get("DOCKER_BRIDGE_IP", "").strip()
1✔
765

766
# Default timeout for Docker API calls sent by the Docker SDK client, in seconds.
767
DOCKER_SDK_DEFAULT_TIMEOUT_SECONDS = int(os.environ.get("DOCKER_SDK_DEFAULT_TIMEOUT_SECONDS") or 60)
1✔
768

769
# Default number of retries to connect to the Docker API by the Docker SDK client.
770
DOCKER_SDK_DEFAULT_RETRIES = int(os.environ.get("DOCKER_SDK_DEFAULT_RETRIES") or 0)
1✔
771

772
# whether to enable API-based updates of configuration variables at runtime
773
ENABLE_CONFIG_UPDATES = is_env_true("ENABLE_CONFIG_UPDATES")
1✔
774

775
# CORS settings
776
DISABLE_CORS_HEADERS = is_env_true("DISABLE_CORS_HEADERS")
1✔
777
DISABLE_CORS_CHECKS = is_env_true("DISABLE_CORS_CHECKS")
1✔
778
DISABLE_CUSTOM_CORS_S3 = is_env_true("DISABLE_CUSTOM_CORS_S3")
1✔
779
DISABLE_CUSTOM_CORS_APIGATEWAY = is_env_true("DISABLE_CUSTOM_CORS_APIGATEWAY")
1✔
780
EXTRA_CORS_ALLOWED_HEADERS = os.environ.get("EXTRA_CORS_ALLOWED_HEADERS", "").strip()
1✔
781
EXTRA_CORS_EXPOSE_HEADERS = os.environ.get("EXTRA_CORS_EXPOSE_HEADERS", "").strip()
1✔
782
EXTRA_CORS_ALLOWED_ORIGINS = os.environ.get("EXTRA_CORS_ALLOWED_ORIGINS", "").strip()
1✔
783
DISABLE_PREFLIGHT_PROCESSING = is_env_true("DISABLE_PREFLIGHT_PROCESSING")
1✔
784

785
# whether to disable publishing events to the API
786
DISABLE_EVENTS = is_env_true("DISABLE_EVENTS")
1✔
787
DEBUG_ANALYTICS = is_env_true("DEBUG_ANALYTICS")
1✔
788

789
# whether to log fine-grained debugging information for the handler chain
790
DEBUG_HANDLER_CHAIN = is_env_true("DEBUG_HANDLER_CHAIN")
1✔
791

792
# whether to eagerly start services
793
EAGER_SERVICE_LOADING = is_env_true("EAGER_SERVICE_LOADING")
1✔
794

795
# whether to selectively load services in SERVICES
796
STRICT_SERVICE_LOADING = is_env_not_false("STRICT_SERVICE_LOADING")
1✔
797

798
# Whether to skip downloading additional infrastructure components (e.g., custom Elasticsearch versions)
799
SKIP_INFRA_DOWNLOADS = os.environ.get("SKIP_INFRA_DOWNLOADS", "").strip()
1✔
800

801
# Whether to skip downloading our signed SSL cert.
802
SKIP_SSL_CERT_DOWNLOAD = is_env_true("SKIP_SSL_CERT_DOWNLOAD")
1✔
803

804
# Absolute path to a custom certificate (pem file)
805
CUSTOM_SSL_CERT_PATH = os.environ.get("CUSTOM_SSL_CERT_PATH", "").strip()
1✔
806

807
# Whether delete the cached signed SSL certificate at startup
808
REMOVE_SSL_CERT = is_env_true("REMOVE_SSL_CERT")
1✔
809

810
# Allow non-standard AWS regions
811
ALLOW_NONSTANDARD_REGIONS = is_env_true("ALLOW_NONSTANDARD_REGIONS")
1✔
812
if ALLOW_NONSTANDARD_REGIONS:
1✔
813
    os.environ["MOTO_ALLOW_NONEXISTENT_REGION"] = "true"
×
814

815
# name of the main Docker container
816
MAIN_CONTAINER_NAME = os.environ.get("MAIN_CONTAINER_NAME", "").strip() or "localstack-main"
1✔
817

818
# the latest commit id of the repository when the docker image was created
819
LOCALSTACK_BUILD_GIT_HASH = os.environ.get("LOCALSTACK_BUILD_GIT_HASH", "").strip() or None
1✔
820

821
# the date on which the docker image was created
822
LOCALSTACK_BUILD_DATE = os.environ.get("LOCALSTACK_BUILD_DATE", "").strip() or None
1✔
823

824
# Equivalent to HTTP_PROXY, but only applicable for external connections
825
OUTBOUND_HTTP_PROXY = os.environ.get("OUTBOUND_HTTP_PROXY", "")
1✔
826

827
# Equivalent to HTTPS_PROXY, but only applicable for external connections
828
OUTBOUND_HTTPS_PROXY = os.environ.get("OUTBOUND_HTTPS_PROXY", "")
1✔
829

830
# Feature flag to enable validation of internal endpoint responses in the handler chain. For test use only.
831
OPENAPI_VALIDATE_RESPONSE = is_env_true("OPENAPI_VALIDATE_RESPONSE")
1✔
832
# Flag to enable the validation of the requests made to the LocalStack internal endpoints. Active by default.
833
OPENAPI_VALIDATE_REQUEST = is_env_true("OPENAPI_VALIDATE_REQUEST")
1✔
834

835
# environment variable to determine whether to include stack traces in http responses
836
INCLUDE_STACK_TRACES_IN_HTTP_RESPONSE = is_env_true("INCLUDE_STACK_TRACES_IN_HTTP_RESPONSE")
1✔
837

838
# whether to skip waiting for the infrastructure to shut down, or exit immediately
839
FORCE_SHUTDOWN = is_env_not_false("FORCE_SHUTDOWN")
1✔
840

841
# set variables no_proxy, i.e., run internal service calls directly
842
no_proxy = ",".join([constants.LOCALHOST_HOSTNAME, LOCALHOST, LOCALHOST_IP, "[::1]"])
1✔
843
if os.environ.get("no_proxy"):
1✔
844
    os.environ["no_proxy"] += "," + no_proxy
×
845
elif os.environ.get("NO_PROXY"):
1✔
846
    os.environ["NO_PROXY"] += "," + no_proxy
×
847
else:
848
    os.environ["no_proxy"] = no_proxy
1✔
849

850
# additional CLI commands, can be set by plugins
851
CLI_COMMANDS = {}
1✔
852

853
# determine IP of Docker bridge
854
if not DOCKER_BRIDGE_IP:
1✔
855
    DOCKER_BRIDGE_IP = "172.17.0.1"
1✔
856
    if is_in_docker:
1✔
857
        candidates = (DOCKER_BRIDGE_IP, "172.18.0.1")
1✔
858
        for ip in candidates:
1✔
859
            # TODO: remove from here - should not perform I/O operations in top-level config.py
860
            if ping(ip):
1✔
861
                DOCKER_BRIDGE_IP = ip
1✔
862
                break
1✔
863

864
# AWS account used to store internal resources such as Lambda archives or internal SQS queues.
865
# It should not be modified by the user, or visible to him, except as through a presigned url with the
866
# get-function call.
867
INTERNAL_RESOURCE_ACCOUNT = os.environ.get("INTERNAL_RESOURCE_ACCOUNT") or "949334387222"
1✔
868

869
# -----
870
# SERVICE-SPECIFIC CONFIGS BELOW
871
# -----
872

873
# port ranges for external service instances (f.e. elasticsearch clusters, opensearch clusters,...)
874
EXTERNAL_SERVICE_PORTS_START = int(
1✔
875
    os.environ.get("EXTERNAL_SERVICE_PORTS_START")
876
    or os.environ.get("SERVICE_INSTANCES_PORTS_START")
877
    or 4510
878
)
879
EXTERNAL_SERVICE_PORTS_END = int(
1✔
880
    os.environ.get("EXTERNAL_SERVICE_PORTS_END")
881
    or os.environ.get("SERVICE_INSTANCES_PORTS_END")
882
    or (EXTERNAL_SERVICE_PORTS_START + 50)
883
)
884

885
# The default container runtime to use
886
CONTAINER_RUNTIME = os.environ.get("CONTAINER_RUNTIME", "").strip() or "docker"
1✔
887

888
# PUBLIC v1: -Xmx512M (example) Currently not supported in new provider but possible via custom entrypoint.
889
# Allow passing custom JVM options to Java Lambdas executed in Docker.
890
LAMBDA_JAVA_OPTS = os.environ.get("LAMBDA_JAVA_OPTS", "").strip()
1✔
891

892
# limit in which to kinesis-mock will start throwing exceptions
893
KINESIS_SHARD_LIMIT = os.environ.get("KINESIS_SHARD_LIMIT", "").strip() or "100"
1✔
894
KINESIS_PERSISTENCE = is_env_not_false("KINESIS_PERSISTENCE")
1✔
895

896
# limit in which to kinesis-mock will start throwing exceptions
897
KINESIS_ON_DEMAND_STREAM_COUNT_LIMIT = (
1✔
898
    os.environ.get("KINESIS_ON_DEMAND_STREAM_COUNT_LIMIT", "").strip() or "10"
899
)
900

901
# delay in kinesis-mock response when making changes to streams
902
KINESIS_LATENCY = os.environ.get("KINESIS_LATENCY", "").strip() or "500"
1✔
903

904
# Delay between data persistence (in seconds)
905
KINESIS_MOCK_PERSIST_INTERVAL = os.environ.get("KINESIS_MOCK_PERSIST_INTERVAL", "").strip() or "5s"
1✔
906

907
# Kinesis mock log level override when inconsistent with LS_LOG (e.g., when LS_LOG=debug)
908
KINESIS_MOCK_LOG_LEVEL = os.environ.get("KINESIS_MOCK_LOG_LEVEL", "").strip()
1✔
909

910
# randomly inject faults to Kinesis
911
KINESIS_ERROR_PROBABILITY = float(os.environ.get("KINESIS_ERROR_PROBABILITY", "").strip() or 0.0)
1✔
912

913
# SEMI-PUBLIC: "node" (default); not actively communicated
914
# Select whether to use the node or scala build when running Kinesis Mock
915
KINESIS_MOCK_PROVIDER_ENGINE = os.environ.get("KINESIS_MOCK_PROVIDER_ENGINE", "").strip() or "node"
1✔
916

917
# set the maximum Java heap size corresponding to the '-Xmx<size>' flag
918
KINESIS_MOCK_MAXIMUM_HEAP_SIZE = (
1✔
919
    os.environ.get("KINESIS_MOCK_MAXIMUM_HEAP_SIZE", "").strip() or "512m"
920
)
921

922
# set the initial Java heap size corresponding to the '-Xms<size>' flag
923
KINESIS_MOCK_INITIAL_HEAP_SIZE = (
1✔
924
    os.environ.get("KINESIS_MOCK_INITIAL_HEAP_SIZE", "").strip() or "256m"
925
)
926

927
# randomly inject faults to DynamoDB
928
DYNAMODB_ERROR_PROBABILITY = float(os.environ.get("DYNAMODB_ERROR_PROBABILITY", "").strip() or 0.0)
1✔
929
DYNAMODB_READ_ERROR_PROBABILITY = float(
1✔
930
    os.environ.get("DYNAMODB_READ_ERROR_PROBABILITY", "").strip() or 0.0
931
)
932
DYNAMODB_WRITE_ERROR_PROBABILITY = float(
1✔
933
    os.environ.get("DYNAMODB_WRITE_ERROR_PROBABILITY", "").strip() or 0.0
934
)
935

936
# JAVA EE heap size for dynamodb
937
DYNAMODB_HEAP_SIZE = os.environ.get("DYNAMODB_HEAP_SIZE", "").strip() or "256m"
1✔
938

939
# single DB instance across multiple credentials are regions
940
DYNAMODB_SHARE_DB = int(os.environ.get("DYNAMODB_SHARE_DB") or 0)
1✔
941

942
# the port on which to expose dynamodblocal
943
DYNAMODB_LOCAL_PORT = int(os.environ.get("DYNAMODB_LOCAL_PORT") or 0)
1✔
944

945
# Enables the automatic removal of stale KV pais based on TTL
946
DYNAMODB_REMOVE_EXPIRED_ITEMS = is_env_true("DYNAMODB_REMOVE_EXPIRED_ITEMS")
1✔
947

948
# Used to toggle PurgeInProgress exceptions when calling purge within 60 seconds
949
SQS_DELAY_PURGE_RETRY = is_env_true("SQS_DELAY_PURGE_RETRY")
1✔
950

951
# Used to toggle QueueDeletedRecently errors when re-creating a queue within 60 seconds of deleting it
952
SQS_DELAY_RECENTLY_DELETED = is_env_true("SQS_DELAY_RECENTLY_DELETED")
1✔
953

954
# Used to toggle MessageRetentionPeriod functionality in SQS queues
955
SQS_ENABLE_MESSAGE_RETENTION_PERIOD = is_env_true("SQS_ENABLE_MESSAGE_RETENTION_PERIOD")
1✔
956

957
# Strategy used when creating SQS queue urls. can be "off", "standard" (default), "domain", "path", or "dynamic"
958
SQS_ENDPOINT_STRATEGY = os.environ.get("SQS_ENDPOINT_STRATEGY", "") or "standard"
1✔
959

960
# Disable the check for MaxNumberOfMessage in SQS ReceiveMessage
961
SQS_DISABLE_MAX_NUMBER_OF_MESSAGE_LIMIT = is_env_true("SQS_DISABLE_MAX_NUMBER_OF_MESSAGE_LIMIT")
1✔
962

963
# Disable cloudwatch metrics for SQS
964
SQS_DISABLE_CLOUDWATCH_METRICS = is_env_true("SQS_DISABLE_CLOUDWATCH_METRICS")
1✔
965

966
# Interval for reporting "approximate" metrics to cloudwatch, default is 60 seconds
967
SQS_CLOUDWATCH_METRICS_REPORT_INTERVAL = int(
1✔
968
    os.environ.get("SQS_CLOUDWATCH_METRICS_REPORT_INTERVAL") or 60
969
)
970

971
# PUBLIC: Endpoint host under which LocalStack APIs are accessible from Lambda Docker containers.
972
HOSTNAME_FROM_LAMBDA = os.environ.get("HOSTNAME_FROM_LAMBDA", "").strip()
1✔
973

974
# PUBLIC: hot-reload (default v2), __local__ (default v1)
975
# Magic S3 bucket name for Hot Reloading. The S3Key points to the source code on the local file system.
976
BUCKET_MARKER_LOCAL = (
1✔
977
    os.environ.get("BUCKET_MARKER_LOCAL", "").strip() or DEFAULT_BUCKET_MARKER_LOCAL
978
)
979

980
# PUBLIC: Opt-out to inject the environment variable AWS_ENDPOINT_URL for automatic configuration of AWS SDKs:
981
# https://docs.aws.amazon.com/sdkref/latest/guide/feature-ss-endpoints.html
982
LAMBDA_DISABLE_AWS_ENDPOINT_URL = is_env_true("LAMBDA_DISABLE_AWS_ENDPOINT_URL")
1✔
983

984
# PUBLIC: bridge (Docker default)
985
# Docker network driver for the Lambda and ECS containers. https://docs.docker.com/network/
986
LAMBDA_DOCKER_NETWORK = os.environ.get("LAMBDA_DOCKER_NETWORK", "").strip()
1✔
987

988
# PUBLIC v1: LocalStack DNS (default)
989
# Custom DNS server for the container running your lambda function.
990
LAMBDA_DOCKER_DNS = os.environ.get("LAMBDA_DOCKER_DNS", "").strip()
1✔
991

992
# PUBLIC: -e KEY=VALUE -v host:container
993
# Additional flags passed to Docker run|create commands.
994
LAMBDA_DOCKER_FLAGS = os.environ.get("LAMBDA_DOCKER_FLAGS", "").strip()
1✔
995

996
# PUBLIC: 0 (default)
997
# Enable this flag to run cross-platform compatible lambda functions natively (i.e., Docker selects architecture) and
998
# ignore the AWS architectures (i.e., x86_64, arm64) configured for the lambda function.
999
LAMBDA_IGNORE_ARCHITECTURE = is_env_true("LAMBDA_IGNORE_ARCHITECTURE")
1✔
1000

1001
# TODO: test and add to docs
1002
# EXPERIMENTAL: 0 (default)
1003
# prebuild images before execution? Increased cold start time on the tradeoff of increased time until lambda is ACTIVE
1004
LAMBDA_PREBUILD_IMAGES = is_env_true("LAMBDA_PREBUILD_IMAGES")
1✔
1005

1006
# PUBLIC: docker (default), kubernetes (pro)
1007
# Where Lambdas will be executed.
1008
LAMBDA_RUNTIME_EXECUTOR = os.environ.get("LAMBDA_RUNTIME_EXECUTOR", CONTAINER_RUNTIME).strip()
1✔
1009

1010
# PUBLIC: 20 (default)
1011
# How many seconds Lambda will wait for the runtime environment to start up.
1012
LAMBDA_RUNTIME_ENVIRONMENT_TIMEOUT = int(os.environ.get("LAMBDA_RUNTIME_ENVIRONMENT_TIMEOUT") or 20)
1✔
1013

1014
# PUBLIC: base images for Lambda (default) https://docs.aws.amazon.com/lambda/latest/dg/runtimes-images.html
1015
# localstack/services/lambda_/invocation/lambda_models.py:IMAGE_MAPPING
1016
# Customize the Docker image of Lambda runtimes, either by:
1017
# a) pattern with <runtime> placeholder, e.g. custom-repo/lambda-<runtime>:2022
1018
# b) json dict mapping the <runtime> to an image, e.g. {"python3.9": "custom-repo/lambda-py:thon3.9"}
1019
LAMBDA_RUNTIME_IMAGE_MAPPING = os.environ.get("LAMBDA_RUNTIME_IMAGE_MAPPING", "").strip()
1✔
1020

1021

1022
# PUBLIC: 0 (default)
1023
# Whether to disable usage of deprecated runtimes
1024
LAMBDA_RUNTIME_VALIDATION = int(os.environ.get("LAMBDA_RUNTIME_VALIDATION") or 0)
1✔
1025

1026
# PUBLIC: 1 (default)
1027
# Whether to remove any Lambda Docker containers.
1028
LAMBDA_REMOVE_CONTAINERS = (
1✔
1029
    os.environ.get("LAMBDA_REMOVE_CONTAINERS", "").lower().strip() not in FALSE_STRINGS
1030
)
1031

1032
# PUBLIC: 600000 (default 10min)
1033
# Time in milliseconds until lambda shuts down the execution environment after the last invocation has been processed.
1034
# Set to 0 to immediately shut down the execution environment after an invocation.
1035
LAMBDA_KEEPALIVE_MS = int(os.environ.get("LAMBDA_KEEPALIVE_MS", 600_000))
1✔
1036

1037
# PUBLIC: 1000 (default)
1038
# The maximum number of events that functions can process simultaneously in the current Region.
1039
# See AWS service quotas: https://docs.aws.amazon.com/general/latest/gr/lambda-service.html
1040
# Concurrency limits. Like on AWS these apply per account and region.
1041
LAMBDA_LIMITS_CONCURRENT_EXECUTIONS = int(
1✔
1042
    os.environ.get("LAMBDA_LIMITS_CONCURRENT_EXECUTIONS", 1_000)
1043
)
1044
# SEMI-PUBLIC: not actively communicated
1045
# per account/region: there must be at least <LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY> unreserved concurrency.
1046
LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY = int(
1✔
1047
    os.environ.get("LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY", 100)
1048
)
1049
# SEMI-PUBLIC: not actively communicated
1050
LAMBDA_LIMITS_TOTAL_CODE_SIZE = int(os.environ.get("LAMBDA_LIMITS_TOTAL_CODE_SIZE", 80_530_636_800))
1✔
1051
# PUBLIC: documented after AWS changed validation around 2023-11
1052
LAMBDA_LIMITS_CODE_SIZE_ZIPPED = int(os.environ.get("LAMBDA_LIMITS_CODE_SIZE_ZIPPED", 52_428_800))
1✔
1053
# SEMI-PUBLIC: not actively communicated
1054
LAMBDA_LIMITS_CODE_SIZE_UNZIPPED = int(
1✔
1055
    os.environ.get("LAMBDA_LIMITS_CODE_SIZE_UNZIPPED", 262_144_000)
1056
)
1057
# PUBLIC: documented upon customer request
1058
LAMBDA_LIMITS_CREATE_FUNCTION_REQUEST_SIZE = int(
1✔
1059
    os.environ.get("LAMBDA_LIMITS_CREATE_FUNCTION_REQUEST_SIZE", 70_167_211)
1060
)
1061
# SEMI-PUBLIC: not actively communicated
1062
LAMBDA_LIMITS_MAX_FUNCTION_ENVVAR_SIZE_BYTES = int(
1✔
1063
    os.environ.get("LAMBDA_LIMITS_MAX_FUNCTION_ENVVAR_SIZE_BYTES", 4 * 1024)
1064
)
1065
# SEMI-PUBLIC: not actively communicated
1066
LAMBDA_LIMITS_MAX_FUNCTION_PAYLOAD_SIZE_BYTES = int(
1✔
1067
    os.environ.get(
1068
        "LAMBDA_LIMITS_MAX_FUNCTION_PAYLOAD_SIZE_BYTES", 6 * 1024 * 1024 + 100
1069
    )  # the 100 comes from the init defaults
1070
)
1071

1072
# DEV: 0 (default unless in host mode on macOS) For LS developers only. Only applies to Docker mode.
1073
# Whether to explicitly expose a free TCP port in lambda containers when invoking functions in host mode for
1074
# systems that cannot reach the container via its IPv4. For example, macOS cannot reach Docker containers:
1075
# https://docs.docker.com/desktop/networking/#i-cannot-ping-my-containers
1076
LAMBDA_DEV_PORT_EXPOSE = (
1✔
1077
    # Enable this dev flag by default on macOS in host mode (i.e., non-Docker environment)
1078
    is_env_not_false("LAMBDA_DEV_PORT_EXPOSE")
1079
    if not is_in_docker and is_in_macos
1080
    else is_env_true("LAMBDA_DEV_PORT_EXPOSE")
1081
)
1082

1083
# DEV: only applies to new lambda provider. All LAMBDA_INIT_* configuration are for LS developers only.
1084
# There are NO stability guarantees, and they may break at any time.
1085

1086
# DEV: Release version of https://github.com/localstack/lambda-runtime-init overriding the current default
1087
LAMBDA_INIT_RELEASE_VERSION = os.environ.get("LAMBDA_INIT_RELEASE_VERSION")
1✔
1088
# DEV: 0 (default) Enable for mounting of RIE init binary and delve debugger
1089
LAMBDA_INIT_DEBUG = is_env_true("LAMBDA_INIT_DEBUG")
1✔
1090
# DEV: path to RIE init binary (e.g., var/rapid/init)
1091
LAMBDA_INIT_BIN_PATH = os.environ.get("LAMBDA_INIT_BIN_PATH")
1✔
1092
# DEV: path to entrypoint script (e.g., var/rapid/entrypoint.sh)
1093
LAMBDA_INIT_BOOTSTRAP_PATH = os.environ.get("LAMBDA_INIT_BOOTSTRAP_PATH")
1✔
1094
# DEV: path to delve debugger (e.g., var/rapid/dlv)
1095
LAMBDA_INIT_DELVE_PATH = os.environ.get("LAMBDA_INIT_DELVE_PATH")
1✔
1096
# DEV: Go Delve debug port
1097
LAMBDA_INIT_DELVE_PORT = int(os.environ.get("LAMBDA_INIT_DELVE_PORT") or 40000)
1✔
1098
# DEV: Time to wait after every invoke as a workaround to fix a race condition in persistence tests
1099
LAMBDA_INIT_POST_INVOKE_WAIT_MS = os.environ.get("LAMBDA_INIT_POST_INVOKE_WAIT_MS")
1✔
1100
# DEV: sbx_user1051 (default when not provided) Alternative system user or empty string to skip dropping privileges.
1101
LAMBDA_INIT_USER = os.environ.get("LAMBDA_INIT_USER")
1✔
1102

1103
# INTERNAL: 1 (default)
1104
# The duration (in seconds) to wait between each poll call to an event source.
1105
LAMBDA_EVENT_SOURCE_MAPPING_POLL_INTERVAL_SEC = float(
1✔
1106
    os.environ.get("LAMBDA_EVENT_SOURCE_MAPPING_POLL_INTERVAL_SEC") or 1
1107
)
1108

1109
# INTERNAL: 60 (default)
1110
# Maximum duration (in seconds) to wait between retries when an event source poll fails.
1111
LAMBDA_EVENT_SOURCE_MAPPING_MAX_BACKOFF_ON_ERROR_SEC = float(
1✔
1112
    os.environ.get("LAMBDA_EVENT_SOURCE_MAPPING_MAX_BACKOFF_ON_ERROR_SEC") or 60
1113
)
1114

1115
# INTERNAL: 10 (default)
1116
# Maximum duration (in seconds) to wait between polls when an event source returns empty results.
1117
LAMBDA_EVENT_SOURCE_MAPPING_MAX_BACKOFF_ON_EMPTY_POLL_SEC = float(
1✔
1118
    os.environ.get("LAMBDA_EVENT_SOURCE_MAPPING_MAX_BACKOFF_ON_EMPTY_POLL_SEC") or 10
1119
)
1120

1121
# Specifies the path to the mock configuration file for Step Functions, commonly named MockConfigFile.json.
1122
SFN_MOCK_CONFIG = os.environ.get("SFN_MOCK_CONFIG", "").strip()
1✔
1123

1124
# path prefix for windows volume mounting
1125
WINDOWS_DOCKER_MOUNT_PREFIX = os.environ.get("WINDOWS_DOCKER_MOUNT_PREFIX", "/host_mnt")
1✔
1126

1127
# whether to skip S3 presign URL signature validation (TODO: currently enabled, until all issues are resolved)
1128
S3_SKIP_SIGNATURE_VALIDATION = is_env_not_false("S3_SKIP_SIGNATURE_VALIDATION")
1✔
1129
# whether to skip S3 validation of provided KMS key
1130
S3_SKIP_KMS_KEY_VALIDATION = is_env_not_false("S3_SKIP_KMS_KEY_VALIDATION")
1✔
1131

1132
# PUBLIC: 2000 (default)
1133
# Allows increasing the default char limit for truncation of lambda log lines when printed in the console.
1134
# This does not affect the logs processing in CloudWatch.
1135
LAMBDA_TRUNCATE_STDOUT = int(os.getenv("LAMBDA_TRUNCATE_STDOUT") or 2000)
1✔
1136

1137
# INTERNAL: 60 (default matching AWS) only applies to new lambda provider
1138
# Base delay in seconds for async retries. Further retries use: NUM_ATTEMPTS * LAMBDA_RETRY_BASE_DELAY_SECONDS
1139
# 300 (5min) is the maximum because NUM_ATTEMPTS can be at most 3 and SQS has a message timer limit of 15 min.
1140
# For example:
1141
# 1x LAMBDA_RETRY_BASE_DELAY_SECONDS: delay between initial invocation and first retry
1142
# 2x LAMBDA_RETRY_BASE_DELAY_SECONDS: delay between the first retry and the second retry
1143
# 3x LAMBDA_RETRY_BASE_DELAY_SECONDS: delay between the second retry and the third retry
1144
LAMBDA_RETRY_BASE_DELAY_SECONDS = int(os.getenv("LAMBDA_RETRY_BASE_DELAY") or 60)
1✔
1145

1146
# PUBLIC: 0 (default)
1147
# Set to 1 to create lambda functions synchronously (not recommended).
1148
# Whether Lambda.CreateFunction will block until the function is in a terminal state (Active or Failed).
1149
# This technically breaks behavior parity but is provided as a simplification over the default AWS behavior and
1150
# to match the behavior of the old lambda provider.
1151
LAMBDA_SYNCHRONOUS_CREATE = is_env_true("LAMBDA_SYNCHRONOUS_CREATE")
1✔
1152

1153
# URL to a custom OpenSearch/Elasticsearch backend cluster. If this is set to a valid URL, then localstack will not
1154
# create OpenSearch/Elasticsearch cluster instances, but instead forward all domains to the given backend.
1155
OPENSEARCH_CUSTOM_BACKEND = os.environ.get("OPENSEARCH_CUSTOM_BACKEND", "").strip()
1✔
1156

1157
# Strategy used when creating OpenSearch/Elasticsearch domain endpoints routed through the edge proxy
1158
# valid values: domain | path | port (off)
1159
OPENSEARCH_ENDPOINT_STRATEGY = (
1✔
1160
    os.environ.get("OPENSEARCH_ENDPOINT_STRATEGY", "").strip() or "domain"
1161
)
1162
if OPENSEARCH_ENDPOINT_STRATEGY == "off":
1✔
1163
    OPENSEARCH_ENDPOINT_STRATEGY = "port"
×
1164

1165
# Whether to start one cluster per domain (default), or multiplex opensearch domains to a single clusters
1166
OPENSEARCH_MULTI_CLUSTER = is_env_not_false("OPENSEARCH_MULTI_CLUSTER")
1✔
1167

1168
# Whether to really publish to GCM while using SNS Platform Application (needs credentials)
1169
LEGACY_SNS_GCM_PUBLISHING = is_env_true("LEGACY_SNS_GCM_PUBLISHING")
1✔
1170

1171
SNS_SES_SENDER_ADDRESS = os.environ.get("SNS_SES_SENDER_ADDRESS", "").strip()
1✔
1172

1173
SNS_CERT_URL_HOST = os.environ.get("SNS_CERT_URL_HOST", "").strip()
1✔
1174

1175
# Whether the Next Gen APIGW invocation logic is enabled (on by default)
1176
APIGW_NEXT_GEN_PROVIDER = os.environ.get("PROVIDER_OVERRIDE_APIGATEWAY", "") in ("next_gen", "")
1✔
1177

1178
# Whether the DynamoDBStreams native provider is enabled
1179
DDB_STREAMS_PROVIDER_V2 = os.environ.get("PROVIDER_OVERRIDE_DYNAMODBSTREAMS", "") == "v2"
1✔
1180
_override_dynamodb_v2 = os.environ.get("PROVIDER_OVERRIDE_DYNAMODB", "")
1✔
1181
if DDB_STREAMS_PROVIDER_V2:
1✔
1182
    # in order to not have conflicts between the 2 implementations, as they are tightly coupled, we need to set DDB
1183
    # to be v2 as well
1184
    if not _override_dynamodb_v2:
×
1185
        os.environ["PROVIDER_OVERRIDE_DYNAMODB"] = "v2"
×
1186
elif _override_dynamodb_v2 == "v2":
1✔
UNCOV
1187
    os.environ["PROVIDER_OVERRIDE_DYNAMODBSTREAMS"] = "v2"
×
UNCOV
1188
    DDB_STREAMS_PROVIDER_V2 = True
×
1189

1190
SNS_PROVIDER_V2 = os.environ.get("PROVIDER_OVERRIDE_SNS", "") == "v2"
1✔
1191

1192
# TODO remove fallback to LAMBDA_DOCKER_NETWORK with next minor version
1193
MAIN_DOCKER_NETWORK = os.environ.get("MAIN_DOCKER_NETWORK", "") or LAMBDA_DOCKER_NETWORK
1✔
1194

1195
# Whether to return and parse access key ids starting with an "A", like on AWS
1196
PARITY_AWS_ACCESS_KEY_ID = is_env_true("PARITY_AWS_ACCESS_KEY_ID")
1✔
1197

1198
# Show exceptions for CloudFormation deploy errors
1199
CFN_VERBOSE_ERRORS = is_env_true("CFN_VERBOSE_ERRORS")
1✔
1200

1201
# The CFN_STRING_REPLACEMENT_DENY_LIST env variable is a comma separated list of strings that are not allowed to be
1202
# replaced in CloudFormation templates (e.g. AWS URLs that are usually edited by Localstack to point to itself if found
1203
# in a CFN template). They are extracted to a list of strings if the env variable is set.
1204
CFN_STRING_REPLACEMENT_DENY_LIST = [
1✔
1205
    x for x in os.environ.get("CFN_STRING_REPLACEMENT_DENY_LIST", "").split(",") if x
1206
]
1207

1208
# Set the timeout to deploy each individual CloudFormation resource
1209
CFN_PER_RESOURCE_TIMEOUT = int(os.environ.get("CFN_PER_RESOURCE_TIMEOUT") or 300)
1✔
1210

1211
# How localstack will react to encountering unsupported resource types.
1212
# By default unsupported resource types will be ignored.
1213
# EXPERIMENTAL
1214
CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES = is_env_not_false("CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES")
1✔
1215

1216
# Comma-separated list of resource type names that CloudFormation will ignore on stack creation
1217
CFN_IGNORE_UNSUPPORTED_TYPE_CREATE = parse_comma_separated_list(
1✔
1218
    "CFN_IGNORE_UNSUPPORTED_TYPE_CREATE"
1219
)
1220
# Comma-separated list of resource type names that CloudFormation will ignore on stack update
1221
CFN_IGNORE_UNSUPPORTED_TYPE_UPDATE = parse_comma_separated_list(
1✔
1222
    "CFN_IGNORE_UNSUPPORTED_TYPE_UPDATE"
1223
)
1224

1225
# Decrease the waiting time for resource deployment
1226
CFN_NO_WAIT_ITERATIONS: str | int | None = os.environ.get("CFN_NO_WAIT_ITERATIONS")
1✔
1227

1228
# bind address of local DNS server
1229
DNS_ADDRESS = os.environ.get("DNS_ADDRESS") or "0.0.0.0"
1✔
1230
# port of the local DNS server
1231
DNS_PORT = int(os.environ.get("DNS_PORT", "53"))
1✔
1232

1233
# Comma-separated list of regex patterns for DNS names to resolve locally.
1234
# Any DNS name not matched against any of the patterns on this whitelist
1235
# will resolve it to the real DNS entry, rather than the local one.
1236
DNS_NAME_PATTERNS_TO_RESOLVE_UPSTREAM = (
1✔
1237
    os.environ.get("DNS_NAME_PATTERNS_TO_RESOLVE_UPSTREAM") or ""
1238
).strip()
1239
DNS_LOCAL_NAME_PATTERNS = (os.environ.get("DNS_LOCAL_NAME_PATTERNS") or "").strip()  # deprecated
1✔
1240

1241
# IP address that AWS endpoints should resolve to in our local DNS server. By default,
1242
# hostnames resolve to 127.0.0.1, which allows to use the LocalStack APIs transparently
1243
# from the host machine. If your code is running in Docker, this should be configured
1244
# to resolve to the Docker bridge network address, e.g., DNS_RESOLVE_IP=172.17.0.1
1245
DNS_RESOLVE_IP = os.environ.get("DNS_RESOLVE_IP") or LOCALHOST_IP
1✔
1246

1247
# fallback DNS server to send upstream requests to
1248
DNS_SERVER = os.environ.get("DNS_SERVER")
1✔
1249
DNS_VERIFICATION_DOMAIN = os.environ.get("DNS_VERIFICATION_DOMAIN") or "localstack.cloud"
1✔
1250

1251

1252
def use_custom_dns():
1✔
1253
    return str(DNS_ADDRESS) not in FALSE_STRINGS
1✔
1254

1255

1256
# s3 virtual host name
1257
S3_VIRTUAL_HOSTNAME = f"s3.{LOCALSTACK_HOST.host}"
1✔
1258
S3_STATIC_WEBSITE_HOSTNAME = f"s3-website.{LOCALSTACK_HOST.host}"
1✔
1259

1260
BOTO_WAITER_DELAY = int(os.environ.get("BOTO_WAITER_DELAY") or "1")
1✔
1261
BOTO_WAITER_MAX_ATTEMPTS = int(os.environ.get("BOTO_WAITER_MAX_ATTEMPTS") or "120")
1✔
1262
DISABLE_CUSTOM_BOTO_WAITER_CONFIG = is_env_true("DISABLE_CUSTOM_BOTO_WAITER_CONFIG")
1✔
1263

1264
# defaults to false
1265
# if `DISABLE_BOTO_RETRIES=1` is set, all our created boto clients will have retries disabled
1266
DISABLE_BOTO_RETRIES = is_env_true("DISABLE_BOTO_RETRIES")
1✔
1267

1268
DISTRIBUTED_MODE = is_env_true("DISTRIBUTED_MODE")
1✔
1269

1270
# This flag enables `connect_to` to be in-memory only and not do networking calls
1271
IN_MEMORY_CLIENT = is_env_true("IN_MEMORY_CLIENT")
1✔
1272

1273
# This flag enables all responses from LocalStack to contain a `x-localstack` HTTP header.
1274
LOCALSTACK_RESPONSE_HEADER_ENABLED = is_env_not_false("LOCALSTACK_RESPONSE_HEADER_ENABLED")
1✔
1275

1276
# Serialization backend for the LocalStack internal state (`dill` is used by default`).
1277
STATE_SERIALIZATION_BACKEND = os.environ.get("STATE_SERIALIZATION_BACKEND", "").strip() or "dill"
1✔
1278

1279
# List of environment variable names used for configuration that are passed from the host into the LocalStack container.
1280
# => Synchronize this list with the above and the configuration docs:
1281
# https://docs.localstack.cloud/references/configuration/
1282
# => Sort this list alphabetically
1283
# => Add deprecated environment variables to deprecations.py and add a comment in this list
1284
# => Move removed legacy variables to the section grouped by release (still relevant for deprecation warnings)
1285
# => Do *not* include any internal developer configurations that apply to host-mode only in this list.
1286
CONFIG_ENV_VARS = [
1✔
1287
    "ALLOW_NONSTANDARD_REGIONS",
1288
    "BOTO_WAITER_DELAY",
1289
    "BOTO_WAITER_MAX_ATTEMPTS",
1290
    "BUCKET_MARKER_LOCAL",
1291
    "CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES",
1292
    "CFN_PER_RESOURCE_TIMEOUT",
1293
    "CFN_STRING_REPLACEMENT_DENY_LIST",
1294
    "CFN_VERBOSE_ERRORS",
1295
    "CI",
1296
    "CONTAINER_RUNTIME",
1297
    "CUSTOM_SSL_CERT_PATH",
1298
    "DEBUG",
1299
    "DEBUG_HANDLER_CHAIN",
1300
    "DEVELOP",
1301
    "DEVELOP_PORT",
1302
    "DISABLE_BOTO_RETRIES",
1303
    "DISABLE_CORS_CHECKS",
1304
    "DISABLE_CORS_HEADERS",
1305
    "DISABLE_CUSTOM_BOTO_WAITER_CONFIG",
1306
    "DISABLE_CUSTOM_CORS_APIGATEWAY",
1307
    "DISABLE_CUSTOM_CORS_S3",
1308
    "DISABLE_EVENTS",
1309
    "DISTRIBUTED_MODE",
1310
    "DNS_ADDRESS",
1311
    "DNS_PORT",
1312
    "DNS_LOCAL_NAME_PATTERNS",
1313
    "DNS_NAME_PATTERNS_TO_RESOLVE_UPSTREAM",
1314
    "DNS_RESOLVE_IP",
1315
    "DNS_SERVER",
1316
    "DNS_VERIFICATION_DOMAIN",
1317
    "DOCKER_BRIDGE_IP",
1318
    "DOCKER_SDK_DEFAULT_TIMEOUT_SECONDS",
1319
    "DYNAMODB_ERROR_PROBABILITY",
1320
    "DYNAMODB_HEAP_SIZE",
1321
    "DYNAMODB_IN_MEMORY",
1322
    "DYNAMODB_LOCAL_PORT",
1323
    "DYNAMODB_SHARE_DB",
1324
    "DYNAMODB_READ_ERROR_PROBABILITY",
1325
    "DYNAMODB_REMOVE_EXPIRED_ITEMS",
1326
    "DYNAMODB_WRITE_ERROR_PROBABILITY",
1327
    "EAGER_SERVICE_LOADING",
1328
    "ENABLE_CONFIG_UPDATES",
1329
    "EVENT_RULE_ENGINE",
1330
    "EXTRA_CORS_ALLOWED_HEADERS",
1331
    "EXTRA_CORS_ALLOWED_ORIGINS",
1332
    "EXTRA_CORS_EXPOSE_HEADERS",
1333
    "GATEWAY_LISTEN",
1334
    "GATEWAY_SERVER",
1335
    "GATEWAY_WORKER_THREAD_COUNT",
1336
    "HOSTNAME",
1337
    "HOSTNAME_FROM_LAMBDA",
1338
    "IN_MEMORY_CLIENT",
1339
    "KINESIS_ERROR_PROBABILITY",
1340
    "KINESIS_MOCK_PERSIST_INTERVAL",
1341
    "KINESIS_MOCK_LOG_LEVEL",
1342
    "KINESIS_ON_DEMAND_STREAM_COUNT_LIMIT",
1343
    "KINESIS_PERSISTENCE",
1344
    "LAMBDA_DEBUG_MODE",
1345
    "LAMBDA_DEBUG_MODE_CONFIG",
1346
    "LAMBDA_DISABLE_AWS_ENDPOINT_URL",
1347
    "LAMBDA_DOCKER_DNS",
1348
    "LAMBDA_DOCKER_FLAGS",
1349
    "LAMBDA_DOCKER_NETWORK",
1350
    "LAMBDA_EVENTS_INTERNAL_SQS",
1351
    "LAMBDA_EVENT_SOURCE_MAPPING",
1352
    "LAMBDA_IGNORE_ARCHITECTURE",
1353
    "LAMBDA_INIT_DEBUG",
1354
    "LAMBDA_INIT_BIN_PATH",
1355
    "LAMBDA_INIT_BOOTSTRAP_PATH",
1356
    "LAMBDA_INIT_DELVE_PATH",
1357
    "LAMBDA_INIT_DELVE_PORT",
1358
    "LAMBDA_INIT_POST_INVOKE_WAIT_MS",
1359
    "LAMBDA_INIT_USER",
1360
    "LAMBDA_INIT_RELEASE_VERSION",
1361
    "LAMBDA_KEEPALIVE_MS",
1362
    "LAMBDA_LIMITS_CONCURRENT_EXECUTIONS",
1363
    "LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY",
1364
    "LAMBDA_LIMITS_TOTAL_CODE_SIZE",
1365
    "LAMBDA_LIMITS_CODE_SIZE_ZIPPED",
1366
    "LAMBDA_LIMITS_CODE_SIZE_UNZIPPED",
1367
    "LAMBDA_LIMITS_CREATE_FUNCTION_REQUEST_SIZE",
1368
    "LAMBDA_LIMITS_MAX_FUNCTION_ENVVAR_SIZE_BYTES",
1369
    "LAMBDA_LIMITS_MAX_FUNCTION_PAYLOAD_SIZE_BYTES",
1370
    "LAMBDA_PREBUILD_IMAGES",
1371
    "LAMBDA_RUNTIME_IMAGE_MAPPING",
1372
    "LAMBDA_REMOVE_CONTAINERS",
1373
    "LAMBDA_RETRY_BASE_DELAY_SECONDS",
1374
    "LAMBDA_RUNTIME_EXECUTOR",
1375
    "LAMBDA_RUNTIME_ENVIRONMENT_TIMEOUT",
1376
    "LAMBDA_RUNTIME_VALIDATION",
1377
    "LAMBDA_SYNCHRONOUS_CREATE",
1378
    "LAMBDA_SQS_EVENT_SOURCE_MAPPING_INTERVAL",
1379
    "LAMBDA_TRUNCATE_STDOUT",
1380
    "LEGACY_DOCKER_CLIENT",
1381
    "LEGACY_SNS_GCM_PUBLISHING",
1382
    "LOCALSTACK_API_KEY",
1383
    "LOCALSTACK_AUTH_TOKEN",
1384
    "LOCALSTACK_HOST",
1385
    "LOCALSTACK_RESPONSE_HEADER_ENABLED",
1386
    "LOG_LICENSE_ISSUES",
1387
    "LS_LOG",
1388
    "MAIN_CONTAINER_NAME",
1389
    "MAIN_DOCKER_NETWORK",
1390
    "OPENAPI_VALIDATE_REQUEST",
1391
    "OPENAPI_VALIDATE_RESPONSE",
1392
    "OPENSEARCH_ENDPOINT_STRATEGY",
1393
    "OUTBOUND_HTTP_PROXY",
1394
    "OUTBOUND_HTTPS_PROXY",
1395
    "PARITY_AWS_ACCESS_KEY_ID",
1396
    "PERSISTENCE",
1397
    "PORTS_CHECK_DOCKER_IMAGE",
1398
    "REQUESTS_CA_BUNDLE",
1399
    "REMOVE_SSL_CERT",
1400
    "S3_SKIP_SIGNATURE_VALIDATION",
1401
    "S3_SKIP_KMS_KEY_VALIDATION",
1402
    "SERVICES",
1403
    "SKIP_INFRA_DOWNLOADS",
1404
    "SKIP_SSL_CERT_DOWNLOAD",
1405
    "SNAPSHOT_LOAD_STRATEGY",
1406
    "SNAPSHOT_SAVE_STRATEGY",
1407
    "SNAPSHOT_FLUSH_INTERVAL",
1408
    "SNS_SES_SENDER_ADDRESS",
1409
    "SQS_DELAY_PURGE_RETRY",
1410
    "SQS_DELAY_RECENTLY_DELETED",
1411
    "SQS_ENABLE_MESSAGE_RETENTION_PERIOD",
1412
    "SQS_ENDPOINT_STRATEGY",
1413
    "SQS_DISABLE_CLOUDWATCH_METRICS",
1414
    "SQS_CLOUDWATCH_METRICS_REPORT_INTERVAL",
1415
    "STATE_SERIALIZATION_BACKEND",
1416
    "STRICT_SERVICE_LOADING",
1417
    "TF_COMPAT_MODE",
1418
    "USE_SSL",
1419
    "WAIT_FOR_DEBUGGER",
1420
    "WINDOWS_DOCKER_MOUNT_PREFIX",
1421
    # Removed legacy variables in 2.0.0
1422
    # DATA_DIR => do *not* include in this list, as it is treated separately.  # deprecated since 1.0.0
1423
    "LEGACY_DIRECTORIES",  # deprecated since 1.0.0
1424
    "SYNCHRONOUS_API_GATEWAY_EVENTS",  # deprecated since 1.3.0
1425
    "SYNCHRONOUS_DYNAMODB_EVENTS",  # deprecated since 1.3.0
1426
    "SYNCHRONOUS_SNS_EVENTS",  # deprecated since 1.3.0
1427
    "SYNCHRONOUS_SQS_EVENTS",  # deprecated since 1.3.0
1428
    # Removed legacy variables in 3.0.0
1429
    "DEFAULT_REGION",  # deprecated since 0.12.7
1430
    "EDGE_BIND_HOST",  # deprecated since 2.0.0
1431
    "EDGE_FORWARD_URL",  # deprecated since 1.4.0
1432
    "EDGE_PORT",  # deprecated since 2.0.0
1433
    "EDGE_PORT_HTTP",  # deprecated since 2.0.0
1434
    "ES_CUSTOM_BACKEND",  # deprecated since 0.14.0
1435
    "ES_ENDPOINT_STRATEGY",  # deprecated since 0.14.0
1436
    "ES_MULTI_CLUSTER",  # deprecated since 0.14.0
1437
    "HOSTNAME_EXTERNAL",  # deprecated since 2.0.0
1438
    "KINESIS_INITIALIZE_STREAMS",  # deprecated since 1.4.0
1439
    "KINESIS_PROVIDER",  # deprecated since 1.3.0
1440
    "KMS_PROVIDER",  # deprecated since 1.4.0
1441
    "LAMBDA_XRAY_INIT",  # deprecated since 2.0.0
1442
    "LAMBDA_CODE_EXTRACT_TIME",  # deprecated since 2.0.0
1443
    "LAMBDA_CONTAINER_REGISTRY",  # deprecated since 2.0.0
1444
    "LAMBDA_EXECUTOR",  # deprecated since 2.0.0
1445
    "LAMBDA_FALLBACK_URL",  # deprecated since 2.0.0
1446
    "LAMBDA_FORWARD_URL",  # deprecated since 2.0.0
1447
    "LAMBDA_JAVA_OPTS",  # currently only supported in old Lambda provider but not officially deprecated
1448
    "LAMBDA_REMOTE_DOCKER",  # deprecated since 2.0.0
1449
    "LAMBDA_STAY_OPEN_MODE",  # deprecated since 2.0.0
1450
    "LEGACY_EDGE_PROXY",  # deprecated since 1.0.0
1451
    "LOCALSTACK_HOSTNAME",  # deprecated since 2.0.0
1452
    "SQS_PORT_EXTERNAL",  # deprecated only in docs since 2022-07-13
1453
    "SYNCHRONOUS_KINESIS_EVENTS",  # deprecated since 1.3.0
1454
    "USE_SINGLE_REGION",  # deprecated since 0.12.7
1455
    "MOCK_UNIMPLEMENTED",  # deprecated since 1.3.0
1456
]
1457

1458

1459
def is_local_test_mode() -> bool:
1✔
1460
    """Returns True if we are running in the context of our local integration tests."""
1461
    return is_env_true(ENV_INTERNAL_TEST_RUN)
1✔
1462

1463

1464
def is_collect_metrics_mode() -> bool:
1✔
1465
    """Returns True if metric collection is enabled."""
1466
    return is_env_true(ENV_INTERNAL_TEST_COLLECT_METRIC)
1✔
1467

1468

1469
def store_test_metrics_in_local_filesystem() -> bool:
1✔
1470
    """Returns True if test metrics should be stored in the local filesystem (instead of the system that runs pytest)."""
1471
    return is_env_true(ENV_INTERNAL_TEST_STORE_METRICS_IN_LOCALSTACK)
1✔
1472

1473

1474
def collect_config_items() -> list[tuple[str, Any]]:
1✔
1475
    """Returns a list of key-value tuples of LocalStack configuration values."""
1476
    none = object()  # sentinel object
×
1477

1478
    # collect which keys to print
1479
    keys = []
×
1480
    keys.extend(CONFIG_ENV_VARS)
×
1481
    keys.append("DATA_DIR")
×
1482
    keys.sort()
×
1483

1484
    values = globals()
×
1485

1486
    result = []
×
1487
    for k in keys:
×
1488
        v = values.get(k, none)
×
1489
        if v is none:
×
1490
            continue
×
1491
        result.append((k, v))
×
1492
    result.sort()
×
1493
    return result
×
1494

1495

1496
def populate_config_env_var_names():
1✔
1497
    global CONFIG_ENV_VARS
1498

1499
    CONFIG_ENV_VARS += [
1✔
1500
        key
1501
        for key in [key.upper() for key in os.environ]
1502
        if (key.startswith("LOCALSTACK_") or key.startswith("PROVIDER_OVERRIDE_"))
1503
        # explicitly exclude LOCALSTACK_CLI (it's prefixed with "LOCALSTACK_",
1504
        # but is only used in the CLI (should not be forwarded to the container)
1505
        and key != "LOCALSTACK_CLI"
1506
    ]
1507

1508
    # create variable aliases prefixed with LOCALSTACK_ (except LOCALSTACK_HOST)
1509
    CONFIG_ENV_VARS += [
1✔
1510
        "LOCALSTACK_" + v for v in CONFIG_ENV_VARS if not v.startswith("LOCALSTACK_")
1511
    ]
1512

1513
    CONFIG_ENV_VARS = list(set(CONFIG_ENV_VARS))
1✔
1514

1515

1516
# populate env var names to be passed to the container
1517
populate_config_env_var_names()
1✔
1518

1519

1520
# helpers to build urls
1521
def get_protocol() -> str:
1✔
1522
    return "https" if USE_SSL else "http"
1✔
1523

1524

1525
def external_service_url(
1✔
1526
    host: str | None = None,
1527
    port: int | None = None,
1528
    protocol: str | None = None,
1529
    subdomains: str | None = None,
1530
) -> str:
1531
    """Returns a service URL (e.g., SQS queue URL) to an external client (e.g., boto3) potentially running on another
1532
    machine than LocalStack. The configurations LOCALSTACK_HOST and USE_SSL can customize these returned URLs.
1533
    The optional parameters can be used to customize the defaults.
1534
    Examples with default configuration:
1535
    * external_service_url() == http://localhost.localstack.cloud:4566
1536
    * external_service_url(subdomains="s3") == http://s3.localhost.localstack.cloud:4566
1537
    """
1538
    protocol = protocol or get_protocol()
1✔
1539
    subdomains = f"{subdomains}." if subdomains else ""
1✔
1540
    host = host or LOCALSTACK_HOST.host
1✔
1541
    port = port or LOCALSTACK_HOST.port
1✔
1542
    return f"{protocol}://{subdomains}{host}:{port}"
1✔
1543

1544

1545
def internal_service_url(
1✔
1546
    host: str | None = None,
1547
    port: int | None = None,
1548
    protocol: str | None = None,
1549
    subdomains: str | None = None,
1550
) -> str:
1551
    """Returns a service URL for internal use within LocalStack (i.e., same host).
1552
    The configuration USE_SSL can customize these returned URLs but LOCALSTACK_HOST has no effect.
1553
    The optional parameters can be used to customize the defaults.
1554
    Examples with default configuration:
1555
    * internal_service_url() == http://localhost:4566
1556
    * internal_service_url(port=8080) == http://localhost:8080
1557
    """
1558
    protocol = protocol or get_protocol()
1✔
1559
    subdomains = f"{subdomains}." if subdomains else ""
1✔
1560
    host = host or LOCALHOST
1✔
1561
    port = port or GATEWAY_LISTEN[0].port
1✔
1562
    return f"{protocol}://{subdomains}{host}:{port}"
1✔
1563

1564

1565
# DEPRECATED: old helpers for building URLs
1566

1567

1568
def service_url(service_key, host=None, port=None):
1✔
1569
    """@deprecated: Use `internal_service_url()` instead. We assume that most usages are internal
1570
    but really need to check and update each usage accordingly.
1571
    """
1572
    warnings.warn(
×
1573
        """@deprecated: Use `internal_service_url()` instead. We assume that most usages are
1574
        internal but really need to check and update each usage accordingly.""",
1575
        DeprecationWarning,
1576
        stacklevel=2,
1577
    )
1578
    return internal_service_url(host=host, port=port)
×
1579

1580

1581
def service_port(service_key: str, external: bool = False) -> int:
1✔
1582
    """@deprecated: Use `localstack_host().port` for external and `GATEWAY_LISTEN[0].port` for
1583
    internal use."""
1584
    warnings.warn(
×
1585
        "Deprecated: use `localstack_host().port` for external and `GATEWAY_LISTEN[0].port` for "
1586
        "internal use.",
1587
        DeprecationWarning,
1588
        stacklevel=2,
1589
    )
1590
    if external:
×
1591
        return LOCALSTACK_HOST.port
×
1592
    return GATEWAY_LISTEN[0].port
×
1593

1594

1595
def get_edge_port_http():
1✔
1596
    """@deprecated: Use `localstack_host().port` for external and `GATEWAY_LISTEN[0].port` for
1597
    internal use. This function is not needed anymore because we don't separate between HTTP
1598
    and HTTP ports anymore since LocalStack listens to both ports."""
1599
    warnings.warn(
×
1600
        """@deprecated: Use `localstack_host().port` for external and `GATEWAY_LISTEN[0].port`
1601
        for internal use. This function is also not needed anymore because we don't separate
1602
        between HTTP and HTTP ports anymore since LocalStack listens to both.""",
1603
        DeprecationWarning,
1604
        stacklevel=2,
1605
    )
1606
    return GATEWAY_LISTEN[0].port
×
1607

1608

1609
def get_edge_url(localstack_hostname=None, protocol=None):
1✔
1610
    """@deprecated: Use `internal_service_url()` instead.
1611
    We assume that most usages are internal but really need to check and update each usage accordingly.
1612
    """
1613
    warnings.warn(
×
1614
        """@deprecated: Use `internal_service_url()` instead.
1615
    We assume that most usages are internal but really need to check and update each usage accordingly.
1616
    """,
1617
        DeprecationWarning,
1618
        stacklevel=2,
1619
    )
1620
    return internal_service_url(host=localstack_hostname, protocol=protocol)
×
1621

1622

1623
class ServiceProviderConfig(Mapping[str, str]):
1✔
1624
    _provider_config: dict[str, str]
1✔
1625
    default_value: str
1✔
1626
    override_prefix: str = "PROVIDER_OVERRIDE_"
1✔
1627

1628
    def __init__(self, default_value: str):
1✔
1629
        self._provider_config = {}
1✔
1630
        self.default_value = default_value
1✔
1631

1632
    def load_from_environment(self, env: Mapping[str, str] = None):
1✔
1633
        if env is None:
1✔
1634
            env = os.environ
1✔
1635
        for key, value in env.items():
1✔
1636
            if key.startswith(self.override_prefix) and value:
1✔
1637
                self.set_provider(key[len(self.override_prefix) :].lower().replace("_", "-"), value)
1✔
1638

1639
    def get_provider(self, service: str) -> str:
1✔
1640
        return self._provider_config.get(service, self.default_value)
1✔
1641

1642
    def set_provider_if_not_exists(self, service: str, provider: str) -> None:
1✔
1643
        if service not in self._provider_config:
1✔
1644
            self._provider_config[service] = provider
1✔
1645

1646
    def set_provider(self, service: str, provider: str):
1✔
1647
        self._provider_config[service] = provider
1✔
1648

1649
    def bulk_set_provider_if_not_exists(self, services: list[str], provider: str):
1✔
1650
        for service in services:
1✔
1651
            self.set_provider_if_not_exists(service, provider)
1✔
1652

1653
    def __getitem__(self, item):
1✔
1654
        return self.get_provider(item)
×
1655

1656
    def __setitem__(self, key, value):
1✔
1657
        self.set_provider(key, value)
×
1658

1659
    def __len__(self):
1✔
1660
        return len(self._provider_config)
×
1661

1662
    def __iter__(self):
1✔
1663
        return self._provider_config.__iter__()
×
1664

1665

1666
SERVICE_PROVIDER_CONFIG = ServiceProviderConfig("default")
1✔
1667

1668
SERVICE_PROVIDER_CONFIG.load_from_environment()
1✔
1669

1670

1671
def init_directories() -> Directories:
1✔
1672
    if is_in_docker:
1✔
1673
        return Directories.for_container()
1✔
1674
    else:
1675
        if is_env_true("LOCALSTACK_CLI"):
1✔
1676
            return Directories.for_cli()
×
1677

1678
        return Directories.for_host()
1✔
1679

1680

1681
# initialize directories
1682
dirs: Directories
1✔
1683
dirs = init_directories()
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc