playground-ls-cli 4.14.1.dev8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- localstack_cli/__init__.py +0 -0
- localstack_cli/cli/__init__.py +10 -0
- localstack_cli/cli/console.py +11 -0
- localstack_cli/cli/core_plugin.py +12 -0
- localstack_cli/cli/exceptions.py +19 -0
- localstack_cli/cli/localstack.py +951 -0
- localstack_cli/cli/lpm.py +138 -0
- localstack_cli/cli/main.py +22 -0
- localstack_cli/cli/plugin.py +39 -0
- localstack_cli/cli/plugins.py +134 -0
- localstack_cli/cli/profiles.py +65 -0
- localstack_cli/config.py +1689 -0
- localstack_cli/constants.py +165 -0
- localstack_cli/logging/__init__.py +0 -0
- localstack_cli/logging/format.py +194 -0
- localstack_cli/logging/setup.py +142 -0
- localstack_cli/packages/__init__.py +25 -0
- localstack_cli/packages/api.py +418 -0
- localstack_cli/packages/core.py +416 -0
- localstack_cli/pro/__init__.py +0 -0
- localstack_cli/pro/core/__init__.py +0 -0
- localstack_cli/pro/core/bootstrap/__init__.py +1 -0
- localstack_cli/pro/core/bootstrap/auth.py +213 -0
- localstack_cli/pro/core/bootstrap/dns_utils.py +55 -0
- localstack_cli/pro/core/bootstrap/entitlements.py +117 -0
- localstack_cli/pro/core/bootstrap/extensions/__init__.py +3 -0
- localstack_cli/pro/core/bootstrap/extensions/__main__.py +106 -0
- localstack_cli/pro/core/bootstrap/extensions/autoinstall.py +63 -0
- localstack_cli/pro/core/bootstrap/extensions/bootstrap.py +97 -0
- localstack_cli/pro/core/bootstrap/extensions/repository.py +374 -0
- localstack_cli/pro/core/bootstrap/licensingv2.py +1259 -0
- localstack_cli/pro/core/bootstrap/pods/__init__.py +0 -0
- localstack_cli/pro/core/bootstrap/pods/api_types.py +17 -0
- localstack_cli/pro/core/bootstrap/pods/constants.py +26 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/__init__.py +0 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/api.py +75 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/configs.py +69 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/params.py +86 -0
- localstack_cli/pro/core/bootstrap/pods_client.py +834 -0
- localstack_cli/pro/core/cli/__init__.py +0 -0
- localstack_cli/pro/core/cli/auth.py +226 -0
- localstack_cli/pro/core/cli/aws.py +16 -0
- localstack_cli/pro/core/cli/cli.py +99 -0
- localstack_cli/pro/core/cli/click_utils.py +21 -0
- localstack_cli/pro/core/cli/cloud_pods.py +465 -0
- localstack_cli/pro/core/cli/diff_view.py +41 -0
- localstack_cli/pro/core/cli/ephemeral.py +199 -0
- localstack_cli/pro/core/cli/extensions.py +492 -0
- localstack_cli/pro/core/cli/iam.py +180 -0
- localstack_cli/pro/core/cli/license.py +90 -0
- localstack_cli/pro/core/cli/localstack.py +118 -0
- localstack_cli/pro/core/cli/replicator.py +378 -0
- localstack_cli/pro/core/cli/state.py +183 -0
- localstack_cli/pro/core/cli/tree_view.py +235 -0
- localstack_cli/pro/core/config.py +556 -0
- localstack_cli/pro/core/constants.py +54 -0
- localstack_cli/pro/core/plugins.py +169 -0
- localstack_cli/runtime/__init__.py +6 -0
- localstack_cli/runtime/exceptions.py +7 -0
- localstack_cli/runtime/hooks.py +73 -0
- localstack_cli/testing/__init__.py +1 -0
- localstack_cli/testing/config.py +4 -0
- localstack_cli/utils/__init__.py +0 -0
- localstack_cli/utils/analytics/__init__.py +12 -0
- localstack_cli/utils/analytics/cli.py +67 -0
- localstack_cli/utils/analytics/client.py +111 -0
- localstack_cli/utils/analytics/events.py +30 -0
- localstack_cli/utils/analytics/logger.py +48 -0
- localstack_cli/utils/analytics/metadata.py +250 -0
- localstack_cli/utils/analytics/publisher.py +160 -0
- localstack_cli/utils/analytics/service_request_aggregator.py +133 -0
- localstack_cli/utils/archives.py +271 -0
- localstack_cli/utils/batching.py +258 -0
- localstack_cli/utils/bootstrap.py +1418 -0
- localstack_cli/utils/checksum.py +313 -0
- localstack_cli/utils/collections.py +554 -0
- localstack_cli/utils/common.py +229 -0
- localstack_cli/utils/container_networking.py +142 -0
- localstack_cli/utils/container_utils/__init__.py +0 -0
- localstack_cli/utils/container_utils/container_client.py +1585 -0
- localstack_cli/utils/container_utils/docker_cmd_client.py +987 -0
- localstack_cli/utils/container_utils/docker_sdk_client.py +1018 -0
- localstack_cli/utils/crypto.py +294 -0
- localstack_cli/utils/docker_utils.py +272 -0
- localstack_cli/utils/files.py +327 -0
- localstack_cli/utils/functions.py +92 -0
- localstack_cli/utils/http.py +326 -0
- localstack_cli/utils/json.py +219 -0
- localstack_cli/utils/net.py +516 -0
- localstack_cli/utils/no_exit_argument_parser.py +19 -0
- localstack_cli/utils/numbers.py +49 -0
- localstack_cli/utils/objects.py +235 -0
- localstack_cli/utils/patch.py +260 -0
- localstack_cli/utils/platform.py +77 -0
- localstack_cli/utils/run.py +514 -0
- localstack_cli/utils/server/__init__.py +0 -0
- localstack_cli/utils/server/tcp_proxy.py +108 -0
- localstack_cli/utils/serving.py +187 -0
- localstack_cli/utils/ssl.py +71 -0
- localstack_cli/utils/strings.py +245 -0
- localstack_cli/utils/sync.py +267 -0
- localstack_cli/utils/threads.py +163 -0
- localstack_cli/utils/time.py +81 -0
- localstack_cli/utils/urls.py +21 -0
- localstack_cli/utils/venv.py +100 -0
- localstack_cli/utils/xml.py +41 -0
- localstack_cli/version.py +34 -0
- playground_ls_cli-4.14.1.dev8.dist-info/METADATA +95 -0
- playground_ls_cli-4.14.1.dev8.dist-info/RECORD +112 -0
- playground_ls_cli-4.14.1.dev8.dist-info/WHEEL +5 -0
- playground_ls_cli-4.14.1.dev8.dist-info/entry_points.txt +17 -0
- playground_ls_cli-4.14.1.dev8.dist-info/top_level.txt +1 -0
localstack_cli/config.py
ADDED
|
@@ -0,0 +1,1689 @@
|
|
|
1
|
+
import ipaddress
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import re
|
|
6
|
+
import socket
|
|
7
|
+
import subprocess
|
|
8
|
+
import tempfile
|
|
9
|
+
import time
|
|
10
|
+
import warnings
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from typing import Any, TypeVar
|
|
14
|
+
|
|
15
|
+
from localstack_cli import constants
|
|
16
|
+
from localstack_cli.constants import (
|
|
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)
|
|
33
|
+
|
|
34
|
+
# keep track of start time, for performance debugging
|
|
35
|
+
load_start_time = time.time()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Directories:
|
|
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
|
|
57
|
+
var_libs: str
|
|
58
|
+
cache: str
|
|
59
|
+
tmp: str
|
|
60
|
+
mounted_tmp: str
|
|
61
|
+
functions: str
|
|
62
|
+
data: str
|
|
63
|
+
config: str
|
|
64
|
+
init: str
|
|
65
|
+
logs: str
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
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__()
|
|
81
|
+
self.static_libs = static_libs
|
|
82
|
+
self.var_libs = var_libs
|
|
83
|
+
self.cache = cache
|
|
84
|
+
self.tmp = tmp
|
|
85
|
+
self.mounted_tmp = mounted_tmp
|
|
86
|
+
self.functions = functions
|
|
87
|
+
self.data = data
|
|
88
|
+
self.config = config
|
|
89
|
+
self.init = init
|
|
90
|
+
self.logs = logs
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def defaults() -> "Directories":
|
|
94
|
+
"""Returns Localstack directory paths based on the localstack filesystem hierarchy."""
|
|
95
|
+
return Directories(
|
|
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
|
|
109
|
+
def for_container() -> "Directories":
|
|
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()
|
|
117
|
+
|
|
118
|
+
return Directories(
|
|
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
|
|
132
|
+
def for_host() -> "Directories":
|
|
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(
|
|
136
|
+
LOCALSTACK_ROOT_FOLDER, ".filesystem"
|
|
137
|
+
)
|
|
138
|
+
root = os.path.abspath(root)
|
|
139
|
+
|
|
140
|
+
defaults = Directories.for_container()
|
|
141
|
+
|
|
142
|
+
tmp = os.path.join(root, defaults.tmp.lstrip("/"))
|
|
143
|
+
data = os.path.join(root, defaults.data.lstrip("/"))
|
|
144
|
+
|
|
145
|
+
return Directories(
|
|
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
|
|
159
|
+
def for_cli() -> "Directories":
|
|
160
|
+
"""Returns directories used for when running localstack CLI commands from the host system. Unlike
|
|
161
|
+
``for_container``, these needs to be cross-platform. Ideally, this should not be needed at all,
|
|
162
|
+
because the localstack runtime and CLI do not share any control paths. There are a handful of
|
|
163
|
+
situations where directories or files may be created lazily for CLI commands. Some paths are
|
|
164
|
+
intentionally set to None to provoke errors if these paths are used from the CLI - which they
|
|
165
|
+
shouldn't. This is a symptom of not having a clear separation between CLI/runtime code, which will
|
|
166
|
+
be a future project."""
|
|
167
|
+
import tempfile
|
|
168
|
+
|
|
169
|
+
from localstack_cli.utils import files
|
|
170
|
+
|
|
171
|
+
tmp_dir = os.path.join(tempfile.gettempdir(), "localstack-cli")
|
|
172
|
+
cache_dir = (files.get_user_cache_dir()).absolute() / "localstack-cli"
|
|
173
|
+
|
|
174
|
+
return Directories(
|
|
175
|
+
static_libs=None,
|
|
176
|
+
var_libs=None,
|
|
177
|
+
cache=str(cache_dir), # used by analytics metadata
|
|
178
|
+
tmp=tmp_dir,
|
|
179
|
+
mounted_tmp=tmp_dir,
|
|
180
|
+
functions=None,
|
|
181
|
+
data=os.path.join(tmp_dir, "state"), # used by localstack-pro config TODO: remove
|
|
182
|
+
logs=os.path.join(tmp_dir, "logs"), # used for container logs
|
|
183
|
+
config=None, # in the context of the CLI, config.CONFIG_DIR should be used
|
|
184
|
+
init=None,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def mkdirs(self):
|
|
188
|
+
for folder in [
|
|
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):
|
|
201
|
+
try:
|
|
202
|
+
os.makedirs(folder)
|
|
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):
|
|
209
|
+
return str(self.__dict__)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def eval_log_type(env_var_name: str) -> str | bool:
|
|
213
|
+
"""Get the log type from environment variable"""
|
|
214
|
+
ls_log = os.environ.get(env_var_name, "").lower().strip()
|
|
215
|
+
return ls_log if ls_log in LOG_LEVELS else False
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def parse_boolean_env(env_var_name: str) -> bool | None:
|
|
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()
|
|
221
|
+
if value in TRUE_STRINGS:
|
|
222
|
+
return True
|
|
223
|
+
if value in FALSE_STRINGS:
|
|
224
|
+
return False
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def parse_comma_separated_list(env_var_name: str) -> list[str]:
|
|
229
|
+
"""Parse a comma separated list from the given environment variable."""
|
|
230
|
+
return os.environ.get(env_var_name, "").strip().split(",")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def is_env_true(env_var_name: str) -> bool:
|
|
234
|
+
"""Whether the given environment variable has a truthy value."""
|
|
235
|
+
return os.environ.get(env_var_name, "").lower().strip() in TRUE_STRINGS
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def is_env_not_false(env_var_name: str) -> bool:
|
|
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
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def load_environment(profiles: str = None, env=os.environ) -> list[str]:
|
|
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:
|
|
250
|
+
profiles = "default"
|
|
251
|
+
|
|
252
|
+
profiles = profiles.split(",")
|
|
253
|
+
environment = {}
|
|
254
|
+
import dotenv
|
|
255
|
+
|
|
256
|
+
for profile in profiles:
|
|
257
|
+
profile = profile.strip()
|
|
258
|
+
path = os.path.join(CONFIG_DIR, f"{profile}.env")
|
|
259
|
+
if not os.path.exists(path):
|
|
260
|
+
continue
|
|
261
|
+
environment.update(dotenv.dotenv_values(path))
|
|
262
|
+
|
|
263
|
+
for k, v in environment.items():
|
|
264
|
+
# we do not want to override the environment
|
|
265
|
+
if k not in env and v is not None:
|
|
266
|
+
env[k] = v
|
|
267
|
+
|
|
268
|
+
return profiles
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def is_persistence_enabled() -> bool:
|
|
272
|
+
return PERSISTENCE and dirs.data
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def is_linux() -> bool:
|
|
276
|
+
return platform.system() == "Linux"
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def is_macos() -> bool:
|
|
280
|
+
return platform.system() == "Darwin"
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def is_windows() -> bool:
|
|
284
|
+
return platform.system().lower() == "windows"
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def is_wsl() -> bool:
|
|
288
|
+
return platform.system().lower() == "linux" and os.environ.get("WSL_DISTRO_NAME") is not None
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def ping(host):
|
|
292
|
+
"""Returns True if the host responds to a ping request"""
|
|
293
|
+
is_in_windows = is_windows()
|
|
294
|
+
ping_opts = "-n 1 -w 2000" if is_in_windows else "-c 1 -W 2"
|
|
295
|
+
args = f"ping {ping_opts} {host}"
|
|
296
|
+
return (
|
|
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():
|
|
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:
|
|
310
|
+
return OVERRIDE_IN_DOCKER
|
|
311
|
+
|
|
312
|
+
# check some marker files that we create in our Dockerfiles
|
|
313
|
+
for path in [
|
|
314
|
+
"/usr/lib/localstack/.community-version",
|
|
315
|
+
"/usr/lib/localstack/.pro-version",
|
|
316
|
+
"/tmp/localstack/.marker",
|
|
317
|
+
]:
|
|
318
|
+
if os.path.isfile(path):
|
|
319
|
+
return True
|
|
320
|
+
|
|
321
|
+
# details: https://github.com/localstack/localstack/pull/4352
|
|
322
|
+
if os.path.exists("/.dockerenv"):
|
|
323
|
+
return True
|
|
324
|
+
if os.path.exists("/run/.containerenv"):
|
|
325
|
+
return True
|
|
326
|
+
|
|
327
|
+
if not os.path.exists("/proc/1/cgroup"):
|
|
328
|
+
return False
|
|
329
|
+
try:
|
|
330
|
+
if any(
|
|
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:
|
|
346
|
+
pass
|
|
347
|
+
with open("/proc/1/cgroup") as ifh:
|
|
348
|
+
content = ifh.read()
|
|
349
|
+
if "docker" in content or "buildkit" in content:
|
|
350
|
+
return True
|
|
351
|
+
os_hostname = socket.gethostname()
|
|
352
|
+
if os_hostname and os_hostname in content:
|
|
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:
|
|
358
|
+
with open("/proc/mounts") as infile:
|
|
359
|
+
for line in infile:
|
|
360
|
+
line = line.strip()
|
|
361
|
+
|
|
362
|
+
if not line:
|
|
363
|
+
continue
|
|
364
|
+
|
|
365
|
+
# skip comments
|
|
366
|
+
if line[0] == "#":
|
|
367
|
+
continue
|
|
368
|
+
|
|
369
|
+
# format (man 5 fstab)
|
|
370
|
+
# <spec> <mount point> <type> <options> <rest>...
|
|
371
|
+
parts = line.split()
|
|
372
|
+
if len(parts) < 4:
|
|
373
|
+
# badly formatted line
|
|
374
|
+
continue
|
|
375
|
+
|
|
376
|
+
mount_point = parts[1]
|
|
377
|
+
options = parts[3]
|
|
378
|
+
|
|
379
|
+
# only consider the root filesystem
|
|
380
|
+
if mount_point != "/":
|
|
381
|
+
continue
|
|
382
|
+
|
|
383
|
+
if "io.containerd" in options:
|
|
384
|
+
return True
|
|
385
|
+
|
|
386
|
+
except FileNotFoundError:
|
|
387
|
+
pass
|
|
388
|
+
|
|
389
|
+
return False
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# whether the `in_docker` check should always return True or False
|
|
393
|
+
OVERRIDE_IN_DOCKER = parse_boolean_env("OVERRIDE_IN_DOCKER")
|
|
394
|
+
|
|
395
|
+
is_in_docker = in_docker()
|
|
396
|
+
is_in_linux = is_linux()
|
|
397
|
+
is_in_macos = is_macos()
|
|
398
|
+
is_in_windows = is_windows()
|
|
399
|
+
is_in_wsl = is_wsl()
|
|
400
|
+
default_ip = "0.0.0.0" if is_in_docker else "127.0.0.1"
|
|
401
|
+
|
|
402
|
+
# CLI specific: the configuration profile to load
|
|
403
|
+
CONFIG_PROFILE = os.environ.get("CONFIG_PROFILE", "").strip()
|
|
404
|
+
|
|
405
|
+
# CLI specific: host configuration directory
|
|
406
|
+
CONFIG_DIR = os.environ.get("CONFIG_DIR", os.path.expanduser("~/.localstack"))
|
|
407
|
+
|
|
408
|
+
# keep this on top to populate the environment
|
|
409
|
+
try:
|
|
410
|
+
# CLI specific: the actually loaded configuration profile
|
|
411
|
+
LOADED_PROFILES = load_environment(CONFIG_PROFILE)
|
|
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()
|
|
418
|
+
|
|
419
|
+
# directory for persisting data (TODO: deprecated, simply use PERSISTENCE=1)
|
|
420
|
+
DATA_DIR = os.environ.get("DATA_DIR", "").strip()
|
|
421
|
+
|
|
422
|
+
# whether localstack should persist service state across localstack runs
|
|
423
|
+
PERSISTENCE = is_env_true("PERSISTENCE")
|
|
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()
|
|
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()
|
|
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)
|
|
433
|
+
|
|
434
|
+
# whether to clear config.dirs.tmp on startup and shutdown
|
|
435
|
+
CLEAR_TMP_FOLDER = is_env_not_false("CLEAR_TMP_FOLDER")
|
|
436
|
+
|
|
437
|
+
# folder for temporary files and data
|
|
438
|
+
TMP_FOLDER = os.path.join(tempfile.gettempdir(), "localstack")
|
|
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
|
|
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}"):
|
|
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")
|
|
449
|
+
DEBUG = is_env_true("DEBUG") or LS_LOG in TRACE_LOG_LEVELS
|
|
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")
|
|
454
|
+
|
|
455
|
+
# path to the lambda debug mode configuration file.
|
|
456
|
+
LAMBDA_DEBUG_MODE_CONFIG_PATH = os.environ.get("LAMBDA_DEBUG_MODE_CONFIG_PATH")
|
|
457
|
+
|
|
458
|
+
# EXPERIMENTAL: allow setting custom log levels for individual loggers
|
|
459
|
+
LOG_LEVEL_OVERRIDES = os.environ.get("LOG_LEVEL_OVERRIDES", "")
|
|
460
|
+
|
|
461
|
+
# whether to enable debugpy
|
|
462
|
+
DEVELOP = is_env_true("DEVELOP")
|
|
463
|
+
|
|
464
|
+
# PORT FOR DEBUGGER
|
|
465
|
+
DEVELOP_PORT = int(os.environ.get("DEVELOP_PORT", "").strip() or DEFAULT_DEVELOP_PORT)
|
|
466
|
+
|
|
467
|
+
# whether to make debugpy wait for a debbuger client
|
|
468
|
+
WAIT_FOR_DEBUGGER = is_env_true("WAIT_FOR_DEBUGGER")
|
|
469
|
+
|
|
470
|
+
# whether to assume http or https for `get_protocol`
|
|
471
|
+
USE_SSL = is_env_true("USE_SSL")
|
|
472
|
+
|
|
473
|
+
# Whether to report internal failures as 500 or 501 errors.
|
|
474
|
+
FAIL_FAST = is_env_true("FAIL_FAST")
|
|
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")
|
|
479
|
+
|
|
480
|
+
# default encoding used to convert strings to byte arrays (mainly for Python 3 compatibility)
|
|
481
|
+
DEFAULT_ENCODING = "utf-8"
|
|
482
|
+
|
|
483
|
+
# path to local Docker UNIX domain socket
|
|
484
|
+
DOCKER_SOCK = os.environ.get("DOCKER_SOCK", "").strip() or "/var/run/docker.sock"
|
|
485
|
+
|
|
486
|
+
# additional flags to pass to "docker run" when starting the stack in Docker
|
|
487
|
+
DOCKER_FLAGS = os.environ.get("DOCKER_FLAGS", "").strip()
|
|
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"
|
|
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")
|
|
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()
|
|
497
|
+
|
|
498
|
+
# global prefix to prepend to Docker image names (e.g., for using a custom registry mirror)
|
|
499
|
+
DOCKER_GLOBAL_IMAGE_PREFIX = os.environ.get("DOCKER_GLOBAL_IMAGE_PREFIX", "").strip()
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def is_trace_logging_enabled():
|
|
503
|
+
if LS_LOG:
|
|
504
|
+
log_level = str(LS_LOG).upper()
|
|
505
|
+
return log_level.lower() in TRACE_LOG_LEVELS
|
|
506
|
+
return False
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
# set log levels immediately, but will be overwritten later by setup_logging
|
|
510
|
+
if DEBUG:
|
|
511
|
+
logging.getLogger("").setLevel(logging.DEBUG)
|
|
512
|
+
logging.getLogger("localstack").setLevel(logging.DEBUG)
|
|
513
|
+
|
|
514
|
+
LOG = logging.getLogger(__name__)
|
|
515
|
+
if is_trace_logging_enabled():
|
|
516
|
+
load_end_time = time.time()
|
|
517
|
+
LOG.debug(
|
|
518
|
+
"Initializing the configuration took %s ms", int((load_end_time - load_start_time) * 1000)
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def is_ipv6_address(host: str) -> bool:
|
|
523
|
+
"""
|
|
524
|
+
Returns True if the given host is an IPv6 address.
|
|
525
|
+
"""
|
|
526
|
+
|
|
527
|
+
if not host:
|
|
528
|
+
return False
|
|
529
|
+
|
|
530
|
+
try:
|
|
531
|
+
ipaddress.IPv6Address(host)
|
|
532
|
+
return True
|
|
533
|
+
except ipaddress.AddressValueError:
|
|
534
|
+
return False
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
class HostAndPort:
|
|
538
|
+
"""
|
|
539
|
+
Definition of an address for a server to listen to.
|
|
540
|
+
|
|
541
|
+
Includes a `parse` method to convert from `str`, allowing for default fallbacks, as well as
|
|
542
|
+
some helper methods to help tests - particularly testing for equality and a hash function
|
|
543
|
+
so that `HostAndPort` instances can be used as keys to dictionaries.
|
|
544
|
+
"""
|
|
545
|
+
|
|
546
|
+
host: str
|
|
547
|
+
port: int
|
|
548
|
+
|
|
549
|
+
def __init__(self, host: str, port: int):
|
|
550
|
+
self.host = host
|
|
551
|
+
self.port = port
|
|
552
|
+
|
|
553
|
+
@classmethod
|
|
554
|
+
def parse(
|
|
555
|
+
cls,
|
|
556
|
+
input: str,
|
|
557
|
+
default_host: str,
|
|
558
|
+
default_port: int,
|
|
559
|
+
) -> "HostAndPort":
|
|
560
|
+
"""
|
|
561
|
+
Parse a `HostAndPort` from strings like:
|
|
562
|
+
- 0.0.0.0:4566 -> host=0.0.0.0, port=4566
|
|
563
|
+
- 0.0.0.0 -> host=0.0.0.0, port=`default_port`
|
|
564
|
+
- :4566 -> host=`default_host`, port=4566
|
|
565
|
+
- [::]:4566 -> host=[::], port=4566
|
|
566
|
+
- [::1] -> host=[::1], port=`default_port`
|
|
567
|
+
"""
|
|
568
|
+
host, port = default_host, default_port
|
|
569
|
+
|
|
570
|
+
# recognize IPv6 addresses (+ port)
|
|
571
|
+
if input.startswith("["):
|
|
572
|
+
ipv6_pattern = re.compile(r"^\[(?P<host>[^]]+)\](:(?P<port>\d+))?$")
|
|
573
|
+
match = ipv6_pattern.match(input)
|
|
574
|
+
|
|
575
|
+
if match:
|
|
576
|
+
host = match.group("host")
|
|
577
|
+
if not is_ipv6_address(host):
|
|
578
|
+
raise ValueError(
|
|
579
|
+
f"input looks like an IPv6 address (is enclosed in square brackets), but is not valid: {host}"
|
|
580
|
+
)
|
|
581
|
+
port_s = match.group("port")
|
|
582
|
+
if port_s:
|
|
583
|
+
port = cls._validate_port(port_s)
|
|
584
|
+
else:
|
|
585
|
+
raise ValueError(
|
|
586
|
+
f'input looks like an IPv6 address, but is invalid. Should be formatted "[ip]:port": {input}'
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
# recognize IPv4 address + port
|
|
590
|
+
elif ":" in input:
|
|
591
|
+
hostname, port_s = input.split(":", 1)
|
|
592
|
+
if hostname.strip():
|
|
593
|
+
host = hostname.strip()
|
|
594
|
+
port = cls._validate_port(port_s)
|
|
595
|
+
else:
|
|
596
|
+
if input.strip():
|
|
597
|
+
host = input.strip()
|
|
598
|
+
|
|
599
|
+
# validation
|
|
600
|
+
if port < 0 or port >= 2**16:
|
|
601
|
+
raise ValueError("port out of range")
|
|
602
|
+
|
|
603
|
+
return cls(host=host, port=port)
|
|
604
|
+
|
|
605
|
+
@classmethod
|
|
606
|
+
def _validate_port(cls, port_s: str) -> int:
|
|
607
|
+
try:
|
|
608
|
+
port = int(port_s)
|
|
609
|
+
except ValueError as e:
|
|
610
|
+
raise ValueError(f"specified port {port_s} not a number") from e
|
|
611
|
+
|
|
612
|
+
return port
|
|
613
|
+
|
|
614
|
+
def _get_unprivileged_port_range_start(self) -> int:
|
|
615
|
+
try:
|
|
616
|
+
with open("/proc/sys/net/ipv4/ip_unprivileged_port_start") as unprivileged_port_start:
|
|
617
|
+
port = unprivileged_port_start.read()
|
|
618
|
+
return int(port.strip())
|
|
619
|
+
except Exception:
|
|
620
|
+
return 1024
|
|
621
|
+
|
|
622
|
+
def is_unprivileged(self) -> bool:
|
|
623
|
+
return self.port >= self._get_unprivileged_port_range_start()
|
|
624
|
+
|
|
625
|
+
def host_and_port(self) -> str:
|
|
626
|
+
formatted_host = f"[{self.host}]" if is_ipv6_address(self.host) else self.host
|
|
627
|
+
return f"{formatted_host}:{self.port}" if self.port is not None else formatted_host
|
|
628
|
+
|
|
629
|
+
def __hash__(self) -> int:
|
|
630
|
+
return hash((self.host, self.port))
|
|
631
|
+
|
|
632
|
+
# easier tests
|
|
633
|
+
def __eq__(self, other: "str | HostAndPort") -> bool:
|
|
634
|
+
if isinstance(other, self.__class__):
|
|
635
|
+
return self.host == other.host and self.port == other.port
|
|
636
|
+
elif isinstance(other, str):
|
|
637
|
+
return str(self) == other
|
|
638
|
+
else:
|
|
639
|
+
raise TypeError(f"cannot compare {self.__class__} to {other.__class__}")
|
|
640
|
+
|
|
641
|
+
def __str__(self) -> str:
|
|
642
|
+
return self.host_and_port()
|
|
643
|
+
|
|
644
|
+
def __repr__(self) -> str:
|
|
645
|
+
return f"HostAndPort(host={self.host}, port={self.port})"
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
class UniqueHostAndPortList(list[HostAndPort]):
|
|
649
|
+
"""
|
|
650
|
+
Container type that ensures that ports added to the list are unique based
|
|
651
|
+
on these rules:
|
|
652
|
+
- :: "trumps" any other binding on the same port, including both IPv6 and IPv4
|
|
653
|
+
addresses. All other bindings for this port are removed, since :: already
|
|
654
|
+
covers all interfaces. For example, adding 127.0.0.1:4566, [::1]:4566,
|
|
655
|
+
and [::]:4566 would result in only [::]:4566 being preserved.
|
|
656
|
+
- 0.0.0.0 "trumps" any other binding on IPv4 addresses only. IPv6 addresses
|
|
657
|
+
are not removed.
|
|
658
|
+
- Identical identical hosts and ports are de-duped
|
|
659
|
+
"""
|
|
660
|
+
|
|
661
|
+
def __init__(self, iterable: list[HostAndPort] | None = None):
|
|
662
|
+
super().__init__(iterable or [])
|
|
663
|
+
self._ensure_unique()
|
|
664
|
+
|
|
665
|
+
def _ensure_unique(self):
|
|
666
|
+
"""
|
|
667
|
+
Ensure that all bindings on the same port are de-duped.
|
|
668
|
+
"""
|
|
669
|
+
if len(self) <= 1:
|
|
670
|
+
return
|
|
671
|
+
|
|
672
|
+
unique: list[HostAndPort] = []
|
|
673
|
+
|
|
674
|
+
# Build a dictionary of hosts by port
|
|
675
|
+
hosts_by_port: dict[int, list[str]] = defaultdict(list)
|
|
676
|
+
for item in self:
|
|
677
|
+
hosts_by_port[item.port].append(item.host)
|
|
678
|
+
|
|
679
|
+
# For any given port, dedupe the hosts
|
|
680
|
+
for port, hosts in hosts_by_port.items():
|
|
681
|
+
deduped_hosts = set(hosts)
|
|
682
|
+
|
|
683
|
+
# IPv6 all interfaces: this is the most general binding.
|
|
684
|
+
# Any others should be removed.
|
|
685
|
+
if "::" in deduped_hosts:
|
|
686
|
+
unique.append(HostAndPort(host="::", port=port))
|
|
687
|
+
continue
|
|
688
|
+
# IPv4 all interfaces: this is the next most general binding.
|
|
689
|
+
# Any others should be removed.
|
|
690
|
+
if "0.0.0.0" in deduped_hosts:
|
|
691
|
+
unique.append(HostAndPort(host="0.0.0.0", port=port))
|
|
692
|
+
continue
|
|
693
|
+
|
|
694
|
+
# All other bindings just need to be unique
|
|
695
|
+
unique.extend([HostAndPort(host=host, port=port) for host in deduped_hosts])
|
|
696
|
+
|
|
697
|
+
self.clear()
|
|
698
|
+
self.extend(unique)
|
|
699
|
+
|
|
700
|
+
def append(self, value: HostAndPort):
|
|
701
|
+
super().append(value)
|
|
702
|
+
self._ensure_unique()
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def populate_edge_configuration(
|
|
706
|
+
environment: Mapping[str, str],
|
|
707
|
+
) -> tuple[HostAndPort, UniqueHostAndPortList]:
|
|
708
|
+
"""Populate the LocalStack edge configuration from environment variables."""
|
|
709
|
+
localstack_host_raw = environment.get("LOCALSTACK_HOST")
|
|
710
|
+
gateway_listen_raw = environment.get("GATEWAY_LISTEN")
|
|
711
|
+
|
|
712
|
+
# parse gateway listen from multiple components
|
|
713
|
+
if gateway_listen_raw is not None:
|
|
714
|
+
gateway_listen = []
|
|
715
|
+
for address in gateway_listen_raw.split(","):
|
|
716
|
+
gateway_listen.append(
|
|
717
|
+
HostAndPort.parse(
|
|
718
|
+
address.strip(),
|
|
719
|
+
default_host=default_ip,
|
|
720
|
+
default_port=constants.DEFAULT_PORT_EDGE,
|
|
721
|
+
)
|
|
722
|
+
)
|
|
723
|
+
else:
|
|
724
|
+
# use default if gateway listen is not defined
|
|
725
|
+
gateway_listen = [HostAndPort(host=default_ip, port=constants.DEFAULT_PORT_EDGE)]
|
|
726
|
+
|
|
727
|
+
# the actual value of the LOCALSTACK_HOST port now depends on what gateway listen actually listens to.
|
|
728
|
+
if localstack_host_raw is None:
|
|
729
|
+
localstack_host = HostAndPort(
|
|
730
|
+
host=constants.LOCALHOST_HOSTNAME, port=gateway_listen[0].port
|
|
731
|
+
)
|
|
732
|
+
else:
|
|
733
|
+
localstack_host = HostAndPort.parse(
|
|
734
|
+
localstack_host_raw,
|
|
735
|
+
default_host=constants.LOCALHOST_HOSTNAME,
|
|
736
|
+
default_port=gateway_listen[0].port,
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
assert gateway_listen is not None
|
|
740
|
+
assert localstack_host is not None
|
|
741
|
+
|
|
742
|
+
return (
|
|
743
|
+
localstack_host,
|
|
744
|
+
UniqueHostAndPortList(gateway_listen),
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
# How to access LocalStack
|
|
749
|
+
(
|
|
750
|
+
# -- Cosmetic
|
|
751
|
+
LOCALSTACK_HOST,
|
|
752
|
+
# -- Edge configuration
|
|
753
|
+
# Main configuration of the listen address of the hypercorn proxy. Of the form
|
|
754
|
+
# <ip_address>:<port>(,<ip_address>:port>)*
|
|
755
|
+
GATEWAY_LISTEN,
|
|
756
|
+
) = populate_edge_configuration(os.environ)
|
|
757
|
+
|
|
758
|
+
GATEWAY_WORKER_COUNT = int(os.environ.get("GATEWAY_WORKER_COUNT") or 1000)
|
|
759
|
+
|
|
760
|
+
# the gateway server that should be used (supported: hypercorn, twisted dev: werkzeug)
|
|
761
|
+
GATEWAY_SERVER = os.environ.get("GATEWAY_SERVER", "").strip() or "twisted"
|
|
762
|
+
|
|
763
|
+
# IP of the docker bridge used to enable access between containers
|
|
764
|
+
DOCKER_BRIDGE_IP = os.environ.get("DOCKER_BRIDGE_IP", "").strip()
|
|
765
|
+
|
|
766
|
+
# Default timeout for Docker API calls sent by the Docker SDK client, in seconds.
|
|
767
|
+
DOCKER_SDK_DEFAULT_TIMEOUT_SECONDS = int(os.environ.get("DOCKER_SDK_DEFAULT_TIMEOUT_SECONDS") or 60)
|
|
768
|
+
|
|
769
|
+
# Default number of retries to connect to the Docker API by the Docker SDK client.
|
|
770
|
+
DOCKER_SDK_DEFAULT_RETRIES = int(os.environ.get("DOCKER_SDK_DEFAULT_RETRIES") or 0)
|
|
771
|
+
|
|
772
|
+
# whether to enable API-based updates of configuration variables at runtime
|
|
773
|
+
ENABLE_CONFIG_UPDATES = is_env_true("ENABLE_CONFIG_UPDATES")
|
|
774
|
+
|
|
775
|
+
# CORS settings
|
|
776
|
+
DISABLE_CORS_HEADERS = is_env_true("DISABLE_CORS_HEADERS")
|
|
777
|
+
DISABLE_CORS_CHECKS = is_env_true("DISABLE_CORS_CHECKS")
|
|
778
|
+
DISABLE_CUSTOM_CORS_S3 = is_env_true("DISABLE_CUSTOM_CORS_S3")
|
|
779
|
+
DISABLE_CUSTOM_CORS_APIGATEWAY = is_env_true("DISABLE_CUSTOM_CORS_APIGATEWAY")
|
|
780
|
+
EXTRA_CORS_ALLOWED_HEADERS = os.environ.get("EXTRA_CORS_ALLOWED_HEADERS", "").strip()
|
|
781
|
+
EXTRA_CORS_EXPOSE_HEADERS = os.environ.get("EXTRA_CORS_EXPOSE_HEADERS", "").strip()
|
|
782
|
+
EXTRA_CORS_ALLOWED_ORIGINS = os.environ.get("EXTRA_CORS_ALLOWED_ORIGINS", "").strip()
|
|
783
|
+
DISABLE_PREFLIGHT_PROCESSING = is_env_true("DISABLE_PREFLIGHT_PROCESSING")
|
|
784
|
+
|
|
785
|
+
# whether to disable publishing events to the API
|
|
786
|
+
DISABLE_EVENTS = is_env_true("DISABLE_EVENTS")
|
|
787
|
+
DEBUG_ANALYTICS = is_env_true("DEBUG_ANALYTICS")
|
|
788
|
+
|
|
789
|
+
# whether to log fine-grained debugging information for the handler chain
|
|
790
|
+
DEBUG_HANDLER_CHAIN = is_env_true("DEBUG_HANDLER_CHAIN")
|
|
791
|
+
|
|
792
|
+
# whether to eagerly start services
|
|
793
|
+
EAGER_SERVICE_LOADING = is_env_true("EAGER_SERVICE_LOADING")
|
|
794
|
+
|
|
795
|
+
# whether to selectively load services in SERVICES
|
|
796
|
+
STRICT_SERVICE_LOADING = is_env_not_false("STRICT_SERVICE_LOADING")
|
|
797
|
+
|
|
798
|
+
# Whether to skip downloading additional infrastructure components (e.g., custom Elasticsearch versions)
|
|
799
|
+
SKIP_INFRA_DOWNLOADS = os.environ.get("SKIP_INFRA_DOWNLOADS", "").strip()
|
|
800
|
+
|
|
801
|
+
# Whether to skip downloading our signed SSL cert.
|
|
802
|
+
SKIP_SSL_CERT_DOWNLOAD = is_env_true("SKIP_SSL_CERT_DOWNLOAD")
|
|
803
|
+
|
|
804
|
+
# Absolute path to a custom certificate (pem file)
|
|
805
|
+
CUSTOM_SSL_CERT_PATH = os.environ.get("CUSTOM_SSL_CERT_PATH", "").strip()
|
|
806
|
+
|
|
807
|
+
# Whether delete the cached signed SSL certificate at startup
|
|
808
|
+
REMOVE_SSL_CERT = is_env_true("REMOVE_SSL_CERT")
|
|
809
|
+
|
|
810
|
+
# Allow non-standard AWS regions
|
|
811
|
+
ALLOW_NONSTANDARD_REGIONS = is_env_true("ALLOW_NONSTANDARD_REGIONS")
|
|
812
|
+
if ALLOW_NONSTANDARD_REGIONS:
|
|
813
|
+
os.environ["MOTO_ALLOW_NONEXISTENT_REGION"] = "true"
|
|
814
|
+
|
|
815
|
+
# name of the main Docker container
|
|
816
|
+
MAIN_CONTAINER_NAME = os.environ.get("MAIN_CONTAINER_NAME", "").strip() or "localstack-main"
|
|
817
|
+
|
|
818
|
+
# the latest commit id of the repository when the docker image was created
|
|
819
|
+
LOCALSTACK_BUILD_GIT_HASH = os.environ.get("LOCALSTACK_BUILD_GIT_HASH", "").strip() or None
|
|
820
|
+
|
|
821
|
+
# the date on which the docker image was created
|
|
822
|
+
LOCALSTACK_BUILD_DATE = os.environ.get("LOCALSTACK_BUILD_DATE", "").strip() or None
|
|
823
|
+
|
|
824
|
+
# Equivalent to HTTP_PROXY, but only applicable for external connections
|
|
825
|
+
OUTBOUND_HTTP_PROXY = os.environ.get("OUTBOUND_HTTP_PROXY", "")
|
|
826
|
+
|
|
827
|
+
# Equivalent to HTTPS_PROXY, but only applicable for external connections
|
|
828
|
+
OUTBOUND_HTTPS_PROXY = os.environ.get("OUTBOUND_HTTPS_PROXY", "")
|
|
829
|
+
|
|
830
|
+
# Feature flag to enable validation of internal endpoint responses in the handler chain. For test use only.
|
|
831
|
+
OPENAPI_VALIDATE_RESPONSE = is_env_true("OPENAPI_VALIDATE_RESPONSE")
|
|
832
|
+
# Flag to enable the validation of the requests made to the LocalStack internal endpoints. Active by default.
|
|
833
|
+
OPENAPI_VALIDATE_REQUEST = is_env_true("OPENAPI_VALIDATE_REQUEST")
|
|
834
|
+
|
|
835
|
+
# environment variable to determine whether to include stack traces in http responses
|
|
836
|
+
INCLUDE_STACK_TRACES_IN_HTTP_RESPONSE = is_env_true("INCLUDE_STACK_TRACES_IN_HTTP_RESPONSE")
|
|
837
|
+
|
|
838
|
+
# whether to skip waiting for the infrastructure to shut down, or exit immediately
|
|
839
|
+
FORCE_SHUTDOWN = is_env_not_false("FORCE_SHUTDOWN")
|
|
840
|
+
|
|
841
|
+
# set variables no_proxy, i.e., run internal service calls directly
|
|
842
|
+
no_proxy = ",".join([constants.LOCALHOST_HOSTNAME, LOCALHOST, LOCALHOST_IP, "[::1]"])
|
|
843
|
+
if os.environ.get("no_proxy"):
|
|
844
|
+
os.environ["no_proxy"] += "," + no_proxy
|
|
845
|
+
elif os.environ.get("NO_PROXY"):
|
|
846
|
+
os.environ["NO_PROXY"] += "," + no_proxy
|
|
847
|
+
else:
|
|
848
|
+
os.environ["no_proxy"] = no_proxy
|
|
849
|
+
|
|
850
|
+
# additional CLI commands, can be set by plugins
|
|
851
|
+
CLI_COMMANDS = {}
|
|
852
|
+
|
|
853
|
+
# determine IP of Docker bridge
|
|
854
|
+
if not DOCKER_BRIDGE_IP:
|
|
855
|
+
DOCKER_BRIDGE_IP = "172.17.0.1"
|
|
856
|
+
if is_in_docker:
|
|
857
|
+
candidates = (DOCKER_BRIDGE_IP, "172.18.0.1")
|
|
858
|
+
for ip in candidates:
|
|
859
|
+
# TODO: remove from here - should not perform I/O operations in top-level config.py
|
|
860
|
+
if ping(ip):
|
|
861
|
+
DOCKER_BRIDGE_IP = ip
|
|
862
|
+
break
|
|
863
|
+
|
|
864
|
+
# AWS account used to store internal resources such as Lambda archives or internal SQS queues.
|
|
865
|
+
# It should not be modified by the user, or visible to him, except as through a presigned url with the
|
|
866
|
+
# get-function call.
|
|
867
|
+
INTERNAL_RESOURCE_ACCOUNT = os.environ.get("INTERNAL_RESOURCE_ACCOUNT") or "949334387222"
|
|
868
|
+
|
|
869
|
+
# TODO: remove with 4.1.0
|
|
870
|
+
# Determine which implementation to use for the event rule / event filtering engine used by multiple services:
|
|
871
|
+
# EventBridge, EventBridge Pipes, Lambda Event Source Mapping
|
|
872
|
+
# Options: python (default) | java (deprecated since 4.0.3)
|
|
873
|
+
EVENT_RULE_ENGINE = os.environ.get("EVENT_RULE_ENGINE", "python").strip()
|
|
874
|
+
|
|
875
|
+
# -----
|
|
876
|
+
# SERVICE-SPECIFIC CONFIGS BELOW
|
|
877
|
+
# -----
|
|
878
|
+
|
|
879
|
+
# port ranges for external service instances (f.e. elasticsearch clusters, opensearch clusters,...)
|
|
880
|
+
EXTERNAL_SERVICE_PORTS_START = int(
|
|
881
|
+
os.environ.get("EXTERNAL_SERVICE_PORTS_START")
|
|
882
|
+
or os.environ.get("SERVICE_INSTANCES_PORTS_START")
|
|
883
|
+
or 4510
|
|
884
|
+
)
|
|
885
|
+
EXTERNAL_SERVICE_PORTS_END = int(
|
|
886
|
+
os.environ.get("EXTERNAL_SERVICE_PORTS_END")
|
|
887
|
+
or os.environ.get("SERVICE_INSTANCES_PORTS_END")
|
|
888
|
+
or (EXTERNAL_SERVICE_PORTS_START + 50)
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
# The default container runtime to use
|
|
892
|
+
CONTAINER_RUNTIME = os.environ.get("CONTAINER_RUNTIME", "").strip() or "docker"
|
|
893
|
+
|
|
894
|
+
# PUBLIC v1: -Xmx512M (example) Currently not supported in new provider but possible via custom entrypoint.
|
|
895
|
+
# Allow passing custom JVM options to Java Lambdas executed in Docker.
|
|
896
|
+
LAMBDA_JAVA_OPTS = os.environ.get("LAMBDA_JAVA_OPTS", "").strip()
|
|
897
|
+
|
|
898
|
+
# limit in which to kinesis-mock will start throwing exceptions
|
|
899
|
+
KINESIS_SHARD_LIMIT = os.environ.get("KINESIS_SHARD_LIMIT", "").strip() or "100"
|
|
900
|
+
KINESIS_PERSISTENCE = is_env_not_false("KINESIS_PERSISTENCE")
|
|
901
|
+
|
|
902
|
+
# limit in which to kinesis-mock will start throwing exceptions
|
|
903
|
+
KINESIS_ON_DEMAND_STREAM_COUNT_LIMIT = (
|
|
904
|
+
os.environ.get("KINESIS_ON_DEMAND_STREAM_COUNT_LIMIT", "").strip() or "10"
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
# delay in kinesis-mock response when making changes to streams
|
|
908
|
+
KINESIS_LATENCY = os.environ.get("KINESIS_LATENCY", "").strip() or "500"
|
|
909
|
+
|
|
910
|
+
# Delay between data persistence (in seconds)
|
|
911
|
+
KINESIS_MOCK_PERSIST_INTERVAL = os.environ.get("KINESIS_MOCK_PERSIST_INTERVAL", "").strip() or "5s"
|
|
912
|
+
|
|
913
|
+
# Kinesis mock log level override when inconsistent with LS_LOG (e.g., when LS_LOG=debug)
|
|
914
|
+
KINESIS_MOCK_LOG_LEVEL = os.environ.get("KINESIS_MOCK_LOG_LEVEL", "").strip()
|
|
915
|
+
|
|
916
|
+
# randomly inject faults to Kinesis
|
|
917
|
+
KINESIS_ERROR_PROBABILITY = float(os.environ.get("KINESIS_ERROR_PROBABILITY", "").strip() or 0.0)
|
|
918
|
+
|
|
919
|
+
# SEMI-PUBLIC: "node" (default); not actively communicated
|
|
920
|
+
# Select whether to use the node or scala build when running Kinesis Mock
|
|
921
|
+
KINESIS_MOCK_PROVIDER_ENGINE = os.environ.get("KINESIS_MOCK_PROVIDER_ENGINE", "").strip() or "node"
|
|
922
|
+
|
|
923
|
+
# set the maximum Java heap size corresponding to the '-Xmx<size>' flag
|
|
924
|
+
KINESIS_MOCK_MAXIMUM_HEAP_SIZE = (
|
|
925
|
+
os.environ.get("KINESIS_MOCK_MAXIMUM_HEAP_SIZE", "").strip() or "512m"
|
|
926
|
+
)
|
|
927
|
+
|
|
928
|
+
# set the initial Java heap size corresponding to the '-Xms<size>' flag
|
|
929
|
+
KINESIS_MOCK_INITIAL_HEAP_SIZE = (
|
|
930
|
+
os.environ.get("KINESIS_MOCK_INITIAL_HEAP_SIZE", "").strip() or "256m"
|
|
931
|
+
)
|
|
932
|
+
|
|
933
|
+
# randomly inject faults to DynamoDB
|
|
934
|
+
DYNAMODB_ERROR_PROBABILITY = float(os.environ.get("DYNAMODB_ERROR_PROBABILITY", "").strip() or 0.0)
|
|
935
|
+
DYNAMODB_READ_ERROR_PROBABILITY = float(
|
|
936
|
+
os.environ.get("DYNAMODB_READ_ERROR_PROBABILITY", "").strip() or 0.0
|
|
937
|
+
)
|
|
938
|
+
DYNAMODB_WRITE_ERROR_PROBABILITY = float(
|
|
939
|
+
os.environ.get("DYNAMODB_WRITE_ERROR_PROBABILITY", "").strip() or 0.0
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
# JAVA EE heap size for dynamodb
|
|
943
|
+
DYNAMODB_HEAP_SIZE = os.environ.get("DYNAMODB_HEAP_SIZE", "").strip() or "256m"
|
|
944
|
+
|
|
945
|
+
# single DB instance across multiple credentials are regions
|
|
946
|
+
DYNAMODB_SHARE_DB = int(os.environ.get("DYNAMODB_SHARE_DB") or 0)
|
|
947
|
+
|
|
948
|
+
# the port on which to expose dynamodblocal
|
|
949
|
+
DYNAMODB_LOCAL_PORT = int(os.environ.get("DYNAMODB_LOCAL_PORT") or 0)
|
|
950
|
+
|
|
951
|
+
# Enables the automatic removal of stale KV pais based on TTL
|
|
952
|
+
DYNAMODB_REMOVE_EXPIRED_ITEMS = is_env_true("DYNAMODB_REMOVE_EXPIRED_ITEMS")
|
|
953
|
+
|
|
954
|
+
# Used to toggle PurgeInProgress exceptions when calling purge within 60 seconds
|
|
955
|
+
SQS_DELAY_PURGE_RETRY = is_env_true("SQS_DELAY_PURGE_RETRY")
|
|
956
|
+
|
|
957
|
+
# Used to toggle QueueDeletedRecently errors when re-creating a queue within 60 seconds of deleting it
|
|
958
|
+
SQS_DELAY_RECENTLY_DELETED = is_env_true("SQS_DELAY_RECENTLY_DELETED")
|
|
959
|
+
|
|
960
|
+
# Used to toggle MessageRetentionPeriod functionality in SQS queues
|
|
961
|
+
SQS_ENABLE_MESSAGE_RETENTION_PERIOD = is_env_true("SQS_ENABLE_MESSAGE_RETENTION_PERIOD")
|
|
962
|
+
|
|
963
|
+
# Strategy used when creating SQS queue urls. can be "off", "standard" (default), "domain", "path", or "dynamic"
|
|
964
|
+
SQS_ENDPOINT_STRATEGY = os.environ.get("SQS_ENDPOINT_STRATEGY", "") or "standard"
|
|
965
|
+
|
|
966
|
+
# Disable the check for MaxNumberOfMessage in SQS ReceiveMessage
|
|
967
|
+
SQS_DISABLE_MAX_NUMBER_OF_MESSAGE_LIMIT = is_env_true("SQS_DISABLE_MAX_NUMBER_OF_MESSAGE_LIMIT")
|
|
968
|
+
|
|
969
|
+
# Disable cloudwatch metrics for SQS
|
|
970
|
+
SQS_DISABLE_CLOUDWATCH_METRICS = is_env_true("SQS_DISABLE_CLOUDWATCH_METRICS")
|
|
971
|
+
|
|
972
|
+
# Interval for reporting "approximate" metrics to cloudwatch, default is 60 seconds
|
|
973
|
+
SQS_CLOUDWATCH_METRICS_REPORT_INTERVAL = int(
|
|
974
|
+
os.environ.get("SQS_CLOUDWATCH_METRICS_REPORT_INTERVAL") or 60
|
|
975
|
+
)
|
|
976
|
+
|
|
977
|
+
# PUBLIC: Endpoint host under which LocalStack APIs are accessible from Lambda Docker containers.
|
|
978
|
+
HOSTNAME_FROM_LAMBDA = os.environ.get("HOSTNAME_FROM_LAMBDA", "").strip()
|
|
979
|
+
|
|
980
|
+
# PUBLIC: hot-reload (default v2), __local__ (default v1)
|
|
981
|
+
# Magic S3 bucket name for Hot Reloading. The S3Key points to the source code on the local file system.
|
|
982
|
+
BUCKET_MARKER_LOCAL = (
|
|
983
|
+
os.environ.get("BUCKET_MARKER_LOCAL", "").strip() or DEFAULT_BUCKET_MARKER_LOCAL
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
# PUBLIC: Opt-out to inject the environment variable AWS_ENDPOINT_URL for automatic configuration of AWS SDKs:
|
|
987
|
+
# https://docs.aws.amazon.com/sdkref/latest/guide/feature-ss-endpoints.html
|
|
988
|
+
LAMBDA_DISABLE_AWS_ENDPOINT_URL = is_env_true("LAMBDA_DISABLE_AWS_ENDPOINT_URL")
|
|
989
|
+
|
|
990
|
+
# PUBLIC: bridge (Docker default)
|
|
991
|
+
# Docker network driver for the Lambda and ECS containers. https://docs.docker.com/network/
|
|
992
|
+
LAMBDA_DOCKER_NETWORK = os.environ.get("LAMBDA_DOCKER_NETWORK", "").strip()
|
|
993
|
+
|
|
994
|
+
# PUBLIC v1: LocalStack DNS (default)
|
|
995
|
+
# Custom DNS server for the container running your lambda function.
|
|
996
|
+
LAMBDA_DOCKER_DNS = os.environ.get("LAMBDA_DOCKER_DNS", "").strip()
|
|
997
|
+
|
|
998
|
+
# PUBLIC: -e KEY=VALUE -v host:container
|
|
999
|
+
# Additional flags passed to Docker run|create commands.
|
|
1000
|
+
LAMBDA_DOCKER_FLAGS = os.environ.get("LAMBDA_DOCKER_FLAGS", "").strip()
|
|
1001
|
+
|
|
1002
|
+
# PUBLIC: 0 (default)
|
|
1003
|
+
# Enable this flag to run cross-platform compatible lambda functions natively (i.e., Docker selects architecture) and
|
|
1004
|
+
# ignore the AWS architectures (i.e., x86_64, arm64) configured for the lambda function.
|
|
1005
|
+
LAMBDA_IGNORE_ARCHITECTURE = is_env_true("LAMBDA_IGNORE_ARCHITECTURE")
|
|
1006
|
+
|
|
1007
|
+
# TODO: test and add to docs
|
|
1008
|
+
# EXPERIMENTAL: 0 (default)
|
|
1009
|
+
# prebuild images before execution? Increased cold start time on the tradeoff of increased time until lambda is ACTIVE
|
|
1010
|
+
LAMBDA_PREBUILD_IMAGES = is_env_true("LAMBDA_PREBUILD_IMAGES")
|
|
1011
|
+
|
|
1012
|
+
# PUBLIC: docker (default), kubernetes (pro)
|
|
1013
|
+
# Where Lambdas will be executed.
|
|
1014
|
+
LAMBDA_RUNTIME_EXECUTOR = os.environ.get("LAMBDA_RUNTIME_EXECUTOR", CONTAINER_RUNTIME).strip()
|
|
1015
|
+
|
|
1016
|
+
# PUBLIC: 20 (default)
|
|
1017
|
+
# How many seconds Lambda will wait for the runtime environment to start up.
|
|
1018
|
+
LAMBDA_RUNTIME_ENVIRONMENT_TIMEOUT = int(os.environ.get("LAMBDA_RUNTIME_ENVIRONMENT_TIMEOUT") or 20)
|
|
1019
|
+
|
|
1020
|
+
# PUBLIC: base images for Lambda (default) https://docs.aws.amazon.com/lambda/latest/dg/runtimes-images.html
|
|
1021
|
+
# localstack/services/lambda_/invocation/lambda_models.py:IMAGE_MAPPING
|
|
1022
|
+
# Customize the Docker image of Lambda runtimes, either by:
|
|
1023
|
+
# a) pattern with <runtime> placeholder, e.g. custom-repo/lambda-<runtime>:2022
|
|
1024
|
+
# b) json dict mapping the <runtime> to an image, e.g. {"python3.9": "custom-repo/lambda-py:thon3.9"}
|
|
1025
|
+
LAMBDA_RUNTIME_IMAGE_MAPPING = os.environ.get("LAMBDA_RUNTIME_IMAGE_MAPPING", "").strip()
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
# PUBLIC: 0 (default)
|
|
1029
|
+
# Whether to disable usage of deprecated runtimes
|
|
1030
|
+
LAMBDA_RUNTIME_VALIDATION = int(os.environ.get("LAMBDA_RUNTIME_VALIDATION") or 0)
|
|
1031
|
+
|
|
1032
|
+
# PUBLIC: 1 (default)
|
|
1033
|
+
# Whether to remove any Lambda Docker containers.
|
|
1034
|
+
LAMBDA_REMOVE_CONTAINERS = (
|
|
1035
|
+
os.environ.get("LAMBDA_REMOVE_CONTAINERS", "").lower().strip() not in FALSE_STRINGS
|
|
1036
|
+
)
|
|
1037
|
+
|
|
1038
|
+
# PUBLIC: 600000 (default 10min)
|
|
1039
|
+
# Time in milliseconds until lambda shuts down the execution environment after the last invocation has been processed.
|
|
1040
|
+
# Set to 0 to immediately shut down the execution environment after an invocation.
|
|
1041
|
+
LAMBDA_KEEPALIVE_MS = int(os.environ.get("LAMBDA_KEEPALIVE_MS", 600_000))
|
|
1042
|
+
|
|
1043
|
+
# PUBLIC: 1000 (default)
|
|
1044
|
+
# The maximum number of events that functions can process simultaneously in the current Region.
|
|
1045
|
+
# See AWS service quotas: https://docs.aws.amazon.com/general/latest/gr/lambda-service.html
|
|
1046
|
+
# Concurrency limits. Like on AWS these apply per account and region.
|
|
1047
|
+
LAMBDA_LIMITS_CONCURRENT_EXECUTIONS = int(
|
|
1048
|
+
os.environ.get("LAMBDA_LIMITS_CONCURRENT_EXECUTIONS", 1_000)
|
|
1049
|
+
)
|
|
1050
|
+
# SEMI-PUBLIC: not actively communicated
|
|
1051
|
+
# per account/region: there must be at least <LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY> unreserved concurrency.
|
|
1052
|
+
LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY = int(
|
|
1053
|
+
os.environ.get("LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY", 100)
|
|
1054
|
+
)
|
|
1055
|
+
# SEMI-PUBLIC: not actively communicated
|
|
1056
|
+
LAMBDA_LIMITS_TOTAL_CODE_SIZE = int(os.environ.get("LAMBDA_LIMITS_TOTAL_CODE_SIZE", 80_530_636_800))
|
|
1057
|
+
# PUBLIC: documented after AWS changed validation around 2023-11
|
|
1058
|
+
LAMBDA_LIMITS_CODE_SIZE_ZIPPED = int(os.environ.get("LAMBDA_LIMITS_CODE_SIZE_ZIPPED", 52_428_800))
|
|
1059
|
+
# SEMI-PUBLIC: not actively communicated
|
|
1060
|
+
LAMBDA_LIMITS_CODE_SIZE_UNZIPPED = int(
|
|
1061
|
+
os.environ.get("LAMBDA_LIMITS_CODE_SIZE_UNZIPPED", 262_144_000)
|
|
1062
|
+
)
|
|
1063
|
+
# PUBLIC: documented upon customer request
|
|
1064
|
+
LAMBDA_LIMITS_CREATE_FUNCTION_REQUEST_SIZE = int(
|
|
1065
|
+
os.environ.get("LAMBDA_LIMITS_CREATE_FUNCTION_REQUEST_SIZE", 70_167_211)
|
|
1066
|
+
)
|
|
1067
|
+
# SEMI-PUBLIC: not actively communicated
|
|
1068
|
+
LAMBDA_LIMITS_MAX_FUNCTION_ENVVAR_SIZE_BYTES = int(
|
|
1069
|
+
os.environ.get("LAMBDA_LIMITS_MAX_FUNCTION_ENVVAR_SIZE_BYTES", 4 * 1024)
|
|
1070
|
+
)
|
|
1071
|
+
# SEMI-PUBLIC: not actively communicated
|
|
1072
|
+
LAMBDA_LIMITS_MAX_FUNCTION_PAYLOAD_SIZE_BYTES = int(
|
|
1073
|
+
os.environ.get(
|
|
1074
|
+
"LAMBDA_LIMITS_MAX_FUNCTION_PAYLOAD_SIZE_BYTES", 6 * 1024 * 1024 + 100
|
|
1075
|
+
) # the 100 comes from the init defaults
|
|
1076
|
+
)
|
|
1077
|
+
|
|
1078
|
+
# DEV: 0 (default unless in host mode on macOS) For LS developers only. Only applies to Docker mode.
|
|
1079
|
+
# Whether to explicitly expose a free TCP port in lambda containers when invoking functions in host mode for
|
|
1080
|
+
# systems that cannot reach the container via its IPv4. For example, macOS cannot reach Docker containers:
|
|
1081
|
+
# https://docs.docker.com/desktop/networking/#i-cannot-ping-my-containers
|
|
1082
|
+
LAMBDA_DEV_PORT_EXPOSE = (
|
|
1083
|
+
# Enable this dev flag by default on macOS in host mode (i.e., non-Docker environment)
|
|
1084
|
+
is_env_not_false("LAMBDA_DEV_PORT_EXPOSE")
|
|
1085
|
+
if not is_in_docker and is_in_macos
|
|
1086
|
+
else is_env_true("LAMBDA_DEV_PORT_EXPOSE")
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1089
|
+
# DEV: only applies to new lambda provider. All LAMBDA_INIT_* configuration are for LS developers only.
|
|
1090
|
+
# There are NO stability guarantees, and they may break at any time.
|
|
1091
|
+
|
|
1092
|
+
# DEV: Release version of https://github.com/localstack/lambda-runtime-init overriding the current default
|
|
1093
|
+
LAMBDA_INIT_RELEASE_VERSION = os.environ.get("LAMBDA_INIT_RELEASE_VERSION")
|
|
1094
|
+
# DEV: 0 (default) Enable for mounting of RIE init binary and delve debugger
|
|
1095
|
+
LAMBDA_INIT_DEBUG = is_env_true("LAMBDA_INIT_DEBUG")
|
|
1096
|
+
# DEV: path to RIE init binary (e.g., var/rapid/init)
|
|
1097
|
+
LAMBDA_INIT_BIN_PATH = os.environ.get("LAMBDA_INIT_BIN_PATH")
|
|
1098
|
+
# DEV: path to entrypoint script (e.g., var/rapid/entrypoint.sh)
|
|
1099
|
+
LAMBDA_INIT_BOOTSTRAP_PATH = os.environ.get("LAMBDA_INIT_BOOTSTRAP_PATH")
|
|
1100
|
+
# DEV: path to delve debugger (e.g., var/rapid/dlv)
|
|
1101
|
+
LAMBDA_INIT_DELVE_PATH = os.environ.get("LAMBDA_INIT_DELVE_PATH")
|
|
1102
|
+
# DEV: Go Delve debug port
|
|
1103
|
+
LAMBDA_INIT_DELVE_PORT = int(os.environ.get("LAMBDA_INIT_DELVE_PORT") or 40000)
|
|
1104
|
+
# DEV: Time to wait after every invoke as a workaround to fix a race condition in persistence tests
|
|
1105
|
+
LAMBDA_INIT_POST_INVOKE_WAIT_MS = os.environ.get("LAMBDA_INIT_POST_INVOKE_WAIT_MS")
|
|
1106
|
+
# DEV: sbx_user1051 (default when not provided) Alternative system user or empty string to skip dropping privileges.
|
|
1107
|
+
LAMBDA_INIT_USER = os.environ.get("LAMBDA_INIT_USER")
|
|
1108
|
+
|
|
1109
|
+
# INTERNAL: 1 (default)
|
|
1110
|
+
# The duration (in seconds) to wait between each poll call to an event source.
|
|
1111
|
+
LAMBDA_EVENT_SOURCE_MAPPING_POLL_INTERVAL_SEC = float(
|
|
1112
|
+
os.environ.get("LAMBDA_EVENT_SOURCE_MAPPING_POLL_INTERVAL_SEC") or 1
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
# INTERNAL: 60 (default)
|
|
1116
|
+
# Maximum duration (in seconds) to wait between retries when an event source poll fails.
|
|
1117
|
+
LAMBDA_EVENT_SOURCE_MAPPING_MAX_BACKOFF_ON_ERROR_SEC = float(
|
|
1118
|
+
os.environ.get("LAMBDA_EVENT_SOURCE_MAPPING_MAX_BACKOFF_ON_ERROR_SEC") or 60
|
|
1119
|
+
)
|
|
1120
|
+
|
|
1121
|
+
# INTERNAL: 10 (default)
|
|
1122
|
+
# Maximum duration (in seconds) to wait between polls when an event source returns empty results.
|
|
1123
|
+
LAMBDA_EVENT_SOURCE_MAPPING_MAX_BACKOFF_ON_EMPTY_POLL_SEC = float(
|
|
1124
|
+
os.environ.get("LAMBDA_EVENT_SOURCE_MAPPING_MAX_BACKOFF_ON_EMPTY_POLL_SEC") or 10
|
|
1125
|
+
)
|
|
1126
|
+
|
|
1127
|
+
# Specifies the path to the mock configuration file for Step Functions, commonly named MockConfigFile.json.
|
|
1128
|
+
SFN_MOCK_CONFIG = os.environ.get("SFN_MOCK_CONFIG", "").strip()
|
|
1129
|
+
|
|
1130
|
+
# path prefix for windows volume mounting
|
|
1131
|
+
WINDOWS_DOCKER_MOUNT_PREFIX = os.environ.get("WINDOWS_DOCKER_MOUNT_PREFIX", "/host_mnt")
|
|
1132
|
+
|
|
1133
|
+
# whether to skip S3 presign URL signature validation (TODO: currently enabled, until all issues are resolved)
|
|
1134
|
+
S3_SKIP_SIGNATURE_VALIDATION = is_env_not_false("S3_SKIP_SIGNATURE_VALIDATION")
|
|
1135
|
+
# whether to skip S3 validation of provided KMS key
|
|
1136
|
+
S3_SKIP_KMS_KEY_VALIDATION = is_env_not_false("S3_SKIP_KMS_KEY_VALIDATION")
|
|
1137
|
+
|
|
1138
|
+
# PUBLIC: 2000 (default)
|
|
1139
|
+
# Allows increasing the default char limit for truncation of lambda log lines when printed in the console.
|
|
1140
|
+
# This does not affect the logs processing in CloudWatch.
|
|
1141
|
+
LAMBDA_TRUNCATE_STDOUT = int(os.getenv("LAMBDA_TRUNCATE_STDOUT") or 2000)
|
|
1142
|
+
|
|
1143
|
+
# INTERNAL: 60 (default matching AWS) only applies to new lambda provider
|
|
1144
|
+
# Base delay in seconds for async retries. Further retries use: NUM_ATTEMPTS * LAMBDA_RETRY_BASE_DELAY_SECONDS
|
|
1145
|
+
# 300 (5min) is the maximum because NUM_ATTEMPTS can be at most 3 and SQS has a message timer limit of 15 min.
|
|
1146
|
+
# For example:
|
|
1147
|
+
# 1x LAMBDA_RETRY_BASE_DELAY_SECONDS: delay between initial invocation and first retry
|
|
1148
|
+
# 2x LAMBDA_RETRY_BASE_DELAY_SECONDS: delay between the first retry and the second retry
|
|
1149
|
+
# 3x LAMBDA_RETRY_BASE_DELAY_SECONDS: delay between the second retry and the third retry
|
|
1150
|
+
LAMBDA_RETRY_BASE_DELAY_SECONDS = int(os.getenv("LAMBDA_RETRY_BASE_DELAY") or 60)
|
|
1151
|
+
|
|
1152
|
+
# PUBLIC: 0 (default)
|
|
1153
|
+
# Set to 1 to create lambda functions synchronously (not recommended).
|
|
1154
|
+
# Whether Lambda.CreateFunction will block until the function is in a terminal state (Active or Failed).
|
|
1155
|
+
# This technically breaks behavior parity but is provided as a simplification over the default AWS behavior and
|
|
1156
|
+
# to match the behavior of the old lambda provider.
|
|
1157
|
+
LAMBDA_SYNCHRONOUS_CREATE = is_env_true("LAMBDA_SYNCHRONOUS_CREATE")
|
|
1158
|
+
|
|
1159
|
+
# URL to a custom OpenSearch/Elasticsearch backend cluster. If this is set to a valid URL, then localstack will not
|
|
1160
|
+
# create OpenSearch/Elasticsearch cluster instances, but instead forward all domains to the given backend.
|
|
1161
|
+
OPENSEARCH_CUSTOM_BACKEND = os.environ.get("OPENSEARCH_CUSTOM_BACKEND", "").strip()
|
|
1162
|
+
|
|
1163
|
+
# Strategy used when creating OpenSearch/Elasticsearch domain endpoints routed through the edge proxy
|
|
1164
|
+
# valid values: domain | path | port (off)
|
|
1165
|
+
OPENSEARCH_ENDPOINT_STRATEGY = (
|
|
1166
|
+
os.environ.get("OPENSEARCH_ENDPOINT_STRATEGY", "").strip() or "domain"
|
|
1167
|
+
)
|
|
1168
|
+
if OPENSEARCH_ENDPOINT_STRATEGY == "off":
|
|
1169
|
+
OPENSEARCH_ENDPOINT_STRATEGY = "port"
|
|
1170
|
+
|
|
1171
|
+
# Whether to start one cluster per domain (default), or multiplex opensearch domains to a single clusters
|
|
1172
|
+
OPENSEARCH_MULTI_CLUSTER = is_env_not_false("OPENSEARCH_MULTI_CLUSTER")
|
|
1173
|
+
|
|
1174
|
+
# Whether to really publish to GCM while using SNS Platform Application (needs credentials)
|
|
1175
|
+
LEGACY_SNS_GCM_PUBLISHING = is_env_true("LEGACY_SNS_GCM_PUBLISHING")
|
|
1176
|
+
|
|
1177
|
+
SNS_SES_SENDER_ADDRESS = os.environ.get("SNS_SES_SENDER_ADDRESS", "").strip()
|
|
1178
|
+
|
|
1179
|
+
SNS_CERT_URL_HOST = os.environ.get("SNS_CERT_URL_HOST", "").strip()
|
|
1180
|
+
|
|
1181
|
+
# Whether the Next Gen APIGW invocation logic is enabled (on by default)
|
|
1182
|
+
APIGW_NEXT_GEN_PROVIDER = os.environ.get("PROVIDER_OVERRIDE_APIGATEWAY", "") in ("next_gen", "")
|
|
1183
|
+
|
|
1184
|
+
# Whether the DynamoDBStreams native provider is enabled
|
|
1185
|
+
DDB_STREAMS_PROVIDER_V2 = os.environ.get("PROVIDER_OVERRIDE_DYNAMODBSTREAMS", "") == "v2"
|
|
1186
|
+
_override_dynamodb_v2 = os.environ.get("PROVIDER_OVERRIDE_DYNAMODB", "")
|
|
1187
|
+
if DDB_STREAMS_PROVIDER_V2:
|
|
1188
|
+
# in order to not have conflicts between the 2 implementations, as they are tightly coupled, we need to set DDB
|
|
1189
|
+
# to be v2 as well
|
|
1190
|
+
if not _override_dynamodb_v2:
|
|
1191
|
+
os.environ["PROVIDER_OVERRIDE_DYNAMODB"] = "v2"
|
|
1192
|
+
elif _override_dynamodb_v2 == "v2":
|
|
1193
|
+
os.environ["PROVIDER_OVERRIDE_DYNAMODBSTREAMS"] = "v2"
|
|
1194
|
+
DDB_STREAMS_PROVIDER_V2 = True
|
|
1195
|
+
|
|
1196
|
+
SNS_PROVIDER_V2 = os.environ.get("PROVIDER_OVERRIDE_SNS", "") == "v2"
|
|
1197
|
+
|
|
1198
|
+
# TODO remove fallback to LAMBDA_DOCKER_NETWORK with next minor version
|
|
1199
|
+
MAIN_DOCKER_NETWORK = os.environ.get("MAIN_DOCKER_NETWORK", "") or LAMBDA_DOCKER_NETWORK
|
|
1200
|
+
|
|
1201
|
+
# Whether to return and parse access key ids starting with an "A", like on AWS
|
|
1202
|
+
PARITY_AWS_ACCESS_KEY_ID = is_env_true("PARITY_AWS_ACCESS_KEY_ID")
|
|
1203
|
+
|
|
1204
|
+
# Show exceptions for CloudFormation deploy errors
|
|
1205
|
+
CFN_VERBOSE_ERRORS = is_env_true("CFN_VERBOSE_ERRORS")
|
|
1206
|
+
|
|
1207
|
+
# The CFN_STRING_REPLACEMENT_DENY_LIST env variable is a comma separated list of strings that are not allowed to be
|
|
1208
|
+
# replaced in CloudFormation templates (e.g. AWS URLs that are usually edited by Localstack to point to itself if found
|
|
1209
|
+
# in a CFN template). They are extracted to a list of strings if the env variable is set.
|
|
1210
|
+
CFN_STRING_REPLACEMENT_DENY_LIST = [
|
|
1211
|
+
x for x in os.environ.get("CFN_STRING_REPLACEMENT_DENY_LIST", "").split(",") if x
|
|
1212
|
+
]
|
|
1213
|
+
|
|
1214
|
+
# Set the timeout to deploy each individual CloudFormation resource
|
|
1215
|
+
CFN_PER_RESOURCE_TIMEOUT = int(os.environ.get("CFN_PER_RESOURCE_TIMEOUT") or 300)
|
|
1216
|
+
|
|
1217
|
+
# How localstack will react to encountering unsupported resource types.
|
|
1218
|
+
# By default unsupported resource types will be ignored.
|
|
1219
|
+
# EXPERIMENTAL
|
|
1220
|
+
CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES = is_env_not_false("CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES")
|
|
1221
|
+
|
|
1222
|
+
# Comma-separated list of resource type names that CloudFormation will ignore on stack creation
|
|
1223
|
+
CFN_IGNORE_UNSUPPORTED_TYPE_CREATE = parse_comma_separated_list(
|
|
1224
|
+
"CFN_IGNORE_UNSUPPORTED_TYPE_CREATE"
|
|
1225
|
+
)
|
|
1226
|
+
# Comma-separated list of resource type names that CloudFormation will ignore on stack update
|
|
1227
|
+
CFN_IGNORE_UNSUPPORTED_TYPE_UPDATE = parse_comma_separated_list(
|
|
1228
|
+
"CFN_IGNORE_UNSUPPORTED_TYPE_UPDATE"
|
|
1229
|
+
)
|
|
1230
|
+
|
|
1231
|
+
# Decrease the waiting time for resource deployment
|
|
1232
|
+
CFN_NO_WAIT_ITERATIONS: str | int | None = os.environ.get("CFN_NO_WAIT_ITERATIONS")
|
|
1233
|
+
|
|
1234
|
+
# bind address of local DNS server
|
|
1235
|
+
DNS_ADDRESS = os.environ.get("DNS_ADDRESS") or "0.0.0.0"
|
|
1236
|
+
# port of the local DNS server
|
|
1237
|
+
DNS_PORT = int(os.environ.get("DNS_PORT", "53"))
|
|
1238
|
+
|
|
1239
|
+
# Comma-separated list of regex patterns for DNS names to resolve locally.
|
|
1240
|
+
# Any DNS name not matched against any of the patterns on this whitelist
|
|
1241
|
+
# will resolve it to the real DNS entry, rather than the local one.
|
|
1242
|
+
DNS_NAME_PATTERNS_TO_RESOLVE_UPSTREAM = (
|
|
1243
|
+
os.environ.get("DNS_NAME_PATTERNS_TO_RESOLVE_UPSTREAM") or ""
|
|
1244
|
+
).strip()
|
|
1245
|
+
DNS_LOCAL_NAME_PATTERNS = (os.environ.get("DNS_LOCAL_NAME_PATTERNS") or "").strip() # deprecated
|
|
1246
|
+
|
|
1247
|
+
# IP address that AWS endpoints should resolve to in our local DNS server. By default,
|
|
1248
|
+
# hostnames resolve to 127.0.0.1, which allows to use the LocalStack APIs transparently
|
|
1249
|
+
# from the host machine. If your code is running in Docker, this should be configured
|
|
1250
|
+
# to resolve to the Docker bridge network address, e.g., DNS_RESOLVE_IP=172.17.0.1
|
|
1251
|
+
DNS_RESOLVE_IP = os.environ.get("DNS_RESOLVE_IP") or LOCALHOST_IP
|
|
1252
|
+
|
|
1253
|
+
# fallback DNS server to send upstream requests to
|
|
1254
|
+
DNS_SERVER = os.environ.get("DNS_SERVER")
|
|
1255
|
+
DNS_VERIFICATION_DOMAIN = os.environ.get("DNS_VERIFICATION_DOMAIN") or "localstack.cloud"
|
|
1256
|
+
|
|
1257
|
+
|
|
1258
|
+
def use_custom_dns():
|
|
1259
|
+
return str(DNS_ADDRESS) not in FALSE_STRINGS
|
|
1260
|
+
|
|
1261
|
+
|
|
1262
|
+
# s3 virtual host name
|
|
1263
|
+
S3_VIRTUAL_HOSTNAME = f"s3.{LOCALSTACK_HOST.host}"
|
|
1264
|
+
S3_STATIC_WEBSITE_HOSTNAME = f"s3-website.{LOCALSTACK_HOST.host}"
|
|
1265
|
+
|
|
1266
|
+
BOTO_WAITER_DELAY = int(os.environ.get("BOTO_WAITER_DELAY") or "1")
|
|
1267
|
+
BOTO_WAITER_MAX_ATTEMPTS = int(os.environ.get("BOTO_WAITER_MAX_ATTEMPTS") or "120")
|
|
1268
|
+
DISABLE_CUSTOM_BOTO_WAITER_CONFIG = is_env_true("DISABLE_CUSTOM_BOTO_WAITER_CONFIG")
|
|
1269
|
+
|
|
1270
|
+
# defaults to false
|
|
1271
|
+
# if `DISABLE_BOTO_RETRIES=1` is set, all our created boto clients will have retries disabled
|
|
1272
|
+
DISABLE_BOTO_RETRIES = is_env_true("DISABLE_BOTO_RETRIES")
|
|
1273
|
+
|
|
1274
|
+
DISTRIBUTED_MODE = is_env_true("DISTRIBUTED_MODE")
|
|
1275
|
+
|
|
1276
|
+
# This flag enables `connect_to` to be in-memory only and not do networking calls
|
|
1277
|
+
IN_MEMORY_CLIENT = is_env_true("IN_MEMORY_CLIENT")
|
|
1278
|
+
|
|
1279
|
+
# This flag enables all responses from LocalStack to contain a `x-localstack` HTTP header.
|
|
1280
|
+
LOCALSTACK_RESPONSE_HEADER_ENABLED = is_env_not_false("LOCALSTACK_RESPONSE_HEADER_ENABLED")
|
|
1281
|
+
|
|
1282
|
+
# Serialization backend for the LocalStack internal state (`dill` is used by default`).
|
|
1283
|
+
STATE_SERIALIZATION_BACKEND = os.environ.get("STATE_SERIALIZATION_BACKEND", "").strip() or "dill"
|
|
1284
|
+
|
|
1285
|
+
# List of environment variable names used for configuration that are passed from the host into the LocalStack container.
|
|
1286
|
+
# => Synchronize this list with the above and the configuration docs:
|
|
1287
|
+
# https://docs.localstack.cloud/references/configuration/
|
|
1288
|
+
# => Sort this list alphabetically
|
|
1289
|
+
# => Add deprecated environment variables to deprecations.py and add a comment in this list
|
|
1290
|
+
# => Move removed legacy variables to the section grouped by release (still relevant for deprecation warnings)
|
|
1291
|
+
# => Do *not* include any internal developer configurations that apply to host-mode only in this list.
|
|
1292
|
+
CONFIG_ENV_VARS = [
|
|
1293
|
+
"ALLOW_NONSTANDARD_REGIONS",
|
|
1294
|
+
"BOTO_WAITER_DELAY",
|
|
1295
|
+
"BOTO_WAITER_MAX_ATTEMPTS",
|
|
1296
|
+
"BUCKET_MARKER_LOCAL",
|
|
1297
|
+
"CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES",
|
|
1298
|
+
"CFN_PER_RESOURCE_TIMEOUT",
|
|
1299
|
+
"CFN_STRING_REPLACEMENT_DENY_LIST",
|
|
1300
|
+
"CFN_VERBOSE_ERRORS",
|
|
1301
|
+
"CI",
|
|
1302
|
+
"CONTAINER_RUNTIME",
|
|
1303
|
+
"CUSTOM_SSL_CERT_PATH",
|
|
1304
|
+
"DEBUG",
|
|
1305
|
+
"DEBUG_HANDLER_CHAIN",
|
|
1306
|
+
"DEVELOP",
|
|
1307
|
+
"DEVELOP_PORT",
|
|
1308
|
+
"DISABLE_BOTO_RETRIES",
|
|
1309
|
+
"DISABLE_CORS_CHECKS",
|
|
1310
|
+
"DISABLE_CORS_HEADERS",
|
|
1311
|
+
"DISABLE_CUSTOM_BOTO_WAITER_CONFIG",
|
|
1312
|
+
"DISABLE_CUSTOM_CORS_APIGATEWAY",
|
|
1313
|
+
"DISABLE_CUSTOM_CORS_S3",
|
|
1314
|
+
"DISABLE_EVENTS",
|
|
1315
|
+
"DISTRIBUTED_MODE",
|
|
1316
|
+
"DNS_ADDRESS",
|
|
1317
|
+
"DNS_PORT",
|
|
1318
|
+
"DNS_LOCAL_NAME_PATTERNS",
|
|
1319
|
+
"DNS_NAME_PATTERNS_TO_RESOLVE_UPSTREAM",
|
|
1320
|
+
"DNS_RESOLVE_IP",
|
|
1321
|
+
"DNS_SERVER",
|
|
1322
|
+
"DNS_VERIFICATION_DOMAIN",
|
|
1323
|
+
"DOCKER_BRIDGE_IP",
|
|
1324
|
+
"DOCKER_SDK_DEFAULT_TIMEOUT_SECONDS",
|
|
1325
|
+
"DYNAMODB_ERROR_PROBABILITY",
|
|
1326
|
+
"DYNAMODB_HEAP_SIZE",
|
|
1327
|
+
"DYNAMODB_IN_MEMORY",
|
|
1328
|
+
"DYNAMODB_LOCAL_PORT",
|
|
1329
|
+
"DYNAMODB_SHARE_DB",
|
|
1330
|
+
"DYNAMODB_READ_ERROR_PROBABILITY",
|
|
1331
|
+
"DYNAMODB_REMOVE_EXPIRED_ITEMS",
|
|
1332
|
+
"DYNAMODB_WRITE_ERROR_PROBABILITY",
|
|
1333
|
+
"EAGER_SERVICE_LOADING",
|
|
1334
|
+
"ENABLE_CONFIG_UPDATES",
|
|
1335
|
+
"EVENT_RULE_ENGINE",
|
|
1336
|
+
"EXTRA_CORS_ALLOWED_HEADERS",
|
|
1337
|
+
"EXTRA_CORS_ALLOWED_ORIGINS",
|
|
1338
|
+
"EXTRA_CORS_EXPOSE_HEADERS",
|
|
1339
|
+
"GATEWAY_LISTEN",
|
|
1340
|
+
"GATEWAY_SERVER",
|
|
1341
|
+
"GATEWAY_WORKER_THREAD_COUNT",
|
|
1342
|
+
"HOSTNAME",
|
|
1343
|
+
"HOSTNAME_FROM_LAMBDA",
|
|
1344
|
+
"IN_MEMORY_CLIENT",
|
|
1345
|
+
"KINESIS_ERROR_PROBABILITY",
|
|
1346
|
+
"KINESIS_MOCK_PERSIST_INTERVAL",
|
|
1347
|
+
"KINESIS_MOCK_LOG_LEVEL",
|
|
1348
|
+
"KINESIS_ON_DEMAND_STREAM_COUNT_LIMIT",
|
|
1349
|
+
"KINESIS_PERSISTENCE",
|
|
1350
|
+
"LAMBDA_DEBUG_MODE",
|
|
1351
|
+
"LAMBDA_DEBUG_MODE_CONFIG",
|
|
1352
|
+
"LAMBDA_DISABLE_AWS_ENDPOINT_URL",
|
|
1353
|
+
"LAMBDA_DOCKER_DNS",
|
|
1354
|
+
"LAMBDA_DOCKER_FLAGS",
|
|
1355
|
+
"LAMBDA_DOCKER_NETWORK",
|
|
1356
|
+
"LAMBDA_EVENTS_INTERNAL_SQS",
|
|
1357
|
+
"LAMBDA_EVENT_SOURCE_MAPPING",
|
|
1358
|
+
"LAMBDA_IGNORE_ARCHITECTURE",
|
|
1359
|
+
"LAMBDA_INIT_DEBUG",
|
|
1360
|
+
"LAMBDA_INIT_BIN_PATH",
|
|
1361
|
+
"LAMBDA_INIT_BOOTSTRAP_PATH",
|
|
1362
|
+
"LAMBDA_INIT_DELVE_PATH",
|
|
1363
|
+
"LAMBDA_INIT_DELVE_PORT",
|
|
1364
|
+
"LAMBDA_INIT_POST_INVOKE_WAIT_MS",
|
|
1365
|
+
"LAMBDA_INIT_USER",
|
|
1366
|
+
"LAMBDA_INIT_RELEASE_VERSION",
|
|
1367
|
+
"LAMBDA_KEEPALIVE_MS",
|
|
1368
|
+
"LAMBDA_LIMITS_CONCURRENT_EXECUTIONS",
|
|
1369
|
+
"LAMBDA_LIMITS_MINIMUM_UNRESERVED_CONCURRENCY",
|
|
1370
|
+
"LAMBDA_LIMITS_TOTAL_CODE_SIZE",
|
|
1371
|
+
"LAMBDA_LIMITS_CODE_SIZE_ZIPPED",
|
|
1372
|
+
"LAMBDA_LIMITS_CODE_SIZE_UNZIPPED",
|
|
1373
|
+
"LAMBDA_LIMITS_CREATE_FUNCTION_REQUEST_SIZE",
|
|
1374
|
+
"LAMBDA_LIMITS_MAX_FUNCTION_ENVVAR_SIZE_BYTES",
|
|
1375
|
+
"LAMBDA_LIMITS_MAX_FUNCTION_PAYLOAD_SIZE_BYTES",
|
|
1376
|
+
"LAMBDA_PREBUILD_IMAGES",
|
|
1377
|
+
"LAMBDA_RUNTIME_IMAGE_MAPPING",
|
|
1378
|
+
"LAMBDA_REMOVE_CONTAINERS",
|
|
1379
|
+
"LAMBDA_RETRY_BASE_DELAY_SECONDS",
|
|
1380
|
+
"LAMBDA_RUNTIME_EXECUTOR",
|
|
1381
|
+
"LAMBDA_RUNTIME_ENVIRONMENT_TIMEOUT",
|
|
1382
|
+
"LAMBDA_RUNTIME_VALIDATION",
|
|
1383
|
+
"LAMBDA_SYNCHRONOUS_CREATE",
|
|
1384
|
+
"LAMBDA_SQS_EVENT_SOURCE_MAPPING_INTERVAL",
|
|
1385
|
+
"LAMBDA_TRUNCATE_STDOUT",
|
|
1386
|
+
"LEGACY_DOCKER_CLIENT",
|
|
1387
|
+
"LEGACY_SNS_GCM_PUBLISHING",
|
|
1388
|
+
"LOCALSTACK_API_KEY",
|
|
1389
|
+
"LOCALSTACK_AUTH_TOKEN",
|
|
1390
|
+
"LOCALSTACK_HOST",
|
|
1391
|
+
"LOCALSTACK_RESPONSE_HEADER_ENABLED",
|
|
1392
|
+
"LOG_LICENSE_ISSUES",
|
|
1393
|
+
"LS_LOG",
|
|
1394
|
+
"MAIN_CONTAINER_NAME",
|
|
1395
|
+
"MAIN_DOCKER_NETWORK",
|
|
1396
|
+
"OPENAPI_VALIDATE_REQUEST",
|
|
1397
|
+
"OPENAPI_VALIDATE_RESPONSE",
|
|
1398
|
+
"OPENSEARCH_ENDPOINT_STRATEGY",
|
|
1399
|
+
"OUTBOUND_HTTP_PROXY",
|
|
1400
|
+
"OUTBOUND_HTTPS_PROXY",
|
|
1401
|
+
"PARITY_AWS_ACCESS_KEY_ID",
|
|
1402
|
+
"PERSISTENCE",
|
|
1403
|
+
"PORTS_CHECK_DOCKER_IMAGE",
|
|
1404
|
+
"REQUESTS_CA_BUNDLE",
|
|
1405
|
+
"REMOVE_SSL_CERT",
|
|
1406
|
+
"S3_SKIP_SIGNATURE_VALIDATION",
|
|
1407
|
+
"S3_SKIP_KMS_KEY_VALIDATION",
|
|
1408
|
+
"SERVICES",
|
|
1409
|
+
"SKIP_INFRA_DOWNLOADS",
|
|
1410
|
+
"SKIP_SSL_CERT_DOWNLOAD",
|
|
1411
|
+
"SNAPSHOT_LOAD_STRATEGY",
|
|
1412
|
+
"SNAPSHOT_SAVE_STRATEGY",
|
|
1413
|
+
"SNAPSHOT_FLUSH_INTERVAL",
|
|
1414
|
+
"SNS_SES_SENDER_ADDRESS",
|
|
1415
|
+
"SQS_DELAY_PURGE_RETRY",
|
|
1416
|
+
"SQS_DELAY_RECENTLY_DELETED",
|
|
1417
|
+
"SQS_ENABLE_MESSAGE_RETENTION_PERIOD",
|
|
1418
|
+
"SQS_ENDPOINT_STRATEGY",
|
|
1419
|
+
"SQS_DISABLE_CLOUDWATCH_METRICS",
|
|
1420
|
+
"SQS_CLOUDWATCH_METRICS_REPORT_INTERVAL",
|
|
1421
|
+
"STATE_SERIALIZATION_BACKEND",
|
|
1422
|
+
"STRICT_SERVICE_LOADING",
|
|
1423
|
+
"TF_COMPAT_MODE",
|
|
1424
|
+
"USE_SSL",
|
|
1425
|
+
"WAIT_FOR_DEBUGGER",
|
|
1426
|
+
"WINDOWS_DOCKER_MOUNT_PREFIX",
|
|
1427
|
+
# Removed legacy variables in 2.0.0
|
|
1428
|
+
# DATA_DIR => do *not* include in this list, as it is treated separately. # deprecated since 1.0.0
|
|
1429
|
+
"LEGACY_DIRECTORIES", # deprecated since 1.0.0
|
|
1430
|
+
"SYNCHRONOUS_API_GATEWAY_EVENTS", # deprecated since 1.3.0
|
|
1431
|
+
"SYNCHRONOUS_DYNAMODB_EVENTS", # deprecated since 1.3.0
|
|
1432
|
+
"SYNCHRONOUS_SNS_EVENTS", # deprecated since 1.3.0
|
|
1433
|
+
"SYNCHRONOUS_SQS_EVENTS", # deprecated since 1.3.0
|
|
1434
|
+
# Removed legacy variables in 3.0.0
|
|
1435
|
+
"DEFAULT_REGION", # deprecated since 0.12.7
|
|
1436
|
+
"EDGE_BIND_HOST", # deprecated since 2.0.0
|
|
1437
|
+
"EDGE_FORWARD_URL", # deprecated since 1.4.0
|
|
1438
|
+
"EDGE_PORT", # deprecated since 2.0.0
|
|
1439
|
+
"EDGE_PORT_HTTP", # deprecated since 2.0.0
|
|
1440
|
+
"ES_CUSTOM_BACKEND", # deprecated since 0.14.0
|
|
1441
|
+
"ES_ENDPOINT_STRATEGY", # deprecated since 0.14.0
|
|
1442
|
+
"ES_MULTI_CLUSTER", # deprecated since 0.14.0
|
|
1443
|
+
"HOSTNAME_EXTERNAL", # deprecated since 2.0.0
|
|
1444
|
+
"KINESIS_INITIALIZE_STREAMS", # deprecated since 1.4.0
|
|
1445
|
+
"KINESIS_PROVIDER", # deprecated since 1.3.0
|
|
1446
|
+
"KMS_PROVIDER", # deprecated since 1.4.0
|
|
1447
|
+
"LAMBDA_XRAY_INIT", # deprecated since 2.0.0
|
|
1448
|
+
"LAMBDA_CODE_EXTRACT_TIME", # deprecated since 2.0.0
|
|
1449
|
+
"LAMBDA_CONTAINER_REGISTRY", # deprecated since 2.0.0
|
|
1450
|
+
"LAMBDA_EXECUTOR", # deprecated since 2.0.0
|
|
1451
|
+
"LAMBDA_FALLBACK_URL", # deprecated since 2.0.0
|
|
1452
|
+
"LAMBDA_FORWARD_URL", # deprecated since 2.0.0
|
|
1453
|
+
"LAMBDA_JAVA_OPTS", # currently only supported in old Lambda provider but not officially deprecated
|
|
1454
|
+
"LAMBDA_REMOTE_DOCKER", # deprecated since 2.0.0
|
|
1455
|
+
"LAMBDA_STAY_OPEN_MODE", # deprecated since 2.0.0
|
|
1456
|
+
"LEGACY_EDGE_PROXY", # deprecated since 1.0.0
|
|
1457
|
+
"LOCALSTACK_HOSTNAME", # deprecated since 2.0.0
|
|
1458
|
+
"SQS_PORT_EXTERNAL", # deprecated only in docs since 2022-07-13
|
|
1459
|
+
"SYNCHRONOUS_KINESIS_EVENTS", # deprecated since 1.3.0
|
|
1460
|
+
"USE_SINGLE_REGION", # deprecated since 0.12.7
|
|
1461
|
+
"MOCK_UNIMPLEMENTED", # deprecated since 1.3.0
|
|
1462
|
+
]
|
|
1463
|
+
|
|
1464
|
+
|
|
1465
|
+
def is_local_test_mode() -> bool:
|
|
1466
|
+
"""Returns True if we are running in the context of our local integration tests."""
|
|
1467
|
+
return is_env_true(ENV_INTERNAL_TEST_RUN)
|
|
1468
|
+
|
|
1469
|
+
|
|
1470
|
+
def is_collect_metrics_mode() -> bool:
|
|
1471
|
+
"""Returns True if metric collection is enabled."""
|
|
1472
|
+
return is_env_true(ENV_INTERNAL_TEST_COLLECT_METRIC)
|
|
1473
|
+
|
|
1474
|
+
|
|
1475
|
+
def store_test_metrics_in_local_filesystem() -> bool:
|
|
1476
|
+
"""Returns True if test metrics should be stored in the local filesystem (instead of the system that runs pytest)."""
|
|
1477
|
+
return is_env_true(ENV_INTERNAL_TEST_STORE_METRICS_IN_LOCALSTACK)
|
|
1478
|
+
|
|
1479
|
+
|
|
1480
|
+
def collect_config_items() -> list[tuple[str, Any]]:
|
|
1481
|
+
"""Returns a list of key-value tuples of LocalStack configuration values."""
|
|
1482
|
+
none = object() # sentinel object
|
|
1483
|
+
|
|
1484
|
+
# collect which keys to print
|
|
1485
|
+
keys = []
|
|
1486
|
+
keys.extend(CONFIG_ENV_VARS)
|
|
1487
|
+
keys.append("DATA_DIR")
|
|
1488
|
+
keys.sort()
|
|
1489
|
+
|
|
1490
|
+
values = globals()
|
|
1491
|
+
|
|
1492
|
+
result = []
|
|
1493
|
+
for k in keys:
|
|
1494
|
+
v = values.get(k, none)
|
|
1495
|
+
if v is none:
|
|
1496
|
+
continue
|
|
1497
|
+
result.append((k, v))
|
|
1498
|
+
result.sort()
|
|
1499
|
+
return result
|
|
1500
|
+
|
|
1501
|
+
|
|
1502
|
+
def populate_config_env_var_names():
|
|
1503
|
+
global CONFIG_ENV_VARS
|
|
1504
|
+
|
|
1505
|
+
CONFIG_ENV_VARS += [
|
|
1506
|
+
key
|
|
1507
|
+
for key in [key.upper() for key in os.environ]
|
|
1508
|
+
if (key.startswith("LOCALSTACK_") or key.startswith("PROVIDER_OVERRIDE_"))
|
|
1509
|
+
# explicitly exclude LOCALSTACK_CLI (it's prefixed with "LOCALSTACK_",
|
|
1510
|
+
# but is only used in the CLI (should not be forwarded to the container)
|
|
1511
|
+
and key != "LOCALSTACK_CLI"
|
|
1512
|
+
]
|
|
1513
|
+
|
|
1514
|
+
# create variable aliases prefixed with LOCALSTACK_ (except LOCALSTACK_HOST)
|
|
1515
|
+
CONFIG_ENV_VARS += [
|
|
1516
|
+
"LOCALSTACK_" + v for v in CONFIG_ENV_VARS if not v.startswith("LOCALSTACK_")
|
|
1517
|
+
]
|
|
1518
|
+
|
|
1519
|
+
CONFIG_ENV_VARS = list(set(CONFIG_ENV_VARS))
|
|
1520
|
+
|
|
1521
|
+
|
|
1522
|
+
# populate env var names to be passed to the container
|
|
1523
|
+
populate_config_env_var_names()
|
|
1524
|
+
|
|
1525
|
+
|
|
1526
|
+
# helpers to build urls
|
|
1527
|
+
def get_protocol() -> str:
|
|
1528
|
+
return "https" if USE_SSL else "http"
|
|
1529
|
+
|
|
1530
|
+
|
|
1531
|
+
def external_service_url(
|
|
1532
|
+
host: str | None = None,
|
|
1533
|
+
port: int | None = None,
|
|
1534
|
+
protocol: str | None = None,
|
|
1535
|
+
subdomains: str | None = None,
|
|
1536
|
+
) -> str:
|
|
1537
|
+
"""Returns a service URL (e.g., SQS queue URL) to an external client (e.g., boto3) potentially running on another
|
|
1538
|
+
machine than LocalStack. The configurations LOCALSTACK_HOST and USE_SSL can customize these returned URLs.
|
|
1539
|
+
The optional parameters can be used to customize the defaults.
|
|
1540
|
+
Examples with default configuration:
|
|
1541
|
+
* external_service_url() == http://localhost.localstack.cloud:4566
|
|
1542
|
+
* external_service_url(subdomains="s3") == http://s3.localhost.localstack.cloud:4566
|
|
1543
|
+
"""
|
|
1544
|
+
protocol = protocol or get_protocol()
|
|
1545
|
+
subdomains = f"{subdomains}." if subdomains else ""
|
|
1546
|
+
host = host or LOCALSTACK_HOST.host
|
|
1547
|
+
port = port or LOCALSTACK_HOST.port
|
|
1548
|
+
return f"{protocol}://{subdomains}{host}:{port}"
|
|
1549
|
+
|
|
1550
|
+
|
|
1551
|
+
def internal_service_url(
|
|
1552
|
+
host: str | None = None,
|
|
1553
|
+
port: int | None = None,
|
|
1554
|
+
protocol: str | None = None,
|
|
1555
|
+
subdomains: str | None = None,
|
|
1556
|
+
) -> str:
|
|
1557
|
+
"""Returns a service URL for internal use within LocalStack (i.e., same host).
|
|
1558
|
+
The configuration USE_SSL can customize these returned URLs but LOCALSTACK_HOST has no effect.
|
|
1559
|
+
The optional parameters can be used to customize the defaults.
|
|
1560
|
+
Examples with default configuration:
|
|
1561
|
+
* internal_service_url() == http://localhost:4566
|
|
1562
|
+
* internal_service_url(port=8080) == http://localhost:8080
|
|
1563
|
+
"""
|
|
1564
|
+
protocol = protocol or get_protocol()
|
|
1565
|
+
subdomains = f"{subdomains}." if subdomains else ""
|
|
1566
|
+
host = host or LOCALHOST
|
|
1567
|
+
port = port or GATEWAY_LISTEN[0].port
|
|
1568
|
+
return f"{protocol}://{subdomains}{host}:{port}"
|
|
1569
|
+
|
|
1570
|
+
|
|
1571
|
+
# DEPRECATED: old helpers for building URLs
|
|
1572
|
+
|
|
1573
|
+
|
|
1574
|
+
def service_url(service_key, host=None, port=None):
|
|
1575
|
+
"""@deprecated: Use `internal_service_url()` instead. We assume that most usages are internal
|
|
1576
|
+
but really need to check and update each usage accordingly.
|
|
1577
|
+
"""
|
|
1578
|
+
warnings.warn(
|
|
1579
|
+
"""@deprecated: Use `internal_service_url()` instead. We assume that most usages are
|
|
1580
|
+
internal but really need to check and update each usage accordingly.""",
|
|
1581
|
+
DeprecationWarning,
|
|
1582
|
+
stacklevel=2,
|
|
1583
|
+
)
|
|
1584
|
+
return internal_service_url(host=host, port=port)
|
|
1585
|
+
|
|
1586
|
+
|
|
1587
|
+
def service_port(service_key: str, external: bool = False) -> int:
|
|
1588
|
+
"""@deprecated: Use `localstack_host().port` for external and `GATEWAY_LISTEN[0].port` for
|
|
1589
|
+
internal use."""
|
|
1590
|
+
warnings.warn(
|
|
1591
|
+
"Deprecated: use `localstack_host().port` for external and `GATEWAY_LISTEN[0].port` for "
|
|
1592
|
+
"internal use.",
|
|
1593
|
+
DeprecationWarning,
|
|
1594
|
+
stacklevel=2,
|
|
1595
|
+
)
|
|
1596
|
+
if external:
|
|
1597
|
+
return LOCALSTACK_HOST.port
|
|
1598
|
+
return GATEWAY_LISTEN[0].port
|
|
1599
|
+
|
|
1600
|
+
|
|
1601
|
+
def get_edge_port_http():
|
|
1602
|
+
"""@deprecated: Use `localstack_host().port` for external and `GATEWAY_LISTEN[0].port` for
|
|
1603
|
+
internal use. This function is not needed anymore because we don't separate between HTTP
|
|
1604
|
+
and HTTP ports anymore since LocalStack listens to both ports."""
|
|
1605
|
+
warnings.warn(
|
|
1606
|
+
"""@deprecated: Use `localstack_host().port` for external and `GATEWAY_LISTEN[0].port`
|
|
1607
|
+
for internal use. This function is also not needed anymore because we don't separate
|
|
1608
|
+
between HTTP and HTTP ports anymore since LocalStack listens to both.""",
|
|
1609
|
+
DeprecationWarning,
|
|
1610
|
+
stacklevel=2,
|
|
1611
|
+
)
|
|
1612
|
+
return GATEWAY_LISTEN[0].port
|
|
1613
|
+
|
|
1614
|
+
|
|
1615
|
+
def get_edge_url(localstack_hostname=None, protocol=None):
|
|
1616
|
+
"""@deprecated: Use `internal_service_url()` instead.
|
|
1617
|
+
We assume that most usages are internal but really need to check and update each usage accordingly.
|
|
1618
|
+
"""
|
|
1619
|
+
warnings.warn(
|
|
1620
|
+
"""@deprecated: Use `internal_service_url()` instead.
|
|
1621
|
+
We assume that most usages are internal but really need to check and update each usage accordingly.
|
|
1622
|
+
""",
|
|
1623
|
+
DeprecationWarning,
|
|
1624
|
+
stacklevel=2,
|
|
1625
|
+
)
|
|
1626
|
+
return internal_service_url(host=localstack_hostname, protocol=protocol)
|
|
1627
|
+
|
|
1628
|
+
|
|
1629
|
+
class ServiceProviderConfig(Mapping[str, str]):
|
|
1630
|
+
_provider_config: dict[str, str]
|
|
1631
|
+
default_value: str
|
|
1632
|
+
override_prefix: str = "PROVIDER_OVERRIDE_"
|
|
1633
|
+
|
|
1634
|
+
def __init__(self, default_value: str):
|
|
1635
|
+
self._provider_config = {}
|
|
1636
|
+
self.default_value = default_value
|
|
1637
|
+
|
|
1638
|
+
def load_from_environment(self, env: Mapping[str, str] = None):
|
|
1639
|
+
if env is None:
|
|
1640
|
+
env = os.environ
|
|
1641
|
+
for key, value in env.items():
|
|
1642
|
+
if key.startswith(self.override_prefix) and value:
|
|
1643
|
+
self.set_provider(key[len(self.override_prefix) :].lower().replace("_", "-"), value)
|
|
1644
|
+
|
|
1645
|
+
def get_provider(self, service: str) -> str:
|
|
1646
|
+
return self._provider_config.get(service, self.default_value)
|
|
1647
|
+
|
|
1648
|
+
def set_provider_if_not_exists(self, service: str, provider: str) -> None:
|
|
1649
|
+
if service not in self._provider_config:
|
|
1650
|
+
self._provider_config[service] = provider
|
|
1651
|
+
|
|
1652
|
+
def set_provider(self, service: str, provider: str):
|
|
1653
|
+
self._provider_config[service] = provider
|
|
1654
|
+
|
|
1655
|
+
def bulk_set_provider_if_not_exists(self, services: list[str], provider: str):
|
|
1656
|
+
for service in services:
|
|
1657
|
+
self.set_provider_if_not_exists(service, provider)
|
|
1658
|
+
|
|
1659
|
+
def __getitem__(self, item):
|
|
1660
|
+
return self.get_provider(item)
|
|
1661
|
+
|
|
1662
|
+
def __setitem__(self, key, value):
|
|
1663
|
+
self.set_provider(key, value)
|
|
1664
|
+
|
|
1665
|
+
def __len__(self):
|
|
1666
|
+
return len(self._provider_config)
|
|
1667
|
+
|
|
1668
|
+
def __iter__(self):
|
|
1669
|
+
return self._provider_config.__iter__()
|
|
1670
|
+
|
|
1671
|
+
|
|
1672
|
+
SERVICE_PROVIDER_CONFIG = ServiceProviderConfig("default")
|
|
1673
|
+
|
|
1674
|
+
SERVICE_PROVIDER_CONFIG.load_from_environment()
|
|
1675
|
+
|
|
1676
|
+
|
|
1677
|
+
def init_directories() -> Directories:
|
|
1678
|
+
if is_in_docker:
|
|
1679
|
+
return Directories.for_container()
|
|
1680
|
+
else:
|
|
1681
|
+
if is_env_true("LOCALSTACK_CLI"):
|
|
1682
|
+
return Directories.for_cli()
|
|
1683
|
+
|
|
1684
|
+
return Directories.for_host()
|
|
1685
|
+
|
|
1686
|
+
|
|
1687
|
+
# initialize directories
|
|
1688
|
+
dirs: Directories
|
|
1689
|
+
dirs = init_directories()
|