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

localstack / localstack / 17313033552

27 Aug 2025 03:39PM UTC coverage: 86.838% (-0.002%) from 86.84%
17313033552

push

github

web-flow
Remove jsonpickle serializer (#13064)

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

32 existing lines in 4 files now uncovered.

67064 of 77229 relevant lines covered (86.84%)

0.87 hits per line

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

88.21
/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, Optional, TypeVar, Union
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
    FALSE_STRINGS,
23
    LOCALHOST,
24
    LOCALHOST_IP,
25
    LOCALSTACK_ROOT_FOLDER,
26
    LOG_LEVELS,
27
    TRACE_LOG_LEVELS,
28
    TRUE_STRINGS,
29
)
30

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

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

36

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

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

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

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

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

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

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

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

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

139
        defaults = Directories.for_container()
1✔
140

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

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

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

168
        from localstack.utils import files
1✔
169

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

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

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

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

210

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

216

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

226

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

231

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

236

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

246
    profiles = profiles.split(",")
1✔
247
    environment = {}
1✔
248
    import dotenv
1✔
249

250
    for profile in profiles:
1✔
251
        profile = profile.strip()
1✔
252
        path = os.path.join(CONFIG_DIR, f"{profile}.env")
1✔
253
        if not os.path.exists(path):
1✔
254
            continue
1✔
255
        environment.update(dotenv.dotenv_values(path))
1✔
256

257
    for k, v in environment.items():
1✔
258
        # we do not want to override the environment
259
        if k not in env and v is not None:
1✔
260
            env[k] = v
1✔
261

262
    return profiles
1✔
263

264

265
def is_persistence_enabled() -> bool:
1✔
266
    return PERSISTENCE and dirs.data
1✔
267

268

269
def is_linux() -> bool:
1✔
270
    return platform.system() == "Linux"
1✔
271

272

273
def is_macos() -> bool:
1✔
274
    return platform.system() == "Darwin"
1✔
275

276

277
def is_windows() -> bool:
1✔
278
    return platform.system().lower() == "windows"
1✔
279

280

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

284

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

297

298
def in_docker():
1✔
299
    """
300
    Returns True if running in a docker container, else False
301
    Ref. https://docs.docker.com/config/containers/runmetrics/#control-groups
302
    """
303
    if OVERRIDE_IN_DOCKER is not None:
1✔
304
        return OVERRIDE_IN_DOCKER
1✔
305

306
    # check some marker files that we create in our Dockerfiles
307
    for path in [
1✔
308
        "/usr/lib/localstack/.community-version",
309
        "/usr/lib/localstack/.pro-version",
310
        "/tmp/localstack/.marker",
311
    ]:
312
        if os.path.isfile(path):
1✔
313
            return True
1✔
314

315
    # details: https://github.com/localstack/localstack/pull/4352
316
    if os.path.exists("/.dockerenv"):
1✔
317
        return True
×
318
    if os.path.exists("/run/.containerenv"):
1✔
319
        return True
×
320

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

349
    # containerd does not set any specific file or config, but it does use
350
    # io.containerd.snapshotter.v1.overlayfs as the overlay filesystem for `/`.
351
    try:
1✔
352
        with open("/proc/mounts") as infile:
1✔
353
            for line in infile:
1✔
354
                line = line.strip()
1✔
355

356
                if not line:
1✔
357
                    continue
×
358

359
                # skip comments
360
                if line[0] == "#":
1✔
361
                    continue
×
362

363
                # format (man 5 fstab)
364
                # <spec> <mount point> <type> <options> <rest>...
365
                parts = line.split()
1✔
366
                if len(parts) < 4:
1✔
367
                    # badly formatted line
368
                    continue
×
369

370
                mount_point = parts[1]
1✔
371
                options = parts[3]
1✔
372

373
                # only consider the root filesystem
374
                if mount_point != "/":
1✔
375
                    continue
1✔
376

377
                if "io.containerd" in options:
1✔
378
                    return True
×
379

380
    except FileNotFoundError:
×
381
        pass
×
382

383
    return False
1✔
384

385

386
# whether the `in_docker` check should always return True or False
387
OVERRIDE_IN_DOCKER = parse_boolean_env("OVERRIDE_IN_DOCKER")
1✔
388

389
is_in_docker = in_docker()
1✔
390
is_in_linux = is_linux()
1✔
391
is_in_macos = is_macos()
1✔
392
is_in_windows = is_windows()
1✔
393
is_in_wsl = is_wsl()
1✔
394
default_ip = "0.0.0.0" if is_in_docker else "127.0.0.1"
1✔
395

396
# CLI specific: the configuration profile to load
397
CONFIG_PROFILE = os.environ.get("CONFIG_PROFILE", "").strip()
1✔
398

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

402
# keep this on top to populate the environment
403
try:
1✔
404
    # CLI specific: the actually loaded configuration profile
405
    LOADED_PROFILES = load_environment(CONFIG_PROFILE)
1✔
406
except ImportError:
×
407
    # dotenv may not be available in lambdas or other environments where config is loaded
408
    LOADED_PROFILES = None
×
409

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

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

416
# whether localstack should persist service state across localstack runs
417
PERSISTENCE = is_env_true("PERSISTENCE")
1✔
418

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

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

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

428
# whether to clear config.dirs.tmp on startup and shutdown
429
CLEAR_TMP_FOLDER = is_env_not_false("CLEAR_TMP_FOLDER")
1✔
430

431
# folder for temporary files and data
432
TMP_FOLDER = os.path.join(tempfile.gettempdir(), "localstack")
1✔
433

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

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

441
# whether to enable verbose debug logging ("LOG" is used when using the CLI with LOCALSTACK_LOG instead of LS_LOG)
442
LS_LOG = eval_log_type("LS_LOG") or eval_log_type("LOG")
1✔
443
DEBUG = is_env_true("DEBUG") or LS_LOG in TRACE_LOG_LEVELS
1✔
444

445
# PUBLIC PREVIEW: 0 (default), 1 (preview)
446
# When enabled it triggers specialised workflows for the debugging.
447
LAMBDA_DEBUG_MODE = is_env_true("LAMBDA_DEBUG_MODE")
1✔
448

449
# path to the lambda debug mode configuration file.
450
LAMBDA_DEBUG_MODE_CONFIG_PATH = os.environ.get("LAMBDA_DEBUG_MODE_CONFIG_PATH")
1✔
451

452
# EXPERIMENTAL: allow setting custom log levels for individual loggers
453
LOG_LEVEL_OVERRIDES = os.environ.get("LOG_LEVEL_OVERRIDES", "")
1✔
454

455
# whether to enable debugpy
456
DEVELOP = is_env_true("DEVELOP")
1✔
457

458
# PORT FOR DEBUGGER
459
DEVELOP_PORT = int(os.environ.get("DEVELOP_PORT", "").strip() or DEFAULT_DEVELOP_PORT)
1✔
460

461
# whether to make debugpy wait for a debbuger client
462
WAIT_FOR_DEBUGGER = is_env_true("WAIT_FOR_DEBUGGER")
1✔
463

464
# whether to assume http or https for `get_protocol`
465
USE_SSL = is_env_true("USE_SSL")
1✔
466

467
# Whether to report internal failures as 500 or 501 errors.
468
FAIL_FAST = is_env_true("FAIL_FAST")
1✔
469

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

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

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

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

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

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

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

492

493
def is_trace_logging_enabled():
1✔
494
    if LS_LOG:
1✔
495
        log_level = str(LS_LOG).upper()
×
496
        return log_level.lower() in TRACE_LOG_LEVELS
×
497
    return False
1✔
498

499

500
# set log levels immediately, but will be overwritten later by setup_logging
501
if DEBUG:
1✔
502
    logging.getLogger("").setLevel(logging.DEBUG)
1✔
503
    logging.getLogger("localstack").setLevel(logging.DEBUG)
1✔
504

505
LOG = logging.getLogger(__name__)
1✔
506
if is_trace_logging_enabled():
1✔
507
    load_end_time = time.time()
×
508
    LOG.debug(
×
509
        "Initializing the configuration took %s ms", int((load_end_time - load_start_time) * 1000)
510
    )
511

512

513
def is_ipv6_address(host: str) -> bool:
1✔
514
    """
515
    Returns True if the given host is an IPv6 address.
516
    """
517

518
    if not host:
1✔
519
        return False
×
520

521
    try:
1✔
522
        ipaddress.IPv6Address(host)
1✔
523
        return True
1✔
524
    except ipaddress.AddressValueError:
1✔
525
        return False
1✔
526

527

528
class HostAndPort:
1✔
529
    """
530
    Definition of an address for a server to listen to.
531

532
    Includes a `parse` method to convert from `str`, allowing for default fallbacks, as well as
533
    some helper methods to help tests - particularly testing for equality and a hash function
534
    so that `HostAndPort` instances can be used as keys to dictionaries.
535
    """
536

537
    host: str
1✔
538
    port: int
1✔
539

540
    def __init__(self, host: str, port: int):
1✔
541
        self.host = host
1✔
542
        self.port = port
1✔
543

544
    @classmethod
1✔
545
    def parse(
1✔
546
        cls,
547
        input: str,
548
        default_host: str,
549
        default_port: int,
550
    ) -> "HostAndPort":
551
        """
552
        Parse a `HostAndPort` from strings like:
553
            - 0.0.0.0:4566 -> host=0.0.0.0, port=4566
554
            - 0.0.0.0      -> host=0.0.0.0, port=`default_port`
555
            - :4566        -> host=`default_host`, port=4566
556
            - [::]:4566    -> host=[::], port=4566
557
            - [::1]        -> host=[::1], port=`default_port`
558
        """
559
        host, port = default_host, default_port
1✔
560

561
        # recognize IPv6 addresses (+ port)
562
        if input.startswith("["):
1✔
563
            ipv6_pattern = re.compile(r"^\[(?P<host>[^]]+)\](:(?P<port>\d+))?$")
1✔
564
            match = ipv6_pattern.match(input)
1✔
565

566
            if match:
1✔
567
                host = match.group("host")
1✔
568
                if not is_ipv6_address(host):
1✔
569
                    raise ValueError(
1✔
570
                        f"input looks like an IPv6 address (is enclosed in square brackets), but is not valid: {host}"
571
                    )
572
                port_s = match.group("port")
1✔
573
                if port_s:
1✔
574
                    port = cls._validate_port(port_s)
1✔
575
            else:
576
                raise ValueError(
×
577
                    f'input looks like an IPv6 address, but is invalid. Should be formatted "[ip]:port": {input}'
578
                )
579

580
        # recognize IPv4 address + port
581
        elif ":" in input:
1✔
582
            hostname, port_s = input.split(":", 1)
1✔
583
            if hostname.strip():
1✔
584
                host = hostname.strip()
1✔
585
            port = cls._validate_port(port_s)
1✔
586
        else:
587
            if input.strip():
1✔
588
                host = input.strip()
1✔
589

590
        # validation
591
        if port < 0 or port >= 2**16:
1✔
592
            raise ValueError("port out of range")
1✔
593

594
        return cls(host=host, port=port)
1✔
595

596
    @classmethod
1✔
597
    def _validate_port(cls, port_s: str) -> int:
1✔
598
        try:
1✔
599
            port = int(port_s)
1✔
600
        except ValueError as e:
1✔
601
            raise ValueError(f"specified port {port_s} not a number") from e
1✔
602

603
        return port
1✔
604

605
    def _get_unprivileged_port_range_start(self) -> int:
1✔
606
        try:
×
607
            with open("/proc/sys/net/ipv4/ip_unprivileged_port_start") as unprivileged_port_start:
×
608
                port = unprivileged_port_start.read()
×
609
                return int(port.strip())
×
610
        except Exception:
×
611
            return 1024
×
612

613
    def is_unprivileged(self) -> bool:
1✔
614
        return self.port >= self._get_unprivileged_port_range_start()
×
615

616
    def host_and_port(self) -> str:
1✔
617
        formatted_host = f"[{self.host}]" if is_ipv6_address(self.host) else self.host
1✔
618
        return f"{formatted_host}:{self.port}" if self.port is not None else formatted_host
1✔
619

620
    def __hash__(self) -> int:
1✔
621
        return hash((self.host, self.port))
×
622

623
    # easier tests
624
    def __eq__(self, other: "str | HostAndPort") -> bool:
1✔
625
        if isinstance(other, self.__class__):
1✔
626
            return self.host == other.host and self.port == other.port
1✔
627
        elif isinstance(other, str):
1✔
628
            return str(self) == other
1✔
629
        else:
630
            raise TypeError(f"cannot compare {self.__class__} to {other.__class__}")
×
631

632
    def __str__(self) -> str:
1✔
633
        return self.host_and_port()
1✔
634

635
    def __repr__(self) -> str:
636
        return f"HostAndPort(host={self.host}, port={self.port})"
637

638

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

652
    def __init__(self, iterable: Union[list[HostAndPort], None] = None):
1✔
653
        super().__init__(iterable or [])
1✔
654
        self._ensure_unique()
1✔
655

656
    def _ensure_unique(self):
1✔
657
        """
658
        Ensure that all bindings on the same port are de-duped.
659
        """
660
        if len(self) <= 1:
1✔
661
            return
1✔
662

663
        unique: list[HostAndPort] = []
1✔
664

665
        # Build a dictionary of hosts by port
666
        hosts_by_port: dict[int, list[str]] = defaultdict(list)
1✔
667
        for item in self:
1✔
668
            hosts_by_port[item.port].append(item.host)
1✔
669

670
        # For any given port, dedupe the hosts
671
        for port, hosts in hosts_by_port.items():
1✔
672
            deduped_hosts = set(hosts)
1✔
673

674
            # IPv6 all interfaces: this is the most general binding.
675
            # Any others should be removed.
676
            if "::" in deduped_hosts:
1✔
677
                unique.append(HostAndPort(host="::", port=port))
1✔
678
                continue
1✔
679
            # IPv4 all interfaces: this is the next most general binding.
680
            # Any others should be removed.
681
            if "0.0.0.0" in deduped_hosts:
1✔
682
                unique.append(HostAndPort(host="0.0.0.0", port=port))
1✔
683
                continue
1✔
684

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

688
        self.clear()
1✔
689
        self.extend(unique)
1✔
690

691
    def append(self, value: HostAndPort):
1✔
692
        super().append(value)
1✔
693
        self._ensure_unique()
1✔
694

695

696
def populate_edge_configuration(
1✔
697
    environment: Mapping[str, str],
698
) -> tuple[HostAndPort, UniqueHostAndPortList]:
699
    """Populate the LocalStack edge configuration from environment variables."""
700
    localstack_host_raw = environment.get("LOCALSTACK_HOST")
1✔
701
    gateway_listen_raw = environment.get("GATEWAY_LISTEN")
1✔
702

703
    # parse gateway listen from multiple components
704
    if gateway_listen_raw is not None:
1✔
705
        gateway_listen = []
1✔
706
        for address in gateway_listen_raw.split(","):
1✔
707
            gateway_listen.append(
1✔
708
                HostAndPort.parse(
709
                    address.strip(),
710
                    default_host=default_ip,
711
                    default_port=constants.DEFAULT_PORT_EDGE,
712
                )
713
            )
714
    else:
715
        # use default if gateway listen is not defined
716
        gateway_listen = [HostAndPort(host=default_ip, port=constants.DEFAULT_PORT_EDGE)]
1✔
717

718
    # the actual value of the LOCALSTACK_HOST port now depends on what gateway listen actually listens to.
719
    if localstack_host_raw is None:
1✔
720
        localstack_host = HostAndPort(
1✔
721
            host=constants.LOCALHOST_HOSTNAME, port=gateway_listen[0].port
722
        )
723
    else:
724
        localstack_host = HostAndPort.parse(
1✔
725
            localstack_host_raw,
726
            default_host=constants.LOCALHOST_HOSTNAME,
727
            default_port=gateway_listen[0].port,
728
        )
729

730
    assert gateway_listen is not None
1✔
731
    assert localstack_host is not None
1✔
732

733
    return (
1✔
734
        localstack_host,
735
        UniqueHostAndPortList(gateway_listen),
736
    )
737

738

739
# How to access LocalStack
740
(
1✔
741
    # -- Cosmetic
742
    LOCALSTACK_HOST,
743
    # -- Edge configuration
744
    # Main configuration of the listen address of the hypercorn proxy. Of the form
745
    # <ip_address>:<port>(,<ip_address>:port>)*
746
    GATEWAY_LISTEN,
747
) = populate_edge_configuration(os.environ)
748

749
GATEWAY_WORKER_COUNT = int(os.environ.get("GATEWAY_WORKER_COUNT") or 1000)
1✔
750

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

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

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

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

763
# whether to enable API-based updates of configuration variables at runtime
764
ENABLE_CONFIG_UPDATES = is_env_true("ENABLE_CONFIG_UPDATES")
1✔
765

766
# CORS settings
767
DISABLE_CORS_HEADERS = is_env_true("DISABLE_CORS_HEADERS")
1✔
768
DISABLE_CORS_CHECKS = is_env_true("DISABLE_CORS_CHECKS")
1✔
769
DISABLE_CUSTOM_CORS_S3 = is_env_true("DISABLE_CUSTOM_CORS_S3")
1✔
770
DISABLE_CUSTOM_CORS_APIGATEWAY = is_env_true("DISABLE_CUSTOM_CORS_APIGATEWAY")
1✔
771
EXTRA_CORS_ALLOWED_HEADERS = os.environ.get("EXTRA_CORS_ALLOWED_HEADERS", "").strip()
1✔
772
EXTRA_CORS_EXPOSE_HEADERS = os.environ.get("EXTRA_CORS_EXPOSE_HEADERS", "").strip()
1✔
773
EXTRA_CORS_ALLOWED_ORIGINS = os.environ.get("EXTRA_CORS_ALLOWED_ORIGINS", "").strip()
1✔
774
DISABLE_PREFLIGHT_PROCESSING = is_env_true("DISABLE_PREFLIGHT_PROCESSING")
1✔
775

776
# whether to disable publishing events to the API
777
DISABLE_EVENTS = is_env_true("DISABLE_EVENTS")
1✔
778
DEBUG_ANALYTICS = is_env_true("DEBUG_ANALYTICS")
1✔
779

780
# whether to log fine-grained debugging information for the handler chain
781
DEBUG_HANDLER_CHAIN = is_env_true("DEBUG_HANDLER_CHAIN")
1✔
782

783
# whether to eagerly start services
784
EAGER_SERVICE_LOADING = is_env_true("EAGER_SERVICE_LOADING")
1✔
785

786
# whether to selectively load services in SERVICES
787
STRICT_SERVICE_LOADING = is_env_not_false("STRICT_SERVICE_LOADING")
1✔
788

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

792
# Whether to skip downloading our signed SSL cert.
793
SKIP_SSL_CERT_DOWNLOAD = is_env_true("SKIP_SSL_CERT_DOWNLOAD")
1✔
794

795
# Absolute path to a custom certificate (pem file)
796
CUSTOM_SSL_CERT_PATH = os.environ.get("CUSTOM_SSL_CERT_PATH", "").strip()
1✔
797

798
# Whether delete the cached signed SSL certificate at startup
799
REMOVE_SSL_CERT = is_env_true("REMOVE_SSL_CERT")
1✔
800

801
# Allow non-standard AWS regions
802
ALLOW_NONSTANDARD_REGIONS = is_env_true("ALLOW_NONSTANDARD_REGIONS")
1✔
803
if ALLOW_NONSTANDARD_REGIONS:
1✔
804
    os.environ["MOTO_ALLOW_NONEXISTENT_REGION"] = "true"
×
805

806
# name of the main Docker container
807
MAIN_CONTAINER_NAME = os.environ.get("MAIN_CONTAINER_NAME", "").strip() or "localstack-main"
1✔
808

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

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

815
# Equivalent to HTTP_PROXY, but only applicable for external connections
816
OUTBOUND_HTTP_PROXY = os.environ.get("OUTBOUND_HTTP_PROXY", "")
1✔
817

818
# Equivalent to HTTPS_PROXY, but only applicable for external connections
819
OUTBOUND_HTTPS_PROXY = os.environ.get("OUTBOUND_HTTPS_PROXY", "")
1✔
820

821
# Feature flag to enable validation of internal endpoint responses in the handler chain. For test use only.
822
OPENAPI_VALIDATE_RESPONSE = is_env_true("OPENAPI_VALIDATE_RESPONSE")
1✔
823
# Flag to enable the validation of the requests made to the LocalStack internal endpoints. Active by default.
824
OPENAPI_VALIDATE_REQUEST = is_env_true("OPENAPI_VALIDATE_REQUEST")
1✔
825

826
# environment variable to determine whether to include stack traces in http responses
827
INCLUDE_STACK_TRACES_IN_HTTP_RESPONSE = is_env_true("INCLUDE_STACK_TRACES_IN_HTTP_RESPONSE")
1✔
828

829
# whether to skip waiting for the infrastructure to shut down, or exit immediately
830
FORCE_SHUTDOWN = is_env_not_false("FORCE_SHUTDOWN")
1✔
831

832
# set variables no_proxy, i.e., run internal service calls directly
833
no_proxy = ",".join([constants.LOCALHOST_HOSTNAME, LOCALHOST, LOCALHOST_IP, "[::1]"])
1✔
834
if os.environ.get("no_proxy"):
1✔
835
    os.environ["no_proxy"] += "," + no_proxy
×
836
elif os.environ.get("NO_PROXY"):
1✔
837
    os.environ["NO_PROXY"] += "," + no_proxy
×
838
else:
839
    os.environ["no_proxy"] = no_proxy
1✔
840

841
# additional CLI commands, can be set by plugins
842
CLI_COMMANDS = {}
1✔
843

844
# determine IP of Docker bridge
845
if not DOCKER_BRIDGE_IP:
1✔
846
    DOCKER_BRIDGE_IP = "172.17.0.1"
1✔
847
    if is_in_docker:
1✔
848
        candidates = (DOCKER_BRIDGE_IP, "172.18.0.1")
1✔
849
        for ip in candidates:
1✔
850
            # TODO: remove from here - should not perform I/O operations in top-level config.py
851
            if ping(ip):
1✔
852
                DOCKER_BRIDGE_IP = ip
1✔
853
                break
1✔
854

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

860
# TODO: remove with 4.1.0
861
# Determine which implementation to use for the event rule / event filtering engine used by multiple services:
862
# EventBridge, EventBridge Pipes, Lambda Event Source Mapping
863
# Options: python (default) | java (deprecated since 4.0.3)
864
EVENT_RULE_ENGINE = os.environ.get("EVENT_RULE_ENGINE", "python").strip()
1✔
865

866
# -----
867
# SERVICE-SPECIFIC CONFIGS BELOW
868
# -----
869

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

882
# The default container runtime to use
883
CONTAINER_RUNTIME = os.environ.get("CONTAINER_RUNTIME", "").strip() or "docker"
1✔
884

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

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

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

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

901
# Delay between data persistence (in seconds)
902
KINESIS_MOCK_PERSIST_INTERVAL = os.environ.get("KINESIS_MOCK_PERSIST_INTERVAL", "").strip() or "5s"
1✔
903

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

907
# randomly inject faults to Kinesis
908
KINESIS_ERROR_PROBABILITY = float(os.environ.get("KINESIS_ERROR_PROBABILITY", "").strip() or 0.0)
1✔
909

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

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

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

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

933
# JAVA EE heap size for dynamodb
934
DYNAMODB_HEAP_SIZE = os.environ.get("DYNAMODB_HEAP_SIZE", "").strip() or "256m"
1✔
935

936
# single DB instance across multiple credentials are regions
937
DYNAMODB_SHARE_DB = int(os.environ.get("DYNAMODB_SHARE_DB") or 0)
1✔
938

939
# the port on which to expose dynamodblocal
940
DYNAMODB_LOCAL_PORT = int(os.environ.get("DYNAMODB_LOCAL_PORT") or 0)
1✔
941

942
# Enables the automatic removal of stale KV pais based on TTL
943
DYNAMODB_REMOVE_EXPIRED_ITEMS = is_env_true("DYNAMODB_REMOVE_EXPIRED_ITEMS")
1✔
944

945
# Used to toggle PurgeInProgress exceptions when calling purge within 60 seconds
946
SQS_DELAY_PURGE_RETRY = is_env_true("SQS_DELAY_PURGE_RETRY")
1✔
947

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

951
# Used to toggle MessageRetentionPeriod functionality in SQS queues
952
SQS_ENABLE_MESSAGE_RETENTION_PERIOD = is_env_true("SQS_ENABLE_MESSAGE_RETENTION_PERIOD")
1✔
953

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

957
# Disable the check for MaxNumberOfMessage in SQS ReceiveMessage
958
SQS_DISABLE_MAX_NUMBER_OF_MESSAGE_LIMIT = is_env_true("SQS_DISABLE_MAX_NUMBER_OF_MESSAGE_LIMIT")
1✔
959

960
# Disable cloudwatch metrics for SQS
961
SQS_DISABLE_CLOUDWATCH_METRICS = is_env_true("SQS_DISABLE_CLOUDWATCH_METRICS")
1✔
962

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

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

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

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

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

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

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

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

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

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

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

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

1018

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

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

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

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

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

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

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

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

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

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

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

1121
# path prefix for windows volume mounting
1122
WINDOWS_DOCKER_MOUNT_PREFIX = os.environ.get("WINDOWS_DOCKER_MOUNT_PREFIX", "/host_mnt")
1✔
1123

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

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

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

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

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

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

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

1165
# Whether to really publish to GCM while using SNS Platform Application (needs credentials)
1166
LEGACY_SNS_GCM_PUBLISHING = is_env_true("LEGACY_SNS_GCM_PUBLISHING")
1✔
1167

1168
SNS_SES_SENDER_ADDRESS = os.environ.get("SNS_SES_SENDER_ADDRESS", "").strip()
1✔
1169

1170
SNS_CERT_URL_HOST = os.environ.get("SNS_CERT_URL_HOST", "").strip()
1✔
1171

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

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

1187
# TODO remove fallback to LAMBDA_DOCKER_NETWORK with next minor version
1188
MAIN_DOCKER_NETWORK = os.environ.get("MAIN_DOCKER_NETWORK", "") or LAMBDA_DOCKER_NETWORK
1✔
1189

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

1193
# Show exceptions for CloudFormation deploy errors
1194
CFN_VERBOSE_ERRORS = is_env_true("CFN_VERBOSE_ERRORS")
1✔
1195

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

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

1206
# How localstack will react to encountering unsupported resource types.
1207
# By default unsupported resource types will be ignored.
1208
# EXPERIMENTAL
1209
CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES = is_env_not_false("CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES")
1✔
1210

1211
# bind address of local DNS server
1212
DNS_ADDRESS = os.environ.get("DNS_ADDRESS") or "0.0.0.0"
1✔
1213
# port of the local DNS server
1214
DNS_PORT = int(os.environ.get("DNS_PORT", "53"))
1✔
1215

1216
# Comma-separated list of regex patterns for DNS names to resolve locally.
1217
# Any DNS name not matched against any of the patterns on this whitelist
1218
# will resolve it to the real DNS entry, rather than the local one.
1219
DNS_NAME_PATTERNS_TO_RESOLVE_UPSTREAM = (
1✔
1220
    os.environ.get("DNS_NAME_PATTERNS_TO_RESOLVE_UPSTREAM") or ""
1221
).strip()
1222
DNS_LOCAL_NAME_PATTERNS = (os.environ.get("DNS_LOCAL_NAME_PATTERNS") or "").strip()  # deprecated
1✔
1223

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

1230
# fallback DNS server to send upstream requests to
1231
DNS_SERVER = os.environ.get("DNS_SERVER")
1✔
1232
DNS_VERIFICATION_DOMAIN = os.environ.get("DNS_VERIFICATION_DOMAIN") or "localstack.cloud"
1✔
1233

1234

1235
def use_custom_dns():
1✔
1236
    return str(DNS_ADDRESS) not in FALSE_STRINGS
1✔
1237

1238

1239
# s3 virtual host name
1240
S3_VIRTUAL_HOSTNAME = f"s3.{LOCALSTACK_HOST.host}"
1✔
1241
S3_STATIC_WEBSITE_HOSTNAME = f"s3-website.{LOCALSTACK_HOST.host}"
1✔
1242

1243
BOTO_WAITER_DELAY = int(os.environ.get("BOTO_WAITER_DELAY") or "1")
1✔
1244
BOTO_WAITER_MAX_ATTEMPTS = int(os.environ.get("BOTO_WAITER_MAX_ATTEMPTS") or "120")
1✔
1245
DISABLE_CUSTOM_BOTO_WAITER_CONFIG = is_env_true("DISABLE_CUSTOM_BOTO_WAITER_CONFIG")
1✔
1246

1247
# defaults to false
1248
# if `DISABLE_BOTO_RETRIES=1` is set, all our created boto clients will have retries disabled
1249
DISABLE_BOTO_RETRIES = is_env_true("DISABLE_BOTO_RETRIES")
1✔
1250

1251
DISTRIBUTED_MODE = is_env_true("DISTRIBUTED_MODE")
1✔
1252

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

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

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

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

1441

1442
def is_local_test_mode() -> bool:
1✔
1443
    """Returns True if we are running in the context of our local integration tests."""
1444
    return is_env_true(ENV_INTERNAL_TEST_RUN)
1✔
1445

1446

1447
def is_collect_metrics_mode() -> bool:
1✔
1448
    """Returns True if metric collection is enabled."""
1449
    return is_env_true(ENV_INTERNAL_TEST_COLLECT_METRIC)
1✔
1450

1451

1452
def collect_config_items() -> list[tuple[str, Any]]:
1✔
1453
    """Returns a list of key-value tuples of LocalStack configuration values."""
UNCOV
1454
    none = object()  # sentinel object
×
1455

1456
    # collect which keys to print
1457
    keys = []
×
1458
    keys.extend(CONFIG_ENV_VARS)
×
1459
    keys.append("DATA_DIR")
×
UNCOV
1460
    keys.sort()
×
1461

UNCOV
1462
    values = globals()
×
1463

1464
    result = []
×
1465
    for k in keys:
×
1466
        v = values.get(k, none)
×
1467
        if v is none:
×
1468
            continue
×
1469
        result.append((k, v))
×
1470
    result.sort()
×
UNCOV
1471
    return result
×
1472

1473

1474
def populate_config_env_var_names():
1✔
1475
    global CONFIG_ENV_VARS
1476

1477
    CONFIG_ENV_VARS += [
1✔
1478
        key
1479
        for key in [key.upper() for key in os.environ]
1480
        if (key.startswith("LOCALSTACK_") or key.startswith("PROVIDER_OVERRIDE_"))
1481
        # explicitly exclude LOCALSTACK_CLI (it's prefixed with "LOCALSTACK_",
1482
        # but is only used in the CLI (should not be forwarded to the container)
1483
        and key != "LOCALSTACK_CLI"
1484
    ]
1485

1486
    # create variable aliases prefixed with LOCALSTACK_ (except LOCALSTACK_HOST)
1487
    CONFIG_ENV_VARS += [
1✔
1488
        "LOCALSTACK_" + v for v in CONFIG_ENV_VARS if not v.startswith("LOCALSTACK_")
1489
    ]
1490

1491
    CONFIG_ENV_VARS = list(set(CONFIG_ENV_VARS))
1✔
1492

1493

1494
# populate env var names to be passed to the container
1495
populate_config_env_var_names()
1✔
1496

1497

1498
# helpers to build urls
1499
def get_protocol() -> str:
1✔
1500
    return "https" if USE_SSL else "http"
1✔
1501

1502

1503
def external_service_url(
1✔
1504
    host: Optional[str] = None,
1505
    port: Optional[int] = None,
1506
    protocol: Optional[str] = None,
1507
    subdomains: Optional[str] = None,
1508
) -> str:
1509
    """Returns a service URL (e.g., SQS queue URL) to an external client (e.g., boto3) potentially running on another
1510
    machine than LocalStack. The configurations LOCALSTACK_HOST and USE_SSL can customize these returned URLs.
1511
    The optional parameters can be used to customize the defaults.
1512
    Examples with default configuration:
1513
    * external_service_url() == http://localhost.localstack.cloud:4566
1514
    * external_service_url(subdomains="s3") == http://s3.localhost.localstack.cloud:4566
1515
    """
1516
    protocol = protocol or get_protocol()
1✔
1517
    subdomains = f"{subdomains}." if subdomains else ""
1✔
1518
    host = host or LOCALSTACK_HOST.host
1✔
1519
    port = port or LOCALSTACK_HOST.port
1✔
1520
    return f"{protocol}://{subdomains}{host}:{port}"
1✔
1521

1522

1523
def internal_service_url(
1✔
1524
    host: Optional[str] = None,
1525
    port: Optional[int] = None,
1526
    protocol: Optional[str] = None,
1527
    subdomains: Optional[str] = None,
1528
) -> str:
1529
    """Returns a service URL for internal use within LocalStack (i.e., same host).
1530
    The configuration USE_SSL can customize these returned URLs but LOCALSTACK_HOST has no effect.
1531
    The optional parameters can be used to customize the defaults.
1532
    Examples with default configuration:
1533
    * internal_service_url() == http://localhost:4566
1534
    * internal_service_url(port=8080) == http://localhost:8080
1535
    """
1536
    protocol = protocol or get_protocol()
1✔
1537
    subdomains = f"{subdomains}." if subdomains else ""
1✔
1538
    host = host or LOCALHOST
1✔
1539
    port = port or GATEWAY_LISTEN[0].port
1✔
1540
    return f"{protocol}://{subdomains}{host}:{port}"
1✔
1541

1542

1543
# DEPRECATED: old helpers for building URLs
1544

1545

1546
def service_url(service_key, host=None, port=None):
1✔
1547
    """@deprecated: Use `internal_service_url()` instead. We assume that most usages are internal
1548
    but really need to check and update each usage accordingly.
1549
    """
UNCOV
1550
    warnings.warn(
×
1551
        """@deprecated: Use `internal_service_url()` instead. We assume that most usages are
1552
        internal but really need to check and update each usage accordingly.""",
1553
        DeprecationWarning,
1554
        stacklevel=2,
1555
    )
UNCOV
1556
    return internal_service_url(host=host, port=port)
×
1557

1558

1559
def service_port(service_key: str, external: bool = False) -> int:
1✔
1560
    """@deprecated: Use `localstack_host().port` for external and `GATEWAY_LISTEN[0].port` for
1561
    internal use."""
UNCOV
1562
    warnings.warn(
×
1563
        "Deprecated: use `localstack_host().port` for external and `GATEWAY_LISTEN[0].port` for "
1564
        "internal use.",
1565
        DeprecationWarning,
1566
        stacklevel=2,
1567
    )
1568
    if external:
×
1569
        return LOCALSTACK_HOST.port
×
UNCOV
1570
    return GATEWAY_LISTEN[0].port
×
1571

1572

1573
def get_edge_port_http():
1✔
1574
    """@deprecated: Use `localstack_host().port` for external and `GATEWAY_LISTEN[0].port` for
1575
    internal use. This function is not needed anymore because we don't separate between HTTP
1576
    and HTTP ports anymore since LocalStack listens to both ports."""
UNCOV
1577
    warnings.warn(
×
1578
        """@deprecated: Use `localstack_host().port` for external and `GATEWAY_LISTEN[0].port`
1579
        for internal use. This function is also not needed anymore because we don't separate
1580
        between HTTP and HTTP ports anymore since LocalStack listens to both.""",
1581
        DeprecationWarning,
1582
        stacklevel=2,
1583
    )
UNCOV
1584
    return GATEWAY_LISTEN[0].port
×
1585

1586

1587
def get_edge_url(localstack_hostname=None, protocol=None):
1✔
1588
    """@deprecated: Use `internal_service_url()` instead.
1589
    We assume that most usages are internal but really need to check and update each usage accordingly.
1590
    """
UNCOV
1591
    warnings.warn(
×
1592
        """@deprecated: Use `internal_service_url()` instead.
1593
    We assume that most usages are internal but really need to check and update each usage accordingly.
1594
    """,
1595
        DeprecationWarning,
1596
        stacklevel=2,
1597
    )
UNCOV
1598
    return internal_service_url(host=localstack_hostname, protocol=protocol)
×
1599

1600

1601
class ServiceProviderConfig(Mapping[str, str]):
1✔
1602
    _provider_config: dict[str, str]
1✔
1603
    default_value: str
1✔
1604
    override_prefix: str = "PROVIDER_OVERRIDE_"
1✔
1605

1606
    def __init__(self, default_value: str):
1✔
1607
        self._provider_config = {}
1✔
1608
        self.default_value = default_value
1✔
1609

1610
    def load_from_environment(self, env: Mapping[str, str] = None):
1✔
1611
        if env is None:
1✔
1612
            env = os.environ
1✔
1613
        for key, value in env.items():
1✔
1614
            if key.startswith(self.override_prefix) and value:
1✔
1615
                self.set_provider(key[len(self.override_prefix) :].lower().replace("_", "-"), value)
1✔
1616

1617
    def get_provider(self, service: str) -> str:
1✔
1618
        return self._provider_config.get(service, self.default_value)
1✔
1619

1620
    def set_provider_if_not_exists(self, service: str, provider: str) -> None:
1✔
1621
        if service not in self._provider_config:
1✔
1622
            self._provider_config[service] = provider
1✔
1623

1624
    def set_provider(self, service: str, provider: str):
1✔
1625
        self._provider_config[service] = provider
1✔
1626

1627
    def bulk_set_provider_if_not_exists(self, services: list[str], provider: str):
1✔
1628
        for service in services:
1✔
1629
            self.set_provider_if_not_exists(service, provider)
1✔
1630

1631
    def __getitem__(self, item):
1✔
1632
        return self.get_provider(item)
1✔
1633

1634
    def __setitem__(self, key, value):
1✔
UNCOV
1635
        self.set_provider(key, value)
×
1636

1637
    def __len__(self):
1✔
UNCOV
1638
        return len(self._provider_config)
×
1639

1640
    def __iter__(self):
1✔
UNCOV
1641
        return self._provider_config.__iter__()
×
1642

1643

1644
SERVICE_PROVIDER_CONFIG = ServiceProviderConfig("default")
1✔
1645

1646
SERVICE_PROVIDER_CONFIG.load_from_environment()
1✔
1647

1648

1649
def init_directories() -> Directories:
1✔
1650
    if is_in_docker:
1✔
1651
        return Directories.for_container()
1✔
1652
    else:
1653
        if is_env_true("LOCALSTACK_CLI"):
1✔
1654
            return Directories.for_cli()
1✔
1655

1656
        return Directories.for_host()
1✔
1657

1658

1659
# initialize directories
1660
dirs: Directories
1✔
1661
dirs = init_directories()
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