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,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)}")