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,465 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from urllib.parse import urlparse
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from localstack_cli.cli import console
|
|
7
|
+
from localstack_cli.cli.exceptions import CLIError
|
|
8
|
+
from localstack_cli.pro.core import config as pro_config
|
|
9
|
+
from localstack_cli.pro.core.bootstrap.auth import get_auth_cache
|
|
10
|
+
from localstack_cli.pro.core.bootstrap.pods.api_types import MergeStrategy
|
|
11
|
+
from localstack_cli.pro.core.bootstrap.pods.remotes.api import CloudPodsRemotesClient
|
|
12
|
+
from localstack_cli.pro.core.bootstrap.pods.remotes.configs import RemoteConfigParams
|
|
13
|
+
from localstack_cli.pro.core.bootstrap.pods_client import (
|
|
14
|
+
CloudPodRemoteAttributes,
|
|
15
|
+
CloudPodsClient,
|
|
16
|
+
list_public_pods,
|
|
17
|
+
)
|
|
18
|
+
from localstack_cli.pro.core.cli.cli import (
|
|
19
|
+
RequiresLicenseGroup,
|
|
20
|
+
_assert_host_reachable,
|
|
21
|
+
)
|
|
22
|
+
from localstack_cli.pro.core.cli.click_utils import print_table
|
|
23
|
+
from localstack_cli.pro.core.cli.diff_view import print_diff
|
|
24
|
+
from localstack_cli.utils.analytics.cli import publish_invocation
|
|
25
|
+
from localstack_cli.utils.collections import is_comma_delimited_list
|
|
26
|
+
from localstack_cli.utils.time import timestamp
|
|
27
|
+
|
|
28
|
+
LOG = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@click.group(
|
|
34
|
+
name="pod",
|
|
35
|
+
help="Manage the state of your instance via Cloud Pods.",
|
|
36
|
+
context_settings={"max_content_width": 120},
|
|
37
|
+
cls=RequiresLicenseGroup if not get_auth_cache().get("token") else None,
|
|
38
|
+
)
|
|
39
|
+
def pod() -> None:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pod.group(name="remote", help="Manage cloud pod remotes")
|
|
44
|
+
def remote() -> None:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@remote.command(
|
|
49
|
+
name="add",
|
|
50
|
+
short_help="Add a remote",
|
|
51
|
+
help="""
|
|
52
|
+
Add a new remote for Cloud Pods.
|
|
53
|
+
|
|
54
|
+
A remote is the place where your Cloud Pods are stored. By default, Cloud Pods are store in the LocalStack platform.
|
|
55
|
+
""",
|
|
56
|
+
)
|
|
57
|
+
@click.argument("name", required=True)
|
|
58
|
+
@click.argument("url", required=True)
|
|
59
|
+
def cmd_add_remote(name: str, url: str) -> None:
|
|
60
|
+
_assert_host_reachable()
|
|
61
|
+
|
|
62
|
+
client = CloudPodsRemotesClient()
|
|
63
|
+
protocol = urlparse(url).scheme
|
|
64
|
+
try:
|
|
65
|
+
client.create_remote(name=name, protocols=[protocol], remote_url=url)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
raise CLIError(f"Unable to determine URL for remote '{name}': {e}") from e
|
|
68
|
+
console.print(f"Successfully added remote '{name}'")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@remote.command(name="delete", short_help="Delete a remote", help="Remove a remote for Cloud Pods.")
|
|
72
|
+
@click.argument("name", required=True)
|
|
73
|
+
def cmd_delete_remote(name: str) -> None:
|
|
74
|
+
_assert_host_reachable()
|
|
75
|
+
|
|
76
|
+
client = CloudPodsRemotesClient()
|
|
77
|
+
try:
|
|
78
|
+
client.delete_remote(name=name)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
raise CLIError(f"Unable to delete remote '{name}': {e}") from e
|
|
81
|
+
console.print(f"Successfully deleted remote '{name}'")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@remote.command(name="list", short_help="Lists the available remotes")
|
|
85
|
+
@click.option(
|
|
86
|
+
"-f",
|
|
87
|
+
"--format",
|
|
88
|
+
"format_",
|
|
89
|
+
type=click.Choice(["table", "json"]),
|
|
90
|
+
default="table",
|
|
91
|
+
help="The formatting style for the remotes command output.",
|
|
92
|
+
)
|
|
93
|
+
def cmd_remotes(format_: str) -> None:
|
|
94
|
+
_assert_host_reachable()
|
|
95
|
+
client = CloudPodsRemotesClient()
|
|
96
|
+
remotes = client.get_remotes()
|
|
97
|
+
if not remotes:
|
|
98
|
+
console.print("[yellow]No remotes[/yellow]")
|
|
99
|
+
return
|
|
100
|
+
if format_ == "json":
|
|
101
|
+
console.print_json(json.dumps(remotes))
|
|
102
|
+
return
|
|
103
|
+
print_table(
|
|
104
|
+
column_headers=["Remote Name", "URL"],
|
|
105
|
+
columns=[[r["name"] for r in remotes], [r["url"] for r in remotes]],
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@pod.command(
|
|
110
|
+
name="delete",
|
|
111
|
+
short_help="Delete a Cloud Pod",
|
|
112
|
+
help="""
|
|
113
|
+
Delete a Cloud Pod registered on a remote (by default, the LocalStack platform).
|
|
114
|
+
|
|
115
|
+
This command will remove all the versions of a Cloud Pod, and the operation is not reversible.
|
|
116
|
+
""",
|
|
117
|
+
)
|
|
118
|
+
@click.argument("name")
|
|
119
|
+
@click.argument("remote", required=False)
|
|
120
|
+
@publish_invocation
|
|
121
|
+
def cmd_pod_delete(name: str, remote: str | None = None) -> None:
|
|
122
|
+
client = CloudPodsClient()
|
|
123
|
+
try:
|
|
124
|
+
remote_params = RemoteConfigParams(remote_name=remote) if remote else None
|
|
125
|
+
client.delete(pod_name=name, remote=remote_params)
|
|
126
|
+
console.print(f"Successfully deleted Cloud Pod '{name}'")
|
|
127
|
+
except Exception as e:
|
|
128
|
+
raise CLIError(f"Unable to delete Cloud Pod '{name}': {e}") from e
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@pod.command(
|
|
132
|
+
name="save",
|
|
133
|
+
short_help="Create a new Cloud Pod",
|
|
134
|
+
help="""
|
|
135
|
+
Save the current state of the LocalStack container in a Cloud Pod.
|
|
136
|
+
|
|
137
|
+
A Cloud Pod can be registered and saved with different storage options, called remotes. By default, Cloud Pods
|
|
138
|
+
are hosted in the LocalStack platform. However, users can decide to store their Cloud Pods in other remotes, such as
|
|
139
|
+
AWS S3 buckets or ORAS registries.
|
|
140
|
+
|
|
141
|
+
An optional message can be attached to any Cloud Pod.
|
|
142
|
+
Furthermore, one could decide to export only a subset of services with the optional --services option.
|
|
143
|
+
|
|
144
|
+
\b
|
|
145
|
+
To use the LocalStack platform for storage, the desired Cloud Pod's name will suffice, e.g.:
|
|
146
|
+
|
|
147
|
+
\b
|
|
148
|
+
localstack pod save <pod_name>
|
|
149
|
+
|
|
150
|
+
Please be aware that each following save invocation with the same name will result in a new version being created.
|
|
151
|
+
|
|
152
|
+
\b
|
|
153
|
+
To save a local copy of your state, you can use the 'localstack state export' command.
|
|
154
|
+
""",
|
|
155
|
+
)
|
|
156
|
+
@click.argument("name")
|
|
157
|
+
@click.argument("remote", required=False)
|
|
158
|
+
@click.option("-m", "--message", help="Add a comment describing this Cloud Pod's version")
|
|
159
|
+
@click.option(
|
|
160
|
+
"-s",
|
|
161
|
+
"--services",
|
|
162
|
+
help="Comma-delimited list of services to push in the Cloud Pod (all by default)",
|
|
163
|
+
)
|
|
164
|
+
@click.option(
|
|
165
|
+
"--visibility",
|
|
166
|
+
type=click.Choice(["public", "private"]),
|
|
167
|
+
help="Set the visibility of the Cloud Pod [`public` or `private`]. Does not create a new version",
|
|
168
|
+
)
|
|
169
|
+
@click.option(
|
|
170
|
+
"-S",
|
|
171
|
+
"--secret",
|
|
172
|
+
"secret",
|
|
173
|
+
help="Secret for the Cloud Pod encryption. Encryption is an Enterprise only feature.",
|
|
174
|
+
)
|
|
175
|
+
@click.option(
|
|
176
|
+
"-f",
|
|
177
|
+
"--format",
|
|
178
|
+
"format_",
|
|
179
|
+
type=click.Choice(["json"]),
|
|
180
|
+
help="The formatting style for the save command output.",
|
|
181
|
+
)
|
|
182
|
+
@publish_invocation
|
|
183
|
+
def cmd_pod_save(
|
|
184
|
+
name: str | None = None,
|
|
185
|
+
remote: str | None = None,
|
|
186
|
+
services: str | None = None,
|
|
187
|
+
message: str | None = None,
|
|
188
|
+
visibility: str | None = None,
|
|
189
|
+
secret: str | None = None,
|
|
190
|
+
format_: str | None = None,
|
|
191
|
+
) -> None:
|
|
192
|
+
client = CloudPodsClient(True)
|
|
193
|
+
_assert_host_reachable()
|
|
194
|
+
|
|
195
|
+
if services and not is_comma_delimited_list(services):
|
|
196
|
+
raise CLIError("Input the services as a comma-delimited list")
|
|
197
|
+
|
|
198
|
+
remote_params = RemoteConfigParams(remote_name=remote) if remote else None
|
|
199
|
+
|
|
200
|
+
if visibility:
|
|
201
|
+
try:
|
|
202
|
+
client.set_remote_attributes(
|
|
203
|
+
pod_name=name,
|
|
204
|
+
attributes=CloudPodRemoteAttributes(is_public=True),
|
|
205
|
+
remote=remote_params,
|
|
206
|
+
)
|
|
207
|
+
except Exception as e:
|
|
208
|
+
raise CLIError(str(e))
|
|
209
|
+
console.print(f"Cloud Pod {name} made {visibility}")
|
|
210
|
+
|
|
211
|
+
service_list: list | None = [x.strip() for x in services.split(",")] if services else None
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
pod_info: dict = client.save(
|
|
215
|
+
pod_name=name,
|
|
216
|
+
attributes=CloudPodRemoteAttributes(
|
|
217
|
+
is_public=False,
|
|
218
|
+
description=message,
|
|
219
|
+
services=service_list,
|
|
220
|
+
),
|
|
221
|
+
remote=remote_params,
|
|
222
|
+
secret=secret,
|
|
223
|
+
)
|
|
224
|
+
except Exception as e:
|
|
225
|
+
raise CLIError(f"Failed to create Cloud Pod {name} ❌ - {e}") from e
|
|
226
|
+
if format_ == "json":
|
|
227
|
+
console.print_json(json.dumps(pod_info))
|
|
228
|
+
else:
|
|
229
|
+
services = ",".join(pod_info["services"]) or "none"
|
|
230
|
+
console.print(
|
|
231
|
+
f"Cloud Pod `{name}` successfully created ✅\n"
|
|
232
|
+
f"Version: {pod_info['version']}\n"
|
|
233
|
+
f"Remote: {pod_info['remote']}\n"
|
|
234
|
+
f"Services: {services}"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@pod.command(
|
|
239
|
+
name="load",
|
|
240
|
+
help="""
|
|
241
|
+
Load the state of a Cloud Pod into the application runtime.
|
|
242
|
+
Users can import Cloud Pods from different remotes, with the LocalStack platform being the default one.
|
|
243
|
+
Users can also load a specific version by appending a version number to the pod name after a colon
|
|
244
|
+
(e.g., `localstack pod load my-pod:3`).
|
|
245
|
+
If not specified, the latest version will be loaded. Use the `localstack pod versions` to list all the available
|
|
246
|
+
versions.
|
|
247
|
+
|
|
248
|
+
Loading the state of a Cloud Pod into LocalStack might cause some conflicts with the current state of the container.
|
|
249
|
+
LocalStack will attempt a best-effort merging strategy between the current state and the one from the
|
|
250
|
+
Cloud Pod.
|
|
251
|
+
For a service X present in both the current state and the Cloud Pod, we will attempt to merge states
|
|
252
|
+
across different accounts and regions. If the service X has a state for the same account and region both in the
|
|
253
|
+
running container and the Cloud Pod, the latter will be used. If a service Y is present in the running container
|
|
254
|
+
but not in the Cloud Pod, it will be left untouched.
|
|
255
|
+
This is the default merge strategy which is activated by either the `--strategy account-region-merge` option or
|
|
256
|
+
by omitting the `--strategy` option at all.
|
|
257
|
+
|
|
258
|
+
In addition to the default one, LocalStack provides two more strategies:
|
|
259
|
+
|
|
260
|
+
\b
|
|
261
|
+
- overwrite, in which the state of LocalStack is completely reset before loading the state from the Cloud Pod.
|
|
262
|
+
This strategy is activated with the `--strategy overwrite` option .
|
|
263
|
+
- service-merge: in which LocalStack merges the state of a service under the same account and region when
|
|
264
|
+
there is no resource overlap. In such a case, the loaded resources are preferred.
|
|
265
|
+
This option is activated with the `--strategy service-merge` option.
|
|
266
|
+
|
|
267
|
+
To load a local copy of a LocalStack state, you can use the 'localstack state import' command.
|
|
268
|
+
""",
|
|
269
|
+
)
|
|
270
|
+
@click.argument("name")
|
|
271
|
+
@click.argument("remote", required=False)
|
|
272
|
+
@click.option(
|
|
273
|
+
"-s",
|
|
274
|
+
"--secret",
|
|
275
|
+
"secret",
|
|
276
|
+
help="Secret for the Cloud Pod encryption. Encryption is an Enterprise only feature.",
|
|
277
|
+
)
|
|
278
|
+
@click.option(
|
|
279
|
+
"--strategy",
|
|
280
|
+
type=click.Choice([e.value for e in MergeStrategy]), # noqa
|
|
281
|
+
default=pro_config.MERGE_STRATEGY,
|
|
282
|
+
help="The merge strategy to adopt when loading the Cloud Pod.",
|
|
283
|
+
)
|
|
284
|
+
@click.option(
|
|
285
|
+
"--yes",
|
|
286
|
+
"-y",
|
|
287
|
+
help="Automatic yes to prompts. Assume a positive answer to all prompts and run non-interactively.",
|
|
288
|
+
is_flag=True,
|
|
289
|
+
default=False,
|
|
290
|
+
)
|
|
291
|
+
@click.option(
|
|
292
|
+
"--dry-run",
|
|
293
|
+
help="Checks the resources added or modified in the application runtime by loading a Cloud Pod.",
|
|
294
|
+
is_flag=True,
|
|
295
|
+
default=False,
|
|
296
|
+
)
|
|
297
|
+
@publish_invocation
|
|
298
|
+
def cmd_pod_load(
|
|
299
|
+
name: str | None = None,
|
|
300
|
+
remote: str | None = None,
|
|
301
|
+
strategy: str | None = None,
|
|
302
|
+
yes: bool = False,
|
|
303
|
+
secret: str | None = None,
|
|
304
|
+
dry_run: bool = False,
|
|
305
|
+
) -> None:
|
|
306
|
+
client = CloudPodsClient(True)
|
|
307
|
+
|
|
308
|
+
_assert_host_reachable()
|
|
309
|
+
pod_name, version = get_pod_name_and_version(name)
|
|
310
|
+
|
|
311
|
+
if dry_run:
|
|
312
|
+
try:
|
|
313
|
+
diffing_ops = client.diff(pod_name=pod_name, remote=remote, version=version)
|
|
314
|
+
print_diff(diffing_ops)
|
|
315
|
+
return
|
|
316
|
+
except Exception as e:
|
|
317
|
+
raise CLIError(f"Failed to dry-run the load operation: {e}") from e
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
remote_params = RemoteConfigParams(remote_name=remote) if remote else None
|
|
321
|
+
client.load(
|
|
322
|
+
pod_name=pod_name,
|
|
323
|
+
remote=remote_params,
|
|
324
|
+
version=version,
|
|
325
|
+
merge_strategy=strategy,
|
|
326
|
+
ignore_version_mismatches=yes,
|
|
327
|
+
secret=secret,
|
|
328
|
+
)
|
|
329
|
+
except Exception as e:
|
|
330
|
+
raise CLIError(f"Failed to load Cloud Pod {name}: {e}") from e
|
|
331
|
+
print(f"Cloud Pod {name} successfully loaded")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def get_pod_name_and_version(pod_name: str) -> tuple[str, int | None]:
|
|
335
|
+
"""
|
|
336
|
+
Retrieve the name and the version number from the pod name. We have a Docker-like convention to specify the version
|
|
337
|
+
numbers i.e., <pod_name>:<version>. If we can't parse a version, we assume the last available version.
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
if ":" not in pod_name:
|
|
341
|
+
return pod_name, None
|
|
342
|
+
|
|
343
|
+
_pod_name, _, version = pod_name.rpartition(":")
|
|
344
|
+
if version.isdigit():
|
|
345
|
+
return _pod_name, int(version)
|
|
346
|
+
return pod_name, None
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@pod.command(
|
|
350
|
+
name="list",
|
|
351
|
+
short_help="List all available Cloud Pods",
|
|
352
|
+
help="""
|
|
353
|
+
List all the Cloud Pods available for a single user, or for an entire organization, if the user is part of one.
|
|
354
|
+
|
|
355
|
+
With the --public flag, it lists the all the available public Cloud Pods. A public Cloud Pod is available across
|
|
356
|
+
the boundary of a user and/or organization. In other words, any public Cloud Pod can be injected by any other
|
|
357
|
+
user holding a LocalStack Pro (or above) license.
|
|
358
|
+
""",
|
|
359
|
+
)
|
|
360
|
+
@click.argument("remote", required=False)
|
|
361
|
+
@click.option(
|
|
362
|
+
"--public", "-p", help="List all the available public Cloud Pods", is_flag=True, default=False
|
|
363
|
+
)
|
|
364
|
+
@click.option(
|
|
365
|
+
"--mine",
|
|
366
|
+
"-m",
|
|
367
|
+
help="List only the Cloud Pods created by the current user",
|
|
368
|
+
is_flag=True,
|
|
369
|
+
default=False,
|
|
370
|
+
)
|
|
371
|
+
@click.option(
|
|
372
|
+
"-f",
|
|
373
|
+
"--format",
|
|
374
|
+
"format_",
|
|
375
|
+
type=click.Choice(["table", "json"]),
|
|
376
|
+
default="table",
|
|
377
|
+
help="The formatting style for the list pods command output.",
|
|
378
|
+
)
|
|
379
|
+
@publish_invocation
|
|
380
|
+
def cmd_pod_list_pods(
|
|
381
|
+
remote: str | None = None, public: bool = False, mine: bool = False, format_: str = None
|
|
382
|
+
) -> None:
|
|
383
|
+
client = CloudPodsClient()
|
|
384
|
+
|
|
385
|
+
_assert_host_reachable()
|
|
386
|
+
|
|
387
|
+
if public:
|
|
388
|
+
public_pods = list_public_pods()
|
|
389
|
+
print_table(column_headers=["Cloud Pod"], columns=[public_pods])
|
|
390
|
+
return
|
|
391
|
+
|
|
392
|
+
remote_params = RemoteConfigParams(remote_name=remote) if remote else None
|
|
393
|
+
pods: list[dict] = client.list(remote=remote_params, creator="me" if mine else None)
|
|
394
|
+
if not pods:
|
|
395
|
+
console.print("[yellow]No pods available[/yellow]")
|
|
396
|
+
|
|
397
|
+
if format_ == "json":
|
|
398
|
+
console.print_json(json.dumps(pods))
|
|
399
|
+
return
|
|
400
|
+
print_table(
|
|
401
|
+
column_headers=["Name", "Max Version", "Last Change"],
|
|
402
|
+
columns=[
|
|
403
|
+
[pod["pod_name"] for pod in pods],
|
|
404
|
+
[str(pod["max_version"]) for pod in pods],
|
|
405
|
+
[
|
|
406
|
+
timestamp(pod["last_change"], format=DATE_FORMAT)
|
|
407
|
+
if pod.get("last_change")
|
|
408
|
+
else "n/a"
|
|
409
|
+
for pod in pods
|
|
410
|
+
],
|
|
411
|
+
],
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
@pod.command(
|
|
416
|
+
name="versions",
|
|
417
|
+
help="""
|
|
418
|
+
List all available versions for a Cloud Pod
|
|
419
|
+
|
|
420
|
+
This command lists the versions available for a Cloud Pod.
|
|
421
|
+
Each invocation of the save command is going to create a new version for a named Cloud Pod, if a Pod with
|
|
422
|
+
such name already does exist in the LocalStack platform.
|
|
423
|
+
""",
|
|
424
|
+
)
|
|
425
|
+
@click.argument("name")
|
|
426
|
+
@click.option(
|
|
427
|
+
"-f",
|
|
428
|
+
"--format",
|
|
429
|
+
"format_",
|
|
430
|
+
type=click.Choice(["table", "json"]),
|
|
431
|
+
default="table",
|
|
432
|
+
help="The formatting style for the version command output.",
|
|
433
|
+
)
|
|
434
|
+
@publish_invocation
|
|
435
|
+
def cmd_pod_versions(name: str, format_: str) -> None:
|
|
436
|
+
client = CloudPodsClient()
|
|
437
|
+
_assert_host_reachable()
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
versions: list[dict] = client.get_versions(pod_name=name)
|
|
441
|
+
versions = [v for v in versions if not v.get("deleted")]
|
|
442
|
+
except Exception as e:
|
|
443
|
+
raise CLIError(str(e)) from e
|
|
444
|
+
if not versions:
|
|
445
|
+
console.print("[yellow]No versions available[/yellow]")
|
|
446
|
+
|
|
447
|
+
if format_ == "json":
|
|
448
|
+
console.print_json(json.dumps(versions))
|
|
449
|
+
return
|
|
450
|
+
print_table(
|
|
451
|
+
column_headers=[
|
|
452
|
+
"Version",
|
|
453
|
+
"Creation Date",
|
|
454
|
+
"LocalStack Version",
|
|
455
|
+
"Services",
|
|
456
|
+
"Description",
|
|
457
|
+
],
|
|
458
|
+
columns=[
|
|
459
|
+
[str(r["version"]) for r in versions],
|
|
460
|
+
[timestamp(r["created_at"], format=DATE_FORMAT) for r in versions],
|
|
461
|
+
[r["localstack_version"] for r in versions],
|
|
462
|
+
[",".join(r["services"] or []) for r in versions],
|
|
463
|
+
[r["description"] for r in versions],
|
|
464
|
+
],
|
|
465
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from localstack_cli.cli import console
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def print_diff(operations: dict[str, list[dict]]) -> None:
|
|
5
|
+
"""
|
|
6
|
+
Prints the list of operations produced by a Cloud Pod loaded into the application runtime in a summarized and
|
|
7
|
+
human-readable way.
|
|
8
|
+
The input has the following shape:
|
|
9
|
+
{ "service_name": list[localstack.pro.core.persistence.pods.diff.models.Operation] }
|
|
10
|
+
:param operations: a map with list of operation associated to services.
|
|
11
|
+
"""
|
|
12
|
+
if not operations:
|
|
13
|
+
console.print("This load operation does not affect the runtime state.")
|
|
14
|
+
return
|
|
15
|
+
|
|
16
|
+
# If there is any modification in the list of operations, we are facing a conflict.
|
|
17
|
+
has_modification = any(
|
|
18
|
+
op_dict.get("operation_type") == "MODIFICATION"
|
|
19
|
+
for op_list in operations.values()
|
|
20
|
+
for op_dict in op_list
|
|
21
|
+
)
|
|
22
|
+
if has_modification:
|
|
23
|
+
console.print(
|
|
24
|
+
"[yellow]This load operation modifies one or more resources in the"
|
|
25
|
+
" application runtime."
|
|
26
|
+
" The result will depend on the selected merge strategy."
|
|
27
|
+
" Use the --help option to read more about it.[/]\n"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
console.print("This load operation will modify the runtime state as follows:")
|
|
31
|
+
for _service, _operations in operations.items():
|
|
32
|
+
addition_operations = [ops for ops in _operations if ops["operation_type"] == "ADDITION"]
|
|
33
|
+
modification_operations = [
|
|
34
|
+
ops for ops in _operations if ops["operation_type"] == "MODIFICATION"
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
if not addition_operations and not modification_operations:
|
|
38
|
+
return
|
|
39
|
+
console.rule(_service)
|
|
40
|
+
console.print(f"[green]+[/] {len(addition_operations)} resources added.")
|
|
41
|
+
console.print(f"[yellow]~[/] {len(modification_operations)} resources modified.")
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
import requests
|
|
5
|
+
from localstack_cli import constants as localstack_constants
|
|
6
|
+
from localstack_cli.cli import console
|
|
7
|
+
from localstack_cli.cli.exceptions import CLIError
|
|
8
|
+
from localstack_cli.pro.core.bootstrap import auth
|
|
9
|
+
from localstack_cli.pro.core.cli.cli import RequiresLicenseGroup
|
|
10
|
+
from localstack_cli.utils.analytics.cli import publish_invocation
|
|
11
|
+
|
|
12
|
+
API_ENDPOINT = localstack_constants.API_ENDPOINT
|
|
13
|
+
API_CREATION_ENDPOINT = f"{API_ENDPOINT}/compute/instances"
|
|
14
|
+
API_DELETION_ENDPOINT = f"{API_ENDPOINT}/compute/instances/{{name}}"
|
|
15
|
+
API_LIST_ENDPOINT = f"{API_ENDPOINT}/compute/instances"
|
|
16
|
+
API_LOGS_ENDPOINT = f"{API_ENDPOINT}/compute/instances/{{name}}/logs"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@click.group(
|
|
20
|
+
name="ephemeral",
|
|
21
|
+
short_help="(Preview) Manage ephemeral LocalStack instances",
|
|
22
|
+
help="""
|
|
23
|
+
(Preview) Manage ephemeral LocalStack instances in the cloud.
|
|
24
|
+
|
|
25
|
+
This command group allows you to create, list, and delete ephemeral LocalStack instances.
|
|
26
|
+
Ephemeral instances are temporary cloud instances that can be used for testing and development.
|
|
27
|
+
""",
|
|
28
|
+
cls=RequiresLicenseGroup,
|
|
29
|
+
)
|
|
30
|
+
def ephemeral() -> None:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@ephemeral.command(
|
|
35
|
+
name="create",
|
|
36
|
+
short_help="Create a new ephemeral instance",
|
|
37
|
+
help="""
|
|
38
|
+
Create a new ephemeral LocalStack instance in the cloud.
|
|
39
|
+
|
|
40
|
+
Specify an instance name and optional parameters like lifetime and environment variables.
|
|
41
|
+
The instance will be created with the specified configuration and its connection details will be returned.
|
|
42
|
+
|
|
43
|
+
\b
|
|
44
|
+
Examples:
|
|
45
|
+
localstack ephemeral create --name my-test-instance
|
|
46
|
+
localstack ephemeral create --name my-instance --lifetime 60
|
|
47
|
+
localstack ephemeral create --name my-instance --env DEBUG=1
|
|
48
|
+
""",
|
|
49
|
+
)
|
|
50
|
+
@click.option("--name", required=True, help="Name of the ephemeral instance")
|
|
51
|
+
@click.option("--lifetime", required=False, type=int, help="Lifetime of the instance in minutes")
|
|
52
|
+
@click.option(
|
|
53
|
+
"--env",
|
|
54
|
+
"-e",
|
|
55
|
+
help="Additional environment variables that are passed to the LocalStack instance",
|
|
56
|
+
multiple=True,
|
|
57
|
+
required=False,
|
|
58
|
+
)
|
|
59
|
+
@publish_invocation
|
|
60
|
+
def create(
|
|
61
|
+
name: str,
|
|
62
|
+
lifetime: int | None,
|
|
63
|
+
env: tuple | None,
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Create a new ephemeral instance with the specified configuration."""
|
|
66
|
+
try:
|
|
67
|
+
env_dict = {}
|
|
68
|
+
if env:
|
|
69
|
+
for var in env:
|
|
70
|
+
if "=" not in var:
|
|
71
|
+
raise CLIError(f"Invalid environment variable format: {var}")
|
|
72
|
+
key, value = var.split("=", 1)
|
|
73
|
+
env_dict[key.strip()] = value.strip()
|
|
74
|
+
|
|
75
|
+
headers = auth.get_platform_auth_headers()
|
|
76
|
+
data = {
|
|
77
|
+
"instance_name": name,
|
|
78
|
+
"lifetime": lifetime or 60,
|
|
79
|
+
"env_vars": env_dict,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
response = requests.post(API_CREATION_ENDPOINT, headers=headers, json=data)
|
|
83
|
+
response.raise_for_status()
|
|
84
|
+
|
|
85
|
+
console.print_json(json.dumps(response.json()))
|
|
86
|
+
|
|
87
|
+
except requests.exceptions.RequestException as e:
|
|
88
|
+
if hasattr(e, "response") and e.response is not None:
|
|
89
|
+
try:
|
|
90
|
+
error_detail = e.response.json()
|
|
91
|
+
raise CLIError(f"Failed to create ephemeral instance: {error_detail}")
|
|
92
|
+
except json.JSONDecodeError:
|
|
93
|
+
raise CLIError(f"Failed to create ephemeral instance: {str(e)}")
|
|
94
|
+
raise CLIError(f"Failed to create ephemeral instance: {str(e)}")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@ephemeral.command(
|
|
98
|
+
name="list",
|
|
99
|
+
short_help="List all ephemeral instances",
|
|
100
|
+
help="""
|
|
101
|
+
List all available ephemeral LocalStack instances.
|
|
102
|
+
|
|
103
|
+
This command shows all ephemeral instances associated with your account,
|
|
104
|
+
including their names, status, and other relevant details.
|
|
105
|
+
|
|
106
|
+
\b
|
|
107
|
+
Examples:
|
|
108
|
+
localstack ephemeral list
|
|
109
|
+
""",
|
|
110
|
+
)
|
|
111
|
+
@publish_invocation
|
|
112
|
+
def list_instances() -> None:
|
|
113
|
+
"""List all ephemeral instances."""
|
|
114
|
+
try:
|
|
115
|
+
headers = auth.get_platform_auth_headers()
|
|
116
|
+
|
|
117
|
+
response = requests.get(API_LIST_ENDPOINT, headers=headers)
|
|
118
|
+
response.raise_for_status()
|
|
119
|
+
|
|
120
|
+
response_data = response.json()
|
|
121
|
+
console.print_json(json.dumps(response_data, indent=2))
|
|
122
|
+
|
|
123
|
+
except requests.exceptions.RequestException as e:
|
|
124
|
+
raise CLIError(f"Failed to list ephemeral instances: {str(e)}")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@ephemeral.command(
|
|
128
|
+
name="delete",
|
|
129
|
+
short_help="Delete an ephemeral instance",
|
|
130
|
+
help="""
|
|
131
|
+
Delete a specific ephemeral LocalStack instance.
|
|
132
|
+
|
|
133
|
+
Specify the name of the instance you want to delete.
|
|
134
|
+
Once deleted, the instance cannot be recovered.
|
|
135
|
+
|
|
136
|
+
\b
|
|
137
|
+
Example:
|
|
138
|
+
localstack ephemeral delete --name my-test-instance
|
|
139
|
+
""",
|
|
140
|
+
)
|
|
141
|
+
@click.option("--name", required=True, help="Name of the ephemeral instance to delete")
|
|
142
|
+
@publish_invocation
|
|
143
|
+
def delete(name: str) -> None:
|
|
144
|
+
"""Delete an ephemeral instance."""
|
|
145
|
+
try:
|
|
146
|
+
url = API_DELETION_ENDPOINT.format(name=name)
|
|
147
|
+
headers = auth.get_platform_auth_headers()
|
|
148
|
+
|
|
149
|
+
response = requests.delete(url, headers=headers)
|
|
150
|
+
response.raise_for_status()
|
|
151
|
+
|
|
152
|
+
console.print(f"Successfully deleted instance: {name} ✅")
|
|
153
|
+
|
|
154
|
+
except requests.exceptions.RequestException as e:
|
|
155
|
+
raise CLIError(f"Failed to delete ephemeral instance: {str(e)}")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@ephemeral.command(
|
|
159
|
+
name="logs",
|
|
160
|
+
short_help="Fetch logs from an ephemeral instance",
|
|
161
|
+
help="""
|
|
162
|
+
Fetch logs from a specific ephemeral LocalStack instance.
|
|
163
|
+
|
|
164
|
+
Retrieve the logs of a running ephemeral instance by specifying its name.
|
|
165
|
+
The logs are returned in chronological order.
|
|
166
|
+
|
|
167
|
+
\b
|
|
168
|
+
Example:
|
|
169
|
+
localstack ephemeral logs --name my-test-instance
|
|
170
|
+
""",
|
|
171
|
+
)
|
|
172
|
+
@click.option("--name", required=True, help="Name of the ephemeral instance to fetch logs from")
|
|
173
|
+
@publish_invocation
|
|
174
|
+
def logs(name: str) -> None:
|
|
175
|
+
"""Fetch logs from an ephemeral instance."""
|
|
176
|
+
try:
|
|
177
|
+
url = API_LOGS_ENDPOINT.format(name=name)
|
|
178
|
+
headers = auth.get_platform_auth_headers()
|
|
179
|
+
|
|
180
|
+
response = requests.get(url, headers=headers)
|
|
181
|
+
response.raise_for_status()
|
|
182
|
+
|
|
183
|
+
log_lines = response.json()
|
|
184
|
+
if not log_lines:
|
|
185
|
+
console.print("No logs available for this instance.")
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
for log_line in log_lines:
|
|
189
|
+
content = log_line.get("content", "")
|
|
190
|
+
console.print(f"{content}")
|
|
191
|
+
|
|
192
|
+
except requests.exceptions.RequestException as e:
|
|
193
|
+
if hasattr(e, "response") and e.response is not None:
|
|
194
|
+
try:
|
|
195
|
+
error_detail = e.response.json()
|
|
196
|
+
raise CLIError(f"Failed to fetch logs: {error_detail}")
|
|
197
|
+
except json.JSONDecodeError:
|
|
198
|
+
raise CLIError(f"Failed to fetch logs: {str(e)}")
|
|
199
|
+
raise CLIError(f"Failed to fetch logs: {str(e)}")
|