playground-ls-cli 4.14.1.dev8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. localstack_cli/__init__.py +0 -0
  2. localstack_cli/cli/__init__.py +10 -0
  3. localstack_cli/cli/console.py +11 -0
  4. localstack_cli/cli/core_plugin.py +12 -0
  5. localstack_cli/cli/exceptions.py +19 -0
  6. localstack_cli/cli/localstack.py +951 -0
  7. localstack_cli/cli/lpm.py +138 -0
  8. localstack_cli/cli/main.py +22 -0
  9. localstack_cli/cli/plugin.py +39 -0
  10. localstack_cli/cli/plugins.py +134 -0
  11. localstack_cli/cli/profiles.py +65 -0
  12. localstack_cli/config.py +1689 -0
  13. localstack_cli/constants.py +165 -0
  14. localstack_cli/logging/__init__.py +0 -0
  15. localstack_cli/logging/format.py +194 -0
  16. localstack_cli/logging/setup.py +142 -0
  17. localstack_cli/packages/__init__.py +25 -0
  18. localstack_cli/packages/api.py +418 -0
  19. localstack_cli/packages/core.py +416 -0
  20. localstack_cli/pro/__init__.py +0 -0
  21. localstack_cli/pro/core/__init__.py +0 -0
  22. localstack_cli/pro/core/bootstrap/__init__.py +1 -0
  23. localstack_cli/pro/core/bootstrap/auth.py +213 -0
  24. localstack_cli/pro/core/bootstrap/dns_utils.py +55 -0
  25. localstack_cli/pro/core/bootstrap/entitlements.py +117 -0
  26. localstack_cli/pro/core/bootstrap/extensions/__init__.py +3 -0
  27. localstack_cli/pro/core/bootstrap/extensions/__main__.py +106 -0
  28. localstack_cli/pro/core/bootstrap/extensions/autoinstall.py +63 -0
  29. localstack_cli/pro/core/bootstrap/extensions/bootstrap.py +97 -0
  30. localstack_cli/pro/core/bootstrap/extensions/repository.py +374 -0
  31. localstack_cli/pro/core/bootstrap/licensingv2.py +1259 -0
  32. localstack_cli/pro/core/bootstrap/pods/__init__.py +0 -0
  33. localstack_cli/pro/core/bootstrap/pods/api_types.py +17 -0
  34. localstack_cli/pro/core/bootstrap/pods/constants.py +26 -0
  35. localstack_cli/pro/core/bootstrap/pods/remotes/__init__.py +0 -0
  36. localstack_cli/pro/core/bootstrap/pods/remotes/api.py +75 -0
  37. localstack_cli/pro/core/bootstrap/pods/remotes/configs.py +69 -0
  38. localstack_cli/pro/core/bootstrap/pods/remotes/params.py +86 -0
  39. localstack_cli/pro/core/bootstrap/pods_client.py +834 -0
  40. localstack_cli/pro/core/cli/__init__.py +0 -0
  41. localstack_cli/pro/core/cli/auth.py +226 -0
  42. localstack_cli/pro/core/cli/aws.py +16 -0
  43. localstack_cli/pro/core/cli/cli.py +99 -0
  44. localstack_cli/pro/core/cli/click_utils.py +21 -0
  45. localstack_cli/pro/core/cli/cloud_pods.py +465 -0
  46. localstack_cli/pro/core/cli/diff_view.py +41 -0
  47. localstack_cli/pro/core/cli/ephemeral.py +199 -0
  48. localstack_cli/pro/core/cli/extensions.py +492 -0
  49. localstack_cli/pro/core/cli/iam.py +180 -0
  50. localstack_cli/pro/core/cli/license.py +90 -0
  51. localstack_cli/pro/core/cli/localstack.py +118 -0
  52. localstack_cli/pro/core/cli/replicator.py +378 -0
  53. localstack_cli/pro/core/cli/state.py +183 -0
  54. localstack_cli/pro/core/cli/tree_view.py +235 -0
  55. localstack_cli/pro/core/config.py +556 -0
  56. localstack_cli/pro/core/constants.py +54 -0
  57. localstack_cli/pro/core/plugins.py +169 -0
  58. localstack_cli/runtime/__init__.py +6 -0
  59. localstack_cli/runtime/exceptions.py +7 -0
  60. localstack_cli/runtime/hooks.py +73 -0
  61. localstack_cli/testing/__init__.py +1 -0
  62. localstack_cli/testing/config.py +4 -0
  63. localstack_cli/utils/__init__.py +0 -0
  64. localstack_cli/utils/analytics/__init__.py +12 -0
  65. localstack_cli/utils/analytics/cli.py +67 -0
  66. localstack_cli/utils/analytics/client.py +111 -0
  67. localstack_cli/utils/analytics/events.py +30 -0
  68. localstack_cli/utils/analytics/logger.py +48 -0
  69. localstack_cli/utils/analytics/metadata.py +250 -0
  70. localstack_cli/utils/analytics/publisher.py +160 -0
  71. localstack_cli/utils/analytics/service_request_aggregator.py +133 -0
  72. localstack_cli/utils/archives.py +271 -0
  73. localstack_cli/utils/batching.py +258 -0
  74. localstack_cli/utils/bootstrap.py +1418 -0
  75. localstack_cli/utils/checksum.py +313 -0
  76. localstack_cli/utils/collections.py +554 -0
  77. localstack_cli/utils/common.py +229 -0
  78. localstack_cli/utils/container_networking.py +142 -0
  79. localstack_cli/utils/container_utils/__init__.py +0 -0
  80. localstack_cli/utils/container_utils/container_client.py +1585 -0
  81. localstack_cli/utils/container_utils/docker_cmd_client.py +987 -0
  82. localstack_cli/utils/container_utils/docker_sdk_client.py +1018 -0
  83. localstack_cli/utils/crypto.py +294 -0
  84. localstack_cli/utils/docker_utils.py +272 -0
  85. localstack_cli/utils/files.py +327 -0
  86. localstack_cli/utils/functions.py +92 -0
  87. localstack_cli/utils/http.py +326 -0
  88. localstack_cli/utils/json.py +219 -0
  89. localstack_cli/utils/net.py +516 -0
  90. localstack_cli/utils/no_exit_argument_parser.py +19 -0
  91. localstack_cli/utils/numbers.py +49 -0
  92. localstack_cli/utils/objects.py +235 -0
  93. localstack_cli/utils/patch.py +260 -0
  94. localstack_cli/utils/platform.py +77 -0
  95. localstack_cli/utils/run.py +514 -0
  96. localstack_cli/utils/server/__init__.py +0 -0
  97. localstack_cli/utils/server/tcp_proxy.py +108 -0
  98. localstack_cli/utils/serving.py +187 -0
  99. localstack_cli/utils/ssl.py +71 -0
  100. localstack_cli/utils/strings.py +245 -0
  101. localstack_cli/utils/sync.py +267 -0
  102. localstack_cli/utils/threads.py +163 -0
  103. localstack_cli/utils/time.py +81 -0
  104. localstack_cli/utils/urls.py +21 -0
  105. localstack_cli/utils/venv.py +100 -0
  106. localstack_cli/utils/xml.py +41 -0
  107. localstack_cli/version.py +34 -0
  108. playground_ls_cli-4.14.1.dev8.dist-info/METADATA +95 -0
  109. playground_ls_cli-4.14.1.dev8.dist-info/RECORD +112 -0
  110. playground_ls_cli-4.14.1.dev8.dist-info/WHEEL +5 -0
  111. playground_ls_cli-4.14.1.dev8.dist-info/entry_points.txt +17 -0
  112. playground_ls_cli-4.14.1.dev8.dist-info/top_level.txt +1 -0
@@ -0,0 +1,951 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ import sys
5
+ import traceback
6
+ from typing import TypedDict
7
+
8
+ import click
9
+ import requests
10
+
11
+ from localstack_cli import config
12
+ from localstack_cli.cli.exceptions import CLIError
13
+ from localstack_cli.constants import VERSION
14
+ from localstack_cli.utils.analytics.cli import publish_invocation
15
+ from localstack_cli.utils.bootstrap import get_container_default_logfile_location
16
+ from localstack_cli.utils.json import CustomEncoder
17
+
18
+ from .console import BANNER, console
19
+ from .plugin import LocalstackCli, load_cli_plugins
20
+
21
+
22
+ class LocalStackCliGroup(click.Group):
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 = [
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):
49
+ try:
50
+ return super().invoke(ctx)
51
+ except click.exceptions.Exit:
52
+ # raise Exit exceptions unmodified (e.g., raised on --help)
53
+ raise
54
+ except click.ClickException:
55
+ # don't handle ClickExceptions, just reraise
56
+ if ctx and ctx.params.get("debug"):
57
+ click.echo(traceback.format_exc())
58
+ raise
59
+ except Exception as e:
60
+ if ctx and ctx.params.get("debug"):
61
+ click.echo(traceback.format_exc())
62
+ from localstack_cli.utils.container_utils.container_client import (
63
+ ContainerException,
64
+ DockerNotAvailable,
65
+ )
66
+
67
+ if isinstance(e, DockerNotAvailable):
68
+ raise CLIError(
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):
73
+ raise CLIError(e.message)
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:
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": []}
82
+
83
+ commands = []
84
+ for subcommand in self.list_commands(ctx):
85
+ cmd = self.get_command(ctx, subcommand)
86
+ # What is this, the tool lied about a command. Ignore it
87
+ if cmd is None:
88
+ continue
89
+ if cmd.hidden:
90
+ continue
91
+
92
+ commands.append((subcommand, cmd))
93
+
94
+ # allow for 3 times the default spacing
95
+ if len(commands):
96
+ limit = formatter.width - 6 - max(len(cmd[0]) for cmd in commands)
97
+
98
+ for subcommand, cmd in commands:
99
+ help = cmd.get_short_help_str(limit)
100
+ categories[self._get_category(cmd)].append((subcommand, help))
101
+
102
+ for category, rows in categories.items():
103
+ if rows:
104
+ with formatter.section(category):
105
+ formatter.write_dl(rows)
106
+
107
+ def _get_category(self, cmd) -> str:
108
+ if cmd.deprecated:
109
+ return "Deprecated"
110
+
111
+ if cmd.name in self.advanced_commands:
112
+ return "Advanced"
113
+
114
+ return "Commands"
115
+
116
+
117
+ def create_with_plugins() -> LocalstackCli:
118
+ """
119
+ Creates a LocalstackCli instance with all cli plugins loaded.
120
+ :return: a LocalstackCli instance
121
+ """
122
+ cli = LocalstackCli()
123
+ cli.group = localstack
124
+ load_cli_plugins(cli)
125
+ return cli
126
+
127
+
128
+ def _setup_cli_debug() -> None:
129
+ from localstack_cli.logging.setup import setup_logging_for_cli
130
+
131
+ config.DEBUG = True
132
+ os.environ["DEBUG"] = "1"
133
+
134
+ setup_logging_for_cli(logging.DEBUG if config.DEBUG else logging.INFO)
135
+
136
+
137
+ # Re-usable format option decorator which can be used across multiple commands
138
+ _click_format_option = click.option(
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(
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(
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")
168
+ @click.option("-p", "--profile", type=str, help="Set the configuration profile")
169
+ def localstack(debug, profile) -> None:
170
+ # --profile is read manually in localstack.cli.main because it needs to be read before localstack.config is read
171
+
172
+ if debug:
173
+ _setup_cli_debug()
174
+
175
+ from localstack_cli.utils.files import cache_dir
176
+
177
+ # overwrite the config variable here to defer import of cache_dir
178
+ if not os.environ.get("LOCALSTACK_VOLUME_DIR", "").strip():
179
+ config.VOLUME_DIR = str(cache_dir() / "volume")
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()
184
+
185
+
186
+ @localstack.group(
187
+ name="config",
188
+ short_help="Manage your LocalStack config",
189
+ )
190
+ def localstack_config() -> None:
191
+ """
192
+ Inspect and validate your LocalStack configuration.
193
+ """
194
+ pass
195
+
196
+
197
+ @localstack_config.command(name="show", short_help="Show your config")
198
+ @_click_format_option
199
+ @publish_invocation
200
+ def cmd_config_show(format_: str) -> None:
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
210
+
211
+ try:
212
+ # only load the pro config if it's available
213
+ from localstack_cli.pro.core import config as pro_config
214
+
215
+ assert pro_config
216
+ except ImportError:
217
+ # the pro package is not available
218
+ return None
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")
233
+ @click.option(
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
241
+ def cmd_config_validate(file: str) -> None:
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_cli.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:
265
+ import json
266
+
267
+ console.print(json.dumps(dict(config.collect_config_items()), cls=CustomEncoder))
268
+
269
+
270
+ def _print_config_pairs() -> None:
271
+ for key, value in config.collect_config_items():
272
+ console.print(f"{key}={value}")
273
+
274
+
275
+ def _print_config_dict() -> None:
276
+ console.print(dict(config.collect_config_items()))
277
+
278
+
279
+ def _print_config_table() -> None:
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(
293
+ name="status",
294
+ short_help="Query status info",
295
+ invoke_without_command=True,
296
+ )
297
+ @click.pass_context
298
+ def localstack_status(ctx: click.Context) -> None:
299
+ """
300
+ Query status information about the currently running LocalStack instance.
301
+ """
302
+ if ctx.invoked_subcommand is None:
303
+ ctx.invoke(localstack_status.get_command(ctx, "docker"))
304
+
305
+
306
+ @localstack_status.command(name="docker", short_help="Query LocalStack Docker status")
307
+ @_click_format_option
308
+ def cmd_status_docker(format_: str) -> None:
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):
318
+ running: bool
319
+ runtime_version: str
320
+ image_tag: str
321
+ image_id: str
322
+ image_created: str
323
+ container_name: str | None
324
+ container_ip: str | None
325
+
326
+
327
+ def _print_docker_status(format_: str) -> None:
328
+ from localstack_cli.utils import docker_utils
329
+ from localstack_cli.utils.bootstrap import get_docker_image_details, get_server_version
330
+ from localstack_cli.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:
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")
382
+ @_click_format_option
383
+ def cmd_status_services(format_: str) -> None:
384
+ """
385
+ Query information about the services of the currently running LocalStack instance.
386
+ """
387
+ url = config.external_service_url()
388
+
389
+ try:
390
+ health = requests.get(f"{url}/_localstack/health", timeout=2)
391
+ doc = health.json()
392
+ services = doc.get("services", [])
393
+ if format_ == "table":
394
+ _print_service_table(services)
395
+ if format_ == "plain":
396
+ for service, status in services.items():
397
+ console.print(f"{service}={status}")
398
+ if format_ == "dict":
399
+ console.print(services)
400
+ if format_ == "json":
401
+ console.print(json.dumps(services))
402
+ except requests.ConnectionError:
403
+ if config.DEBUG:
404
+ console.print_exception()
405
+ raise CLIError(f"could not connect to LocalStack health endpoint at {url}")
406
+
407
+
408
+ def _print_service_table(services: dict[str, str]) -> None:
409
+ from rich.table import Table
410
+
411
+ status_display = {
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()
419
+ table.add_column("Service")
420
+ table.add_column("Status")
421
+
422
+ services = list(services.items())
423
+ services.sort(key=lambda item: item[0])
424
+
425
+ for service, status in services:
426
+ if status in status_display:
427
+ status = status_display[status]
428
+
429
+ table.add_row(service, status)
430
+
431
+ console.print(table)
432
+
433
+
434
+ @localstack.command(name="start", short_help="Start LocalStack")
435
+ @click.option("--docker", is_flag=True, help="Start LocalStack in a docker container [default]")
436
+ @click.option("--host", is_flag=True, help="Start LocalStack directly on the host", deprecated=True)
437
+ @click.option("--no-banner", is_flag=True, help="Disable LocalStack banner", default=False)
438
+ @click.option(
439
+ "-d", "--detached", is_flag=True, help="Start LocalStack in the background", default=False
440
+ )
441
+ @click.option(
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(
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(
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(
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(
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(
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
483
+ def cmd_start(
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:
503
+ raise CLIError("Please specify either --docker or --host")
504
+ if host and detached:
505
+ raise CLIError("Cannot start detached in host mode")
506
+
507
+ if stack:
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:
520
+ print_banner()
521
+ print_version()
522
+ print_profile()
523
+ print_app()
524
+ console.line()
525
+
526
+ from localstack_cli.utils import bootstrap
527
+
528
+ if not no_banner:
529
+ if host:
530
+ console.log("starting LocalStack in host mode :laptop_computer:")
531
+ else:
532
+ console.log("starting LocalStack in Docker mode :whale:")
533
+
534
+ if host:
535
+ console.log(
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)
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"
546
+ config.dirs = config.init_directories()
547
+
548
+ try:
549
+ bootstrap.start_infra_locally()
550
+ except ImportError:
551
+ if config.DEBUG:
552
+ console.print_exception()
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
562
+ config.is_in_docker = False
563
+ config.dirs = config.init_directories()
564
+
565
+ # call hooks to prepare host (note that this call should stay below the config overrides above)
566
+ bootstrap.prepare_host(console)
567
+
568
+ # pass the parsed cli params to the start infra command
569
+ params = click.get_current_context().params
570
+
571
+ if network:
572
+ # reconciles the network config and makes sure that MAIN_DOCKER_NETWORK is set automatically if
573
+ # `--network` is set.
574
+ if config.MAIN_DOCKER_NETWORK:
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:
581
+ config.MAIN_DOCKER_NETWORK = network
582
+ os.environ["MAIN_DOCKER_NETWORK"] = network
583
+
584
+ if detached:
585
+ bootstrap.start_infra_in_docker_detached(console, params)
586
+ else:
587
+ bootstrap.start_infra_in_docker(console, params)
588
+
589
+
590
+ @localstack.command(name="stop", short_help="Stop LocalStack")
591
+ @publish_invocation
592
+ def cmd_stop() -> None:
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_cli.utils.docker_utils import DOCKER_CLIENT
603
+
604
+ from ..utils.container_utils.container_client import NoSuchContainer
605
+
606
+ container_name = config.MAIN_CONTAINER_NAME
607
+
608
+ try:
609
+ DOCKER_CLIENT.stop_container(container_name)
610
+ console.print(f"container stopped: {container_name}")
611
+ except NoSuchContainer:
612
+ raise CLIError(
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")
618
+ @publish_invocation
619
+ def cmd_restart() -> None:
620
+ """
621
+ Restarts the current LocalStack runtime.
622
+ """
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()
631
+ console.print("LocalStack restarted within the container.")
632
+ except requests.ConnectionError:
633
+ if config.DEBUG:
634
+ console.print_exception()
635
+ raise CLIError("could not restart the LocalStack container")
636
+
637
+
638
+ @localstack.command(
639
+ name="logs",
640
+ short_help="Show LocalStack logs",
641
+ )
642
+ @click.option(
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(
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
658
+ def cmd_logs(follow: bool, tail: int) -> None:
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_cli.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):
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
684
+ for line in DOCKER_CLIENT.stream_container_logs(container_name):
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:
691
+ logs = DOCKER_CLIENT.get_container_logs(container_name)
692
+ if tail is not None:
693
+ logs = "\n".join(logs.split("\n")[-tail:])
694
+ print(logs)
695
+
696
+
697
+ @localstack.command(name="wait", short_help="Wait for LocalStack")
698
+ @click.option(
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
707
+ def cmd_wait(timeout: float | None = None) -> None:
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
+ """
718
+ from localstack_cli.utils.bootstrap import wait_container_is_ready
719
+
720
+ if not wait_container_is_ready(timeout=timeout):
721
+ raise CLIError("timeout")
722
+
723
+
724
+ @localstack.command(name="ssh", short_help="Obtain a shell in LocalStack")
725
+ @publish_invocation
726
+ def cmd_ssh() -> None:
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_cli.utils.docker_utils import DOCKER_CLIENT
737
+
738
+ if not DOCKER_CLIENT.is_container_running(config.MAIN_CONTAINER_NAME):
739
+ raise CLIError(
740
+ f'Expected a running LocalStack container named "{config.MAIN_CONTAINER_NAME}", but found none'
741
+ )
742
+ os.execlp("docker", "docker", "exec", "-it", config.MAIN_CONTAINER_NAME, "bash")
743
+
744
+
745
+ @localstack.group(name="update", short_help="Update LocalStack")
746
+ def localstack_update() -> None:
747
+ """
748
+ Update different LocalStack components.
749
+ """
750
+ pass
751
+
752
+
753
+ @localstack_update.command(name="all", short_help="Update all LocalStack components")
754
+ @click.pass_context
755
+ @publish_invocation
756
+ def cmd_update_all(ctx: click.Context) -> None:
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
+ """
766
+ ctx.invoke(localstack_update.get_command(ctx, "localstack-cli"))
767
+ ctx.invoke(localstack_update.get_command(ctx, "docker-images"))
768
+
769
+
770
+ @localstack_update.command(name="localstack-cli", short_help="Update LocalStack CLI")
771
+ @publish_invocation
772
+ def cmd_update_localstack_cli() -> None:
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
+ """
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
+
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
+ )
796
+ console.print(":heavy_check_mark: LocalStack CLI updated")
797
+ except CalledProcessError:
798
+ console.print(":heavy_multiplication_x: LocalStack CLI update failed", style="bold red")
799
+
800
+
801
+ @localstack_update.command(
802
+ name="docker-images", short_help="Update docker images LocalStack depends on"
803
+ )
804
+ @publish_invocation
805
+ def cmd_update_docker_images() -> None:
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_cli.utils.docker_utils import DOCKER_CLIENT
813
+
814
+ console.rule("Updating docker images")
815
+
816
+ all_images = DOCKER_CLIENT.get_docker_image_names(strip_latest=False)
817
+ image_prefixes = [
818
+ "localstack/",
819
+ "public.ecr.aws/lambda",
820
+ ]
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:
834
+ from rich.markup import escape
835
+ from rich.progress import MofNCompleteColumn, Progress
836
+
837
+ from localstack_cli.utils.container_utils.container_client import ContainerException
838
+ from localstack_cli.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..."):
847
+ try:
848
+ updated = False
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
+ ):
855
+ updated = True
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
+ )
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
+ )
868
+ failed_count += 1
869
+ console.rule()
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")
876
+ @click.pass_context
877
+ @click.argument(
878
+ "shell", required=True, type=click.Choice(["bash", "zsh", "fish"], case_sensitive=False)
879
+ )
880
+ @publish_invocation
881
+ def localstack_completion(ctx: click.Context, shell: str) -> None:
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
910
+
911
+ comp_cls = click.shell_completion.get_completion_class(shell)
912
+ if comp_cls is None:
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]
917
+ prog_name = os.path.basename(path)
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()
922
+
923
+ # instantiate the completion class and print the completion source
924
+ comp = comp_cls(ctx.command, {}, prog_name, complete_var)
925
+ click.echo(comp.source())
926
+
927
+
928
+ def print_version() -> None:
929
+ console.print(f"- [bold]LocalStack CLI:[/bold] [blue]{VERSION}[/blue]")
930
+
931
+
932
+ def print_profile() -> None:
933
+ if config.LOADED_PROFILES:
934
+ console.print(f"- [bold]Profile:[/bold] [blue]{', '.join(config.LOADED_PROFILES)}[/blue]")
935
+
936
+
937
+ def print_app() -> None:
938
+ console.print("- [bold]App:[/bold] https://app.localstack.cloud")
939
+
940
+
941
+ def print_banner() -> None:
942
+ print(BANNER)
943
+
944
+
945
+ def is_frozen_bundle() -> bool:
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")