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,492 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from collections.abc import Iterable
6
+
7
+ import click
8
+ from click import ClickException
9
+ from localstack_cli import constants
10
+ from localstack_cli.cli import console
11
+ from localstack_cli.utils.analytics.cli import publish_invocation
12
+ from localstack_cli.utils.container_utils.container_client import (
13
+ BindMount,
14
+ ContainerConfiguration,
15
+ ContainerConfigurator,
16
+ ContainerException,
17
+ )
18
+ from rich.status import Status
19
+ from rich.table import Table
20
+
21
+ from .cli import RequiresLicenseGroup
22
+
23
+ _PYTHON_IN_CONTAINER = "/opt/code/localstack/.venv/bin/python"
24
+ """Path inside the container pointing to the Python executable."""
25
+
26
+
27
+ @click.group(
28
+ name="extensions",
29
+ short_help="(Preview) Manage LocalStack extensions",
30
+ help="""
31
+ (Preview) Manage LocalStack extensions.
32
+
33
+ LocalStack Extensions allow developers to extend and customize LocalStack.
34
+ The feature and the API are currently in a preview stage and may be subject to change.
35
+
36
+ If you are using LocalStack extensions with docker-compose, you can use the CLI by pointing the
37
+ `LOCALSTACK_VOLUME_DIR=` variable to localstack volume directory on your host. By default, the volume
38
+ on your host is located in `~/.cache/localstack` on Linux, and `~/Library/Caches` on Mac.
39
+
40
+ Visit https://docs.localstack.cloud/references/localstack-extensions/ for more information on LocalStack
41
+ Extensions.
42
+ """,
43
+ cls=RequiresLicenseGroup,
44
+ )
45
+ @click.pass_context
46
+ @click.option("-v", "--verbose", is_flag=True, default=False, help="Print more output")
47
+ def extensions(ctx: click.Context, verbose: bool) -> None:
48
+ ctx.ensure_object(dict)
49
+ ctx.obj["VERBOSE"] = verbose
50
+
51
+
52
+ @extensions.command(
53
+ "init",
54
+ help="""
55
+ Initialize the LocalStack extensions environment.
56
+
57
+ The environment variable `LOCALSTACK_VOLUME_DIR` currently defaults to ~/.cache/localstack, where the
58
+ extension environment will be installed into ./lib/extensions/
59
+ """,
60
+ )
61
+ @publish_invocation
62
+ def cmd_extensions_init() -> None:
63
+ for line in _stream_localstack_container_command(
64
+ [_PYTHON_IN_CONTAINER, "-m", "localstack.pro.core.bootstrap.extensions", "init"]
65
+ ):
66
+ console.log(line)
67
+
68
+
69
+ @extensions.command(
70
+ "install",
71
+ help="""
72
+ Install a LocalStack extension.
73
+
74
+ This command installs a LocalStack extension, where the name can be any valid pip dependency
75
+ identifier. Additionally, we support the installation of distribution files from disk, which you can
76
+ indicate by a ``file://`` prefix in the name
77
+
78
+ \b
79
+ Example invocations:
80
+ localstack extensions install localstack-extension-stripe
81
+ localstack extensions install "git+https://github.com/localstack/localstack-stripe.git#egg=localstack-stripe"
82
+ localstack extensions install file://./dist/localstack-extension-hello-world-0.1.0.tar.gz
83
+ localstack extensions install file://. # assumes the current directory is a source distribution
84
+ """,
85
+ )
86
+ @click.pass_context
87
+ @click.argument("name", required=True)
88
+ @publish_invocation
89
+ def cmd_extensions_install(ctx: click.Context, name: str) -> None:
90
+ configurators = []
91
+ if name.startswith("file://"):
92
+ file_path = name[7:]
93
+ if not os.path.exists(file_path):
94
+ raise ClickException(f"No such file {file_path}")
95
+ file_path = os.path.abspath(file_path)
96
+
97
+ # map the host path to a container path
98
+ # not using os.path here because it needs to be a unix path in the container
99
+ name = f"/tmp/{os.path.basename(file_path)}"
100
+ configurators.append(lambda cfg: cfg.volumes.add(BindMount(file_path, name)))
101
+
102
+ status = console.status("Initializing")
103
+
104
+ with status:
105
+ _ensure_venv_initialized()
106
+
107
+ stream = _stream_localstack_container_command(
108
+ [
109
+ _PYTHON_IN_CONTAINER,
110
+ "-m",
111
+ "localstack.pro.core.bootstrap.extensions",
112
+ "install",
113
+ name,
114
+ ],
115
+ configurators,
116
+ )
117
+ _process_extensions_event_stream(stream, status, ctx.obj["VERBOSE"])
118
+
119
+
120
+ @extensions.command(
121
+ "uninstall",
122
+ help="""
123
+ Remove a LocalStack extension.
124
+
125
+ This command removes a previously installed LocalStack extension, where the name can be any valid package name.
126
+
127
+ \b
128
+ Example invocations:
129
+ localstack extensions uninstall localstack-extension-stripe
130
+ """,
131
+ )
132
+ @click.pass_context
133
+ @click.argument("name", required=True)
134
+ @publish_invocation
135
+ def cmd_extensions_uninstall(ctx: click.Context, name: str) -> None:
136
+ status = console.status("Initializing")
137
+
138
+ with status:
139
+ _ensure_venv_initialized()
140
+
141
+ stream = _stream_localstack_container_command(
142
+ [
143
+ _PYTHON_IN_CONTAINER,
144
+ "-m",
145
+ "localstack.pro.core.bootstrap.extensions",
146
+ "uninstall",
147
+ name,
148
+ ]
149
+ )
150
+ _process_extensions_event_stream(stream, status, ctx.obj["VERBOSE"])
151
+
152
+
153
+ @extensions.command(
154
+ "list",
155
+ help="""
156
+ List installed extension.
157
+
158
+ The environment variable `LOCALSTACK_VOLUME_DIR` currently defaults to ~/.cache/localstack, where the
159
+ extension environment will be installed into ./lib/extensions/
160
+ """,
161
+ )
162
+ @publish_invocation
163
+ def cmd_extensions_list() -> None:
164
+ cmd = [_PYTHON_IN_CONTAINER, "-m", "localstack.pro.core.bootstrap.extensions", "list"]
165
+
166
+ status = console.status("Querying ...")
167
+ status.start()
168
+ try:
169
+ lines = _stream_localstack_container_command(cmd)
170
+
171
+ extensions_ = []
172
+
173
+ for line in lines:
174
+ line = line.strip()
175
+ if not line:
176
+ continue
177
+ try:
178
+ doc = json.loads(line)
179
+ extensions_.append(doc)
180
+ except json.JSONDecodeError:
181
+ # skip lines that cannot be parsed as JSON
182
+ pass
183
+
184
+ status.stop()
185
+
186
+ console.print(_extension_metadata_table(extensions_))
187
+ finally:
188
+ status.stop()
189
+
190
+
191
+ def _ensure_venv_initialized():
192
+ try:
193
+ _assert_venv_initialized()
194
+ except ClickException:
195
+ for line in _stream_localstack_container_command(
196
+ [_PYTHON_IN_CONTAINER, "-m", "localstack.pro.core.bootstrap.extensions", "init"]
197
+ ):
198
+ console.log(line)
199
+
200
+ _assert_venv_initialized()
201
+
202
+
203
+ def _assert_venv_initialized() -> None:
204
+ from localstack_cli import config
205
+ from localstack_cli.pro.core.bootstrap.extensions import repository
206
+
207
+ path = os.path.join(config.VOLUME_DIR, "lib", repository.VENV_DIRECTORY)
208
+
209
+ if not os.path.exists(path):
210
+ raise ClickException(
211
+ "extensions dir not initialized, please run `localstack extensions init` "
212
+ "first or check if `LOCALSTACK_VOLUME_DIR` is set correctly"
213
+ )
214
+
215
+
216
+ def _process_extensions_event_stream(stream: Iterable[str | dict], status: Status, verbose: bool):
217
+ """
218
+ The extension event stream is
219
+ :param stream:
220
+ :param status:
221
+ :param verbose:
222
+ :return:
223
+ """
224
+ extensions_ = []
225
+ exception = False
226
+
227
+ for line in stream:
228
+ try:
229
+ if isinstance(line, dict):
230
+ event = line
231
+ else:
232
+ event = json.loads(line)
233
+ except json.JSONDecodeError:
234
+ console.log("couldn't parse container response", line)
235
+ continue
236
+
237
+ if "event" not in event:
238
+ console.log("couldn't parse container response", line)
239
+ elif event["event"] == "status":
240
+ status.update(event["message"])
241
+ elif event["event"] == "log":
242
+ console.log(event["message"])
243
+ elif event["event"] == "error":
244
+ console.log("Error:", event["message"])
245
+ elif event["event"] == "pip":
246
+ if verbose:
247
+ console.log(event["message"])
248
+ elif event["event"] == "extension":
249
+ extensions_.append(event["extra"])
250
+ elif event["event"] == "exception":
251
+ console.log(event["message"])
252
+ exception = True
253
+ if verbose and "extra" in event:
254
+ console.log(event["extra"].get("traceback"))
255
+ else:
256
+ console.log("unknown event type in container response", line)
257
+
258
+ if exception and not verbose:
259
+ console.log(
260
+ "An error occurred while processing the extension. You can run the the extensions command again "
261
+ "with the --verbose flag to get more information about the error."
262
+ )
263
+
264
+ # this is only used for the install command to list the extensions that were installed
265
+ extensions_ = [e for e in extensions_ if e]
266
+ if extensions_:
267
+ console.print(_extension_metadata_table(extensions_))
268
+
269
+
270
+ def _extension_metadata_table(extensions_: list[dict]) -> Table:
271
+ """Creates a rich.Table from the given list of extension metadata objects."""
272
+ t = Table()
273
+ t.add_column("Name")
274
+ t.add_column("Summary")
275
+ t.add_column("Version")
276
+ t.add_column("Author")
277
+ t.add_column("Plugin name")
278
+
279
+ for e in extensions_:
280
+ if not (author := e["distribution"].get("author")):
281
+ author = e["distribution"].get("author_email")
282
+
283
+ t.add_row(
284
+ e["distribution"]["name"],
285
+ e["distribution"]["summary"],
286
+ e["distribution"]["version"],
287
+ author,
288
+ e["name"],
289
+ )
290
+
291
+ return t
292
+
293
+
294
+ @extensions.group("dev")
295
+ def dev() -> None:
296
+ """
297
+ Developer tools for developing LocalStack extensions.
298
+ """
299
+ pass
300
+
301
+
302
+ @dev.command(
303
+ "new",
304
+ help="""
305
+ Create a new LocalStack extension from the official extension template.
306
+
307
+ \b
308
+ The templating relies on cookiecutter, which you can install with
309
+ pip install cookiecutter
310
+
311
+ \b
312
+ The template can be found at
313
+ https://github.com/localstack/localstack-extensions/tree/main/template.
314
+
315
+ The new extension will be created in your current working directory under the <project_slug> parameter.
316
+ """,
317
+ )
318
+ @click.option(
319
+ "--template",
320
+ default="basic",
321
+ help="Specify the template to use from "
322
+ "https://github.com/localstack/localstack-extensions/tree/main/templates",
323
+ )
324
+ @publish_invocation
325
+ def cmd_dev_new(template: str) -> None:
326
+ try:
327
+ from cookiecutter.main import cookiecutter
328
+ except ImportError:
329
+ msg = "this command requires the cookiecutter CLI, please run:\npip install cookiecutter"
330
+ raise ClickException(msg)
331
+
332
+ cookiecutter(
333
+ "https://github.com/localstack/localstack-extensions", directory=f"templates/{template}"
334
+ )
335
+
336
+
337
+ @dev.command(
338
+ "enable",
339
+ help="""
340
+ Enables an extension on the host for developer mode.
341
+
342
+ Extensions for which dev mode is enabled will be mounted into the LocalStack container the next time it runs.
343
+
344
+ PATH: the path to the extension (can be relative).
345
+ """,
346
+ )
347
+ @click.argument("path", type=click.Path(exists=True))
348
+ @publish_invocation
349
+ def cmd_dev_enable(path: str) -> None:
350
+ from localstack_cli import config
351
+ from localstack_cli.utils.json import FileMappedDocument
352
+
353
+ path = os.path.abspath(path)
354
+
355
+ config = FileMappedDocument(os.path.join(config.CONFIG_DIR, "extensions-dev.json"))
356
+
357
+ if "extensions" not in config:
358
+ config["extensions"] = []
359
+
360
+ for ext in config["extensions"]:
361
+ if ext["host_path"] == path:
362
+ click.echo(f"{path} already enabled")
363
+ return
364
+
365
+ config["extensions"].append({"host_path": path})
366
+ config.save()
367
+ click.echo(f"{path} enabled")
368
+
369
+
370
+ @dev.command(
371
+ "disable",
372
+ help="""
373
+ Disables an extension on the host for developer mode.
374
+
375
+ Extensions for which dev mode is enabled will be mounted into the LocalStack container the next time it runs.
376
+
377
+ PATH: the path to the extension (can be relative).
378
+ """,
379
+ )
380
+ @click.argument("path", type=click.Path(exists=False))
381
+ @publish_invocation
382
+ def cmd_dev_disable(path: str) -> None:
383
+ from localstack_cli import config
384
+ from localstack_cli.utils.json import FileMappedDocument
385
+
386
+ path = os.path.abspath(path)
387
+
388
+ config = FileMappedDocument(os.path.join(config.CONFIG_DIR, "extensions-dev.json"))
389
+
390
+ if "extensions" not in config:
391
+ config["extensions"] = []
392
+
393
+ len_before = len(config["extensions"])
394
+ config["extensions"] = [ext for ext in config["extensions"] if ext["host_path"] != path]
395
+ len_after = len(config["extensions"])
396
+
397
+ if len_before == len_after:
398
+ click.echo(f"{path} not enabled")
399
+ return
400
+
401
+ config.save()
402
+ click.echo(f"{path} disabled")
403
+
404
+
405
+ @dev.command(
406
+ "list",
407
+ help="""
408
+ List LocalStack extensions for which dev mode is enabled.
409
+ """,
410
+ )
411
+ def cmd_dev_list() -> None:
412
+ from localstack_cli import config
413
+ from localstack_cli.utils.json import FileMappedDocument
414
+
415
+ config = FileMappedDocument(os.path.join(config.CONFIG_DIR, "extensions-dev.json"))
416
+
417
+ if "extensions" not in config:
418
+ return
419
+
420
+ for ext in config["extensions"]:
421
+ click.echo(ext["host_path"])
422
+
423
+
424
+ def _stream_localstack_container_command(
425
+ cmd: list[str],
426
+ additional_configurators: Iterable[ContainerConfigurator] = (),
427
+ ):
428
+ """
429
+ Convenience function to run a command inside a fresh localstack pro container and stream its output as
430
+ a generator.
431
+
432
+ :param cmd: the command to run inside the localstack container.
433
+ :param additional_configurators: additional container configurators
434
+ :return: a generator that yields each line from stdout from the container command
435
+ """
436
+ from localstack_cli import config
437
+ from localstack_cli.pro.core.bootstrap import licensingv2
438
+ from localstack_cli.utils import docker_utils
439
+ from localstack_cli.utils.bootstrap import (
440
+ Container,
441
+ ContainerConfigurators,
442
+ )
443
+
444
+ # either use the content of the IMAGE_NAME env var, or default to the pro image
445
+ # (extensions are not supported in community)
446
+ image_name = os.environ.get("IMAGE_NAME")
447
+ if not image_name:
448
+ image_name = constants.DOCKER_IMAGE_NAME_PRO
449
+
450
+ container = Container(
451
+ ContainerConfiguration(image_name=image_name, remove=False),
452
+ docker_client=docker_utils.DOCKER_CLIENT,
453
+ )
454
+
455
+ # recipe for extensions command container
456
+ configurators = [
457
+ ContainerConfigurators.env_vars(
458
+ {
459
+ "DEBUG": "0",
460
+ }
461
+ ),
462
+ ContainerConfigurators.custom_command(cmd),
463
+ ContainerConfigurators.mount_localstack_volume(config.VOLUME_DIR),
464
+ licensingv2.configure_container_licensing,
465
+ *additional_configurators,
466
+ ]
467
+ # if you want to test local changes to the backend, you can add this volume mount:
468
+ # configurators.append(
469
+ # ContainerConfigurators.volume(
470
+ # VolumeBind(
471
+ # "/path/to/localstack-pro/localstack-pro-core/localstack/pro/core/bootstrap",
472
+ # "/opt/code/localstack/.venv/lib/python3.11/site-packages/localstack/pro/core/bootstrap"
473
+ # )
474
+ # )
475
+ # )
476
+ container.configure(configurators)
477
+
478
+ running_container = container.start()
479
+ try:
480
+ stream = running_container.stream_logs()
481
+ for line in stream:
482
+ yield line.decode("utf-8")
483
+ result = running_container.inspect()
484
+ exit_code = result["State"]["ExitCode"]
485
+ if exit_code != 0:
486
+ logs = running_container.get_logs()
487
+ console.log(logs)
488
+ raise ContainerException(
489
+ f"container returned with a non-zero exit code {exit_code}", stdout=logs
490
+ )
491
+ finally:
492
+ running_container.shutdown(remove=True)
@@ -0,0 +1,180 @@
1
+ import json
2
+ import subprocess
3
+ from typing import Any
4
+
5
+ import click
6
+ import requests
7
+ from click import ClickException
8
+ from localstack_cli import config
9
+ from localstack_cli.cli import console
10
+ from localstack_cli.pro.core.cli.aws import aws
11
+ from localstack_cli.pro.core.cli.cli import RequiresPlatformLicenseGroup
12
+ from localstack_cli.utils.analytics.cli import publish_invocation
13
+ from localstack_cli.utils.platform import is_windows
14
+ from localstack_cli.utils.strings import to_str
15
+
16
+
17
+ def _print_plain(generated_policy: dict[str, Any]):
18
+ console.print(
19
+ f'Attached to {generated_policy["policy_type"]}: "{generated_policy["resource"]}"'
20
+ )
21
+ console.line()
22
+ console.print("Policy: ")
23
+ console.print_json(data=generated_policy["policy_document"])
24
+ console.line()
25
+ console.rule()
26
+ console.line()
27
+
28
+
29
+ def print_generated_policy_plain(generated_policy: bytes):
30
+ generated_policy = json.loads(to_str(generated_policy))
31
+ _print_plain(generated_policy)
32
+
33
+
34
+ def print_generated_policy_json(generated_policy: bytes):
35
+ console.print(to_str(generated_policy), highlight=False)
36
+
37
+
38
+ def get_iam_endpoint():
39
+ edge_url = config.external_service_url()
40
+ return f"{edge_url}/_aws/iam"
41
+
42
+
43
+ class IAMCliGroup(RequiresPlatformLicenseGroup):
44
+ name = "iam-stream"
45
+ # TODO this should be the plan name, but those should not necessarily be hardcoded. Maybe
46
+ # change the wording in the error message for RequiresPlatformLicenseGroup altogether
47
+ tier = "higher"
48
+
49
+
50
+ @aws.group(
51
+ name="iam",
52
+ short_help="(Preview) Access LocalStack IAM features",
53
+ help="""
54
+ Access LocalStack IAM features.
55
+
56
+ This command provides tools to make it easier to write IAM policies for your cloud application.
57
+ """,
58
+ cls=IAMCliGroup,
59
+ )
60
+ def iam() -> None:
61
+ pass
62
+
63
+
64
+ @iam.command(
65
+ name="stream",
66
+ short_help="Stream policies for all requests enforced on LocalStack",
67
+ help="""
68
+ Live stream of policies as requests are coming into LocalStack.
69
+
70
+ This command generates a live stream of policies and the principals or resources they should be attached to.
71
+
72
+ For every request, it will print the principal or resource the policy should be attached to first.
73
+ (will be a service resource if it is a resource based policy, an IAM principal otherwise)
74
+ After that the recommended policy will be printed.
75
+ """,
76
+ )
77
+ @click.option(
78
+ "-f",
79
+ "--format",
80
+ "format_",
81
+ type=click.Choice(["plain", "json"]),
82
+ default="plain",
83
+ help="The formatting style for the command output. Use plain if it should be human readable, and json to get a "
84
+ "newline-separated list of json documents.",
85
+ )
86
+ @publish_invocation
87
+ def cmd_iam_stream(format_: str) -> None:
88
+ try:
89
+ with requests.get(f"{get_iam_endpoint()}/policies/stream", stream=True) as response:
90
+ empty_policy_hint = "Please perform request against LocalStack to start seeing policies here. Waiting for policies..."
91
+ console.print(empty_policy_hint)
92
+ for generated_policy in response.iter_lines():
93
+ if format_ == "plain":
94
+ print_generated_policy_plain(generated_policy)
95
+ elif format_ == "json":
96
+ print_generated_policy_json(generated_policy)
97
+ except requests.ConnectionError:
98
+ raise ClickException(
99
+ "Unable to connect to the LocalStack Pro instance.\n"
100
+ "Please make sure you have an instance up and running!"
101
+ )
102
+ except json.JSONDecodeError:
103
+ raise ClickException(
104
+ "Invalid response from the LocalStack instance.\n"
105
+ "Please update your LocalStack instance!"
106
+ )
107
+ except Exception as e:
108
+ raise ClickException(f"Error while streaming Policies: {e}")
109
+
110
+
111
+ def clear_terminal():
112
+ if is_windows():
113
+ subprocess.run("cls")
114
+ else:
115
+ subprocess.run("clear")
116
+
117
+
118
+ @iam.command(
119
+ name="summary",
120
+ short_help="Summary of policies for all requests enforced on LocalStack",
121
+ help="""
122
+ Live view of all policies required for running your current stack on LocalStack
123
+
124
+ This command generates a live view of policies and the principals or resources they should be attached to.
125
+
126
+ This will clear your terminal.
127
+ The policies will update if a requests requires additional permissions for the principal making it.
128
+ """,
129
+ )
130
+ @click.option("-o", "--output", help="File location to write the json output to.")
131
+ @click.option(
132
+ "--follow",
133
+ "-f",
134
+ is_flag=True,
135
+ default=False,
136
+ help="Whether to continuously monitor the summary changes.",
137
+ )
138
+ @publish_invocation
139
+ def cmd_iam_summary(output: str | None, follow: bool) -> None:
140
+ policy_set = None
141
+ try:
142
+ if follow:
143
+ with requests.get(
144
+ f"{get_iam_endpoint()}/policies/summary?stream=1", stream=True
145
+ ) as response:
146
+ for policy_set in response.iter_lines():
147
+ clear_terminal()
148
+ policy_set = json.loads(to_str(policy_set))
149
+ if not policy_set:
150
+ console.print(
151
+ "Please perform request against LocalStack to start seeing policies here. Waiting for policies..."
152
+ )
153
+ for policy in policy_set:
154
+ _print_plain(policy)
155
+ else:
156
+ response = requests.get(f"{get_iam_endpoint()}/policies/summary")
157
+ policy_set = response.json()
158
+ if not policy_set:
159
+ console.print(
160
+ "No policies available yet. Please perform requests against LocalStack to get generated policies."
161
+ )
162
+ for policy in policy_set:
163
+ _print_plain(policy)
164
+
165
+ except requests.ConnectionError:
166
+ raise ClickException(
167
+ "Unable to connect to the LocalStack Pro instance.\n"
168
+ "Please make sure you have an instance up and running!"
169
+ )
170
+ except json.JSONDecodeError:
171
+ raise ClickException(
172
+ "Invalid response from the LocalStack instance.\n"
173
+ "Please update your LocalStack instance!"
174
+ )
175
+ except Exception as e:
176
+ raise ClickException(f"Error while streaming Policies: {e}")
177
+ finally:
178
+ if policy_set and output:
179
+ with open(output, mode="w") as f:
180
+ json.dump(policy_set, fp=f)