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

localstack / localstack / 21422875763

27 Jan 2026 01:25PM UTC coverage: 86.973% (+0.01%) from 86.96%
21422875763

push

github

web-flow
fix(docs): repair broken links in CONTRIBUTING.md (#13647)

70356 of 80894 relevant lines covered (86.97%)

0.87 hits per line

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

59.95
/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
    # check if we are about to activate Pro, if not raise a warning about the upcoming changes
527
    # LOCALSTACK_ACTIVATE_PRO is set by the Pro config if the auth token is cached or explicitly set in the env
528
    if not config.is_env_true("LOCALSTACK_ACTIVATE_PRO"):
1✔
529
        console.log(
1✔
530
            "Warning: You are starting LocalStack without an auth token. "
531
            "Starting in March 2026, LocalStack will require an auth token. "
532
            "Go to this page for more infos: https://localstack.cloud/2026-updates",
533
            style="bold red",
534
        )
535

536
    from localstack.utils import bootstrap
1✔
537

538
    if not no_banner:
1✔
539
        if host:
1✔
540
            console.log("starting LocalStack in host mode :laptop_computer:")
1✔
541
        else:
542
            console.log("starting LocalStack in Docker mode :whale:")
1✔
543

544
    if host:
1✔
545
        console.log(
1✔
546
            "Warning: Starting LocalStack in host mode from the CLI is deprecated and will be removed soon. Please use the default Docker mode instead.",
547
            style="bold red",
548
        )
549

550
        # call hooks to prepare host
551
        bootstrap.prepare_host(console)
1✔
552

553
        # from here we abandon the regular CLI control path and start treating the process like a localstack
554
        # runtime process
555
        os.environ["LOCALSTACK_CLI"] = "0"
1✔
556
        config.dirs = config.init_directories()
1✔
557

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

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

578
        # pass the parsed cli params to the start infra command
579
        params = click.get_current_context().params
1✔
580

581
        if network:
1✔
582
            # reconciles the network config and makes sure that MAIN_DOCKER_NETWORK is set automatically if
583
            # `--network` is set.
584
            if config.MAIN_DOCKER_NETWORK:
×
585
                if config.MAIN_DOCKER_NETWORK != network:
×
586
                    raise CLIError(
×
587
                        f"Values of MAIN_DOCKER_NETWORK={config.MAIN_DOCKER_NETWORK} and --network={network} "
588
                        f"do not match"
589
                    )
590
            else:
591
                config.MAIN_DOCKER_NETWORK = network
×
592
                os.environ["MAIN_DOCKER_NETWORK"] = network
×
593

594
        if detached:
1✔
595
            bootstrap.start_infra_in_docker_detached(console, params)
×
596
        else:
597
            bootstrap.start_infra_in_docker(console, params)
1✔
598

599

600
@localstack.command(name="stop", short_help="Stop LocalStack")
1✔
601
@publish_invocation
1✔
602
def cmd_stop() -> None:
1✔
603
    """
604
    Stops the current LocalStack runtime.
605

606
    This command stops the currently running LocalStack docker container.
607
    By default, this command looks for a container named `localstack-main` (which is the default
608
    container name used by the `localstack start` command).
609
    If your LocalStack container has a different name, set the config variable
610
    `MAIN_CONTAINER_NAME`.
611
    """
612
    from localstack.utils.docker_utils import DOCKER_CLIENT
1✔
613

614
    from ..utils.container_utils.container_client import NoSuchContainer
1✔
615

616
    container_name = config.MAIN_CONTAINER_NAME
1✔
617

618
    try:
1✔
619
        DOCKER_CLIENT.stop_container(container_name)
1✔
620
        console.print(f"container stopped: {container_name}")
×
621
    except NoSuchContainer:
1✔
622
        raise CLIError(
1✔
623
            f'Expected a running LocalStack container named "{container_name}", but found none'
624
        )
625

626

627
@localstack.command(name="restart", short_help="Restart LocalStack")
1✔
628
@publish_invocation
1✔
629
def cmd_restart() -> None:
1✔
630
    """
631
    Restarts the current LocalStack runtime.
632
    """
633
    url = config.external_service_url()
×
634

635
    try:
×
636
        response = requests.post(
×
637
            f"{url}/_localstack/health",
638
            json={"action": "restart"},
639
        )
640
        response.raise_for_status()
×
641
        console.print("LocalStack restarted within the container.")
×
642
    except requests.ConnectionError:
×
643
        if config.DEBUG:
×
644
            console.print_exception()
×
645
        raise CLIError("could not restart the LocalStack container")
×
646

647

648
@localstack.command(
1✔
649
    name="logs",
650
    short_help="Show LocalStack logs",
651
)
652
@click.option(
1✔
653
    "-f",
654
    "--follow",
655
    is_flag=True,
656
    help="Block the terminal and follow the log output",
657
    default=False,
658
)
659
@click.option(
1✔
660
    "-n",
661
    "--tail",
662
    type=int,
663
    help="Print only the last <N> lines of the log output",
664
    default=None,
665
    metavar="N",
666
)
667
@publish_invocation
1✔
668
def cmd_logs(follow: bool, tail: int) -> None:
1✔
669
    """
670
    Show the logs of the current LocalStack runtime.
671

672
    This command shows the logs of the currently running LocalStack docker container.
673
    By default, this command looks for a container named `localstack-main` (which is the default
674
    container name used by the `localstack start` command).
675
    If your LocalStack container has a different name, set the config variable
676
    `MAIN_CONTAINER_NAME`.
677
    """
678
    from localstack.utils.docker_utils import DOCKER_CLIENT
×
679

680
    container_name = config.MAIN_CONTAINER_NAME
×
681
    logfile = get_container_default_logfile_location(container_name)
×
682

683
    if not DOCKER_CLIENT.is_container_running(container_name):
×
684
        console.print("localstack container not running")
×
685
        if os.path.exists(logfile):
×
686
            console.print("printing logs from previous run")
×
687
            with open(logfile) as fd:
×
688
                for line in fd:
×
689
                    click.echo(line, nl=False)
×
690
        sys.exit(1)
×
691

692
    if follow:
×
693
        num_lines = 0
×
694
        for line in DOCKER_CLIENT.stream_container_logs(container_name):
×
695
            print(line.decode("utf-8").rstrip("\r\n"))
×
696
            num_lines += 1
×
697
            if tail is not None and num_lines >= tail:
×
698
                break
×
699

700
    else:
701
        logs = DOCKER_CLIENT.get_container_logs(container_name)
×
702
        if tail is not None:
×
703
            logs = "\n".join(logs.split("\n")[-tail:])
×
704
        print(logs)
×
705

706

707
@localstack.command(name="wait", short_help="Wait for LocalStack")
1✔
708
@click.option(
1✔
709
    "-t",
710
    "--timeout",
711
    type=float,
712
    help="Only wait for <N> seconds before raising a timeout error",
713
    default=None,
714
    metavar="N",
715
)
716
@publish_invocation
1✔
717
def cmd_wait(timeout: float | None = None) -> None:
1✔
718
    """
719
    Wait for the LocalStack runtime to be up and running.
720

721
    This commands waits for a started LocalStack runtime to be up and running, ready to serve
722
    requests.
723
    By default, this command looks for a container named `localstack-main` (which is the default
724
    container name used by the `localstack start` command).
725
    If your LocalStack container has a different name, set the config variable
726
    `MAIN_CONTAINER_NAME`.
727
    """
728
    from localstack.utils.bootstrap import wait_container_is_ready
×
729

730
    if not wait_container_is_ready(timeout=timeout):
×
731
        raise CLIError("timeout")
×
732

733

734
@localstack.command(name="ssh", short_help="Obtain a shell in LocalStack")
1✔
735
@publish_invocation
1✔
736
def cmd_ssh() -> None:
1✔
737
    """
738
    Obtain a shell in the current LocalStack runtime.
739

740
    This command starts a new interactive shell in the currently running LocalStack container.
741
    By default, this command looks for a container named `localstack-main` (which is the default
742
    container name used by the `localstack start` command).
743
    If your LocalStack container has a different name, set the config variable
744
    `MAIN_CONTAINER_NAME`.
745
    """
746
    from localstack.utils.docker_utils import DOCKER_CLIENT
1✔
747

748
    if not DOCKER_CLIENT.is_container_running(config.MAIN_CONTAINER_NAME):
1✔
749
        raise CLIError(
1✔
750
            f'Expected a running LocalStack container named "{config.MAIN_CONTAINER_NAME}", but found none'
751
        )
752
    os.execlp("docker", "docker", "exec", "-it", config.MAIN_CONTAINER_NAME, "bash")
×
753

754

755
@localstack.group(name="update", short_help="Update LocalStack")
1✔
756
def localstack_update() -> None:
1✔
757
    """
758
    Update different LocalStack components.
759
    """
760
    pass
×
761

762

763
@localstack_update.command(name="all", short_help="Update all LocalStack components")
1✔
764
@click.pass_context
1✔
765
@publish_invocation
1✔
766
def cmd_update_all(ctx: click.Context) -> None:
1✔
767
    """
768
    Update all LocalStack components.
769

770
    This is the same as executing `localstack update localstack-cli` and
771
    `localstack update docker-images`.
772
    Updating the LocalStack CLI is currently only supported if the CLI
773
    is installed and run via Python / PIP. If you used a different installation method,
774
    please follow the instructions on https://docs.localstack.cloud/.
775
    """
776
    ctx.invoke(localstack_update.get_command(ctx, "localstack-cli"))
×
777
    ctx.invoke(localstack_update.get_command(ctx, "docker-images"))
×
778

779

780
@localstack_update.command(name="localstack-cli", short_help="Update LocalStack CLI")
1✔
781
@publish_invocation
1✔
782
def cmd_update_localstack_cli() -> None:
1✔
783
    """
784
    Update the LocalStack CLI.
785

786
    This command updates the LocalStack CLI. This is currently only supported if the CLI
787
    is installed and run via Python / PIP. If you used a different installation method,
788
    please follow the instructions on https://docs.localstack.cloud/.
789
    """
790
    if is_frozen_bundle():
×
791
        # "update" can only be performed if running from source / in a non-frozen interpreter
792
        raise CLIError(
×
793
            "The LocalStack CLI can only update itself if installed via PIP. "
794
            "Please follow the instructions on https://docs.localstack.cloud/ to update your CLI."
795
        )
796

797
    import subprocess
×
798
    from subprocess import CalledProcessError
×
799

800
    console.rule("Updating LocalStack CLI")
×
801
    with console.status("Updating LocalStack CLI..."):
×
802
        try:
×
803
            subprocess.check_output(
×
804
                [sys.executable, "-m", "pip", "install", "--upgrade", "localstack"]
805
            )
806
            console.print(":heavy_check_mark: LocalStack CLI updated")
×
807
        except CalledProcessError:
×
808
            console.print(":heavy_multiplication_x: LocalStack CLI update failed", style="bold red")
×
809

810

811
@localstack_update.command(
1✔
812
    name="docker-images", short_help="Update docker images LocalStack depends on"
813
)
814
@publish_invocation
1✔
815
def cmd_update_docker_images() -> None:
1✔
816
    """
817
    Update all Docker images LocalStack depends on.
818

819
    This command updates all Docker LocalStack docker images, as well as other Docker images
820
    LocalStack depends on (and which have been used before / are present on the machine).
821
    """
822
    from localstack.utils.docker_utils import DOCKER_CLIENT
×
823

824
    console.rule("Updating docker images")
×
825

826
    all_images = DOCKER_CLIENT.get_docker_image_names(strip_latest=False)
×
827
    image_prefixes = [
×
828
        "localstack/",
829
        "public.ecr.aws/lambda",
830
    ]
831
    localstack_images = [
×
832
        image
833
        for image in all_images
834
        if any(
835
            image.startswith(image_prefix) or image.startswith(f"docker.io/{image_prefix}")
836
            for image_prefix in image_prefixes
837
        )
838
        and not image.endswith(":<none>")  # ignore dangling images
839
    ]
840
    update_images(localstack_images)
×
841

842

843
def update_images(image_list: list[str]) -> None:
1✔
844
    from rich.markup import escape
×
845
    from rich.progress import MofNCompleteColumn, Progress
×
846

847
    from localstack.utils.container_utils.container_client import ContainerException
×
848
    from localstack.utils.docker_utils import DOCKER_CLIENT
×
849

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

884

885
@localstack.command(name="completion", short_help="CLI shell completion")
1✔
886
@click.pass_context
1✔
887
@click.argument(
1✔
888
    "shell", required=True, type=click.Choice(["bash", "zsh", "fish"], case_sensitive=False)
889
)
890
@publish_invocation
1✔
891
def localstack_completion(ctx: click.Context, shell: str) -> None:
1✔
892
    """
893
     Print shell completion code for the specified shell (bash, zsh, or fish).
894
     The shell code must be evaluated to enable the interactive shell completion of LocalStack CLI commands.
895
     This is usually done by sourcing it from the .bash_profile.
896

897
     \b
898
     Examples:
899
       # Bash
900
       ## Bash completion on Linux depends on the 'bash-completion' package.
901
       ## Write the LocalStack CLI completion code for bash to a file and source it from .bash_profile
902
       localstack completion bash > ~/.localstack/completion.bash.inc
903
       printf "
904
       # LocalStack CLI bash completion
905
       source '$HOME/.localstack/completion.bash.inc'
906
       " >> $HOME/.bash_profile
907
       source $HOME/.bash_profile
908
    \b
909
       # zsh
910
       ## Set the LocalStack completion code for zsh to autoload on startup:
911
       localstack completion zsh > "${fpath[1]}/_localstack"
912
    \b
913
       # fish
914
       ## Set the LocalStack completion code for fish to autoload on startup:
915
       localstack completion fish > ~/.config/fish/completions/localstack.fish
916
    """
917

918
    # lookup the completion, raise an error if the given completion is not found
919
    import click.shell_completion
1✔
920

921
    comp_cls = click.shell_completion.get_completion_class(shell)
1✔
922
    if comp_cls is None:
1✔
923
        raise CLIError("Completion for given shell could not be found.")
×
924

925
    # Click's program name is the base path of sys.argv[0]
926
    path = sys.argv[0]
1✔
927
    prog_name = os.path.basename(path)
1✔
928

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

933
    # instantiate the completion class and print the completion source
934
    comp = comp_cls(ctx.command, {}, prog_name, complete_var)
1✔
935
    click.echo(comp.source())
1✔
936

937

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

941

942
def print_profile() -> None:
1✔
943
    if config.LOADED_PROFILES:
1✔
944
        console.print(f"- [bold]Profile:[/bold] [blue]{', '.join(config.LOADED_PROFILES)}[/blue]")
1✔
945

946

947
def print_app() -> None:
1✔
948
    console.print("- [bold]App:[/bold] https://app.localstack.cloud")
1✔
949

950

951
def print_banner() -> None:
1✔
952
    print(BANNER)
1✔
953

954

955
def is_frozen_bundle() -> bool:
1✔
956
    """
957
    :return: true if we are currently running in a frozen bundle / a pyinstaller binary.
958
    """
959
    # check if we are in a PyInstaller binary
960
    # https://pyinstaller.org/en/stable/runtime-information.html
961
    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