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

localstack / localstack / 7056d8d9-7428-4543-abf2-c5dedb2c98c0

30 May 2025 02:03PM UTC coverage: 86.654% (-0.003%) from 86.657%
7056d8d9-7428-4543-abf2-c5dedb2c98c0

push

circleci

web-flow
Add stack option for CLI start command (#12675)

Co-authored-by: Silvio Vasiljevic <silvio.vasiljevic@gmail.com>
Co-authored-by: Erudit Morina <83708693+eruditmorina@users.noreply.github.com>

2 of 9 new or added lines in 1 file covered. (22.22%)

37 existing lines in 4 files now uncovered.

64641 of 74597 relevant lines covered (86.65%)

0.87 hits per line

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

60.86
/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 Dict, List, Optional, Tuple, 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(LocalStackCliGroup, self).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
1✔
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 ext config if it's available
213
        from localstack.pro.core import config as ext_config
1✔
214

215
        assert ext_config
×
216
    except ImportError:
1✔
217
        # the ext 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
1✔
255

256
    if bootstrap.validate_localstack_config(file):
1✔
257
        console.print("[green]:heavy_check_mark:[/green] config valid")
1✔
258
        sys.exit(0)
1✔
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: Optional[str]
1✔
324
    container_ip: Optional[str]
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")
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
NEW
509
        stack_name = stack.split(":")[0]
×
NEW
510
        allowed_stacks = ("localstack", "localstack-pro", "snowflake")
×
NEW
511
        if stack_name.lower() not in allowed_stacks:
×
NEW
512
            raise CLIError(f"Invalid stack '{stack_name}'. Allowed stacks: {allowed_stacks}.")
×
513

514
        # Set IMAGE_NAME, defaulting to :latest if no version specified
NEW
515
        if ":" not in stack:
×
NEW
516
            stack = f"{stack}:latest"
×
NEW
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
        # call hooks to prepare host
536
        bootstrap.prepare_host(console)
1✔
537

538
        # from here we abandon the regular CLI control path and start treating the process like a localstack
539
        # runtime process
540
        os.environ["LOCALSTACK_CLI"] = "0"
1✔
541
        config.dirs = config.init_directories()
1✔
542

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

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

563
        # pass the parsed cli params to the start infra command
564
        params = click.get_current_context().params
1✔
565

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

579
        if detached:
1✔
580
            bootstrap.start_infra_in_docker_detached(console, params)
×
581
        else:
582
            bootstrap.start_infra_in_docker(console, params)
1✔
583

584

585
@localstack.command(name="stop", short_help="Stop LocalStack")
1✔
586
@publish_invocation
1✔
587
def cmd_stop() -> None:
1✔
588
    """
589
    Stops the current LocalStack runtime.
590

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

599
    from ..utils.container_utils.container_client import NoSuchContainer
1✔
600

601
    container_name = config.MAIN_CONTAINER_NAME
1✔
602

603
    try:
1✔
604
        DOCKER_CLIENT.stop_container(container_name)
1✔
605
        console.print("container stopped: %s" % container_name)
×
606
    except NoSuchContainer:
1✔
607
        raise CLIError(
1✔
608
            f'Expected a running LocalStack container named "{container_name}", but found none'
609
        )
610

611

612
@localstack.command(name="restart", short_help="Restart LocalStack")
1✔
613
@publish_invocation
1✔
614
def cmd_restart() -> None:
1✔
615
    """
616
    Restarts the current LocalStack runtime.
617
    """
618
    url = config.external_service_url()
×
619

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

632

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

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

665
    container_name = config.MAIN_CONTAINER_NAME
×
666
    logfile = get_container_default_logfile_location(container_name)
×
667

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

677
    if follow:
×
678
        num_lines = 0
×
679
        for line in DOCKER_CLIENT.stream_container_logs(container_name):
×
680
            print(line.decode("utf-8").rstrip("\r\n"))
×
681
            num_lines += 1
×
682
            if tail is not None and num_lines >= tail:
×
683
                break
×
684

685
    else:
686
        logs = DOCKER_CLIENT.get_container_logs(container_name)
×
687
        if tail is not None:
×
688
            logs = "\n".join(logs.split("\n")[-tail:])
×
689
        print(logs)
×
690

691

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

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

715
    if not wait_container_is_ready(timeout=timeout):
×
716
        raise CLIError("timeout")
×
717

718

719
@localstack.command(name="ssh", short_help="Obtain a shell in LocalStack")
1✔
720
@publish_invocation
1✔
721
def cmd_ssh() -> None:
1✔
722
    """
723
    Obtain a shell in the current LocalStack runtime.
724

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

733
    if not DOCKER_CLIENT.is_container_running(config.MAIN_CONTAINER_NAME):
1✔
734
        raise CLIError(
1✔
735
            f'Expected a running LocalStack container named "{config.MAIN_CONTAINER_NAME}", but found none'
736
        )
737
    os.execlp("docker", "docker", "exec", "-it", config.MAIN_CONTAINER_NAME, "bash")
×
738

739

740
@localstack.group(name="update", short_help="Update LocalStack")
1✔
741
def localstack_update() -> None:
1✔
742
    """
743
    Update different LocalStack components.
744
    """
745
    pass
×
746

747

748
@localstack_update.command(name="all", short_help="Update all LocalStack components")
1✔
749
@click.pass_context
1✔
750
@publish_invocation
1✔
751
def cmd_update_all(ctx: click.Context) -> None:
1✔
752
    """
753
    Update all LocalStack components.
754

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

764

765
@localstack_update.command(name="localstack-cli", short_help="Update LocalStack CLI")
1✔
766
@publish_invocation
1✔
767
def cmd_update_localstack_cli() -> None:
1✔
768
    """
769
    Update the LocalStack CLI.
770

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

782
    import subprocess
×
783
    from subprocess import CalledProcessError
×
784

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

795

796
@localstack_update.command(
1✔
797
    name="docker-images", short_help="Update docker images LocalStack depends on"
798
)
799
@publish_invocation
1✔
800
def cmd_update_docker_images() -> None:
1✔
801
    """
802
    Update all Docker images LocalStack depends on.
803

804
    This command updates all Docker LocalStack docker images, as well as other Docker images
805
    LocalStack depends on (and which have been used before / are present on the machine).
806
    """
807
    from localstack.utils.docker_utils import DOCKER_CLIENT
×
808

809
    console.rule("Updating docker images")
×
810

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

827

828
def update_images(image_list: List[str]) -> None:
1✔
829
    from rich.markup import escape
×
830
    from rich.progress import MofNCompleteColumn, Progress
×
831

832
    from localstack.utils.container_utils.container_client import ContainerException
×
833
    from localstack.utils.docker_utils import DOCKER_CLIENT
×
834

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

869

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

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

903
    # lookup the completion, raise an error if the given completion is not found
904
    import click.shell_completion
1✔
905

906
    comp_cls = click.shell_completion.get_completion_class(shell)
1✔
907
    if comp_cls is None:
1✔
908
        raise CLIError("Completion for given shell could not be found.")
×
909

910
    # Click's program name is the base path of sys.argv[0]
911
    path = sys.argv[0]
1✔
912
    prog_name = os.path.basename(path)
1✔
913

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

918
    # instantiate the completion class and print the completion source
919
    comp = comp_cls(ctx.command, {}, prog_name, complete_var)
1✔
920
    click.echo(comp.source())
1✔
921

922

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

926

927
def print_profile() -> None:
1✔
928
    if config.LOADED_PROFILES:
1✔
929
        console.print(f"- [bold]Profile:[/bold] [blue]{', '.join(config.LOADED_PROFILES)}[/blue]")
1✔
930

931

932
def print_app() -> None:
1✔
933
    console.print("- [bold]App:[/bold] https://app.localstack.cloud")
1✔
934

935

936
def print_banner() -> None:
1✔
937
    print(BANNER)
1✔
938

939

940
def is_frozen_bundle() -> bool:
1✔
941
    """
942
    :return: true if we are currently running in a frozen bundle / a pyinstaller binary.
943
    """
944
    # check if we are in a PyInstaller binary
945
    # https://pyinstaller.org/en/stable/runtime-information.html
946
    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