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

localstack / localstack / 4a3afc72-d58f-4801-aaa2-a67e3fffc00a

29 Apr 2025 11:32AM UTC coverage: 86.551% (+1.3%) from 85.269%
4a3afc72-d58f-4801-aaa2-a67e3fffc00a

push

circleci

web-flow
CFn: add v2 tests to CI (#12556)

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

2 existing lines in 2 files now uncovered.

64228 of 74208 relevant lines covered (86.55%)

0.87 hits per line

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

87.26
/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 typing import Any, Dict, List, Mapping, Optional, Tuple, TypeVar, Union
1✔
13

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

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

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

35

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

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

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

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

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

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

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

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

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

138
        defaults = Directories.for_container()
1✔
139

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

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

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

167
        from localstack.utils import files
1✔
168

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

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

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

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

209

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

215

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

225

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

230

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

235

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

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

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

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

261
    return profiles
1✔
262

263

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

267

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

271

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

275

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

279

280
def ping(host):
1✔
281
    """Returns True if the host responds to a ping request"""
282
    is_in_windows = is_windows()
1✔
283
    ping_opts = "-n 1 -w 2000" if is_in_windows else "-c 1 -W 2"
1✔
284
    args = "ping %s %s" % (ping_opts, host)
1✔
285
    return (
1✔
286
        subprocess.call(
287
            args, shell=not is_in_windows, stdout=subprocess.PIPE, stderr=subprocess.PIPE
288
        )
289
        == 0
290
    )
291

292

293
def in_docker():
1✔
294
    """
295
    Returns True if running in a docker container, else False
296
    Ref. https://docs.docker.com/config/containers/runmetrics/#control-groups
297
    """
298
    if OVERRIDE_IN_DOCKER is not None:
1✔
299
        return OVERRIDE_IN_DOCKER
×
300

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

310
    # details: https://github.com/localstack/localstack/pull/4352
311
    if os.path.exists("/.dockerenv"):
1✔
312
        return True
×
313
    if os.path.exists("/run/.containerenv"):
1✔
314
        return True
×
315

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

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

351
                if not line:
1✔
352
                    continue
×
353

354
                # skip comments
355
                if line[0] == "#":
1✔
356
                    continue
×
357

358
                # format (man 5 fstab)
359
                # <spec> <mount point> <type> <options> <rest>...
360
                parts = line.split()
1✔
361
                if len(parts) < 4:
1✔
362
                    # badly formatted line
363
                    continue
×
364

365
                mount_point = parts[1]
1✔
366
                options = parts[3]
1✔
367

368
                # only consider the root filesystem
369
                if mount_point != "/":
1✔
370
                    continue
1✔
371

372
                if "io.containerd" in options:
1✔
373
                    return True
×
374

375
    except FileNotFoundError:
×
376
        pass
×
377

378
    return False
1✔
379

380

381
# whether the `in_docker` check should always return True or False
382
OVERRIDE_IN_DOCKER = parse_boolean_env("OVERRIDE_IN_DOCKER")
1✔
383

384
is_in_docker = in_docker()
1✔
385
is_in_linux = is_linux()
1✔
386
is_in_macos = is_macos()
1✔
387
default_ip = "0.0.0.0" if is_in_docker else "127.0.0.1"
1✔
388

389
# CLI specific: the configuration profile to load
390
CONFIG_PROFILE = os.environ.get("CONFIG_PROFILE", "").strip()
1✔
391

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

395
# keep this on top to populate the environment
396
try:
1✔
397
    # CLI specific: the actually loaded configuration profile
398
    LOADED_PROFILES = load_environment(CONFIG_PROFILE)
1✔
399
except ImportError:
×
400
    # dotenv may not be available in lambdas or other environments where config is loaded
401
    LOADED_PROFILES = None
×
402

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

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

409
# whether localstack should persist service state across localstack runs
410
PERSISTENCE = is_env_true("PERSISTENCE")
1✔
411

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

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

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

421
# whether to clear config.dirs.tmp on startup and shutdown
422
CLEAR_TMP_FOLDER = is_env_not_false("CLEAR_TMP_FOLDER")
1✔
423

424
# folder for temporary files and data
425
TMP_FOLDER = os.path.join(tempfile.gettempdir(), "localstack")
1✔
426

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

430
# fix for Mac OS, to be able to mount /var/folders in Docker
431
if TMP_FOLDER.startswith("/var/folders/") and os.path.exists("/private%s" % TMP_FOLDER):
1✔
432
    TMP_FOLDER = "/private%s" % TMP_FOLDER
×
433

434
# whether to enable verbose debug logging ("LOG" is used when using the CLI with LOCALSTACK_LOG instead of LS_LOG)
435
LS_LOG = eval_log_type("LS_LOG") or eval_log_type("LOG")
1✔
436
DEBUG = is_env_true("DEBUG") or LS_LOG in TRACE_LOG_LEVELS
1✔
437

438
# PUBLIC PREVIEW: 0 (default), 1 (preview)
439
# When enabled it triggers specialised workflows for the debugging.
440
LAMBDA_DEBUG_MODE = is_env_true("LAMBDA_DEBUG_MODE")
1✔
441

442
# path to the lambda debug mode configuration file.
443
LAMBDA_DEBUG_MODE_CONFIG_PATH = os.environ.get("LAMBDA_DEBUG_MODE_CONFIG_PATH")
1✔
444

445
# whether to enable debugpy
446
DEVELOP = is_env_true("DEVELOP")
1✔
447

448
# PORT FOR DEBUGGER
449
DEVELOP_PORT = int(os.environ.get("DEVELOP_PORT", "").strip() or DEFAULT_DEVELOP_PORT)
1✔
450

451
# whether to make debugpy wait for a debbuger client
452
WAIT_FOR_DEBUGGER = is_env_true("WAIT_FOR_DEBUGGER")
1✔
453

454
# whether to assume http or https for `get_protocol`
455
USE_SSL = is_env_true("USE_SSL")
1✔
456

457
# Whether to report internal failures as 500 or 501 errors.
458
FAIL_FAST = is_env_true("FAIL_FAST")
1✔
459

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

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

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

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

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

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

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

482

483
def is_trace_logging_enabled():
1✔
484
    if LS_LOG:
1✔
485
        log_level = str(LS_LOG).upper()
×
486
        return log_level.lower() in TRACE_LOG_LEVELS
×
487
    return False
1✔
488

489

490
# set log levels immediately, but will be overwritten later by setup_logging
491
if DEBUG:
1✔
492
    logging.getLogger("").setLevel(logging.DEBUG)
1✔
493
    logging.getLogger("localstack").setLevel(logging.DEBUG)
1✔
494

495
LOG = logging.getLogger(__name__)
1✔
496
if is_trace_logging_enabled():
1✔
497
    load_end_time = time.time()
×
498
    LOG.debug(
×
499
        "Initializing the configuration took %s ms", int((load_end_time - load_start_time) * 1000)
500
    )
501

502

503
def is_ipv6_address(host: str) -> bool:
1✔
504
    """
505
    Returns True if the given host is an IPv6 address.
506
    """
507

508
    if not host:
1✔
509
        return False
×
510

511
    try:
1✔
512
        ipaddress.IPv6Address(host)
1✔
513
        return True
1✔
514
    except ipaddress.AddressValueError:
1✔
515
        return False
1✔
516

517

518
class HostAndPort:
1✔
519
    """
520
    Definition of an address for a server to listen to.
521

522
    Includes a `parse` method to convert from `str`, allowing for default fallbacks, as well as
523
    some helper methods to help tests - particularly testing for equality and a hash function
524
    so that `HostAndPort` instances can be used as keys to dictionaries.
525
    """
526

527
    host: str
1✔
528
    port: int
1✔
529

530
    def __init__(self, host: str, port: int):
1✔
531
        self.host = host
1✔
532
        self.port = port
1✔
533

534
    @classmethod
1✔
535
    def parse(
1✔
536
        cls,
537
        input: str,
538
        default_host: str,
539
        default_port: int,
540
    ) -> "HostAndPort":
541
        """
542
        Parse a `HostAndPort` from strings like:
543
            - 0.0.0.0:4566 -> host=0.0.0.0, port=4566
544
            - 0.0.0.0      -> host=0.0.0.0, port=`default_port`
545
            - :4566        -> host=`default_host`, port=4566
546
            - [::]:4566    -> host=[::], port=4566
547
            - [::1]        -> host=[::1], port=`default_port`
548
        """
549
        host, port = default_host, default_port
1✔
550

551
        # recognize IPv6 addresses (+ port)
552
        if input.startswith("["):
1✔
553
            ipv6_pattern = re.compile(r"^\[(?P<host>[^]]+)\](:(?P<port>\d+))?$")
1✔
554
            match = ipv6_pattern.match(input)
1✔
555

556
            if match:
1✔
557
                host = match.group("host")
1✔
558
                if not is_ipv6_address(host):
1✔
559
                    raise ValueError(
1✔
560
                        f"input looks like an IPv6 address (is enclosed in square brackets), but is not valid: {host}"
561
                    )
562
                port_s = match.group("port")
1✔
563
                if port_s:
1✔
564
                    port = cls._validate_port(port_s)
1✔
565
            else:
566
                raise ValueError(
×
567
                    f'input looks like an IPv6 address, but is invalid. Should be formatted "[ip]:port": {input}'
568
                )
569

570
        # recognize IPv4 address + port
571
        elif ":" in input:
1✔
572
            hostname, port_s = input.split(":", 1)
1✔
573
            if hostname.strip():
1✔
574
                host = hostname.strip()
1✔
575
            port = cls._validate_port(port_s)
1✔
576
        else:
577
            if input.strip():
1✔
578
                host = input.strip()
1✔
579

580
        # validation
581
        if port < 0 or port >= 2**16:
1✔
582
            raise ValueError("port out of range")
1✔
583

584
        return cls(host=host, port=port)
1✔
585

586
    @classmethod
1✔
587
    def _validate_port(cls, port_s: str) -> int:
1✔
588
        try:
1✔
589
            port = int(port_s)
1✔
590
        except ValueError as e:
1✔
591
            raise ValueError(f"specified port {port_s} not a number") from e
1✔
592

593
        return port
1✔
594

595
    def _get_unprivileged_port_range_start(self) -> int:
1✔
596
        try:
×
597
            with open(
×
598
                "/proc/sys/net/ipv4/ip_unprivileged_port_start", "rt"
599
            ) as unprivileged_port_start:
600
                port = unprivileged_port_start.read()
×
601
                return int(port.strip())
×
602
        except Exception:
×
603
            return 1024
×
604

605
    def is_unprivileged(self) -> bool:
1✔
606
        return self.port >= self._get_unprivileged_port_range_start()
×
607

608
    def host_and_port(self) -> str:
1✔
609
        formatted_host = f"[{self.host}]" if is_ipv6_address(self.host) else self.host
1✔
610
        return f"{formatted_host}:{self.port}" if self.port is not None else formatted_host
1✔
611

612
    def __hash__(self) -> int:
1✔
613
        return hash((self.host, self.port))
×
614

615
    # easier tests
616
    def __eq__(self, other: "str | HostAndPort") -> bool:
1✔
617
        if isinstance(other, self.__class__):
1✔
618
            return self.host == other.host and self.port == other.port
1✔
619
        elif isinstance(other, str):
1✔
620
            return str(self) == other
1✔
621
        else:
622
            raise TypeError(f"cannot compare {self.__class__} to {other.__class__}")
×
623

624
    def __str__(self) -> str:
1✔
625
        return self.host_and_port()
1✔
626

627
    def __repr__(self) -> str:
628
        return f"HostAndPort(host={self.host}, port={self.port})"
629

630

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

644
    def __init__(self, iterable: Union[List[HostAndPort], None] = None):
1✔
645
        super().__init__(iterable or [])
1✔
646
        self._ensure_unique()
1✔
647

648
    def _ensure_unique(self):
1✔
649
        """
650
        Ensure that all bindings on the same port are de-duped.
651
        """
652
        if len(self) <= 1:
1✔
653
            return
1✔
654

655
        unique: List[HostAndPort] = list()
1✔
656

657
        # Build a dictionary of hosts by port
658
        hosts_by_port: Dict[int, List[str]] = defaultdict(list)
1✔
659
        for item in self:
1✔
660
            hosts_by_port[item.port].append(item.host)
1✔
661

662
        # For any given port, dedupe the hosts
663
        for port, hosts in hosts_by_port.items():
1✔
664
            deduped_hosts = set(hosts)
1✔
665

666
            # IPv6 all interfaces: this is the most general binding.
667
            # Any others should be removed.
668
            if "::" in deduped_hosts:
1✔
669
                unique.append(HostAndPort(host="::", port=port))
1✔
670
                continue
1✔
671
            # IPv4 all interfaces: this is the next most general binding.
672
            # Any others should be removed.
673
            if "0.0.0.0" in deduped_hosts:
1✔
674
                unique.append(HostAndPort(host="0.0.0.0", port=port))
1✔
675
                continue
1✔
676

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

680
        self.clear()
1✔
681
        self.extend(unique)
1✔
682

683
    def append(self, value: HostAndPort):
1✔
684
        super().append(value)
1✔
685
        self._ensure_unique()
1✔
686

687

688
def populate_edge_configuration(
1✔
689
    environment: Mapping[str, str],
690
) -> Tuple[HostAndPort, UniqueHostAndPortList]:
691
    """Populate the LocalStack edge configuration from environment variables."""
692
    localstack_host_raw = environment.get("LOCALSTACK_HOST")
1✔
693
    gateway_listen_raw = environment.get("GATEWAY_LISTEN")
1✔
694

695
    # parse gateway listen from multiple components
696
    if gateway_listen_raw is not None:
1✔
697
        gateway_listen = []
1✔
698
        for address in gateway_listen_raw.split(","):
1✔
699
            gateway_listen.append(
1✔
700
                HostAndPort.parse(
701
                    address.strip(),
702
                    default_host=default_ip,
703
                    default_port=constants.DEFAULT_PORT_EDGE,
704
                )
705
            )
706
    else:
707
        # use default if gateway listen is not defined
708
        gateway_listen = [HostAndPort(host=default_ip, port=constants.DEFAULT_PORT_EDGE)]
1✔
709

710
    # the actual value of the LOCALSTACK_HOST port now depends on what gateway listen actually listens to.
711
    if localstack_host_raw is None:
1✔
712
        localstack_host = HostAndPort(
1✔
713
            host=constants.LOCALHOST_HOSTNAME, port=gateway_listen[0].port
714
        )
715
    else:
716
        localstack_host = HostAndPort.parse(
1✔
717
            localstack_host_raw,
718
            default_host=constants.LOCALHOST_HOSTNAME,
719
            default_port=gateway_listen[0].port,
720
        )
721

722
    assert gateway_listen is not None
1✔
723
    assert localstack_host is not None
1✔
724

725
    return (
1✔
726
        localstack_host,
727
        UniqueHostAndPortList(gateway_listen),
728
    )
729

730

731
# How to access LocalStack
732
(
1✔
733
    # -- Cosmetic
734
    LOCALSTACK_HOST,
735
    # -- Edge configuration
736
    # Main configuration of the listen address of the hypercorn proxy. Of the form
737
    # <ip_address>:<port>(,<ip_address>:port>)*
738
    GATEWAY_LISTEN,
739
) = populate_edge_configuration(os.environ)
740

741
GATEWAY_WORKER_COUNT = int(os.environ.get("GATEWAY_WORKER_COUNT") or 1000)
1✔
742

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

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

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

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

755
# whether to enable API-based updates of configuration variables at runtime
756
ENABLE_CONFIG_UPDATES = is_env_true("ENABLE_CONFIG_UPDATES")
1✔
757

758
# CORS settings
759
DISABLE_CORS_HEADERS = is_env_true("DISABLE_CORS_HEADERS")
1✔
760
DISABLE_CORS_CHECKS = is_env_true("DISABLE_CORS_CHECKS")
1✔
761
DISABLE_CUSTOM_CORS_S3 = is_env_true("DISABLE_CUSTOM_CORS_S3")
1✔
762
DISABLE_CUSTOM_CORS_APIGATEWAY = is_env_true("DISABLE_CUSTOM_CORS_APIGATEWAY")
1✔
763
EXTRA_CORS_ALLOWED_HEADERS = os.environ.get("EXTRA_CORS_ALLOWED_HEADERS", "").strip()
1✔
764
EXTRA_CORS_EXPOSE_HEADERS = os.environ.get("EXTRA_CORS_EXPOSE_HEADERS", "").strip()
1✔
765
EXTRA_CORS_ALLOWED_ORIGINS = os.environ.get("EXTRA_CORS_ALLOWED_ORIGINS", "").strip()
1✔
766
DISABLE_PREFLIGHT_PROCESSING = is_env_true("DISABLE_PREFLIGHT_PROCESSING")
1✔
767

768
# whether to disable publishing events to the API
769
DISABLE_EVENTS = is_env_true("DISABLE_EVENTS")
1✔
770
DEBUG_ANALYTICS = is_env_true("DEBUG_ANALYTICS")
1✔
771

772
# whether to log fine-grained debugging information for the handler chain
773
DEBUG_HANDLER_CHAIN = is_env_true("DEBUG_HANDLER_CHAIN")
1✔
774

775
# whether to eagerly start services
776
EAGER_SERVICE_LOADING = is_env_true("EAGER_SERVICE_LOADING")
1✔
777

778
# whether to selectively load services in SERVICES
779
STRICT_SERVICE_LOADING = is_env_not_false("STRICT_SERVICE_LOADING")
1✔
780

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

784
# Whether to skip downloading our signed SSL cert.
785
SKIP_SSL_CERT_DOWNLOAD = is_env_true("SKIP_SSL_CERT_DOWNLOAD")
1✔
786

787
# Absolute path to a custom certificate (pem file)
788
CUSTOM_SSL_CERT_PATH = os.environ.get("CUSTOM_SSL_CERT_PATH", "").strip()
1✔
789

790
# Whether delete the cached signed SSL certificate at startup
791
REMOVE_SSL_CERT = is_env_true("REMOVE_SSL_CERT")
1✔
792

793
# Allow non-standard AWS regions
794
ALLOW_NONSTANDARD_REGIONS = is_env_true("ALLOW_NONSTANDARD_REGIONS")
1✔
795
if ALLOW_NONSTANDARD_REGIONS:
1✔
796
    os.environ["MOTO_ALLOW_NONEXISTENT_REGION"] = "true"
×
797

798
# name of the main Docker container
799
MAIN_CONTAINER_NAME = os.environ.get("MAIN_CONTAINER_NAME", "").strip() or "localstack-main"
1✔
800

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

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

807
# Equivalent to HTTP_PROXY, but only applicable for external connections
808
OUTBOUND_HTTP_PROXY = os.environ.get("OUTBOUND_HTTP_PROXY", "")
1✔
809

810
# Equivalent to HTTPS_PROXY, but only applicable for external connections
811
OUTBOUND_HTTPS_PROXY = os.environ.get("OUTBOUND_HTTPS_PROXY", "")
1✔
812

813
# Feature flag to enable validation of internal endpoint responses in the handler chain. For test use only.
814
OPENAPI_VALIDATE_RESPONSE = is_env_true("OPENAPI_VALIDATE_RESPONSE")
1✔
815
# Flag to enable the validation of the requests made to the LocalStack internal endpoints. Active by default.
816
OPENAPI_VALIDATE_REQUEST = is_env_true("OPENAPI_VALIDATE_REQUEST")
1✔
817

818
# whether to skip waiting for the infrastructure to shut down, or exit immediately
819
FORCE_SHUTDOWN = is_env_not_false("FORCE_SHUTDOWN")
1✔
820

821
# set variables no_proxy, i.e., run internal service calls directly
822
no_proxy = ",".join([constants.LOCALHOST_HOSTNAME, LOCALHOST, LOCALHOST_IP, "[::1]"])
1✔
823
if os.environ.get("no_proxy"):
1✔
824
    os.environ["no_proxy"] += "," + no_proxy
×
825
elif os.environ.get("NO_PROXY"):
1✔
826
    os.environ["NO_PROXY"] += "," + no_proxy
×
827
else:
828
    os.environ["no_proxy"] = no_proxy
1✔
829

830
# additional CLI commands, can be set by plugins
831
CLI_COMMANDS = {}
1✔
832

833
# determine IP of Docker bridge
834
if not DOCKER_BRIDGE_IP:
1✔
835
    DOCKER_BRIDGE_IP = "172.17.0.1"
1✔
836
    if is_in_docker:
1✔
837
        candidates = (DOCKER_BRIDGE_IP, "172.18.0.1")
1✔
838
        for ip in candidates:
1✔
839
            # TODO: remove from here - should not perform I/O operations in top-level config.py
840
            if ping(ip):
1✔
841
                DOCKER_BRIDGE_IP = ip
1✔
842
                break
1✔
843

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

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

855
# -----
856
# SERVICE-SPECIFIC CONFIGS BELOW
857
# -----
858

859
# port ranges for external service instances (f.e. elasticsearch clusters, opensearch clusters,...)
860
EXTERNAL_SERVICE_PORTS_START = int(
1✔
861
    os.environ.get("EXTERNAL_SERVICE_PORTS_START")
862
    or os.environ.get("SERVICE_INSTANCES_PORTS_START")
863
    or 4510
864
)
865
EXTERNAL_SERVICE_PORTS_END = int(
1✔
866
    os.environ.get("EXTERNAL_SERVICE_PORTS_END")
867
    or os.environ.get("SERVICE_INSTANCES_PORTS_END")
868
    or (EXTERNAL_SERVICE_PORTS_START + 50)
869
)
870

871
# The default container runtime to use
872
CONTAINER_RUNTIME = os.environ.get("CONTAINER_RUNTIME", "").strip() or "docker"
1✔
873

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

878
# limit in which to kinesis-mock will start throwing exceptions
879
KINESIS_SHARD_LIMIT = os.environ.get("KINESIS_SHARD_LIMIT", "").strip() or "100"
1✔
880
KINESIS_PERSISTENCE = is_env_not_false("KINESIS_PERSISTENCE")
1✔
881

882
# limit in which to kinesis-mock will start throwing exceptions
883
KINESIS_ON_DEMAND_STREAM_COUNT_LIMIT = (
1✔
884
    os.environ.get("KINESIS_ON_DEMAND_STREAM_COUNT_LIMIT", "").strip() or "10"
885
)
886

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

890
# Delay between data persistence (in seconds)
891
KINESIS_MOCK_PERSIST_INTERVAL = os.environ.get("KINESIS_MOCK_PERSIST_INTERVAL", "").strip() or "5s"
1✔
892

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

896
# randomly inject faults to Kinesis
897
KINESIS_ERROR_PROBABILITY = float(os.environ.get("KINESIS_ERROR_PROBABILITY", "").strip() or 0.0)
1✔
898

899
# randomly inject faults to DynamoDB
900
DYNAMODB_ERROR_PROBABILITY = float(os.environ.get("DYNAMODB_ERROR_PROBABILITY", "").strip() or 0.0)
1✔
901
DYNAMODB_READ_ERROR_PROBABILITY = float(
1✔
902
    os.environ.get("DYNAMODB_READ_ERROR_PROBABILITY", "").strip() or 0.0
903
)
904
DYNAMODB_WRITE_ERROR_PROBABILITY = float(
1✔
905
    os.environ.get("DYNAMODB_WRITE_ERROR_PROBABILITY", "").strip() or 0.0
906
)
907

908
# JAVA EE heap size for dynamodb
909
DYNAMODB_HEAP_SIZE = os.environ.get("DYNAMODB_HEAP_SIZE", "").strip() or "256m"
1✔
910

911
# single DB instance across multiple credentials are regions
912
DYNAMODB_SHARE_DB = int(os.environ.get("DYNAMODB_SHARE_DB") or 0)
1✔
913

914
# the port on which to expose dynamodblocal
915
DYNAMODB_LOCAL_PORT = int(os.environ.get("DYNAMODB_LOCAL_PORT") or 0)
1✔
916

917
# Enables the automatic removal of stale KV pais based on TTL
918
DYNAMODB_REMOVE_EXPIRED_ITEMS = is_env_true("DYNAMODB_REMOVE_EXPIRED_ITEMS")
1✔
919

920
# Used to toggle PurgeInProgress exceptions when calling purge within 60 seconds
921
SQS_DELAY_PURGE_RETRY = is_env_true("SQS_DELAY_PURGE_RETRY")
1✔
922

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

926
# Used to toggle MessageRetentionPeriod functionality in SQS queues
927
SQS_ENABLE_MESSAGE_RETENTION_PERIOD = is_env_true("SQS_ENABLE_MESSAGE_RETENTION_PERIOD")
1✔
928

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

932
# Disable the check for MaxNumberOfMessage in SQS ReceiveMessage
933
SQS_DISABLE_MAX_NUMBER_OF_MESSAGE_LIMIT = is_env_true("SQS_DISABLE_MAX_NUMBER_OF_MESSAGE_LIMIT")
1✔
934

935
# Disable cloudwatch metrics for SQS
936
SQS_DISABLE_CLOUDWATCH_METRICS = is_env_true("SQS_DISABLE_CLOUDWATCH_METRICS")
1✔
937

938
# Interval for reporting "approximate" metrics to cloudwatch, default is 60 seconds
939
SQS_CLOUDWATCH_METRICS_REPORT_INTERVAL = int(
1✔
940
    os.environ.get("SQS_CLOUDWATCH_METRICS_REPORT_INTERVAL") or 60
941
)
942

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

946
# PUBLIC: hot-reload (default v2), __local__ (default v1)
947
# Magic S3 bucket name for Hot Reloading. The S3Key points to the source code on the local file system.
948
BUCKET_MARKER_LOCAL = (
1✔
949
    os.environ.get("BUCKET_MARKER_LOCAL", "").strip() or DEFAULT_BUCKET_MARKER_LOCAL
950
)
951

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

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

960
# PUBLIC v1: LocalStack DNS (default)
961
# Custom DNS server for the container running your lambda function.
962
LAMBDA_DOCKER_DNS = os.environ.get("LAMBDA_DOCKER_DNS", "").strip()
1✔
963

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

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

973
# TODO: test and add to docs
974
# EXPERIMENTAL: 0 (default)
975
# prebuild images before execution? Increased cold start time on the tradeoff of increased time until lambda is ACTIVE
976
LAMBDA_PREBUILD_IMAGES = is_env_true("LAMBDA_PREBUILD_IMAGES")
1✔
977

978
# PUBLIC: docker (default), kubernetes (pro)
979
# Where Lambdas will be executed.
980
LAMBDA_RUNTIME_EXECUTOR = os.environ.get("LAMBDA_RUNTIME_EXECUTOR", CONTAINER_RUNTIME).strip()
1✔
981

982
# PUBLIC: 20 (default)
983
# How many seconds Lambda will wait for the runtime environment to start up.
984
LAMBDA_RUNTIME_ENVIRONMENT_TIMEOUT = int(os.environ.get("LAMBDA_RUNTIME_ENVIRONMENT_TIMEOUT") or 20)
1✔
985

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

993
# PUBLIC: 0 (default)
994
# Whether to disable usage of deprecated runtimes
995
LAMBDA_RUNTIME_VALIDATION = int(os.environ.get("LAMBDA_RUNTIME_VALIDATION") or 0)
1✔
996

997
# PUBLIC: 1 (default)
998
# Whether to remove any Lambda Docker containers.
999
LAMBDA_REMOVE_CONTAINERS = (
1✔
1000
    os.environ.get("LAMBDA_REMOVE_CONTAINERS", "").lower().strip() not in FALSE_STRINGS
1001
)
1002

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

1008
# PUBLIC: 1000 (default)
1009
# The maximum number of events that functions can process simultaneously in the current Region.
1010
# See AWS service quotas: https://docs.aws.amazon.com/general/latest/gr/lambda-service.html
1011
# Concurrency limits. Like on AWS these apply per account and region.
1012
LAMBDA_LIMITS_CONCURRENT_EXECUTIONS = int(
1✔
1013
    os.environ.get("LAMBDA_LIMITS_CONCURRENT_EXECUTIONS", 1_000)
1014
)
1015
# SEMI-PUBLIC: not actively communicated
1016
# per account/region: there must be at least <LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY> unreserved concurrency.
1017
LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY = int(
1✔
1018
    os.environ.get("LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY", 100)
1019
)
1020
# SEMI-PUBLIC: not actively communicated
1021
LAMBDA_LIMITS_TOTAL_CODE_SIZE = int(os.environ.get("LAMBDA_LIMITS_TOTAL_CODE_SIZE", 80_530_636_800))
1✔
1022
# PUBLIC: documented after AWS changed validation around 2023-11
1023
LAMBDA_LIMITS_CODE_SIZE_ZIPPED = int(os.environ.get("LAMBDA_LIMITS_CODE_SIZE_ZIPPED", 52_428_800))
1✔
1024
# SEMI-PUBLIC: not actively communicated
1025
LAMBDA_LIMITS_CODE_SIZE_UNZIPPED = int(
1✔
1026
    os.environ.get("LAMBDA_LIMITS_CODE_SIZE_UNZIPPED", 262_144_000)
1027
)
1028
# PUBLIC: documented upon customer request
1029
LAMBDA_LIMITS_CREATE_FUNCTION_REQUEST_SIZE = int(
1✔
1030
    os.environ.get("LAMBDA_LIMITS_CREATE_FUNCTION_REQUEST_SIZE", 70_167_211)
1031
)
1032
# SEMI-PUBLIC: not actively communicated
1033
LAMBDA_LIMITS_MAX_FUNCTION_ENVVAR_SIZE_BYTES = int(
1✔
1034
    os.environ.get("LAMBDA_LIMITS_MAX_FUNCTION_ENVVAR_SIZE_BYTES", 4 * 1024)
1035
)
1036
# SEMI-PUBLIC: not actively communicated
1037
LAMBDA_LIMITS_MAX_FUNCTION_PAYLOAD_SIZE_BYTES = int(
1✔
1038
    os.environ.get(
1039
        "LAMBDA_LIMITS_MAX_FUNCTION_PAYLOAD_SIZE_BYTES", 6 * 1024 * 1024 + 100
1040
    )  # the 100 comes from the init defaults
1041
)
1042

1043
# DEV: 0 (default unless in host mode on macOS) For LS developers only. Only applies to Docker mode.
1044
# Whether to explicitly expose a free TCP port in lambda containers when invoking functions in host mode for
1045
# systems that cannot reach the container via its IPv4. For example, macOS cannot reach Docker containers:
1046
# https://docs.docker.com/desktop/networking/#i-cannot-ping-my-containers
1047
LAMBDA_DEV_PORT_EXPOSE = (
1✔
1048
    # Enable this dev flag by default on macOS in host mode (i.e., non-Docker environment)
1049
    is_env_not_false("LAMBDA_DEV_PORT_EXPOSE")
1050
    if not is_in_docker and is_in_macos
1051
    else is_env_true("LAMBDA_DEV_PORT_EXPOSE")
1052
)
1053

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

1057
# DEV: Release version of https://github.com/localstack/lambda-runtime-init overriding the current default
1058
LAMBDA_INIT_RELEASE_VERSION = os.environ.get("LAMBDA_INIT_RELEASE_VERSION")
1✔
1059
# DEV: 0 (default) Enable for mounting of RIE init binary and delve debugger
1060
LAMBDA_INIT_DEBUG = is_env_true("LAMBDA_INIT_DEBUG")
1✔
1061
# DEV: path to RIE init binary (e.g., var/rapid/init)
1062
LAMBDA_INIT_BIN_PATH = os.environ.get("LAMBDA_INIT_BIN_PATH")
1✔
1063
# DEV: path to entrypoint script (e.g., var/rapid/entrypoint.sh)
1064
LAMBDA_INIT_BOOTSTRAP_PATH = os.environ.get("LAMBDA_INIT_BOOTSTRAP_PATH")
1✔
1065
# DEV: path to delve debugger (e.g., var/rapid/dlv)
1066
LAMBDA_INIT_DELVE_PATH = os.environ.get("LAMBDA_INIT_DELVE_PATH")
1✔
1067
# DEV: Go Delve debug port
1068
LAMBDA_INIT_DELVE_PORT = int(os.environ.get("LAMBDA_INIT_DELVE_PORT") or 40000)
1✔
1069
# DEV: Time to wait after every invoke as a workaround to fix a race condition in persistence tests
1070
LAMBDA_INIT_POST_INVOKE_WAIT_MS = os.environ.get("LAMBDA_INIT_POST_INVOKE_WAIT_MS")
1✔
1071
# DEV: sbx_user1051 (default when not provided) Alternative system user or empty string to skip dropping privileges.
1072
LAMBDA_INIT_USER = os.environ.get("LAMBDA_INIT_USER")
1✔
1073

1074
# INTERNAL: 1 (default)
1075
# The duration (in seconds) to wait between each poll call to an event source.
1076
LAMBDA_EVENT_SOURCE_MAPPING_POLL_INTERVAL_SEC = float(
1✔
1077
    os.environ.get("LAMBDA_EVENT_SOURCE_MAPPING_POLL_INTERVAL_SEC") or 1
1078
)
1079

1080
# INTERNAL: 60 (default)
1081
# Maximum duration (in seconds) to wait between retries when an event source poll fails.
1082
LAMBDA_EVENT_SOURCE_MAPPING_MAX_BACKOFF_ON_ERROR_SEC = float(
1✔
1083
    os.environ.get("LAMBDA_EVENT_SOURCE_MAPPING_MAX_BACKOFF_ON_ERROR_SEC") or 60
1084
)
1085

1086
# INTERNAL: 10 (default)
1087
# Maximum duration (in seconds) to wait between polls when an event source returns empty results.
1088
LAMBDA_EVENT_SOURCE_MAPPING_MAX_BACKOFF_ON_EMPTY_POLL_SEC = float(
1✔
1089
    os.environ.get("LAMBDA_EVENT_SOURCE_MAPPING_MAX_BACKOFF_ON_EMPTY_POLL_SEC") or 10
1090
)
1091

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

1095
# path prefix for windows volume mounting
1096
WINDOWS_DOCKER_MOUNT_PREFIX = os.environ.get("WINDOWS_DOCKER_MOUNT_PREFIX", "/host_mnt")
1✔
1097

1098
# whether to skip S3 presign URL signature validation (TODO: currently enabled, until all issues are resolved)
1099
S3_SKIP_SIGNATURE_VALIDATION = is_env_not_false("S3_SKIP_SIGNATURE_VALIDATION")
1✔
1100
# whether to skip S3 validation of provided KMS key
1101
S3_SKIP_KMS_KEY_VALIDATION = is_env_not_false("S3_SKIP_KMS_KEY_VALIDATION")
1✔
1102

1103
# PUBLIC: 2000 (default)
1104
# Allows increasing the default char limit for truncation of lambda log lines when printed in the console.
1105
# This does not affect the logs processing in CloudWatch.
1106
LAMBDA_TRUNCATE_STDOUT = int(os.getenv("LAMBDA_TRUNCATE_STDOUT") or 2000)
1✔
1107

1108
# INTERNAL: 60 (default matching AWS) only applies to new lambda provider
1109
# Base delay in seconds for async retries. Further retries use: NUM_ATTEMPTS * LAMBDA_RETRY_BASE_DELAY_SECONDS
1110
# 300 (5min) is the maximum because NUM_ATTEMPTS can be at most 3 and SQS has a message timer limit of 15 min.
1111
# For example:
1112
# 1x LAMBDA_RETRY_BASE_DELAY_SECONDS: delay between initial invocation and first retry
1113
# 2x LAMBDA_RETRY_BASE_DELAY_SECONDS: delay between the first retry and the second retry
1114
# 3x LAMBDA_RETRY_BASE_DELAY_SECONDS: delay between the second retry and the third retry
1115
LAMBDA_RETRY_BASE_DELAY_SECONDS = int(os.getenv("LAMBDA_RETRY_BASE_DELAY") or 60)
1✔
1116

1117
# PUBLIC: 0 (default)
1118
# Set to 1 to create lambda functions synchronously (not recommended).
1119
# Whether Lambda.CreateFunction will block until the function is in a terminal state (Active or Failed).
1120
# This technically breaks behavior parity but is provided as a simplification over the default AWS behavior and
1121
# to match the behavior of the old lambda provider.
1122
LAMBDA_SYNCHRONOUS_CREATE = is_env_true("LAMBDA_SYNCHRONOUS_CREATE")
1✔
1123

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

1128
# Strategy used when creating OpenSearch/Elasticsearch domain endpoints routed through the edge proxy
1129
# valid values: domain | path | port (off)
1130
OPENSEARCH_ENDPOINT_STRATEGY = (
1✔
1131
    os.environ.get("OPENSEARCH_ENDPOINT_STRATEGY", "").strip() or "domain"
1132
)
1133
if OPENSEARCH_ENDPOINT_STRATEGY == "off":
1✔
1134
    OPENSEARCH_ENDPOINT_STRATEGY = "port"
×
1135

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

1139
# Whether to really publish to GCM while using SNS Platform Application (needs credentials)
1140
LEGACY_SNS_GCM_PUBLISHING = is_env_true("LEGACY_SNS_GCM_PUBLISHING")
1✔
1141

1142
SNS_SES_SENDER_ADDRESS = os.environ.get("SNS_SES_SENDER_ADDRESS", "").strip()
1✔
1143

1144
SNS_CERT_URL_HOST = os.environ.get("SNS_CERT_URL_HOST", "").strip()
1✔
1145

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

1149
# Whether the DynamoDBStreams native provider is enabled
1150
DDB_STREAMS_PROVIDER_V2 = os.environ.get("PROVIDER_OVERRIDE_DYNAMODBSTREAMS", "") == "v2"
1✔
1151
_override_dynamodb_v2 = os.environ.get("PROVIDER_OVERRIDE_DYNAMODB", "")
1✔
1152
if DDB_STREAMS_PROVIDER_V2:
1✔
1153
    # in order to not have conflicts between the 2 implementations, as they are tightly coupled, we need to set DDB
1154
    # to be v2 as well
1155
    if not _override_dynamodb_v2:
×
1156
        os.environ["PROVIDER_OVERRIDE_DYNAMODB"] = "v2"
×
1157
elif _override_dynamodb_v2 == "v2":
1✔
1158
    os.environ["PROVIDER_OVERRIDE_DYNAMODBSTREAMS"] = "v2"
1✔
1159
    DDB_STREAMS_PROVIDER_V2 = True
1✔
1160

1161
# TODO remove fallback to LAMBDA_DOCKER_NETWORK with next minor version
1162
MAIN_DOCKER_NETWORK = os.environ.get("MAIN_DOCKER_NETWORK", "") or LAMBDA_DOCKER_NETWORK
1✔
1163

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

1167
# Show exceptions for CloudFormation deploy errors
1168
CFN_VERBOSE_ERRORS = is_env_true("CFN_VERBOSE_ERRORS")
1✔
1169

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

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

1180
# How localstack will react to encountering unsupported resource types.
1181
# By default unsupported resource types will be ignored.
1182
# EXPERIMENTAL
1183
CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES = is_env_not_false("CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES")
1✔
1184

1185
# bind address of local DNS server
1186
DNS_ADDRESS = os.environ.get("DNS_ADDRESS") or "0.0.0.0"
1✔
1187
# port of the local DNS server
1188
DNS_PORT = int(os.environ.get("DNS_PORT", "53"))
1✔
1189

1190
# Comma-separated list of regex patterns for DNS names to resolve locally.
1191
# Any DNS name not matched against any of the patterns on this whitelist
1192
# will resolve it to the real DNS entry, rather than the local one.
1193
DNS_NAME_PATTERNS_TO_RESOLVE_UPSTREAM = (
1✔
1194
    os.environ.get("DNS_NAME_PATTERNS_TO_RESOLVE_UPSTREAM") or ""
1195
).strip()
1196
DNS_LOCAL_NAME_PATTERNS = (os.environ.get("DNS_LOCAL_NAME_PATTERNS") or "").strip()  # deprecated
1✔
1197

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

1204
# fallback DNS server to send upstream requests to
1205
DNS_SERVER = os.environ.get("DNS_SERVER")
1✔
1206
DNS_VERIFICATION_DOMAIN = os.environ.get("DNS_VERIFICATION_DOMAIN") or "localstack.cloud"
1✔
1207

1208

1209
def use_custom_dns():
1✔
1210
    return str(DNS_ADDRESS) not in FALSE_STRINGS
1✔
1211

1212

1213
# s3 virtual host name
1214
S3_VIRTUAL_HOSTNAME = "s3.%s" % LOCALSTACK_HOST.host
1✔
1215
S3_STATIC_WEBSITE_HOSTNAME = "s3-website.%s" % LOCALSTACK_HOST.host
1✔
1216

1217
BOTO_WAITER_DELAY = int(os.environ.get("BOTO_WAITER_DELAY") or "1")
1✔
1218
BOTO_WAITER_MAX_ATTEMPTS = int(os.environ.get("BOTO_WAITER_MAX_ATTEMPTS") or "120")
1✔
1219
DISABLE_CUSTOM_BOTO_WAITER_CONFIG = is_env_true("DISABLE_CUSTOM_BOTO_WAITER_CONFIG")
1✔
1220

1221
# defaults to false
1222
# if `DISABLE_BOTO_RETRIES=1` is set, all our created boto clients will have retries disabled
1223
DISABLE_BOTO_RETRIES = is_env_true("DISABLE_BOTO_RETRIES")
1✔
1224

1225
DISTRIBUTED_MODE = is_env_true("DISTRIBUTED_MODE")
1✔
1226

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

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

1407

1408
def is_local_test_mode() -> bool:
1✔
1409
    """Returns True if we are running in the context of our local integration tests."""
1410
    return is_env_true(ENV_INTERNAL_TEST_RUN)
1✔
1411

1412

1413
def is_collect_metrics_mode() -> bool:
1✔
1414
    """Returns True if metric collection is enabled."""
1415
    return is_env_true(ENV_INTERNAL_TEST_COLLECT_METRIC)
1✔
1416

1417

1418
def collect_config_items() -> List[Tuple[str, Any]]:
1✔
1419
    """Returns a list of key-value tuples of LocalStack configuration values."""
1420
    none = object()  # sentinel object
×
1421

1422
    # collect which keys to print
1423
    keys = []
×
1424
    keys.extend(CONFIG_ENV_VARS)
×
1425
    keys.append("DATA_DIR")
×
1426
    keys.sort()
×
1427

1428
    values = globals()
×
1429

1430
    result = []
×
1431
    for k in keys:
×
1432
        v = values.get(k, none)
×
1433
        if v is none:
×
1434
            continue
×
1435
        result.append((k, v))
×
1436
    result.sort()
×
1437
    return result
×
1438

1439

1440
def populate_config_env_var_names():
1✔
1441
    global CONFIG_ENV_VARS
1442

1443
    CONFIG_ENV_VARS += [
1✔
1444
        key
1445
        for key in [key.upper() for key in os.environ]
1446
        if (key.startswith("LOCALSTACK_") or key.startswith("PROVIDER_OVERRIDE_"))
1447
        # explicitly exclude LOCALSTACK_CLI (it's prefixed with "LOCALSTACK_",
1448
        # but is only used in the CLI (should not be forwarded to the container)
1449
        and key != "LOCALSTACK_CLI"
1450
    ]
1451

1452
    # create variable aliases prefixed with LOCALSTACK_ (except LOCALSTACK_HOST)
1453
    CONFIG_ENV_VARS += [
1✔
1454
        "LOCALSTACK_" + v for v in CONFIG_ENV_VARS if not v.startswith("LOCALSTACK_")
1455
    ]
1456

1457
    CONFIG_ENV_VARS = list(set(CONFIG_ENV_VARS))
1✔
1458

1459

1460
# populate env var names to be passed to the container
1461
populate_config_env_var_names()
1✔
1462

1463

1464
# helpers to build urls
1465
def get_protocol() -> str:
1✔
1466
    return "https" if USE_SSL else "http"
1✔
1467

1468

1469
def external_service_url(
1✔
1470
    host: Optional[str] = None,
1471
    port: Optional[int] = None,
1472
    protocol: Optional[str] = None,
1473
    subdomains: Optional[str] = None,
1474
) -> str:
1475
    """Returns a service URL (e.g., SQS queue URL) to an external client (e.g., boto3) potentially running on another
1476
    machine than LocalStack. The configurations LOCALSTACK_HOST and USE_SSL can customize these returned URLs.
1477
    The optional parameters can be used to customize the defaults.
1478
    Examples with default configuration:
1479
    * external_service_url() == http://localhost.localstack.cloud:4566
1480
    * external_service_url(subdomains="s3") == http://s3.localhost.localstack.cloud:4566
1481
    """
1482
    protocol = protocol or get_protocol()
1✔
1483
    subdomains = f"{subdomains}." if subdomains else ""
1✔
1484
    host = host or LOCALSTACK_HOST.host
1✔
1485
    port = port or LOCALSTACK_HOST.port
1✔
1486
    return f"{protocol}://{subdomains}{host}:{port}"
1✔
1487

1488

1489
def internal_service_url(
1✔
1490
    host: Optional[str] = None,
1491
    port: Optional[int] = None,
1492
    protocol: Optional[str] = None,
1493
    subdomains: Optional[str] = None,
1494
) -> str:
1495
    """Returns a service URL for internal use within LocalStack (i.e., same host).
1496
    The configuration USE_SSL can customize these returned URLs but LOCALSTACK_HOST has no effect.
1497
    The optional parameters can be used to customize the defaults.
1498
    Examples with default configuration:
1499
    * internal_service_url() == http://localhost:4566
1500
    * internal_service_url(port=8080) == http://localhost:8080
1501
    """
1502
    protocol = protocol or get_protocol()
1✔
1503
    subdomains = f"{subdomains}." if subdomains else ""
1✔
1504
    host = host or LOCALHOST
1✔
1505
    port = port or GATEWAY_LISTEN[0].port
1✔
1506
    return f"{protocol}://{subdomains}{host}:{port}"
1✔
1507

1508

1509
# DEPRECATED: old helpers for building URLs
1510

1511

1512
def service_url(service_key, host=None, port=None):
1✔
1513
    """@deprecated: Use `internal_service_url()` instead. We assume that most usages are internal
1514
    but really need to check and update each usage accordingly.
1515
    """
1516
    warnings.warn(
×
1517
        """@deprecated: Use `internal_service_url()` instead. We assume that most usages are
1518
        internal but really need to check and update each usage accordingly.""",
1519
        DeprecationWarning,
1520
        stacklevel=2,
1521
    )
1522
    return internal_service_url(host=host, port=port)
×
1523

1524

1525
def service_port(service_key: str, external: bool = False) -> int:
1✔
1526
    """@deprecated: Use `localstack_host().port` for external and `GATEWAY_LISTEN[0].port` for
1527
    internal use."""
1528
    warnings.warn(
×
1529
        "Deprecated: use `localstack_host().port` for external and `GATEWAY_LISTEN[0].port` for "
1530
        "internal use.",
1531
        DeprecationWarning,
1532
        stacklevel=2,
1533
    )
1534
    if external:
×
1535
        return LOCALSTACK_HOST.port
×
1536
    return GATEWAY_LISTEN[0].port
×
1537

1538

1539
def get_edge_port_http():
1✔
1540
    """@deprecated: Use `localstack_host().port` for external and `GATEWAY_LISTEN[0].port` for
1541
    internal use. This function is not needed anymore because we don't separate between HTTP
1542
    and HTTP ports anymore since LocalStack listens to both ports."""
1543
    warnings.warn(
×
1544
        """@deprecated: Use `localstack_host().port` for external and `GATEWAY_LISTEN[0].port`
1545
        for internal use. This function is also not needed anymore because we don't separate
1546
        between HTTP and HTTP ports anymore since LocalStack listens to both.""",
1547
        DeprecationWarning,
1548
        stacklevel=2,
1549
    )
1550
    return GATEWAY_LISTEN[0].port
×
1551

1552

1553
def get_edge_url(localstack_hostname=None, protocol=None):
1✔
1554
    """@deprecated: Use `internal_service_url()` instead.
1555
    We assume that most usages are internal but really need to check and update each usage accordingly.
1556
    """
1557
    warnings.warn(
×
1558
        """@deprecated: Use `internal_service_url()` instead.
1559
    We assume that most usages are internal but really need to check and update each usage accordingly.
1560
    """,
1561
        DeprecationWarning,
1562
        stacklevel=2,
1563
    )
1564
    return internal_service_url(host=localstack_hostname, protocol=protocol)
×
1565

1566

1567
class ServiceProviderConfig(Mapping[str, str]):
1✔
1568
    _provider_config: Dict[str, str]
1✔
1569
    default_value: str
1✔
1570
    override_prefix: str = "PROVIDER_OVERRIDE_"
1✔
1571

1572
    def __init__(self, default_value: str):
1✔
1573
        self._provider_config = {}
1✔
1574
        self.default_value = default_value
1✔
1575

1576
    def load_from_environment(self, env: Mapping[str, str] = None):
1✔
1577
        if env is None:
1✔
1578
            env = os.environ
1✔
1579
        for key, value in env.items():
1✔
1580
            if key.startswith(self.override_prefix) and value:
1✔
1581
                self.set_provider(key[len(self.override_prefix) :].lower().replace("_", "-"), value)
1✔
1582

1583
    def get_provider(self, service: str) -> str:
1✔
1584
        return self._provider_config.get(service, self.default_value)
1✔
1585

1586
    def set_provider_if_not_exists(self, service: str, provider: str) -> None:
1✔
1587
        if service not in self._provider_config:
1✔
1588
            self._provider_config[service] = provider
1✔
1589

1590
    def set_provider(self, service: str, provider: str):
1✔
1591
        self._provider_config[service] = provider
1✔
1592

1593
    def bulk_set_provider_if_not_exists(self, services: List[str], provider: str):
1✔
1594
        for service in services:
1✔
1595
            self.set_provider_if_not_exists(service, provider)
1✔
1596

1597
    def __getitem__(self, item):
1✔
UNCOV
1598
        return self.get_provider(item)
×
1599

1600
    def __setitem__(self, key, value):
1✔
1601
        self.set_provider(key, value)
×
1602

1603
    def __len__(self):
1✔
1604
        return len(self._provider_config)
×
1605

1606
    def __iter__(self):
1✔
1607
        return self._provider_config.__iter__()
×
1608

1609

1610
SERVICE_PROVIDER_CONFIG = ServiceProviderConfig("default")
1✔
1611

1612
SERVICE_PROVIDER_CONFIG.load_from_environment()
1✔
1613

1614

1615
def init_directories() -> Directories:
1✔
1616
    if is_in_docker:
1✔
1617
        return Directories.for_container()
1✔
1618
    else:
1619
        if is_env_true("LOCALSTACK_CLI"):
1✔
1620
            return Directories.for_cli()
×
1621

1622
        return Directories.for_host()
1✔
1623

1624

1625
# initialize directories
1626
dirs: Directories
1✔
1627
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