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

localstack / localstack / 20565403496

29 Dec 2025 05:11AM UTC coverage: 84.103% (-2.8%) from 86.921%
20565403496

Pull #13567

github

web-flow
Merge 4816837a5 into 2417384aa
Pull Request #13567: Update ASF APIs

67166 of 79862 relevant lines covered (84.1%)

0.84 hits per line

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

88.03
/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
1✔
168

169
        from localstack.utils import files
1✔
170

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

174
        return Directories(
1✔
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
1✔
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

499
def is_trace_logging_enabled():
1✔
500
    if LS_LOG:
1✔
501
        log_level = str(LS_LOG).upper()
×
502
        return log_level.lower() in TRACE_LOG_LEVELS
×
503
    return False
1✔
504

505

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

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

518

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

524
    if not host:
1✔
525
        return False
×
526

527
    try:
1✔
528
        ipaddress.IPv6Address(host)
1✔
529
        return True
1✔
530
    except ipaddress.AddressValueError:
1✔
531
        return False
1✔
532

533

534
class HostAndPort:
1✔
535
    """
536
    Definition of an address for a server to listen to.
537

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

543
    host: str
1✔
544
    port: int
1✔
545

546
    def __init__(self, host: str, port: int):
1✔
547
        self.host = host
1✔
548
        self.port = port
1✔
549

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

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

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

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

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

600
        return cls(host=host, port=port)
1✔
601

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

609
        return port
1✔
610

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

619
    def is_unprivileged(self) -> bool:
1✔
620
        return self.port >= self._get_unprivileged_port_range_start()
×
621

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

626
    def __hash__(self) -> int:
1✔
627
        return hash((self.host, self.port))
×
628

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

638
    def __str__(self) -> str:
1✔
639
        return self.host_and_port()
1✔
640

641
    def __repr__(self) -> str:
642
        return f"HostAndPort(host={self.host}, port={self.port})"
643

644

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

658
    def __init__(self, iterable: list[HostAndPort] | None = None):
1✔
659
        super().__init__(iterable or [])
1✔
660
        self._ensure_unique()
1✔
661

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

669
        unique: list[HostAndPort] = []
1✔
670

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

676
        # For any given port, dedupe the hosts
677
        for port, hosts in hosts_by_port.items():
1✔
678
            deduped_hosts = set(hosts)
1✔
679

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

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

694
        self.clear()
1✔
695
        self.extend(unique)
1✔
696

697
    def append(self, value: HostAndPort):
1✔
698
        super().append(value)
1✔
699
        self._ensure_unique()
1✔
700

701

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

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

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

736
    assert gateway_listen is not None
1✔
737
    assert localstack_host is not None
1✔
738

739
    return (
1✔
740
        localstack_host,
741
        UniqueHostAndPortList(gateway_listen),
742
    )
743

744

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

755
GATEWAY_WORKER_COUNT = int(os.environ.get("GATEWAY_WORKER_COUNT") or 1000)
1✔
756

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

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

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

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

769
# whether to enable API-based updates of configuration variables at runtime
770
ENABLE_CONFIG_UPDATES = is_env_true("ENABLE_CONFIG_UPDATES")
1✔
771

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

782
# whether to disable publishing events to the API
783
DISABLE_EVENTS = is_env_true("DISABLE_EVENTS")
1✔
784
DEBUG_ANALYTICS = is_env_true("DEBUG_ANALYTICS")
1✔
785

786
# whether to log fine-grained debugging information for the handler chain
787
DEBUG_HANDLER_CHAIN = is_env_true("DEBUG_HANDLER_CHAIN")
1✔
788

789
# whether to eagerly start services
790
EAGER_SERVICE_LOADING = is_env_true("EAGER_SERVICE_LOADING")
1✔
791

792
# whether to selectively load services in SERVICES
793
STRICT_SERVICE_LOADING = is_env_not_false("STRICT_SERVICE_LOADING")
1✔
794

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

798
# Whether to skip downloading our signed SSL cert.
799
SKIP_SSL_CERT_DOWNLOAD = is_env_true("SKIP_SSL_CERT_DOWNLOAD")
1✔
800

801
# Absolute path to a custom certificate (pem file)
802
CUSTOM_SSL_CERT_PATH = os.environ.get("CUSTOM_SSL_CERT_PATH", "").strip()
1✔
803

804
# Whether delete the cached signed SSL certificate at startup
805
REMOVE_SSL_CERT = is_env_true("REMOVE_SSL_CERT")
1✔
806

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

812
# name of the main Docker container
813
MAIN_CONTAINER_NAME = os.environ.get("MAIN_CONTAINER_NAME", "").strip() or "localstack-main"
1✔
814

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

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

821
# Equivalent to HTTP_PROXY, but only applicable for external connections
822
OUTBOUND_HTTP_PROXY = os.environ.get("OUTBOUND_HTTP_PROXY", "")
1✔
823

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

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

832
# environment variable to determine whether to include stack traces in http responses
833
INCLUDE_STACK_TRACES_IN_HTTP_RESPONSE = is_env_true("INCLUDE_STACK_TRACES_IN_HTTP_RESPONSE")
1✔
834

835
# whether to skip waiting for the infrastructure to shut down, or exit immediately
836
FORCE_SHUTDOWN = is_env_not_false("FORCE_SHUTDOWN")
1✔
837

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

847
# additional CLI commands, can be set by plugins
848
CLI_COMMANDS = {}
1✔
849

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

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

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

872
# -----
873
# SERVICE-SPECIFIC CONFIGS BELOW
874
# -----
875

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

888
# The default container runtime to use
889
CONTAINER_RUNTIME = os.environ.get("CONTAINER_RUNTIME", "").strip() or "docker"
1✔
890

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

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

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

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

907
# Delay between data persistence (in seconds)
908
KINESIS_MOCK_PERSIST_INTERVAL = os.environ.get("KINESIS_MOCK_PERSIST_INTERVAL", "").strip() or "5s"
1✔
909

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

913
# randomly inject faults to Kinesis
914
KINESIS_ERROR_PROBABILITY = float(os.environ.get("KINESIS_ERROR_PROBABILITY", "").strip() or 0.0)
1✔
915

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

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

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

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

939
# JAVA EE heap size for dynamodb
940
DYNAMODB_HEAP_SIZE = os.environ.get("DYNAMODB_HEAP_SIZE", "").strip() or "256m"
1✔
941

942
# single DB instance across multiple credentials are regions
943
DYNAMODB_SHARE_DB = int(os.environ.get("DYNAMODB_SHARE_DB") or 0)
1✔
944

945
# the port on which to expose dynamodblocal
946
DYNAMODB_LOCAL_PORT = int(os.environ.get("DYNAMODB_LOCAL_PORT") or 0)
1✔
947

948
# Enables the automatic removal of stale KV pais based on TTL
949
DYNAMODB_REMOVE_EXPIRED_ITEMS = is_env_true("DYNAMODB_REMOVE_EXPIRED_ITEMS")
1✔
950

951
# Used to toggle PurgeInProgress exceptions when calling purge within 60 seconds
952
SQS_DELAY_PURGE_RETRY = is_env_true("SQS_DELAY_PURGE_RETRY")
1✔
953

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

957
# Used to toggle MessageRetentionPeriod functionality in SQS queues
958
SQS_ENABLE_MESSAGE_RETENTION_PERIOD = is_env_true("SQS_ENABLE_MESSAGE_RETENTION_PERIOD")
1✔
959

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

963
# Disable the check for MaxNumberOfMessage in SQS ReceiveMessage
964
SQS_DISABLE_MAX_NUMBER_OF_MESSAGE_LIMIT = is_env_true("SQS_DISABLE_MAX_NUMBER_OF_MESSAGE_LIMIT")
1✔
965

966
# Disable cloudwatch metrics for SQS
967
SQS_DISABLE_CLOUDWATCH_METRICS = is_env_true("SQS_DISABLE_CLOUDWATCH_METRICS")
1✔
968

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

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

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

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

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

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

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

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

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

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

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

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

1024

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

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

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

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

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

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

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

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

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

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

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

1127
# path prefix for windows volume mounting
1128
WINDOWS_DOCKER_MOUNT_PREFIX = os.environ.get("WINDOWS_DOCKER_MOUNT_PREFIX", "/host_mnt")
1✔
1129

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

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

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

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

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

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

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

1171
# Whether to really publish to GCM while using SNS Platform Application (needs credentials)
1172
LEGACY_SNS_GCM_PUBLISHING = is_env_true("LEGACY_SNS_GCM_PUBLISHING")
1✔
1173

1174
SNS_SES_SENDER_ADDRESS = os.environ.get("SNS_SES_SENDER_ADDRESS", "").strip()
1✔
1175

1176
SNS_CERT_URL_HOST = os.environ.get("SNS_CERT_URL_HOST", "").strip()
1✔
1177

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

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

1193
SNS_PROVIDER_V2 = os.environ.get("PROVIDER_OVERRIDE_SNS", "") == "v2"
1✔
1194

1195
# TODO remove fallback to LAMBDA_DOCKER_NETWORK with next minor version
1196
MAIN_DOCKER_NETWORK = os.environ.get("MAIN_DOCKER_NETWORK", "") or LAMBDA_DOCKER_NETWORK
1✔
1197

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

1201
# Show exceptions for CloudFormation deploy errors
1202
CFN_VERBOSE_ERRORS = is_env_true("CFN_VERBOSE_ERRORS")
1✔
1203

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

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

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

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

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

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

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

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

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

1254

1255
def use_custom_dns():
1✔
1256
    return str(DNS_ADDRESS) not in FALSE_STRINGS
1✔
1257

1258

1259
# s3 virtual host name
1260
S3_VIRTUAL_HOSTNAME = f"s3.{LOCALSTACK_HOST.host}"
1✔
1261
S3_STATIC_WEBSITE_HOSTNAME = f"s3-website.{LOCALSTACK_HOST.host}"
1✔
1262

1263
BOTO_WAITER_DELAY = int(os.environ.get("BOTO_WAITER_DELAY") or "1")
1✔
1264
BOTO_WAITER_MAX_ATTEMPTS = int(os.environ.get("BOTO_WAITER_MAX_ATTEMPTS") or "120")
1✔
1265
DISABLE_CUSTOM_BOTO_WAITER_CONFIG = is_env_true("DISABLE_CUSTOM_BOTO_WAITER_CONFIG")
1✔
1266

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

1271
DISTRIBUTED_MODE = is_env_true("DISTRIBUTED_MODE")
1✔
1272

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

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

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

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

1461

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

1466

1467
def is_collect_metrics_mode() -> bool:
1✔
1468
    """Returns True if metric collection is enabled."""
1469
    return is_env_true(ENV_INTERNAL_TEST_COLLECT_METRIC)
1✔
1470

1471

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

1476

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

1481
    # collect which keys to print
1482
    keys = []
×
1483
    keys.extend(CONFIG_ENV_VARS)
×
1484
    keys.append("DATA_DIR")
×
1485
    keys.sort()
×
1486

1487
    values = globals()
×
1488

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

1498

1499
def populate_config_env_var_names():
1✔
1500
    global CONFIG_ENV_VARS
1501

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

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

1516
    CONFIG_ENV_VARS = list(set(CONFIG_ENV_VARS))
1✔
1517

1518

1519
# populate env var names to be passed to the container
1520
populate_config_env_var_names()
1✔
1521

1522

1523
# helpers to build urls
1524
def get_protocol() -> str:
1✔
1525
    return "https" if USE_SSL else "http"
1✔
1526

1527

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

1547

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

1567

1568
# DEPRECATED: old helpers for building URLs
1569

1570

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

1583

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

1597

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

1611

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

1625

1626
class ServiceProviderConfig(Mapping[str, str]):
1✔
1627
    _provider_config: dict[str, str]
1✔
1628
    default_value: str
1✔
1629
    override_prefix: str = "PROVIDER_OVERRIDE_"
1✔
1630

1631
    def __init__(self, default_value: str):
1✔
1632
        self._provider_config = {}
1✔
1633
        self.default_value = default_value
1✔
1634

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

1642
    def get_provider(self, service: str) -> str:
1✔
1643
        return self._provider_config.get(service, self.default_value)
1✔
1644

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

1649
    def set_provider(self, service: str, provider: str):
1✔
1650
        self._provider_config[service] = provider
1✔
1651

1652
    def bulk_set_provider_if_not_exists(self, services: list[str], provider: str):
1✔
1653
        for service in services:
1✔
1654
            self.set_provider_if_not_exists(service, provider)
1✔
1655

1656
    def __getitem__(self, item):
1✔
1657
        return self.get_provider(item)
1✔
1658

1659
    def __setitem__(self, key, value):
1✔
1660
        self.set_provider(key, value)
×
1661

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

1665
    def __iter__(self):
1✔
1666
        return self._provider_config.__iter__()
×
1667

1668

1669
SERVICE_PROVIDER_CONFIG = ServiceProviderConfig("default")
1✔
1670

1671
SERVICE_PROVIDER_CONFIG.load_from_environment()
1✔
1672

1673

1674
def init_directories() -> Directories:
1✔
1675
    if is_in_docker:
1✔
1676
        return Directories.for_container()
1✔
1677
    else:
1678
        if is_env_true("LOCALSTACK_CLI"):
1✔
1679
            return Directories.for_cli()
1✔
1680

1681
        return Directories.for_host()
1✔
1682

1683

1684
# initialize directories
1685
dirs: Directories
1✔
1686
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

© 2025 Coveralls, Inc