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

localstack / localstack / 6523d139-4c8d-4daf-a514-97baaa202bd9

04 Jun 2025 04:30PM UTC coverage: 86.762% (-0.006%) from 86.768%
6523d139-4c8d-4daf-a514-97baaa202bd9

push

circleci

web-flow
test(esm/sqs): Skip flaky test_report_batch_item_failures test (#12713)

65076 of 75005 relevant lines covered (86.76%)

0.87 hits per line

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

44.54
/localstack-core/localstack/utils/bootstrap.py
1
from __future__ import annotations
1✔
2

3
import copy
1✔
4
import functools
1✔
5
import logging
1✔
6
import os
1✔
7
import re
1✔
8
import shlex
1✔
9
import signal
1✔
10
import threading
1✔
11
import time
1✔
12
from functools import wraps
1✔
13
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Union
1✔
14

15
from localstack import config, constants
1✔
16
from localstack.config import (
1✔
17
    HostAndPort,
18
    default_ip,
19
    is_env_not_false,
20
    load_environment,
21
)
22
from localstack.constants import VERSION
1✔
23
from localstack.runtime import hooks
1✔
24
from localstack.utils.container_networking import get_main_container_name
1✔
25
from localstack.utils.container_utils.container_client import (
1✔
26
    BindMount,
27
    CancellableStream,
28
    ContainerClient,
29
    ContainerConfiguration,
30
    ContainerConfigurator,
31
    ContainerException,
32
    NoSuchContainer,
33
    NoSuchImage,
34
    NoSuchNetwork,
35
    PortMappings,
36
    VolumeDirMount,
37
    VolumeMappings,
38
)
39
from localstack.utils.container_utils.docker_cmd_client import CmdDockerClient
1✔
40
from localstack.utils.docker_utils import DOCKER_CLIENT
1✔
41
from localstack.utils.files import cache_dir, mkdir
1✔
42
from localstack.utils.functions import call_safe
1✔
43
from localstack.utils.net import get_free_tcp_port, get_free_tcp_port_range
1✔
44
from localstack.utils.run import is_command_available, run, to_str
1✔
45
from localstack.utils.serving import Server
1✔
46
from localstack.utils.strings import short_uid
1✔
47
from localstack.utils.sync import poll_condition
1✔
48

49
LOG = logging.getLogger(__name__)
1✔
50

51
# Mandatory dependencies of services on other services
52
# - maps from API names to list of other API names that they _explicitly_ depend on: <service>:<dependent-services>
53
# - an explicit service dependency is a service without which another service's basic functionality breaks
54
# - this mapping is used when enabling strict service loading (use SERVICES env var to allow-list services)
55
# - do not add "optional" dependencies of services here, use API_DEPENDENCIES_OPTIONAL instead
56
API_DEPENDENCIES = {
1✔
57
    "dynamodb": ["dynamodbstreams"],
58
    # dynamodbstreams uses kinesis under the hood
59
    "dynamodbstreams": ["kinesis"],
60
    # es forwards all requests to opensearch (basically an API deprecation path in AWS)
61
    "es": ["opensearch"],
62
    "cloudformation": ["s3", "sts"],
63
    "lambda": ["s3", "sts"],
64
    # firehose currently only supports kinesis as source, this could become optional when more sources are supported
65
    "firehose": ["kinesis"],
66
    "transcribe": ["s3"],
67
    # secretsmanager uses lambda for rotation
68
    "secretsmanager": ["kms", "lambda"],
69
}
70

71
# Optional dependencies of services on other services
72
# - maps from API names to list of other API names that they _optionally_ depend on: <service>:<dependent-services>
73
# - an optional service dependency is a service without which a service's basic functionality doesn't break,
74
#   but which is needed for certain features (f.e. for one of multiple integrations)
75
# - this mapping is used f.e. used for the selective test execution (localstack.testing.testselection)
76
# - only add optional dependencies of services here, use API_DEPENDENCIES for mandatory dependencies
77
API_DEPENDENCIES_OPTIONAL = {
1✔
78
    # firehose's optional dependencies are supported delivery stream destinations
79
    "firehose": ["es", "opensearch", "s3", "redshift"],
80
    "lambda": [
81
        "cloudwatch",  # Lambda metrics
82
        "dynamodbstreams",  # Event source mapping source
83
        "events",  # Lambda destination
84
        "logs",  # Function logging
85
        "kinesis",  # Event source mapping source
86
        "sqs",  # Event source mapping source + Lambda destination
87
        "sns",  # Lambda destination
88
        "sts",  # Credentials injection
89
        # Additional dependencies to Pro-only services are defined in ext
90
    ],
91
    "ses": ["sns"],
92
    "sns": ["sqs", "lambda", "firehose", "ses", "logs"],
93
    "sqs": ["cloudwatch"],
94
    "logs": ["lambda", "kinesis", "firehose"],
95
    "cloudformation": ["secretsmanager", "ssm", "lambda"],
96
    "events": ["lambda", "kinesis", "firehose", "sns", "sqs", "stepfunctions", "logs"],
97
    "stepfunctions": ["logs", "lambda", "dynamodb", "ecs", "sns", "sqs", "apigateway", "events"],
98
    "apigateway": [
99
        "s3",
100
        "sqs",
101
        "sns",
102
        "kinesis",
103
        "route53",
104
        "servicediscovery",
105
        "lambda",
106
        "dynamodb",
107
        "stepfunctions",
108
        "events",
109
    ],
110
    # This is for S3 notifications and S3 KMS key
111
    "s3": ["events", "sqs", "sns", "lambda", "kms"],
112
    # IAM and STS are tightly coupled
113
    "sts": ["iam"],
114
    "iam": ["sts"],
115
}
116

117
# composites define an abstract name like "serverless" that maps to a set of services
118
API_COMPOSITES = {
1✔
119
    "serverless": [
120
        "cloudformation",
121
        "cloudwatch",
122
        "iam",
123
        "sts",
124
        "lambda",
125
        "dynamodb",
126
        "apigateway",
127
        "s3",
128
    ],
129
    "cognito": ["cognito-idp", "cognito-identity"],
130
    "timestream": ["timestream-write", "timestream-query"],
131
}
132

133

134
def log_duration(name=None, min_ms=500):
1✔
135
    """Function decorator to log the duration of function invocations."""
136

137
    def wrapper(f):
1✔
138
        @wraps(f)
1✔
139
        def wrapped(*args, **kwargs):
1✔
140
            from time import perf_counter
1✔
141

142
            start_time = perf_counter()
1✔
143
            try:
1✔
144
                return f(*args, **kwargs)
1✔
145
            finally:
146
                end_time = perf_counter()
1✔
147
                func_name = name or f.__name__
1✔
148
                duration = (end_time - start_time) * 1000
1✔
149
                if duration > min_ms:
1✔
150
                    LOG.info('Execution of "%s" took %.2fms', func_name, duration)
1✔
151

152
        return wrapped
1✔
153

154
    return wrapper
1✔
155

156

157
def get_docker_image_details(image_name: str = None) -> Dict[str, str]:
1✔
158
    image_name = image_name or get_docker_image_to_start()
1✔
159
    try:
1✔
160
        result = DOCKER_CLIENT.inspect_image(image_name)
1✔
161
    except ContainerException:
×
162
        return {}
×
163

164
    digests = result.get("RepoDigests")
1✔
165
    sha256 = digests[0].rpartition(":")[2] if digests else "Unavailable"
1✔
166
    result = {
1✔
167
        "id": result["Id"].replace("sha256:", "")[:12],
168
        "sha256": sha256,
169
        "tag": (result.get("RepoTags") or ["latest"])[0].split(":")[-1],
170
        "created": result["Created"].split(".")[0],
171
    }
172
    return result
1✔
173

174

175
def get_image_environment_variable(env_name: str) -> Optional[str]:
1✔
176
    image_name = get_docker_image_to_start()
×
177
    image_info = DOCKER_CLIENT.inspect_image(image_name)
×
178
    image_envs = image_info["Config"]["Env"]
×
179

180
    try:
×
181
        found_env = next(env for env in image_envs if env.startswith(env_name))
×
182
    except StopIteration:
×
183
        return None
×
184
    return found_env.split("=")[1]
×
185

186

187
def get_container_default_logfile_location(container_name: str) -> str:
1✔
188
    return os.path.join(config.dirs.mounted_tmp, f"{container_name}_container.log")
×
189

190

191
def get_server_version_from_running_container() -> str:
1✔
192
    try:
×
193
        # try to extract from existing running container
194
        container_name = get_main_container_name()
×
195
        version, _ = DOCKER_CLIENT.exec_in_container(
×
196
            container_name, interactive=True, command=["bin/localstack", "--version"]
197
        )
198
        version = to_str(version).strip().splitlines()[-1]
×
199
        return version
×
200
    except ContainerException:
×
201
        try:
×
202
            # try to extract by starting a new container
203
            img_name = get_docker_image_to_start()
×
204
            version, _ = DOCKER_CLIENT.run_container(
×
205
                img_name,
206
                remove=True,
207
                interactive=True,
208
                entrypoint="",
209
                command=["bin/localstack", "--version"],
210
            )
211
            version = to_str(version).strip().splitlines()[-1]
×
212
            return version
×
213
        except ContainerException:
×
214
            # fall back to default constant
215
            return VERSION
×
216

217

218
def get_server_version() -> str:
1✔
219
    image_hash = get_docker_image_details()["id"]
×
220
    version_cache = cache_dir() / "image_metadata" / image_hash / "localstack_version"
×
221
    if version_cache.exists():
×
222
        cached_version = version_cache.read_text()
×
223
        return cached_version.strip()
×
224

225
    env_version = get_image_environment_variable("LOCALSTACK_BUILD_VERSION")
×
226
    if env_version is not None:
×
227
        version_cache.parent.mkdir(exist_ok=True, parents=True)
×
228
        version_cache.write_text(env_version)
×
229
        return env_version
×
230

231
    container_version = get_server_version_from_running_container()
×
232
    version_cache.parent.mkdir(exist_ok=True, parents=True)
×
233
    version_cache.write_text(container_version)
×
234

235
    return container_version
×
236

237

238
def setup_logging():
1✔
239
    """Determine and set log level. The singleton factory makes sure the logging is only set up once."""
240
    from localstack.logging.setup import setup_logging_from_config
1✔
241

242
    setup_logging_from_config()
1✔
243

244

245
# --------------
246
# INFRA STARTUP
247
# --------------
248

249

250
def resolve_apis(services: Iterable[str]) -> Set[str]:
1✔
251
    """
252
    Resolves recursively for the given collection of services (e.g., ["serverless", "cognito"]) the list of actual
253
    API services that need to be included (e.g., {'dynamodb', 'cloudformation', 'logs', 'kinesis', 'sts',
254
    'cognito-identity', 's3', 'dynamodbstreams', 'apigateway', 'cloudwatch', 'lambda', 'cognito-idp', 'iam'}).
255

256
    More specifically, it does this by:
257
    (1) resolving and adding dependencies (e.g., "dynamodbstreams" requires "kinesis"),
258
    (2) resolving and adding composites (e.g., "serverless" describes an ensemble
259
            including "iam", "lambda", "dynamodb", "apigateway", "s3", "sns", and "logs"), and
260
    (3) removing duplicates from the list.
261

262
    :param services: a collection of services that can include composites (e.g., "serverless").
263
    :returns a set of canonical service names
264
    """
265
    stack = []
1✔
266
    result = set()
1✔
267

268
    # perform a graph search
269
    stack.extend(services)
1✔
270
    while stack:
1✔
271
        service = stack.pop()
1✔
272

273
        if service in result:
1✔
274
            continue
1✔
275

276
        # resolve composites (like "serverless"), but do not add it to the list of results
277
        if service in API_COMPOSITES:
1✔
278
            stack.extend(API_COMPOSITES[service])
1✔
279
            continue
1✔
280

281
        result.add(service)
1✔
282

283
        # add dependencies to stack
284
        if service in API_DEPENDENCIES:
1✔
285
            stack.extend(API_DEPENDENCIES[service])
1✔
286

287
    return result
1✔
288

289

290
@functools.lru_cache()
1✔
291
def get_enabled_apis() -> Set[str]:
1✔
292
    """
293
    Returns the list of APIs that are enabled through the combination of the SERVICES variable and
294
    STRICT_SERVICE_LOADING variable. If the SERVICES variable is empty, then it will return all available services.
295
    Meta-services like "serverless" or "cognito", and dependencies are resolved.
296

297
    The result is cached, so it's safe to call. Clear the cache with get_enabled_apis.cache_clear().
298
    """
299
    from localstack.services.plugins import SERVICE_PLUGINS
1✔
300

301
    services_env = os.environ.get("SERVICES", "").strip()
1✔
302
    services = SERVICE_PLUGINS.list_available()
1✔
303

304
    if services_env and is_env_not_false("STRICT_SERVICE_LOADING"):
1✔
305
        # SERVICES and STRICT_SERVICE_LOADING are set
306
        # we filter the result of SERVICE_PLUGINS.list_available() to cross the user-provided list with
307
        # the available ones
308
        enabled_services = []
1✔
309
        for service_port in re.split(r"\s*,\s*", services_env):
1✔
310
            # Only extract the service name, discard the port
311
            parts = re.split(r"[:=]", service_port)
1✔
312
            service = parts[0]
1✔
313
            enabled_services.append(service)
1✔
314

315
        services = [service for service in enabled_services if service in services]
1✔
316
        # TODO: log a message if a service was not supported? see with pro loading
317

318
    return resolve_apis(services)
1✔
319

320

321
def is_api_enabled(api: str) -> bool:
1✔
322
    return api in get_enabled_apis()
1✔
323

324

325
@functools.lru_cache()
1✔
326
def get_preloaded_services() -> Set[str]:
1✔
327
    """
328
    Returns the list of APIs that are marked to be eager loaded through the combination of SERVICES variable and
329
    EAGER_SERVICE_LOADING. If the SERVICES variable is empty, then it will return all available services.
330
    Meta-services like "serverless" or "cognito", and dependencies are resolved.
331

332
    The result is cached, so it's safe to call. Clear the cache with get_preloaded_services.cache_clear().
333
    """
334
    services_env = os.environ.get("SERVICES", "").strip()
1✔
335
    services = []
1✔
336

337
    if services_env:
1✔
338
        # SERVICES and EAGER_SERVICE_LOADING are set
339
        # SERVICES env var might contain ports, but we do not support these anymore
340
        for service_port in re.split(r"\s*,\s*", services_env):
1✔
341
            # Only extract the service name, discard the port
342
            parts = re.split(r"[:=]", service_port)
1✔
343
            service = parts[0]
1✔
344
            services.append(service)
1✔
345

346
    if not services:
1✔
347
        from localstack.services.plugins import SERVICE_PLUGINS
1✔
348

349
        services = SERVICE_PLUGINS.list_available()
1✔
350

351
    return resolve_apis(services)
1✔
352

353

354
def start_infra_locally():
1✔
355
    from localstack.runtime.main import main
×
356

357
    return main()
×
358

359

360
def validate_localstack_config(name: str):
1✔
361
    # TODO: separate functionality from CLI output
362
    #  (use exceptions to communicate errors, and return list of warnings)
363
    from subprocess import CalledProcessError
1✔
364

365
    from localstack.cli import console
1✔
366

367
    dirname = os.getcwd()
1✔
368
    compose_file_name = name if os.path.isabs(name) else os.path.join(dirname, name)
1✔
369
    warns = []
1✔
370

371
    # some systems do not have "docker-compose" aliased to "docker compose", and older systems do not have
372
    # "docker compose" at all. By preferring the old way and falling back on the new, we should get docker compose in
373
    # any way, if installed
374
    if is_command_available("docker-compose"):
1✔
375
        compose_command = ["docker-compose"]
1✔
376
    else:
377
        compose_command = ["docker", "compose"]
×
378
    # validating docker-compose file
379
    cmd = [*compose_command, "-f", compose_file_name, "config"]
1✔
380
    try:
1✔
381
        run(cmd, shell=False, print_error=False)
1✔
382
    except CalledProcessError as e:
1✔
383
        msg = f"{e}\n{to_str(e.output)}".strip()
1✔
384
        raise ValueError(msg)
1✔
385

386
    import yaml  # keep import here to avoid issues in test Lambdas
1✔
387

388
    # validating docker-compose variable
389
    with open(compose_file_name) as file:
1✔
390
        compose_content = yaml.full_load(file)
1✔
391
    services_config = compose_content.get("services", {})
1✔
392
    ls_service_name = [
1✔
393
        name for name, svc in services_config.items() if "localstack" in svc.get("image", "")
394
    ]
395
    if not ls_service_name:
1✔
396
        raise Exception(
×
397
            'No LocalStack service found in config (looking for image names containing "localstack")'
398
        )
399
    if len(ls_service_name) > 1:
1✔
400
        warns.append(f"Multiple candidates found for LocalStack service: {ls_service_name}")
×
401
    ls_service_name = ls_service_name[0]
1✔
402
    ls_service_details = services_config[ls_service_name]
1✔
403
    image_name = ls_service_details.get("image", "")
1✔
404
    if image_name.split(":")[0] not in constants.OFFICIAL_IMAGES:
1✔
405
        warns.append(
×
406
            f'Using custom image "{image_name}", we recommend using an official image: {constants.OFFICIAL_IMAGES}'
407
        )
408

409
    # prepare config options
410
    container_name = ls_service_details.get("container_name") or ""
1✔
411
    docker_ports = (port.split(":")[-2] for port in ls_service_details.get("ports", []))
1✔
412
    docker_env = {
1✔
413
        env.split("=")[0]: env.split("=")[1] for env in ls_service_details.get("environment", {})
414
    }
415
    edge_port = config.GATEWAY_LISTEN[0].port
1✔
416
    main_container = config.MAIN_CONTAINER_NAME
1✔
417

418
    # docker-compose file validation cases
419

420
    if (main_container not in container_name) and not docker_env.get("MAIN_CONTAINER_NAME"):
1✔
421
        warns.append(
×
422
            f'Please use "container_name: {main_container}" or add "MAIN_CONTAINER_NAME" in "environment".'
423
        )
424

425
    def port_exposed(port):
1✔
426
        for exposed in docker_ports:
1✔
427
            if re.match(r"^([0-9]+-)?%s(-[0-9]+)?$" % port, exposed):
1✔
428
                return True
1✔
429

430
    if not port_exposed(edge_port):
1✔
431
        warns.append(
×
432
            (
433
                f"Edge port {edge_port} is not exposed. You may have to add the entry "
434
                'to the "ports" section of the docker-compose file.'
435
            )
436
        )
437

438
    # print warning/info messages
439
    for warning in warns:
1✔
440
        console.print("[yellow]:warning:[/yellow]", warning)
×
441
    if not warns:
1✔
442
        return True
1✔
443
    return False
×
444

445

446
def get_docker_image_to_start():
1✔
447
    image_name = os.environ.get("IMAGE_NAME")
×
448
    if not image_name:
×
449
        image_name = constants.DOCKER_IMAGE_NAME
×
450
        if is_auth_token_configured():
×
451
            image_name = constants.DOCKER_IMAGE_NAME_PRO
×
452
    return image_name
×
453

454

455
def extract_port_flags(user_flags, port_mappings: PortMappings):
1✔
456
    regex = r"-p\s+([0-9]+)(\-([0-9]+))?:([0-9]+)(\-([0-9]+))?"
1✔
457
    matches = re.match(".*%s" % regex, user_flags)
1✔
458
    if matches:
1✔
459
        for match in re.findall(regex, user_flags):
1✔
460
            start = int(match[0])
1✔
461
            end = int(match[2] or match[0])
1✔
462
            start_target = int(match[3] or start)
1✔
463
            end_target = int(match[5] or end)
1✔
464
            port_mappings.add([start, end], [start_target, end_target])
1✔
465
        user_flags = re.sub(regex, r"", user_flags)
1✔
466
    return user_flags
1✔
467

468

469
class ContainerConfigurators:
1✔
470
    """
471
    A set of useful container configurators that are typical for starting the localstack container.
472
    """
473

474
    @staticmethod
1✔
475
    def mount_docker_socket(cfg: ContainerConfiguration):
1✔
476
        source = config.DOCKER_SOCK
×
477
        target = "/var/run/docker.sock"
×
478
        if cfg.volumes.find_target_mapping(target):
×
479
            return
×
480
        cfg.volumes.add(BindMount(source, target))
×
481
        cfg.env_vars["DOCKER_HOST"] = f"unix://{target}"
×
482

483
    @staticmethod
1✔
484
    def mount_localstack_volume(host_path: str | os.PathLike = None):
1✔
485
        host_path = host_path or config.VOLUME_DIR
×
486

487
        def _cfg(cfg: ContainerConfiguration):
×
488
            if cfg.volumes.find_target_mapping(constants.DEFAULT_VOLUME_DIR):
×
489
                return
×
490
            cfg.volumes.add(BindMount(str(host_path), constants.DEFAULT_VOLUME_DIR))
×
491

492
        return _cfg
×
493

494
    @staticmethod
1✔
495
    def config_env_vars(cfg: ContainerConfiguration):
1✔
496
        """Sets all env vars from config.CONFIG_ENV_VARS."""
497

498
        profile_env = {}
×
499
        if config.LOADED_PROFILES:
×
500
            load_environment(profiles=",".join(config.LOADED_PROFILES), env=profile_env)
×
501

502
        non_prefixed_env_vars = []
×
503
        for env_var in config.CONFIG_ENV_VARS:
×
504
            value = os.environ.get(env_var, None)
×
505
            if value is not None:
×
506
                if (
×
507
                    env_var != "CI"
508
                    and not env_var.startswith("LOCALSTACK_")
509
                    and env_var not in profile_env
510
                ):
511
                    # Collect all env vars that are directly forwarded from the system env
512
                    # to the container which has not been prefixed with LOCALSTACK_ here.
513
                    # Suppress the "CI" env var.
514
                    # Suppress if the env var was set from the profile.
515
                    non_prefixed_env_vars.append(env_var)
×
516
                cfg.env_vars[env_var] = value
×
517

518
        # collectively log deprecation warnings for non-prefixed sys env vars
519
        if non_prefixed_env_vars:
×
520
            from localstack.utils.analytics import log
×
521

522
            for non_prefixed_env_var in non_prefixed_env_vars:
×
523
                # Show a deprecation warning for each individual env var collected above
524
                LOG.warning(
×
525
                    "Non-prefixed environment variable %(env_var)s is forwarded to the LocalStack container! "
526
                    "Please use `LOCALSTACK_%(env_var)s` instead of %(env_var)s to explicitly mark this environment variable to be forwarded form the CLI to the LocalStack Runtime.",
527
                    {"env_var": non_prefixed_env_var},
528
                )
529

530
            log.event(
×
531
                event="non_prefixed_cli_env_vars", payload={"env_vars": non_prefixed_env_vars}
532
            )
533

534
    @staticmethod
1✔
535
    def random_gateway_port(cfg: ContainerConfiguration):
1✔
536
        """Gets a random port on the host and maps it to the default edge port 4566."""
537
        return ContainerConfigurators.gateway_listen(get_free_tcp_port())(cfg)
×
538

539
    @staticmethod
1✔
540
    def default_gateway_port(cfg: ContainerConfiguration):
1✔
541
        """Adds 4566 to the list of port mappings"""
542
        return ContainerConfigurators.gateway_listen(constants.DEFAULT_PORT_EDGE)(cfg)
×
543

544
    @staticmethod
1✔
545
    def gateway_listen(
1✔
546
        port: Union[int, Iterable[int], HostAndPort, Iterable[HostAndPort]],
547
    ):
548
        """
549
        Uses the given ports to configure GATEWAY_LISTEN. For instance, ``gateway_listen([4566, 443])`` would
550
        result in the port mappings 4566:4566, 443:443, as well as ``GATEWAY_LISTEN=:4566,:443``.
551

552
        :param port: a single or list of ports, can either be int ports or HostAndPort instances
553
        :return: a configurator
554
        """
555
        if isinstance(port, int):
×
556
            ports = [HostAndPort("", port)]
×
557
        elif isinstance(port, HostAndPort):
×
558
            ports = [port]
×
559
        else:
560
            ports = []
×
561
            for p in port:
×
562
                if isinstance(p, int):
×
563
                    ports.append(HostAndPort("", p))
×
564
                else:
565
                    ports.append(p)
×
566

567
        def _cfg(cfg: ContainerConfiguration):
×
568
            for _p in ports:
×
569
                cfg.ports.add(_p.port)
×
570

571
            # gateway listen should be compiled s.t. even if we set "127.0.0.1:4566" from the host,
572
            # it will be correctly exposed on "0.0.0.0:4566" in the container.
573
            cfg.env_vars["GATEWAY_LISTEN"] = ",".join(
×
574
                [f"{p.host if p.host != default_ip else ''}:{p.port}" for p in ports]
575
            )
576

577
        return _cfg
×
578

579
    @staticmethod
1✔
580
    def container_name(name: str):
1✔
581
        def _cfg(cfg: ContainerConfiguration):
×
582
            cfg.name = name
×
583
            cfg.env_vars["MAIN_CONTAINER_NAME"] = cfg.name
×
584

585
        return _cfg
×
586

587
    @staticmethod
1✔
588
    def random_container_name(cfg: ContainerConfiguration):
1✔
589
        cfg.name = f"localstack-{short_uid()}"
×
590
        cfg.env_vars["MAIN_CONTAINER_NAME"] = cfg.name
×
591

592
    @staticmethod
1✔
593
    def default_container_name(cfg: ContainerConfiguration):
1✔
594
        cfg.name = config.MAIN_CONTAINER_NAME
×
595
        cfg.env_vars["MAIN_CONTAINER_NAME"] = cfg.name
×
596

597
    @staticmethod
1✔
598
    def service_port_range(cfg: ContainerConfiguration):
1✔
599
        cfg.ports.add([config.EXTERNAL_SERVICE_PORTS_START, config.EXTERNAL_SERVICE_PORTS_END])
×
600
        cfg.env_vars["EXTERNAL_SERVICE_PORTS_START"] = config.EXTERNAL_SERVICE_PORTS_START
×
601
        cfg.env_vars["EXTERNAL_SERVICE_PORTS_END"] = config.EXTERNAL_SERVICE_PORTS_END
×
602

603
    @staticmethod
1✔
604
    def random_service_port_range(num: int = 50):
1✔
605
        """
606
        Tries to find a contiguous list of random ports on the host to map to the external service port
607
        range in the container.
608
        """
609

610
        def _cfg(cfg: ContainerConfiguration):
×
611
            port_range = get_free_tcp_port_range(num)
×
612
            cfg.ports.add([port_range.start, port_range.end])
×
613
            cfg.env_vars["EXTERNAL_SERVICE_PORTS_START"] = str(port_range.start)
×
614
            cfg.env_vars["EXTERNAL_SERVICE_PORTS_END"] = str(port_range.end)
×
615

616
        return _cfg
×
617

618
    @staticmethod
1✔
619
    def debug(cfg: ContainerConfiguration):
1✔
620
        cfg.env_vars["DEBUG"] = "1"
×
621

622
    @classmethod
1✔
623
    def develop(cls, cfg: ContainerConfiguration):
1✔
624
        cls.env_vars(
×
625
            {
626
                "DEVELOP": "1",
627
            }
628
        )(cfg)
629
        cls.port(5678)(cfg)
×
630

631
    @staticmethod
1✔
632
    def network(network: str):
1✔
633
        def _cfg(cfg: ContainerConfiguration):
×
634
            cfg.network = network
×
635

636
        return _cfg
×
637

638
    @staticmethod
1✔
639
    def custom_command(cmd: List[str]):
1✔
640
        """
641
        Overwrites the container command and unsets the default entrypoint.
642

643
        :param cmd: the command to run in the container
644
        :return: a configurator
645
        """
646

647
        def _cfg(cfg: ContainerConfiguration):
×
648
            cfg.command = cmd
×
649
            cfg.entrypoint = ""
×
650

651
        return _cfg
×
652

653
    @staticmethod
1✔
654
    def env_vars(env_vars: Dict[str, str]):
1✔
655
        def _cfg(cfg: ContainerConfiguration):
×
656
            cfg.env_vars.update(env_vars)
×
657

658
        return _cfg
×
659

660
    @staticmethod
1✔
661
    def port(*args, **kwargs):
1✔
662
        def _cfg(cfg: ContainerConfiguration):
×
663
            cfg.ports.add(*args, **kwargs)
×
664

665
        return _cfg
×
666

667
    @staticmethod
1✔
668
    def volume(volume: BindMount | VolumeDirMount):
1✔
669
        def _cfg(cfg: ContainerConfiguration):
×
670
            cfg.volumes.add(volume)
×
671

672
        return _cfg
×
673

674
    @staticmethod
1✔
675
    def cli_params(params: Dict[str, Any]):
1✔
676
        """
677
        Parse docker CLI parameters and add them to the config. The currently known CLI params are::
678

679
            --network=my-network       <- stored in "network"
680
            -e FOO=BAR -e BAR=ed       <- stored in "env"
681
            -p 4566:4566 -p 4510-4559  <- stored in "publish"
682
            -v ./bar:/foo/bar          <- stored in "volume"
683

684
        When parsed by click, the parameters would look like this::
685

686
            {
687
                "network": "my-network",
688
                "env": ("FOO=BAR", "BAR=ed"),
689
                "publish": ("4566:4566", "4510-4559"),
690
                "volume": ("./bar:/foo/bar",),
691
            }
692

693
        :param params: a dict of parsed parameters
694
        :return: a configurator
695
        """
696

697
        # TODO: consolidate with container_client.Util.parse_additional_flags
698
        def _cfg(cfg: ContainerConfiguration):
1✔
699
            if params.get("network"):
1✔
700
                cfg.network = params.get("network")
1✔
701

702
            if params.get("host_dns"):
1✔
703
                cfg.ports.add(config.DNS_PORT, config.DNS_PORT, "udp")
×
704
                cfg.ports.add(config.DNS_PORT, config.DNS_PORT, "tcp")
×
705

706
            # processed parsed -e, -p, and -v flags
707
            ContainerConfigurators.env_cli_params(params.get("env"))(cfg)
1✔
708
            ContainerConfigurators.port_cli_params(params.get("publish"))(cfg)
1✔
709
            ContainerConfigurators.volume_cli_params(params.get("volume"))(cfg)
1✔
710

711
        return _cfg
1✔
712

713
    @staticmethod
1✔
714
    def env_cli_params(params: Iterable[str] = None):
1✔
715
        """
716
        Configures environment variables from additional CLI input through the ``-e`` options.
717

718
        :param params: a list of environment variable declarations, e.g.,: ``("foo=bar", "baz=ed")``
719
        :return: a configurator
720
        """
721

722
        def _cfg(cfg: ContainerConfiguration):
1✔
723
            if not params:
1✔
724
                return
×
725

726
            for e in params:
1✔
727
                if "=" in e:
1✔
728
                    k, v = e.split("=", maxsplit=1)
1✔
729
                    cfg.env_vars[k] = v
1✔
730
                else:
731
                    # there's currently no way in our abstraction to only pass the variable name (as
732
                    # you can do in docker) so we resolve the value here.
733
                    cfg.env_vars[e] = os.getenv(e)
1✔
734

735
        return _cfg
1✔
736

737
    @staticmethod
1✔
738
    def port_cli_params(params: Iterable[str] = None):
1✔
739
        """
740
        Configures port variables from additional CLI input through the ``-p`` options.
741

742
        :param params: a list of port assignments, e.g.,: ``("4000-5000", "8080:80")``
743
        :return: a configurator
744
        """
745

746
        def _cfg(cfg: ContainerConfiguration):
1✔
747
            if not params:
1✔
748
                return
×
749

750
            for port_mapping in params:
1✔
751
                port_split = port_mapping.split(":")
1✔
752
                protocol = "tcp"
1✔
753
                if len(port_split) == 1:
1✔
754
                    host_port = container_port = port_split[0]
1✔
755
                elif len(port_split) == 2:
1✔
756
                    host_port, container_port = port_split
1✔
757
                elif len(port_split) == 3:
×
758
                    _, host_port, container_port = port_split
×
759
                else:
760
                    raise ValueError(f"Invalid port string provided: {port_mapping}")
×
761

762
                host_port_split = host_port.split("-")
1✔
763
                if len(host_port_split) == 2:
1✔
764
                    host_port = [int(host_port_split[0]), int(host_port_split[1])]
1✔
765
                elif len(host_port_split) == 1:
1✔
766
                    host_port = int(host_port)
1✔
767
                else:
768
                    raise ValueError(f"Invalid port string provided: {port_mapping}")
×
769

770
                if "/" in container_port:
1✔
771
                    container_port, protocol = container_port.split("/")
1✔
772

773
                container_port_split = container_port.split("-")
1✔
774
                if len(container_port_split) == 2:
1✔
775
                    container_port = [int(container_port_split[0]), int(container_port_split[1])]
1✔
776
                elif len(container_port_split) == 1:
1✔
777
                    container_port = int(container_port)
1✔
778
                else:
779
                    raise ValueError(f"Invalid port string provided: {port_mapping}")
×
780

781
                cfg.ports.add(host_port, container_port, protocol)
1✔
782

783
        return _cfg
1✔
784

785
    @staticmethod
1✔
786
    def volume_cli_params(params: Iterable[str] = None):
1✔
787
        """
788
        Configures volumes from additional CLI input through the ``-v`` options.
789

790
        :param params: a list of volume declarations, e.g.,: ``("./bar:/foo/bar",)``
791
        :return: a configurator
792
        """
793

794
        def _cfg(cfg: ContainerConfiguration):
1✔
795
            for param in params:
1✔
796
                cfg.volumes.append(BindMount.parse(param))
1✔
797

798
        return _cfg
1✔
799

800

801
def get_gateway_port(container: Container) -> int:
1✔
802
    """
803
    Heuristically determines for the given container the port the gateway will be reachable from the host.
804
    Parses the container's ``GATEWAY_LISTEN`` if necessary and finds the appropriate port mapping.
805

806
    :param container: the localstack container
807
    :return: the gateway port reachable from the host
808
    """
809
    candidates: List[int]
810

811
    gateway_listen = container.config.env_vars.get("GATEWAY_LISTEN")
1✔
812
    if gateway_listen:
1✔
813
        candidates = [
1✔
814
            HostAndPort.parse(
815
                value,
816
                default_host=constants.LOCALHOST_HOSTNAME,
817
                default_port=constants.DEFAULT_PORT_EDGE,
818
            ).port
819
            for value in gateway_listen.split(",")
820
        ]
821
    else:
822
        candidates = [constants.DEFAULT_PORT_EDGE]
1✔
823

824
    exposed = container.config.ports.to_dict()
1✔
825

826
    for candidate in candidates:
1✔
827
        port = exposed.get(f"{candidate}/tcp")
1✔
828
        if port:
1✔
829
            return port
1✔
830

831
    raise ValueError("no gateway port mapping found")
1✔
832

833

834
def get_gateway_url(
1✔
835
    container: Container,
836
    hostname: str = constants.LOCALHOST_HOSTNAME,
837
    protocol: str = "http",
838
) -> str:
839
    """
840
    Returns the localstack container's gateway URL reachable from the host. In most cases this will be
841
    ``http://localhost.localstack.cloud:4566``.
842

843
    :param container: the container
844
    :param hostname: the hostname to use (default localhost.localstack.cloud)
845
    :param protocol: the URI scheme (default http)
846
    :return: a URL
847
    `"""
848
    return f"{protocol}://{hostname}:{get_gateway_port(container)}"
×
849

850

851
class Container:
1✔
852
    def __init__(
1✔
853
        self, container_config: ContainerConfiguration, docker_client: ContainerClient | None = None
854
    ):
855
        self.config = container_config
1✔
856
        # marker to access the running container
857
        self.running_container: RunningContainer | None = None
1✔
858
        self.container_client = docker_client or DOCKER_CLIENT
1✔
859

860
    def configure(self, configurators: ContainerConfigurator | Iterable[ContainerConfigurator]):
1✔
861
        """
862
        Apply the given configurators to the config of this container.
863

864
        :param configurators:
865
        :return:
866
        """
867
        try:
×
868
            iterator = iter(configurators)
×
869
        except TypeError:
×
870
            configurators(self.config)
×
871
            return
×
872

873
        for configurator in iterator:
×
874
            configurator(self.config)
×
875

876
    def start(self, attach: bool = False) -> RunningContainer:
1✔
877
        # FIXME: this is pretty awkward, but additional_flags in the LocalstackContainer API was
878
        #  always a list of ["-e FOO=BAR", ...], whereas in the DockerClient it is expected to be
879
        #  a string. so we need to re-assemble it here. the better way would be to not use
880
        #  additional_flags here all together. it is still used in ext in
881
        #  `configure_pro_container` which could be refactored to use the additional port bindings.
882
        cfg = copy.deepcopy(self.config)
×
883
        if not cfg.additional_flags:
×
884
            cfg.additional_flags = ""
×
885

886
        # TODO: there could be a --network flag in `additional_flags`. we solve a similar problem
887
        #  for the ports using `extract_port_flags`. maybe it would be better to consolidate all
888
        #  this into the ContainerConfig object, like ContainerConfig.update_from_flags(str).
889
        self._ensure_container_network(cfg.network)
×
890

891
        try:
×
892
            id = self.container_client.create_container_from_config(cfg)
×
893
        except ContainerException as e:
×
894
            if LOG.isEnabledFor(logging.DEBUG):
×
895
                LOG.exception("Error while creating container")
×
896
            else:
897
                LOG.error(
×
898
                    "Error while creating container: %s\n%s", e.message, to_str(e.stderr or "?")
899
                )
900
            raise
×
901

902
        try:
×
903
            self.container_client.start_container(id, attach=attach)
×
904
        except ContainerException as e:
×
905
            LOG.error(
×
906
                "Error while starting LocalStack container: %s\n%s",
907
                e.message,
908
                to_str(e.stderr),
909
                exc_info=LOG.isEnabledFor(logging.DEBUG),
910
            )
911
            raise
×
912

913
        self.running_container = RunningContainer(id, container_config=self.config)
×
914
        return self.running_container
×
915

916
    def _ensure_container_network(self, network: str | None = None):
1✔
917
        """Makes sure the configured container network exists"""
918
        if network:
×
919
            if network in ["host", "bridge"]:
×
920
                return
×
921
            try:
×
922
                self.container_client.inspect_network(network)
×
923
            except NoSuchNetwork:
×
924
                LOG.debug("Container network %s not found, creating it", network)
×
925
                self.container_client.create_network(network)
×
926

927

928
class RunningContainer:
1✔
929
    """
930
    Represents a LocalStack container that is running.
931
    """
932

933
    def __init__(
1✔
934
        self,
935
        id: str,
936
        container_config: ContainerConfiguration,
937
        docker_client: ContainerClient | None = None,
938
    ):
939
        self.id = id
×
940
        self.config = container_config
×
941
        self.container_client = docker_client or DOCKER_CLIENT
×
942
        self.name = self.container_client.get_container_name(self.id)
×
943
        self._shutdown = False
×
944
        self._mutex = threading.Lock()
×
945

946
    def __enter__(self):
1✔
947
        return self
×
948

949
    def __exit__(self, exc_type, exc_value, traceback):
1✔
950
        self.shutdown()
×
951

952
    def ip_address(self, docker_network: str | None = None) -> str:
1✔
953
        """
954
        Get the IP address of the container
955

956
        Optionally specify the docker network
957
        """
958
        if docker_network is None:
×
959
            return self.container_client.get_container_ip(container_name_or_id=self.id)
×
960
        else:
961
            return self.container_client.get_container_ipv4_for_network(
×
962
                container_name_or_id=self.id, container_network=docker_network
963
            )
964

965
    def is_running(self) -> bool:
1✔
966
        try:
×
967
            self.container_client.inspect_container(self.id)
×
968
            return True
×
969
        except NoSuchContainer:
×
970
            return False
×
971

972
    def get_logs(self) -> str:
1✔
973
        return self.container_client.get_container_logs(self.id, safe=True)
×
974

975
    def stream_logs(self) -> CancellableStream:
1✔
976
        return self.container_client.stream_container_logs(self.id)
×
977

978
    def wait_until_ready(self, timeout: float = None) -> bool:
1✔
979
        return poll_condition(self.is_running, timeout)
×
980

981
    def shutdown(self, timeout: int = 10, remove: bool = True):
1✔
982
        with self._mutex:
×
983
            if self._shutdown:
×
984
                return
×
985
            self._shutdown = True
×
986

987
        try:
×
988
            self.container_client.stop_container(container_name=self.id, timeout=timeout)
×
989
        except NoSuchContainer:
×
990
            pass
×
991

992
        if remove:
×
993
            try:
×
994
                self.container_client.remove_container(
×
995
                    container_name=self.id, force=True, check_existence=False
996
                )
997
            except ContainerException as e:
×
998
                if "is already in progress" in str(e):
×
999
                    return
×
1000
                raise
×
1001

1002
    def inspect(self) -> Dict[str, Union[Dict, str]]:
1✔
1003
        return self.container_client.inspect_container(container_name_or_id=self.id)
×
1004

1005
    def attach(self):
1✔
1006
        self.container_client.attach_to_container(container_name_or_id=self.id)
×
1007

1008
    def exec_in_container(self, *args, **kwargs):
1✔
1009
        return self.container_client.exec_in_container(
×
1010
            *args, container_name_or_id=self.id, **kwargs
1011
        )
1012

1013
    def stopped(self) -> Container:
1✔
1014
        """
1015
        Convert this running instance to a stopped instance ready to be restarted
1016
        """
1017
        return Container(container_config=self.config, docker_client=self.container_client)
×
1018

1019

1020
class ContainerLogPrinter:
1✔
1021
    """
1022
    Waits on a container to start and then uses ``stream_logs`` to print each line of the logs.
1023
    """
1024

1025
    def __init__(self, container: Container, callback: Callable[[str], None] = print):
1✔
1026
        self.container = container
×
1027
        self.callback = callback
×
1028

1029
        self._closed = threading.Event()
×
1030
        self._stream: Optional[CancellableStream] = None
×
1031

1032
    def _can_start_streaming(self):
1✔
1033
        if self._closed.is_set():
×
1034
            raise IOError("Already stopped")
×
1035
        if not self.container.running_container:
×
1036
            return False
×
1037
        return self.container.running_container.is_running()
×
1038

1039
    def run(self):
1✔
1040
        try:
×
1041
            poll_condition(self._can_start_streaming)
×
1042
        except IOError:
×
1043
            return
×
1044
        self._stream = self.container.running_container.stream_logs()
×
1045
        for line in self._stream:
×
1046
            self.callback(line.rstrip(b"\r\n").decode("utf-8"))
×
1047

1048
    def close(self):
1✔
1049
        self._closed.set()
×
1050
        if self._stream:
×
1051
            self._stream.close()
×
1052

1053

1054
class LocalstackContainerServer(Server):
1✔
1055
    container: Container | RunningContainer
1✔
1056

1057
    def __init__(
1✔
1058
        self, container_configuration: ContainerConfiguration | Container | None = None
1059
    ) -> None:
1060
        super().__init__(config.GATEWAY_LISTEN[0].port, config.GATEWAY_LISTEN[0].host)
×
1061

1062
        if container_configuration is None:
×
1063
            port_configuration = PortMappings(bind_host=config.GATEWAY_LISTEN[0].host)
×
1064
            for addr in config.GATEWAY_LISTEN:
×
1065
                port_configuration.add(addr.port)
×
1066

1067
            container_configuration = ContainerConfiguration(
×
1068
                image_name=get_docker_image_to_start(),
1069
                name=config.MAIN_CONTAINER_NAME,
1070
                volumes=VolumeMappings(),
1071
                remove=True,
1072
                ports=port_configuration,
1073
                entrypoint=os.environ.get("ENTRYPOINT"),
1074
                command=shlex.split(os.environ.get("CMD", "")) or None,
1075
                env_vars={},
1076
            )
1077

1078
        if isinstance(container_configuration, Container):
×
1079
            self.container = container_configuration
×
1080
        else:
1081
            self.container = Container(container_configuration)
×
1082

1083
    def is_up(self) -> bool:
1✔
1084
        """
1085
        Checks whether the container is running, and the Ready marker has been printed to the logs.
1086
        """
1087
        if not self.is_container_running():
×
1088
            return False
×
1089

1090
        logs = self.container.get_logs()
×
1091

1092
        if constants.READY_MARKER_OUTPUT not in logs.splitlines():
×
1093
            return False
×
1094

1095
        # also checks the edge port health status
1096
        return super().is_up()
×
1097

1098
    def is_container_running(self) -> bool:
1✔
1099
        # if we have not started the container then we are not up
1100
        if not isinstance(self.container, RunningContainer):
×
1101
            return False
×
1102

1103
        return self.container.is_running()
×
1104

1105
    def wait_is_container_running(self, timeout=None) -> bool:
1✔
1106
        return poll_condition(self.is_container_running, timeout)
×
1107

1108
    def start(self) -> bool:
1✔
1109
        if isinstance(self.container, RunningContainer):
×
1110
            raise RuntimeError("cannot start container as container reference has been started")
×
1111

1112
        return super().start()
×
1113

1114
    def do_run(self):
1✔
1115
        if self.is_container_running():
×
1116
            raise ContainerExists(
×
1117
                'LocalStack container named "%s" is already running' % self.container.name
1118
            )
1119

1120
        config.dirs.mkdirs()
×
1121
        if not isinstance(self.container, Container):
×
1122
            raise ValueError(f"Invalid container type: {type(self.container)}")
×
1123

1124
        LOG.debug("starting LocalStack container")
×
1125
        self.container = self.container.start(attach=False)
×
1126
        if isinstance(DOCKER_CLIENT, CmdDockerClient):
×
1127
            DOCKER_CLIENT.default_run_outfile = get_container_default_logfile_location(
×
1128
                self.container.config.name
1129
            )
1130

1131
        # block the current thread
1132
        self.container.attach()
×
1133
        return self.container
×
1134

1135
    def shutdown(self):
1✔
1136
        if not isinstance(self.container, RunningContainer):
×
1137
            raise ValueError(f"Container {self.container} not started")
×
1138

1139
        return super().shutdown()
×
1140

1141
    def do_shutdown(self):
1✔
1142
        try:
×
1143
            self.container.shutdown(timeout=10)
×
1144
            self.container = self.container.stopped()
×
1145
        except Exception as e:
×
1146
            LOG.info("error cleaning up localstack container %s: %s", self.container.name, e)
×
1147

1148

1149
class ContainerExists(Exception):
1✔
1150
    pass
1✔
1151

1152

1153
def prepare_docker_start():
1✔
1154
    # prepare environment for docker start
1155
    container_name = config.MAIN_CONTAINER_NAME
×
1156

1157
    if DOCKER_CLIENT.is_container_running(container_name):
×
1158
        raise ContainerExists('LocalStack container named "%s" is already running' % container_name)
×
1159

1160
    config.dirs.mkdirs()
×
1161

1162

1163
def configure_container(container: Container):
1✔
1164
    """
1165
    Configuration routine for the LocalstackContainer.
1166
    """
1167
    port_configuration = PortMappings(bind_host=config.GATEWAY_LISTEN[0].host)
×
1168

1169
    # base configuration
1170
    container.config.image_name = get_docker_image_to_start()
×
1171
    container.config.name = config.MAIN_CONTAINER_NAME
×
1172
    container.config.volumes = VolumeMappings()
×
1173
    container.config.remove = True
×
1174
    container.config.ports = port_configuration
×
1175
    container.config.entrypoint = os.environ.get("ENTRYPOINT")
×
1176
    container.config.command = shlex.split(os.environ.get("CMD", "")) or None
×
1177
    container.config.env_vars = {}
×
1178

1179
    # parse `DOCKER_FLAGS` and add them appropriately
1180
    user_flags = config.DOCKER_FLAGS
×
1181
    user_flags = extract_port_flags(user_flags, container.config.ports)
×
1182
    if container.config.additional_flags is None:
×
1183
        container.config.additional_flags = user_flags
×
1184
    else:
1185
        container.config.additional_flags = f"{container.config.additional_flags} {user_flags}"
×
1186

1187
    # get additional parameters from plux
1188
    hooks.configure_localstack_container.run(container)
×
1189

1190
    if config.DEVELOP:
×
1191
        container.config.ports.add(config.DEVELOP_PORT)
×
1192

1193
    container.configure(
×
1194
        [
1195
            # external service port range
1196
            ContainerConfigurators.service_port_range,
1197
            ContainerConfigurators.mount_localstack_volume(config.VOLUME_DIR),
1198
            ContainerConfigurators.mount_docker_socket,
1199
            # overwrites any env vars set in the config that were previously set by configurators
1200
            ContainerConfigurators.config_env_vars,
1201
            # ensure that GATEWAY_LISTEN is taken from the config and not
1202
            # overridden by the `config_env_vars` configurator
1203
            # (when not specified in the environment).
1204
            ContainerConfigurators.gateway_listen(config.GATEWAY_LISTEN),
1205
        ]
1206
    )
1207

1208

1209
@log_duration()
1✔
1210
def prepare_host(console):
1✔
1211
    """
1212
    Prepare the host environment for running LocalStack, this should be called before start_infra_*.
1213
    """
1214
    if os.environ.get(constants.LOCALSTACK_INFRA_PROCESS) in constants.TRUE_STRINGS:
1✔
1215
        return
×
1216

1217
    try:
1✔
1218
        mkdir(config.VOLUME_DIR)
1✔
1219
    except Exception as e:
×
1220
        console.print(f"Error while creating volume dir {config.VOLUME_DIR}: {e}")
×
1221
        if config.DEBUG:
×
1222
            console.print_exception()
×
1223

1224
    setup_logging()
1✔
1225
    hooks.prepare_host.run()
1✔
1226

1227

1228
def start_infra_in_docker(console, cli_params: Dict[str, Any] = None):
1✔
1229
    prepare_docker_start()
×
1230

1231
    # create and prepare container
1232
    container_config = ContainerConfiguration(get_docker_image_to_start())
×
1233
    container = Container(container_config)
×
1234
    ensure_container_image(console, container)
×
1235

1236
    configure_container(container)
×
1237
    container.configure(ContainerConfigurators.cli_params(cli_params or {}))
×
1238

1239
    status = console.status("Starting LocalStack container")
×
1240
    status.start()
×
1241

1242
    # printing the container log is the current way we're occupying the terminal
1243
    def _init_log_printer(line):
×
1244
        """Prints the console rule separator on the first line, then re-configures the callback
1245
        to print."""
1246
        status.stop()
×
1247
        console.rule("LocalStack Runtime Log (press [bold][yellow]CTRL-C[/yellow][/bold] to quit)")
×
1248
        print(line)
×
1249
        log_printer.callback = print
×
1250

1251
    log_printer = ContainerLogPrinter(container, callback=_init_log_printer)
×
1252

1253
    # Set up signal handler, to enable clean shutdown across different operating systems.
1254
    #  There are subtle differences across operating systems and terminal emulators when it
1255
    #  comes to handling of CTRL-C - in particular, Linux sends SIGINT to the parent process,
1256
    #  whereas macOS sends SIGINT to the process group, which can result in multiple SIGINT signals
1257
    #  being received (e.g., when running the localstack CLI as part of a "npm run .." script).
1258
    #  Hence, using a shutdown handler and synchronization event here, to avoid inconsistencies.
1259
    def shutdown_handler(*args):
×
1260
        with shutdown_event_lock:
×
1261
            if shutdown_event.is_set():
×
1262
                return
×
1263
            shutdown_event.set()
×
1264
        print("Shutting down...")
×
1265
        server.shutdown()
×
1266

1267
    shutdown_event = threading.Event()
×
1268
    shutdown_event_lock = threading.RLock()
×
1269
    signal.signal(signal.SIGINT, shutdown_handler)
×
1270

1271
    # start the Localstack container as a Server
1272
    server = LocalstackContainerServer(container)
×
1273
    log_printer_thread = threading.Thread(
×
1274
        target=log_printer.run, name="container-log-printer", daemon=True
1275
    )
1276
    try:
×
1277
        server.start()
×
1278
        log_printer_thread.start()
×
1279
        server.join()
×
1280
        error = server.get_error()
×
1281
        if error:
×
1282
            # if the server failed, raise the error
1283
            raise error
×
1284
    except KeyboardInterrupt:
×
1285
        print("ok, bye!")
×
1286
        shutdown_handler()
×
1287
    finally:
1288
        log_printer.close()
×
1289

1290

1291
def ensure_container_image(console, container: Container):
1✔
1292
    try:
×
1293
        DOCKER_CLIENT.inspect_image(container.config.image_name, pull=False)
×
1294
        return
×
1295
    except NoSuchImage:
×
1296
        console.log("container image not found on host")
×
1297

1298
    with console.status(f"Pulling container image {container.config.image_name}"):
×
1299
        DOCKER_CLIENT.pull_image(container.config.image_name)
×
1300
        console.log("download complete")
×
1301

1302

1303
def start_infra_in_docker_detached(console, cli_params: Dict[str, Any] = None):
1✔
1304
    """
1305
    An alternative to start_infra_in_docker where the terminal is not blocked by the follow on the logfile.
1306
    """
1307
    console.log("preparing environment")
×
1308
    try:
×
1309
        prepare_docker_start()
×
1310
    except ContainerExists as e:
×
1311
        console.print(str(e))
×
1312
        return
×
1313

1314
    # create and prepare container
1315
    console.log("configuring container")
×
1316
    container_config = ContainerConfiguration(get_docker_image_to_start())
×
1317
    container = Container(container_config)
×
1318
    ensure_container_image(console, container)
×
1319
    configure_container(container)
×
1320
    container.configure(ContainerConfigurators.cli_params(cli_params or {}))
×
1321

1322
    container_config.detach = True
×
1323

1324
    # start the Localstack container as a Server
1325
    console.log("starting container")
×
1326
    server = LocalstackContainerServer(container_config)
×
1327
    server.start()
×
1328
    server.wait_is_container_running()
×
1329
    console.log("detaching")
×
1330

1331

1332
def wait_container_is_ready(timeout: Optional[float] = None):
1✔
1333
    """Blocks until the localstack main container is running and the ready marker has been printed."""
1334
    container_name = config.MAIN_CONTAINER_NAME
×
1335
    started = time.time()
×
1336

1337
    def is_container_running():
×
1338
        return DOCKER_CLIENT.is_container_running(container_name)
×
1339

1340
    if not poll_condition(is_container_running, timeout=timeout):
×
1341
        return False
×
1342

1343
    stream = DOCKER_CLIENT.stream_container_logs(container_name)
×
1344

1345
    # create a timer that will terminate the log stream after the remaining timeout
1346
    timer = None
×
1347
    if timeout:
×
1348
        waited = time.time() - started
×
1349
        remaining = timeout - waited
×
1350
        # check the rare case that the timeout has already been reached
1351
        if remaining <= 0:
×
1352
            stream.close()
×
1353
            return False
×
1354
        timer = threading.Timer(remaining, stream.close)
×
1355
        timer.start()
×
1356

1357
    try:
×
1358
        for line in stream:
×
1359
            line = line.decode("utf-8").strip()
×
1360
            if line == constants.READY_MARKER_OUTPUT:
×
1361
                return True
×
1362

1363
        # EOF was reached or the stream was closed
1364
        return False
×
1365
    finally:
1366
        call_safe(stream.close)
×
1367
        if timer:
×
1368
            # make sure the timer is stopped (does nothing if it has already run)
1369
            timer.cancel()
×
1370

1371

1372
# ---------------
1373
# UTIL FUNCTIONS
1374
# ---------------
1375

1376

1377
def in_ci():
1✔
1378
    """Whether or not we are running in a CI environment"""
1379
    for key in ("CI", "TRAVIS"):
1✔
1380
        if os.environ.get(key, "") not in [False, "", "0", "false"]:
1✔
1381
            return True
1✔
1382
    return False
×
1383

1384

1385
def is_auth_token_configured() -> bool:
1✔
1386
    """Whether an API key is set in the environment."""
1387
    return (
×
1388
        True
1389
        if os.environ.get("LOCALSTACK_AUTH_TOKEN", "").strip()
1390
        or os.environ.get("LOCALSTACK_API_KEY", "").strip()
1391
        else False
1392
    )
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