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

localstack / localstack / 19533426081

20 Nov 2025 08:52AM UTC coverage: 86.922% (+0.02%) from 86.903%
19533426081

push

github

web-flow
SQS: fix FIFO message visiblity when extending timeout (#13386)

21 of 21 new or added lines in 1 file covered. (100.0%)

254 existing lines in 10 files now uncovered.

68677 of 79010 relevant lines covered (86.92%)

0.87 hits per line

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

59.76
/localstack-core/localstack/cli/localstack.py
1
import json
1✔
2
import logging
1✔
3
import os
1✔
4
import sys
1✔
5
import traceback
1✔
6
from typing import TypedDict
1✔
7

8
import click
1✔
9
import requests
1✔
10

11
from localstack import config
1✔
12
from localstack.cli.exceptions import CLIError
1✔
13
from localstack.constants import VERSION
1✔
14
from localstack.utils.analytics.cli import publish_invocation
1✔
15
from localstack.utils.bootstrap import get_container_default_logfile_location
1✔
16
from localstack.utils.json import CustomEncoder
1✔
17

18
from .console import BANNER, console
1✔
19
from .plugin import LocalstackCli, load_cli_plugins
1✔
20

21

22
class LocalStackCliGroup(click.Group):
1✔
23
    """
24
    A Click group used for the top-level ``localstack`` command group. It implements global exception handling
25
    by:
26

27
    - Ignoring click exceptions (already handled)
28
    - Handling common exceptions (like DockerNotAvailable)
29
    - Wrapping all unexpected exceptions in a ClickException (for a unified error message)
30

31
    It also implements a custom help formatter to build more fine-grained groups.
32
    """
33

34
    # FIXME: find a way to communicate this from the actual command
35
    advanced_commands = [
1✔
36
        "aws",
37
        "dns",
38
        "extensions",
39
        "license",
40
        "login",
41
        "logout",
42
        "pod",
43
        "state",
44
        "ephemeral",
45
        "replicator",
46
    ]
47

48
    def invoke(self, ctx: click.Context):
1✔
49
        try:
1✔
50
            return super().invoke(ctx)
1✔
51
        except click.exceptions.Exit:
1✔
52
            # raise Exit exceptions unmodified (e.g., raised on --help)
53
            raise
1✔
54
        except click.ClickException:
1✔
55
            # don't handle ClickExceptions, just reraise
56
            if ctx and ctx.params.get("debug"):
1✔
57
                click.echo(traceback.format_exc())
×
58
            raise
1✔
59
        except Exception as e:
1✔
60
            if ctx and ctx.params.get("debug"):
1✔
61
                click.echo(traceback.format_exc())
×
62
            from localstack.utils.container_utils.container_client import (
1✔
63
                ContainerException,
64
                DockerNotAvailable,
65
            )
66

67
            if isinstance(e, DockerNotAvailable):
1✔
68
                raise CLIError(
1✔
69
                    "Docker could not be found on the system.\n"
70
                    "Please make sure that you have a working docker environment on your machine."
71
                )
72
            elif isinstance(e, ContainerException):
1✔
73
                raise CLIError(e.message)
1✔
74
            else:
75
                # If we have a generic exception, we wrap it in a ClickException
76
                raise CLIError(str(e)) from e
×
77

78
    def format_commands(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
1✔
79
        """Extra format methods for multi methods that adds all the commands after the options. It also
80
        groups commands into command categories."""
81
        categories = {"Commands": [], "Advanced": [], "Deprecated": []}
1✔
82

83
        commands = []
1✔
84
        for subcommand in self.list_commands(ctx):
1✔
85
            cmd = self.get_command(ctx, subcommand)
1✔
86
            # What is this, the tool lied about a command.  Ignore it
87
            if cmd is None:
1✔
88
                continue
×
89
            if cmd.hidden:
1✔
90
                continue
×
91

92
            commands.append((subcommand, cmd))
1✔
93

94
        # allow for 3 times the default spacing
95
        if len(commands):
1✔
96
            limit = formatter.width - 6 - max(len(cmd[0]) for cmd in commands)
1✔
97

98
            for subcommand, cmd in commands:
1✔
99
                help = cmd.get_short_help_str(limit)
1✔
100
                categories[self._get_category(cmd)].append((subcommand, help))
1✔
101

102
        for category, rows in categories.items():
1✔
103
            if rows:
1✔
104
                with formatter.section(category):
1✔
105
                    formatter.write_dl(rows)
1✔
106

107
    def _get_category(self, cmd) -> str:
1✔
108
        if cmd.deprecated:
1✔
109
            return "Deprecated"
×
110

111
        if cmd.name in self.advanced_commands:
1✔
112
            return "Advanced"
×
113

114
        return "Commands"
1✔
115

116

117
def create_with_plugins() -> LocalstackCli:
1✔
118
    """
119
    Creates a LocalstackCli instance with all cli plugins loaded.
120
    :return: a LocalstackCli instance
121
    """
122
    cli = LocalstackCli()
1✔
123
    cli.group = localstack
1✔
124
    load_cli_plugins(cli)
1✔
125
    return cli
1✔
126

127

128
def _setup_cli_debug() -> None:
1✔
129
    from localstack.logging.setup import setup_logging_for_cli
1✔
130

131
    config.DEBUG = True
1✔
132
    os.environ["DEBUG"] = "1"
1✔
133

134
    setup_logging_for_cli(logging.DEBUG if config.DEBUG else logging.INFO)
1✔
135

136

137
# Re-usable format option decorator which can be used across multiple commands
138
_click_format_option = click.option(
1✔
139
    "-f",
140
    "--format",
141
    "format_",
142
    type=click.Choice(["table", "plain", "dict", "json"]),
143
    default="table",
144
    help="The formatting style for the command output.",
145
)
146

147

148
@click.group(
1✔
149
    name="localstack",
150
    help="The LocalStack Command Line Interface (CLI)",
151
    cls=LocalStackCliGroup,
152
    context_settings={
153
        # add "-h" as a synonym for "--help"
154
        # https://click.palletsprojects.com/en/8.1.x/documentation/#help-parameter-customization
155
        "help_option_names": ["-h", "--help"],
156
        # show default values for options by default - https://github.com/pallets/click/pull/1225
157
        "show_default": True,
158
    },
159
)
160
@click.version_option(
1✔
161
    VERSION,
162
    "--version",
163
    "-v",
164
    message="LocalStack CLI %(version)s",
165
    help="Show the version of the LocalStack CLI and exit",
166
)
167
@click.option("-d", "--debug", is_flag=True, help="Enable CLI debugging mode")
1✔
168
@click.option("-p", "--profile", type=str, help="Set the configuration profile")
1✔
169
def localstack(debug, profile) -> None:
1✔
170
    # --profile is read manually in localstack.cli.main because it needs to be read before localstack.config is read
171

172
    if debug:
1✔
173
        _setup_cli_debug()
1✔
174

175
    from localstack.utils.files import cache_dir
1✔
176

177
    # overwrite the config variable here to defer import of cache_dir
178
    if not os.environ.get("LOCALSTACK_VOLUME_DIR", "").strip():
1✔
179
        config.VOLUME_DIR = str(cache_dir() / "volume")
1✔
180

181
    # FIXME: at some point we should remove the use of `config.dirs` for the CLI,
182
    #  see https://github.com/localstack/localstack/pull/7906
183
    config.dirs.for_cli().mkdirs()
1✔
184

185

186
@localstack.group(
1✔
187
    name="config",
188
    short_help="Manage your LocalStack config",
189
)
190
def localstack_config() -> None:
1✔
191
    """
192
    Inspect and validate your LocalStack configuration.
193
    """
194
    pass
1✔
195

196

197
@localstack_config.command(name="show", short_help="Show your config")
1✔
198
@_click_format_option
1✔
199
@publish_invocation
1✔
200
def cmd_config_show(format_: str) -> None:
1✔
201
    """
202
    Print the current LocalStack config values.
203

204
    This command prints the LocalStack configuration values from your environment.
205
    It analyzes the environment variables as well as the LocalStack CLI profile.
206
    It does _not_ analyze a specific file (like a docker-compose-yml).
207
    """
208
    # TODO: parse values from potential docker-compose file?
209
    assert config
1✔
210

211
    try:
1✔
212
        # only load the pro config if it's available
213
        from localstack.pro.core import config as pro_config
1✔
214

215
        assert pro_config
×
216
    except ImportError:
1✔
217
        # the pro package is not available
218
        return None
1✔
219

220
    if format_ == "table":
×
221
        _print_config_table()
×
222
    elif format_ == "plain":
×
223
        _print_config_pairs()
×
224
    elif format_ == "dict":
×
225
        _print_config_dict()
×
226
    elif format_ == "json":
×
227
        _print_config_json()
×
228
    else:
229
        _print_config_pairs()  # fall back to plain
×
230

231

232
@localstack_config.command(name="validate", short_help="Validate your config")
1✔
233
@click.option(
1✔
234
    "-f",
235
    "--file",
236
    help="Path to compose file",
237
    default="docker-compose.yml",
238
    type=click.Path(exists=True, file_okay=True, readable=True),
239
)
240
@publish_invocation
1✔
241
def cmd_config_validate(file: str) -> None:
1✔
242
    """
243
    Validate your LocalStack configuration (docker compose).
244

245
    This command inspects the given docker-compose file (by default docker-compose.yml in the current working
246
    directory) and validates if the configuration is valid.
247

248
    \b
249
    It will show an error and return a non-zero exit code if:
250
    - The docker-compose file is syntactically incorrect.
251
    - If the file contains common issues when configuring LocalStack.
252
    """
253

254
    from localstack.utils import bootstrap
×
255

256
    if bootstrap.validate_localstack_config(file):
×
257
        console.print("[green]:heavy_check_mark:[/green] config valid")
×
258
        sys.exit(0)
×
259
    else:
260
        console.print("[red]:heavy_multiplication_x:[/red] validation error")
×
261
        sys.exit(1)
×
262

263

264
def _print_config_json() -> None:
1✔
265
    import json
×
266

267
    console.print(json.dumps(dict(config.collect_config_items()), cls=CustomEncoder))
×
268

269

270
def _print_config_pairs() -> None:
1✔
271
    for key, value in config.collect_config_items():
×
272
        console.print(f"{key}={value}")
×
273

274

275
def _print_config_dict() -> None:
1✔
276
    console.print(dict(config.collect_config_items()))
×
277

278

279
def _print_config_table() -> None:
1✔
280
    from rich.table import Table
×
281

282
    grid = Table(show_header=True)
×
283
    grid.add_column("Key")
×
284
    grid.add_column("Value")
×
285

286
    for key, value in config.collect_config_items():
×
287
        grid.add_row(key, str(value))
×
288

289
    console.print(grid)
×
290

291

292
@localstack.group(
1✔
293
    name="status",
294
    short_help="Query status info",
295
    invoke_without_command=True,
296
)
297
@click.pass_context
1✔
298
def localstack_status(ctx: click.Context) -> None:
1✔
299
    """
300
    Query status information about the currently running LocalStack instance.
301
    """
302
    if ctx.invoked_subcommand is None:
1✔
303
        ctx.invoke(localstack_status.get_command(ctx, "docker"))
×
304

305

306
@localstack_status.command(name="docker", short_help="Query LocalStack Docker status")
1✔
307
@_click_format_option
1✔
308
def cmd_status_docker(format_: str) -> None:
1✔
309
    """
310
    Query information about the currently running LocalStack Docker image, its container,
311
    and the LocalStack runtime.
312
    """
313
    with console.status("Querying Docker status"):
×
314
        _print_docker_status(format_)
×
315

316

317
class DockerStatus(TypedDict, total=False):
1✔
318
    running: bool
1✔
319
    runtime_version: str
1✔
320
    image_tag: str
1✔
321
    image_id: str
1✔
322
    image_created: str
1✔
323
    container_name: str | None
1✔
324
    container_ip: str | None
1✔
325

326

327
def _print_docker_status(format_: str) -> None:
1✔
328
    from localstack.utils import docker_utils
×
329
    from localstack.utils.bootstrap import get_docker_image_details, get_server_version
×
330
    from localstack.utils.container_networking import get_main_container_ip, get_main_container_name
×
331

332
    img = get_docker_image_details()
×
333
    cont_name = config.MAIN_CONTAINER_NAME
×
334
    running = docker_utils.DOCKER_CLIENT.is_container_running(cont_name)
×
335
    status = DockerStatus(
×
336
        runtime_version=get_server_version(),
337
        image_tag=img["tag"],
338
        image_id=img["id"],
339
        image_created=img["created"],
340
        running=running,
341
    )
342
    if running:
×
343
        status["container_name"] = get_main_container_name()
×
344
        status["container_ip"] = get_main_container_ip()
×
345

346
    if format_ == "dict":
×
347
        console.print(status)
×
348
    if format_ == "table":
×
349
        _print_docker_status_table(status)
×
350
    if format_ == "json":
×
351
        console.print(json.dumps(status))
×
352
    if format_ == "plain":
×
353
        for key, value in status.items():
×
354
            console.print(f"{key}={value}")
×
355

356

357
def _print_docker_status_table(status: DockerStatus) -> None:
1✔
358
    from rich.table import Table
×
359

360
    grid = Table(show_header=False)
×
361
    grid.add_column()
×
362
    grid.add_column()
×
363

364
    grid.add_row("Runtime version", f"[bold]{status['runtime_version']}[/bold]")
×
365
    grid.add_row(
×
366
        "Docker image",
367
        f"tag: {status['image_tag']}, "
368
        f"id: {status['image_id']}, "
369
        f":calendar: {status['image_created']}",
370
    )
371
    cont_status = "[bold][red]:heavy_multiplication_x: stopped"
×
372
    if status["running"]:
×
373
        cont_status = (
×
374
            f"[bold][green]:heavy_check_mark: running[/green][/bold] "
375
            f'(name: "[italic]{status["container_name"]}[/italic]", IP: {status["container_ip"]})'
376
        )
377
    grid.add_row("Runtime status", cont_status)
×
378
    console.print(grid)
×
379

380

381
@localstack_status.command(name="services", short_help="Query LocalStack services status")
1✔
382
@_click_format_option
1✔
383
def cmd_status_services(format_: str) -> None:
1✔
384
    """
385
    Query information about the services of the currently running LocalStack instance.
386
    """
387
    url = config.external_service_url()
1✔
388

389
    try:
1✔
390
        health = requests.get(f"{url}/_localstack/health", timeout=2)
1✔
391
        doc = health.json()
1✔
392
        services = doc.get("services", [])
1✔
393
        if format_ == "table":
1✔
394
            _print_service_table(services)
1✔
395
        if format_ == "plain":
1✔
396
            for service, status in services.items():
×
397
                console.print(f"{service}={status}")
×
398
        if format_ == "dict":
1✔
399
            console.print(services)
×
400
        if format_ == "json":
1✔
401
            console.print(json.dumps(services))
×
402
    except requests.ConnectionError:
1✔
403
        if config.DEBUG:
1✔
404
            console.print_exception()
1✔
405
        raise CLIError(f"could not connect to LocalStack health endpoint at {url}")
1✔
406

407

408
def _print_service_table(services: dict[str, str]) -> None:
1✔
409
    from rich.table import Table
1✔
410

411
    status_display = {
1✔
412
        "running": "[green]:heavy_check_mark:[/green] running",
413
        "starting": ":hourglass_flowing_sand: starting",
414
        "available": "[grey]:heavy_check_mark:[/grey] available",
415
        "error": "[red]:heavy_multiplication_x:[/red] error",
416
    }
417

418
    table = Table()
1✔
419
    table.add_column("Service")
1✔
420
    table.add_column("Status")
1✔
421

422
    services = list(services.items())
1✔
423
    services.sort(key=lambda item: item[0])
1✔
424

425
    for service, status in services:
1✔
426
        if status in status_display:
1✔
427
            status = status_display[status]
1✔
428

429
        table.add_row(service, status)
1✔
430

431
    console.print(table)
1✔
432

433

434
@localstack.command(name="start", short_help="Start LocalStack")
1✔
435
@click.option("--docker", is_flag=True, help="Start LocalStack in a docker container [default]")
1✔
436
@click.option("--host", is_flag=True, help="Start LocalStack directly on the host", deprecated=True)
1✔
437
@click.option("--no-banner", is_flag=True, help="Disable LocalStack banner", default=False)
1✔
438
@click.option(
1✔
439
    "-d", "--detached", is_flag=True, help="Start LocalStack in the background", default=False
440
)
441
@click.option(
1✔
442
    "--network",
443
    type=str,
444
    help="The container network the LocalStack container should be started in. By default, the default docker bridge network is used.",
445
    required=False,
446
)
447
@click.option(
1✔
448
    "--env",
449
    "-e",
450
    help="Additional environment variables that are passed to the LocalStack container",
451
    multiple=True,
452
    required=False,
453
)
454
@click.option(
1✔
455
    "--publish",
456
    "-p",
457
    help="Additional port mappings that are passed to the LocalStack container",
458
    multiple=True,
459
    required=False,
460
)
461
@click.option(
1✔
462
    "--volume",
463
    "-v",
464
    help="Additional volume mounts that are passed to the LocalStack container",
465
    multiple=True,
466
    required=False,
467
)
468
@click.option(
1✔
469
    "--host-dns",
470
    help="Expose the LocalStack DNS server to the host using port bindings.",
471
    required=False,
472
    is_flag=True,
473
    default=False,
474
)
475
@click.option(
1✔
476
    "--stack",
477
    "-s",
478
    type=str,
479
    help="Use a specific stack with optional version. Examples: [localstack:4.5, snowflake]",
480
    required=False,
481
)
482
@publish_invocation
1✔
483
def cmd_start(
1✔
484
    docker: bool,
485
    host: bool,
486
    no_banner: bool,
487
    detached: bool,
488
    network: str = None,
489
    env: tuple = (),
490
    publish: tuple = (),
491
    volume: tuple = (),
492
    host_dns: bool = False,
493
    stack: str = None,
494
) -> None:
495
    """
496
    Start the LocalStack runtime.
497

498
    This command starts the LocalStack runtime with your current configuration.
499
    By default, it will start a new Docker container from the latest LocalStack(-Pro) Docker image
500
    with best-practice volume mounts and port mappings.
501
    """
502
    if docker and host:
1✔
503
        raise CLIError("Please specify either --docker or --host")
×
504
    if host and detached:
1✔
505
        raise CLIError("Cannot start detached in host mode")
×
506

507
    if stack:
1✔
508
        # Validate allowed stacks
509
        stack_name = stack.split(":")[0]
×
510
        allowed_stacks = ("localstack", "localstack-pro", "snowflake")
×
511
        if stack_name.lower() not in allowed_stacks:
×
512
            raise CLIError(f"Invalid stack '{stack_name}'. Allowed stacks: {allowed_stacks}.")
×
513

514
        # Set IMAGE_NAME, defaulting to :latest if no version specified
515
        if ":" not in stack:
×
516
            stack = f"{stack}:latest"
×
517
        os.environ["IMAGE_NAME"] = f"localstack/{stack}"
×
518

519
    if not no_banner:
1✔
520
        print_banner()
1✔
521
        print_version()
1✔
522
        print_profile()
1✔
523
        print_app()
1✔
524
        console.line()
1✔
525

526
    from localstack.utils import bootstrap
1✔
527

528
    if not no_banner:
1✔
529
        if host:
1✔
530
            console.log("starting LocalStack in host mode :laptop_computer:")
1✔
531
        else:
532
            console.log("starting LocalStack in Docker mode :whale:")
1✔
533

534
    if host:
1✔
535
        console.log(
1✔
536
            "Warning: Starting LocalStack in host mode from the CLI is deprecated and will be removed soon. Please use the default Docker mode instead.",
537
            style="bold red",
538
        )
539

540
        # call hooks to prepare host
541
        bootstrap.prepare_host(console)
1✔
542

543
        # from here we abandon the regular CLI control path and start treating the process like a localstack
544
        # runtime process
545
        os.environ["LOCALSTACK_CLI"] = "0"
1✔
546
        config.dirs = config.init_directories()
1✔
547

548
        try:
1✔
549
            bootstrap.start_infra_locally()
1✔
550
        except ImportError:
1✔
UNCOV
551
            if config.DEBUG:
×
UNCOV
552
                console.print_exception()
×
UNCOV
553
            raise CLIError(
×
554
                "It appears you have a light install of localstack which only supports running in docker.\n"
555
                "If you would like to use --host, please install localstack with Python using "
556
                "`pip install localstack[runtime]` instead."
557
            )
558
    else:
559
        # make sure to initialize the bootstrap environment and directories for the host (even if we're executing
560
        # in Docker), to allow starting the container from within other containers (e.g., Github Codespaces).
561
        config.OVERRIDE_IN_DOCKER = False
1✔
562
        config.is_in_docker = False
1✔
563
        config.dirs = config.init_directories()
1✔
564

565
        # call hooks to prepare host (note that this call should stay below the config overrides above)
566
        bootstrap.prepare_host(console)
1✔
567

568
        # pass the parsed cli params to the start infra command
569
        params = click.get_current_context().params
1✔
570

571
        if network:
1✔
572
            # reconciles the network config and makes sure that MAIN_DOCKER_NETWORK is set automatically if
573
            # `--network` is set.
UNCOV
574
            if config.MAIN_DOCKER_NETWORK:
×
UNCOV
575
                if config.MAIN_DOCKER_NETWORK != network:
×
576
                    raise CLIError(
×
577
                        f"Values of MAIN_DOCKER_NETWORK={config.MAIN_DOCKER_NETWORK} and --network={network} "
578
                        f"do not match"
579
                    )
580
            else:
UNCOV
581
                config.MAIN_DOCKER_NETWORK = network
×
UNCOV
582
                os.environ["MAIN_DOCKER_NETWORK"] = network
×
583

584
        if detached:
1✔
UNCOV
585
            bootstrap.start_infra_in_docker_detached(console, params)
×
586
        else:
587
            bootstrap.start_infra_in_docker(console, params)
1✔
588

589

590
@localstack.command(name="stop", short_help="Stop LocalStack")
1✔
591
@publish_invocation
1✔
592
def cmd_stop() -> None:
1✔
593
    """
594
    Stops the current LocalStack runtime.
595

596
    This command stops the currently running LocalStack docker container.
597
    By default, this command looks for a container named `localstack-main` (which is the default
598
    container name used by the `localstack start` command).
599
    If your LocalStack container has a different name, set the config variable
600
    `MAIN_CONTAINER_NAME`.
601
    """
602
    from localstack.utils.docker_utils import DOCKER_CLIENT
1✔
603

604
    from ..utils.container_utils.container_client import NoSuchContainer
1✔
605

606
    container_name = config.MAIN_CONTAINER_NAME
1✔
607

608
    try:
1✔
609
        DOCKER_CLIENT.stop_container(container_name)
1✔
UNCOV
610
        console.print(f"container stopped: {container_name}")
×
611
    except NoSuchContainer:
1✔
612
        raise CLIError(
1✔
613
            f'Expected a running LocalStack container named "{container_name}", but found none'
614
        )
615

616

617
@localstack.command(name="restart", short_help="Restart LocalStack")
1✔
618
@publish_invocation
1✔
619
def cmd_restart() -> None:
1✔
620
    """
621
    Restarts the current LocalStack runtime.
622
    """
UNCOV
623
    url = config.external_service_url()
×
624

625
    try:
×
626
        response = requests.post(
×
627
            f"{url}/_localstack/health",
628
            json={"action": "restart"},
629
        )
630
        response.raise_for_status()
×
UNCOV
631
        console.print("LocalStack restarted within the container.")
×
UNCOV
632
    except requests.ConnectionError:
×
UNCOV
633
        if config.DEBUG:
×
UNCOV
634
            console.print_exception()
×
UNCOV
635
        raise CLIError("could not restart the LocalStack container")
×
636

637

638
@localstack.command(
1✔
639
    name="logs",
640
    short_help="Show LocalStack logs",
641
)
642
@click.option(
1✔
643
    "-f",
644
    "--follow",
645
    is_flag=True,
646
    help="Block the terminal and follow the log output",
647
    default=False,
648
)
649
@click.option(
1✔
650
    "-n",
651
    "--tail",
652
    type=int,
653
    help="Print only the last <N> lines of the log output",
654
    default=None,
655
    metavar="N",
656
)
657
@publish_invocation
1✔
658
def cmd_logs(follow: bool, tail: int) -> None:
1✔
659
    """
660
    Show the logs of the current LocalStack runtime.
661

662
    This command shows the logs of the currently running LocalStack docker container.
663
    By default, this command looks for a container named `localstack-main` (which is the default
664
    container name used by the `localstack start` command).
665
    If your LocalStack container has a different name, set the config variable
666
    `MAIN_CONTAINER_NAME`.
667
    """
668
    from localstack.utils.docker_utils import DOCKER_CLIENT
×
669

670
    container_name = config.MAIN_CONTAINER_NAME
×
671
    logfile = get_container_default_logfile_location(container_name)
×
672

673
    if not DOCKER_CLIENT.is_container_running(container_name):
×
674
        console.print("localstack container not running")
×
675
        if os.path.exists(logfile):
×
UNCOV
676
            console.print("printing logs from previous run")
×
677
            with open(logfile) as fd:
×
678
                for line in fd:
×
679
                    click.echo(line, nl=False)
×
680
        sys.exit(1)
×
681

682
    if follow:
×
683
        num_lines = 0
×
UNCOV
684
        for line in DOCKER_CLIENT.stream_container_logs(container_name):
×
UNCOV
685
            print(line.decode("utf-8").rstrip("\r\n"))
×
686
            num_lines += 1
×
687
            if tail is not None and num_lines >= tail:
×
688
                break
×
689

690
    else:
UNCOV
691
        logs = DOCKER_CLIENT.get_container_logs(container_name)
×
UNCOV
692
        if tail is not None:
×
UNCOV
693
            logs = "\n".join(logs.split("\n")[-tail:])
×
UNCOV
694
        print(logs)
×
695

696

697
@localstack.command(name="wait", short_help="Wait for LocalStack")
1✔
698
@click.option(
1✔
699
    "-t",
700
    "--timeout",
701
    type=float,
702
    help="Only wait for <N> seconds before raising a timeout error",
703
    default=None,
704
    metavar="N",
705
)
706
@publish_invocation
1✔
707
def cmd_wait(timeout: float | None = None) -> None:
1✔
708
    """
709
    Wait for the LocalStack runtime to be up and running.
710

711
    This commands waits for a started LocalStack runtime to be up and running, ready to serve
712
    requests.
713
    By default, this command looks for a container named `localstack-main` (which is the default
714
    container name used by the `localstack start` command).
715
    If your LocalStack container has a different name, set the config variable
716
    `MAIN_CONTAINER_NAME`.
717
    """
UNCOV
718
    from localstack.utils.bootstrap import wait_container_is_ready
×
719

UNCOV
720
    if not wait_container_is_ready(timeout=timeout):
×
UNCOV
721
        raise CLIError("timeout")
×
722

723

724
@localstack.command(name="ssh", short_help="Obtain a shell in LocalStack")
1✔
725
@publish_invocation
1✔
726
def cmd_ssh() -> None:
1✔
727
    """
728
    Obtain a shell in the current LocalStack runtime.
729

730
    This command starts a new interactive shell in the currently running LocalStack container.
731
    By default, this command looks for a container named `localstack-main` (which is the default
732
    container name used by the `localstack start` command).
733
    If your LocalStack container has a different name, set the config variable
734
    `MAIN_CONTAINER_NAME`.
735
    """
736
    from localstack.utils.docker_utils import DOCKER_CLIENT
1✔
737

738
    if not DOCKER_CLIENT.is_container_running(config.MAIN_CONTAINER_NAME):
1✔
739
        raise CLIError(
1✔
740
            f'Expected a running LocalStack container named "{config.MAIN_CONTAINER_NAME}", but found none'
741
        )
UNCOV
742
    os.execlp("docker", "docker", "exec", "-it", config.MAIN_CONTAINER_NAME, "bash")
×
743

744

745
@localstack.group(name="update", short_help="Update LocalStack")
1✔
746
def localstack_update() -> None:
1✔
747
    """
748
    Update different LocalStack components.
749
    """
UNCOV
750
    pass
×
751

752

753
@localstack_update.command(name="all", short_help="Update all LocalStack components")
1✔
754
@click.pass_context
1✔
755
@publish_invocation
1✔
756
def cmd_update_all(ctx: click.Context) -> None:
1✔
757
    """
758
    Update all LocalStack components.
759

760
    This is the same as executing `localstack update localstack-cli` and
761
    `localstack update docker-images`.
762
    Updating the LocalStack CLI is currently only supported if the CLI
763
    is installed and run via Python / PIP. If you used a different installation method,
764
    please follow the instructions on https://docs.localstack.cloud/.
765
    """
UNCOV
766
    ctx.invoke(localstack_update.get_command(ctx, "localstack-cli"))
×
UNCOV
767
    ctx.invoke(localstack_update.get_command(ctx, "docker-images"))
×
768

769

770
@localstack_update.command(name="localstack-cli", short_help="Update LocalStack CLI")
1✔
771
@publish_invocation
1✔
772
def cmd_update_localstack_cli() -> None:
1✔
773
    """
774
    Update the LocalStack CLI.
775

776
    This command updates the LocalStack CLI. This is currently only supported if the CLI
777
    is installed and run via Python / PIP. If you used a different installation method,
778
    please follow the instructions on https://docs.localstack.cloud/.
779
    """
UNCOV
780
    if is_frozen_bundle():
×
781
        # "update" can only be performed if running from source / in a non-frozen interpreter
782
        raise CLIError(
×
783
            "The LocalStack CLI can only update itself if installed via PIP. "
784
            "Please follow the instructions on https://docs.localstack.cloud/ to update your CLI."
785
        )
786

787
    import subprocess
×
788
    from subprocess import CalledProcessError
×
789

UNCOV
790
    console.rule("Updating LocalStack CLI")
×
791
    with console.status("Updating LocalStack CLI..."):
×
792
        try:
×
793
            subprocess.check_output(
×
794
                [sys.executable, "-m", "pip", "install", "--upgrade", "localstack"]
795
            )
UNCOV
796
            console.print(":heavy_check_mark: LocalStack CLI updated")
×
UNCOV
797
        except CalledProcessError:
×
UNCOV
798
            console.print(":heavy_multiplication_x: LocalStack CLI update failed", style="bold red")
×
799

800

801
@localstack_update.command(
1✔
802
    name="docker-images", short_help="Update docker images LocalStack depends on"
803
)
804
@publish_invocation
1✔
805
def cmd_update_docker_images() -> None:
1✔
806
    """
807
    Update all Docker images LocalStack depends on.
808

809
    This command updates all Docker LocalStack docker images, as well as other Docker images
810
    LocalStack depends on (and which have been used before / are present on the machine).
811
    """
812
    from localstack.utils.docker_utils import DOCKER_CLIENT
×
813

UNCOV
814
    console.rule("Updating docker images")
×
815

816
    all_images = DOCKER_CLIENT.get_docker_image_names(strip_latest=False)
×
UNCOV
817
    image_prefixes = [
×
818
        "localstack/",
819
        "public.ecr.aws/lambda",
820
    ]
UNCOV
821
    localstack_images = [
×
822
        image
823
        for image in all_images
824
        if any(
825
            image.startswith(image_prefix) or image.startswith(f"docker.io/{image_prefix}")
826
            for image_prefix in image_prefixes
827
        )
828
        and not image.endswith(":<none>")  # ignore dangling images
829
    ]
830
    update_images(localstack_images)
×
831

832

833
def update_images(image_list: list[str]) -> None:
1✔
UNCOV
834
    from rich.markup import escape
×
835
    from rich.progress import MofNCompleteColumn, Progress
×
836

837
    from localstack.utils.container_utils.container_client import ContainerException
×
UNCOV
838
    from localstack.utils.docker_utils import DOCKER_CLIENT
×
839

840
    updated_count = 0
×
841
    failed_count = 0
×
842
    progress = Progress(
×
843
        *Progress.get_default_columns(), MofNCompleteColumn(), transient=True, console=console
844
    )
845
    with progress:
×
846
        for image in progress.track(image_list, description="Processing image..."):
×
UNCOV
847
            try:
×
UNCOV
848
                updated = False
×
UNCOV
849
                hash_before_pull = DOCKER_CLIENT.inspect_image(image_name=image, pull=False)["Id"]
×
850
                DOCKER_CLIENT.pull_image(image)
×
851
                if (
×
852
                    hash_before_pull
853
                    != DOCKER_CLIENT.inspect_image(image_name=image, pull=False)["Id"]
854
                ):
UNCOV
855
                    updated = True
×
UNCOV
856
                    updated_count += 1
×
857
                console.print(
×
858
                    f":heavy_check_mark: Image {escape(image)} {'updated' if updated else 'up-to-date'}.",
859
                    style="bold" if updated else None,
860
                    highlight=False,
861
                )
UNCOV
862
            except ContainerException as e:
×
863
                console.print(
×
864
                    f":heavy_multiplication_x: Image {escape(image)} pull failed: {e.message}",
865
                    style="bold red",
866
                    highlight=False,
867
                )
UNCOV
868
                failed_count += 1
×
UNCOV
869
    console.rule()
×
UNCOV
870
    console.print(
×
871
        f"Images updated: {updated_count}, Images failed: {failed_count}, total images processed: {len(image_list)}."
872
    )
873

874

875
@localstack.command(name="completion", short_help="CLI shell completion")
1✔
876
@click.pass_context
1✔
877
@click.argument(
1✔
878
    "shell", required=True, type=click.Choice(["bash", "zsh", "fish"], case_sensitive=False)
879
)
880
@publish_invocation
1✔
881
def localstack_completion(ctx: click.Context, shell: str) -> None:
1✔
882
    """
883
     Print shell completion code for the specified shell (bash, zsh, or fish).
884
     The shell code must be evaluated to enable the interactive shell completion of LocalStack CLI commands.
885
     This is usually done by sourcing it from the .bash_profile.
886

887
     \b
888
     Examples:
889
       # Bash
890
       ## Bash completion on Linux depends on the 'bash-completion' package.
891
       ## Write the LocalStack CLI completion code for bash to a file and source it from .bash_profile
892
       localstack completion bash > ~/.localstack/completion.bash.inc
893
       printf "
894
       # LocalStack CLI bash completion
895
       source '$HOME/.localstack/completion.bash.inc'
896
       " >> $HOME/.bash_profile
897
       source $HOME/.bash_profile
898
    \b
899
       # zsh
900
       ## Set the LocalStack completion code for zsh to autoload on startup:
901
       localstack completion zsh > "${fpath[1]}/_localstack"
902
    \b
903
       # fish
904
       ## Set the LocalStack completion code for fish to autoload on startup:
905
       localstack completion fish > ~/.config/fish/completions/localstack.fish
906
    """
907

908
    # lookup the completion, raise an error if the given completion is not found
909
    import click.shell_completion
1✔
910

911
    comp_cls = click.shell_completion.get_completion_class(shell)
1✔
912
    if comp_cls is None:
1✔
UNCOV
913
        raise CLIError("Completion for given shell could not be found.")
×
914

915
    # Click's program name is the base path of sys.argv[0]
916
    path = sys.argv[0]
1✔
917
    prog_name = os.path.basename(path)
1✔
918

919
    # create the completion variable according to the docs
920
    # https://click.palletsprojects.com/en/8.1.x/shell-completion/#enabling-completion
921
    complete_var = f"_{prog_name}_COMPLETE".replace("-", "_").upper()
1✔
922

923
    # instantiate the completion class and print the completion source
924
    comp = comp_cls(ctx.command, {}, prog_name, complete_var)
1✔
925
    click.echo(comp.source())
1✔
926

927

928
def print_version() -> None:
1✔
929
    console.print(f"- [bold]LocalStack CLI:[/bold] [blue]{VERSION}[/blue]")
1✔
930

931

932
def print_profile() -> None:
1✔
933
    if config.LOADED_PROFILES:
1✔
934
        console.print(f"- [bold]Profile:[/bold] [blue]{', '.join(config.LOADED_PROFILES)}[/blue]")
1✔
935

936

937
def print_app() -> None:
1✔
938
    console.print("- [bold]App:[/bold] https://app.localstack.cloud")
1✔
939

940

941
def print_banner() -> None:
1✔
942
    print(BANNER)
1✔
943

944

945
def is_frozen_bundle() -> bool:
1✔
946
    """
947
    :return: true if we are currently running in a frozen bundle / a pyinstaller binary.
948
    """
949
    # check if we are in a PyInstaller binary
950
    # https://pyinstaller.org/en/stable/runtime-information.html
951
    return getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS")
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc