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.
- localstack_cli/__init__.py +0 -0
- localstack_cli/cli/__init__.py +10 -0
- localstack_cli/cli/console.py +11 -0
- localstack_cli/cli/core_plugin.py +12 -0
- localstack_cli/cli/exceptions.py +19 -0
- localstack_cli/cli/localstack.py +951 -0
- localstack_cli/cli/lpm.py +138 -0
- localstack_cli/cli/main.py +22 -0
- localstack_cli/cli/plugin.py +39 -0
- localstack_cli/cli/plugins.py +134 -0
- localstack_cli/cli/profiles.py +65 -0
- localstack_cli/config.py +1689 -0
- localstack_cli/constants.py +165 -0
- localstack_cli/logging/__init__.py +0 -0
- localstack_cli/logging/format.py +194 -0
- localstack_cli/logging/setup.py +142 -0
- localstack_cli/packages/__init__.py +25 -0
- localstack_cli/packages/api.py +418 -0
- localstack_cli/packages/core.py +416 -0
- localstack_cli/pro/__init__.py +0 -0
- localstack_cli/pro/core/__init__.py +0 -0
- localstack_cli/pro/core/bootstrap/__init__.py +1 -0
- localstack_cli/pro/core/bootstrap/auth.py +213 -0
- localstack_cli/pro/core/bootstrap/dns_utils.py +55 -0
- localstack_cli/pro/core/bootstrap/entitlements.py +117 -0
- localstack_cli/pro/core/bootstrap/extensions/__init__.py +3 -0
- localstack_cli/pro/core/bootstrap/extensions/__main__.py +106 -0
- localstack_cli/pro/core/bootstrap/extensions/autoinstall.py +63 -0
- localstack_cli/pro/core/bootstrap/extensions/bootstrap.py +97 -0
- localstack_cli/pro/core/bootstrap/extensions/repository.py +374 -0
- localstack_cli/pro/core/bootstrap/licensingv2.py +1259 -0
- localstack_cli/pro/core/bootstrap/pods/__init__.py +0 -0
- localstack_cli/pro/core/bootstrap/pods/api_types.py +17 -0
- localstack_cli/pro/core/bootstrap/pods/constants.py +26 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/__init__.py +0 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/api.py +75 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/configs.py +69 -0
- localstack_cli/pro/core/bootstrap/pods/remotes/params.py +86 -0
- localstack_cli/pro/core/bootstrap/pods_client.py +834 -0
- localstack_cli/pro/core/cli/__init__.py +0 -0
- localstack_cli/pro/core/cli/auth.py +226 -0
- localstack_cli/pro/core/cli/aws.py +16 -0
- localstack_cli/pro/core/cli/cli.py +99 -0
- localstack_cli/pro/core/cli/click_utils.py +21 -0
- localstack_cli/pro/core/cli/cloud_pods.py +465 -0
- localstack_cli/pro/core/cli/diff_view.py +41 -0
- localstack_cli/pro/core/cli/ephemeral.py +199 -0
- localstack_cli/pro/core/cli/extensions.py +492 -0
- localstack_cli/pro/core/cli/iam.py +180 -0
- localstack_cli/pro/core/cli/license.py +90 -0
- localstack_cli/pro/core/cli/localstack.py +118 -0
- localstack_cli/pro/core/cli/replicator.py +378 -0
- localstack_cli/pro/core/cli/state.py +183 -0
- localstack_cli/pro/core/cli/tree_view.py +235 -0
- localstack_cli/pro/core/config.py +556 -0
- localstack_cli/pro/core/constants.py +54 -0
- localstack_cli/pro/core/plugins.py +169 -0
- localstack_cli/runtime/__init__.py +6 -0
- localstack_cli/runtime/exceptions.py +7 -0
- localstack_cli/runtime/hooks.py +73 -0
- localstack_cli/testing/__init__.py +1 -0
- localstack_cli/testing/config.py +4 -0
- localstack_cli/utils/__init__.py +0 -0
- localstack_cli/utils/analytics/__init__.py +12 -0
- localstack_cli/utils/analytics/cli.py +67 -0
- localstack_cli/utils/analytics/client.py +111 -0
- localstack_cli/utils/analytics/events.py +30 -0
- localstack_cli/utils/analytics/logger.py +48 -0
- localstack_cli/utils/analytics/metadata.py +250 -0
- localstack_cli/utils/analytics/publisher.py +160 -0
- localstack_cli/utils/analytics/service_request_aggregator.py +133 -0
- localstack_cli/utils/archives.py +271 -0
- localstack_cli/utils/batching.py +258 -0
- localstack_cli/utils/bootstrap.py +1418 -0
- localstack_cli/utils/checksum.py +313 -0
- localstack_cli/utils/collections.py +554 -0
- localstack_cli/utils/common.py +229 -0
- localstack_cli/utils/container_networking.py +142 -0
- localstack_cli/utils/container_utils/__init__.py +0 -0
- localstack_cli/utils/container_utils/container_client.py +1585 -0
- localstack_cli/utils/container_utils/docker_cmd_client.py +987 -0
- localstack_cli/utils/container_utils/docker_sdk_client.py +1018 -0
- localstack_cli/utils/crypto.py +294 -0
- localstack_cli/utils/docker_utils.py +272 -0
- localstack_cli/utils/files.py +327 -0
- localstack_cli/utils/functions.py +92 -0
- localstack_cli/utils/http.py +326 -0
- localstack_cli/utils/json.py +219 -0
- localstack_cli/utils/net.py +516 -0
- localstack_cli/utils/no_exit_argument_parser.py +19 -0
- localstack_cli/utils/numbers.py +49 -0
- localstack_cli/utils/objects.py +235 -0
- localstack_cli/utils/patch.py +260 -0
- localstack_cli/utils/platform.py +77 -0
- localstack_cli/utils/run.py +514 -0
- localstack_cli/utils/server/__init__.py +0 -0
- localstack_cli/utils/server/tcp_proxy.py +108 -0
- localstack_cli/utils/serving.py +187 -0
- localstack_cli/utils/ssl.py +71 -0
- localstack_cli/utils/strings.py +245 -0
- localstack_cli/utils/sync.py +267 -0
- localstack_cli/utils/threads.py +163 -0
- localstack_cli/utils/time.py +81 -0
- localstack_cli/utils/urls.py +21 -0
- localstack_cli/utils/venv.py +100 -0
- localstack_cli/utils/xml.py +41 -0
- localstack_cli/version.py +34 -0
- playground_ls_cli-4.14.1.dev8.dist-info/METADATA +95 -0
- playground_ls_cli-4.14.1.dev8.dist-info/RECORD +112 -0
- playground_ls_cli-4.14.1.dev8.dist-info/WHEEL +5 -0
- playground_ls_cli-4.14.1.dev8.dist-info/entry_points.txt +17 -0
- 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)
|